Add symbol picker and user creation
This commit is contained in:
parent
44a279bc58
commit
ecb9ab21fd
35 changed files with 754 additions and 109 deletions
|
@ -33,6 +33,9 @@
|
|||
"@typescript-eslint/consistent-type-imports": ["error", {
|
||||
"prefer": "type-imports",
|
||||
"disallowTypeAnnotations": true
|
||||
}],
|
||||
"no-underscore-dangle": ["error", {
|
||||
"allow": ["_id"]
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
|
13
backend/package-lock.json
generated
13
backend/package-lock.json
generated
|
@ -629,15 +629,16 @@
|
|||
}
|
||||
},
|
||||
"@wulkanowy/sdk": {
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@wulkanowy/sdk/-/sdk-0.1.1.tgz",
|
||||
"integrity": "sha512-ktBYmd4nNeAs5STH9rf9EaAxPxSCN2PWtuP+rqW980RcWIv6IJddYvW1j8u60VFoThxMPWMpRHr7MeRqjKaC4A==",
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@wulkanowy/sdk/-/sdk-0.1.3.tgz",
|
||||
"integrity": "sha512-Ki2ZsyudbbmHr12/sne0Ssjj+kHxf2AUbUJEbeTgZbJUkocAk/rI0UaqjdnZ06uLGruC9FuKt+H1AuIoAbPcxQ==",
|
||||
"requires": {
|
||||
"axios": "^0.21.1",
|
||||
"axios-cookiejar-support": "^1.0.1",
|
||||
"cheerio": "github:dominik-korsa/cheerio#export-types",
|
||||
"date-fns": "^2.16.1",
|
||||
"date-fns-tz": "^1.0.12",
|
||||
"lodash": "^4.17.20",
|
||||
"querystring": "^0.2.0",
|
||||
"tough-cookie": "^4.0.0"
|
||||
}
|
||||
|
@ -1662,9 +1663,9 @@
|
|||
}
|
||||
},
|
||||
"entities": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
|
||||
"integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w=="
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz",
|
||||
"integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A=="
|
||||
},
|
||||
"error-ex": {
|
||||
"version": "1.3.2",
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
"@types/lodash": "^4.14.167",
|
||||
"@types/node": "^14.14.21",
|
||||
"@types/url-join": "^4.0.0",
|
||||
"@wulkanowy/sdk": "^0.1.1",
|
||||
"@wulkanowy/sdk": "^0.1.3",
|
||||
"apollo-server-fastify": "^2.19.2",
|
||||
"base64url": "^3.0.1",
|
||||
"date-fns": "^2.16.1",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { addSeconds, isAfter } from 'date-fns';
|
||||
import type { FastifyLoggerInstance } from 'fastify';
|
||||
import { nanoid } from 'nanoid';
|
||||
import type { ObjectID } from 'typeorm';
|
||||
import { UnknownCodeError } from './errors';
|
||||
import type { CodeChallenge, CodeContent, CodeInfo } from './types';
|
||||
import { createKey, decryptSymmetrical, encryptSymmetrical } from './utils';
|
||||
|
@ -11,6 +12,7 @@ export function createCode(options: {
|
|||
clientId: string;
|
||||
scopes: string[];
|
||||
studentIds: number[];
|
||||
userId: ObjectID;
|
||||
publicKey: string;
|
||||
tokenKey: string;
|
||||
encryptedPrivateKey: string;
|
||||
|
@ -29,6 +31,7 @@ export function createCode(options: {
|
|||
expires,
|
||||
id,
|
||||
clientId: options.clientId,
|
||||
userId: options.userId,
|
||||
scopes: options.scopes,
|
||||
studentIds: options.studentIds,
|
||||
publicKey: options.publicKey,
|
||||
|
|
29
backend/src/database/entities/user.ts
Normal file
29
backend/src/database/entities/user.ts
Normal file
|
@ -0,0 +1,29 @@
|
|||
import type { ObjectID } from 'typeorm';
|
||||
import {
|
||||
BaseEntity, Column, Entity, ObjectIdColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
export default class User extends BaseEntity {
|
||||
@ObjectIdColumn()
|
||||
public _id!: ObjectID;
|
||||
|
||||
@Column()
|
||||
public username!: string;
|
||||
|
||||
@Column()
|
||||
public host!: string;
|
||||
|
||||
@Column()
|
||||
public symbol!: string;
|
||||
|
||||
@Column()
|
||||
public email!: string;
|
||||
|
||||
@Column()
|
||||
public loginIds!: string[];
|
||||
|
||||
public static getLoginId(senderId: number, unitId: number): string {
|
||||
return `${senderId}@${unitId}`;
|
||||
}
|
||||
}
|
|
@ -66,7 +66,9 @@ export default function registerToken(server: MyFastifyInstance): void {
|
|||
}
|
||||
|
||||
const application = await database.applicationRepo.findOne({
|
||||
clientId: request.body.client_id,
|
||||
where: {
|
||||
clientId: request.body.client_id,
|
||||
},
|
||||
});
|
||||
if (!application) {
|
||||
await sendCustomError(reply, 'invalid_client', 'Application not found', 401);
|
||||
|
@ -98,7 +100,7 @@ export default function registerToken(server: MyFastifyInstance): void {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Generate and return code;
|
||||
// TODO: Generate and return token;
|
||||
|
||||
invalidateCode(codeInfo.id);
|
||||
await reply.code(500).send('Not implemented');
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import { URL } from 'url';
|
||||
import type { SerializedClient } from '@wulkanowy/sdk/dist/diary/interfaces/serialized-client';
|
||||
import type { SerializedDiary } from '@wulkanowy/sdk/dist/diary/interfaces/serialized-diary';
|
||||
import { createCode } from '../../codes';
|
||||
import { ParamError } from '../../errors';
|
||||
import type { MyFastifyInstance, SerializedSDK } from '../../types';
|
||||
|
@ -33,9 +35,12 @@ export default function registerAllow(server: MyFastifyInstance): void {
|
|||
const prompt = sessionData.prompts.get(request.query.prompt_id);
|
||||
if (!prompt) throw server.httpErrors.badRequest('Prompt data not found');
|
||||
if (!prompt.loginInfo) throw server.httpErrors.badRequest('Login data not provided');
|
||||
if (!prompt.loginInfo.symbolInfo) throw server.httpErrors.badRequest('Symbol not provided');
|
||||
if (!prompt.loginInfo.symbolInfo.userId) throw server.httpErrors.badRequest('User not registered');
|
||||
|
||||
const tokenKey = decryptSymmetrical(encryptedTokenKey, prompt.promptSecret);
|
||||
const serializedSDK = JSON.parse(decryptSymmetrical(prompt.loginInfo.encryptedSDK, tokenKey)) as SerializedSDK;
|
||||
const serializedClient = JSON.parse(decryptSymmetrical(prompt.loginInfo.encryptedClient, tokenKey)) as SerializedClient;
|
||||
const serializedDiaries = JSON.parse(decryptSymmetrical(prompt.loginInfo.symbolInfo.encryptedDiaries, tokenKey)) as SerializedDiary[];
|
||||
|
||||
let studentIds: number[] = [];
|
||||
if (prompt.studentsMode !== StudentsMode.None) {
|
||||
|
@ -50,10 +55,11 @@ export default function registerAllow(server: MyFastifyInstance): void {
|
|||
throw server.httpErrors.internalServerError();
|
||||
}
|
||||
}
|
||||
// TODO: Verify studentIds with availableStudentIds
|
||||
|
||||
const newSerializedSDK: SerializedSDK = {
|
||||
client: serializedSDK.client,
|
||||
diaries: serializedSDK.diaries.filter((diary) => studentIds.includes(diary.info.studentId)),
|
||||
client: serializedClient,
|
||||
diaries: serializedDiaries.filter((diary) => studentIds.includes(diary.info.studentId)),
|
||||
};
|
||||
const newEncryptedSDK = encryptSymmetrical(JSON.stringify(newSerializedSDK), tokenKey);
|
||||
|
||||
|
@ -61,6 +67,7 @@ export default function registerAllow(server: MyFastifyInstance): void {
|
|||
studentIds,
|
||||
scopes: prompt.scopes,
|
||||
clientId: prompt.clientId,
|
||||
userId: prompt.loginInfo.symbolInfo.userId,
|
||||
publicKey: prompt.loginInfo.publicKey,
|
||||
encryptedSDK: newEncryptedSDK,
|
||||
encryptedPassword: prompt.loginInfo.encryptedPassword,
|
||||
|
|
|
@ -23,3 +23,11 @@ export class InvalidVulcanCredentialsError extends ApolloError {
|
|||
super('Invalid vulcan credentials', 'INVALID_VULCAN_CREDENTIALS');
|
||||
}
|
||||
}
|
||||
|
||||
export class InvalidSymbolError extends ApolloError {
|
||||
public name = 'InvalidSymbolError';
|
||||
|
||||
public constructor() {
|
||||
super('Invalid symbol', 'INVALID_SYMBOL');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,8 +4,10 @@ import type { ApolloContext, MyFastifyInstance } from '../../types';
|
|||
import { getSessionData } from '../../utils';
|
||||
import registerAllow from './allow';
|
||||
import registerDeny from './deny';
|
||||
import CreateUserResolver from './resolvers/create-user-resolver';
|
||||
import LoginResolver from './resolvers/login-resolver';
|
||||
import PromptInfoResolver from './resolvers/prompt-info-resolver';
|
||||
import SetSymbolResolver from './resolvers/set-symbol-resolver';
|
||||
import type { WebsiteAPIContext } from './types';
|
||||
|
||||
export default async function registerWebsiteApi(server: MyFastifyInstance): Promise<void> {
|
||||
|
@ -14,6 +16,8 @@ export default async function registerWebsiteApi(server: MyFastifyInstance): Pro
|
|||
resolvers: [
|
||||
PromptInfoResolver,
|
||||
LoginResolver,
|
||||
SetSymbolResolver,
|
||||
CreateUserResolver,
|
||||
],
|
||||
});
|
||||
const apolloServer = new ApolloServer({
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import { Field, ObjectType } from 'type-graphql';
|
||||
|
||||
@ObjectType()
|
||||
export default class CreateUserResult {
|
||||
@Field(() => Boolean)
|
||||
public success!: true; // GraphQL doesn't allow empty result
|
||||
}
|
|
@ -1,8 +1,7 @@
|
|||
import { Field, ObjectType } from 'type-graphql';
|
||||
import LoginResultStudent from './login-result-student';
|
||||
|
||||
@ObjectType()
|
||||
export default class LoginResult {
|
||||
@Field(() => [LoginResultStudent])
|
||||
public students!: LoginResultStudent[];
|
||||
@Field(() => [String])
|
||||
public symbols!: string[];
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { Field, Int, ObjectType } from 'type-graphql';
|
||||
|
||||
@ObjectType()
|
||||
export default class LoginResultStudent {
|
||||
export default class LoginStudent {
|
||||
@Field(() => Int)
|
||||
public studentId!: number;
|
||||
|
11
backend/src/routes/website-api/models/set-symbol-result.ts
Normal file
11
backend/src/routes/website-api/models/set-symbol-result.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { Field, ObjectType } from 'type-graphql';
|
||||
import LoginStudent from './login-student';
|
||||
|
||||
@ObjectType()
|
||||
export default class SetSymbolResult {
|
||||
@Field(() => [LoginStudent])
|
||||
public students!: LoginStudent[];
|
||||
|
||||
@Field(() => Boolean)
|
||||
public registered!: boolean;
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
import { UserInputError } from 'apollo-server-fastify';
|
||||
import {
|
||||
Arg, Ctx, Mutation, Resolver,
|
||||
} from 'type-graphql';
|
||||
import database from '../../../database/database';
|
||||
import User from '../../../database/entities/user';
|
||||
import { UnknownPromptError } from '../errors';
|
||||
import CreateUserResult from '../models/create-user-result';
|
||||
import type { WebsiteAPIContext } from '../types';
|
||||
|
||||
@Resolver()
|
||||
export default class CreateUserResolver {
|
||||
@Mutation(() => CreateUserResult)
|
||||
public async createUser(
|
||||
@Arg('promptId') promptId: string,
|
||||
@Arg('email') email: string,
|
||||
@Ctx() { sessionData }: WebsiteAPIContext,
|
||||
): Promise<CreateUserResult> {
|
||||
if (email !== email.trim()) throw new UserInputError('Email should be trimmed');
|
||||
const prompt = sessionData.prompts.get(promptId);
|
||||
if (!prompt) throw new UnknownPromptError();
|
||||
if (!prompt.loginInfo) throw new UserInputError('Login data not provided');
|
||||
if (!prompt.loginInfo.symbolInfo) throw new UserInputError('Symbol not provided');
|
||||
|
||||
const existingUser = await database.userRepo.findOne({
|
||||
where: {
|
||||
host: prompt.loginInfo.host,
|
||||
symbol: prompt.loginInfo.symbolInfo.symbol,
|
||||
loginIds: {
|
||||
$in: prompt.loginInfo.symbolInfo.loginIds,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (existingUser !== undefined) throw new UserInputError('User already exists');
|
||||
const user = new User();
|
||||
user.host = prompt.loginInfo.host;
|
||||
user.symbol = prompt.loginInfo.symbolInfo.symbol;
|
||||
user.username = prompt.loginInfo.username;
|
||||
user.loginIds = prompt.loginInfo.symbolInfo.loginIds;
|
||||
user.email = email;
|
||||
await database.userRepo.save(user);
|
||||
prompt.loginInfo.symbolInfo.userId = user._id;
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,18 +1,15 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
import { Client } from '@wulkanowy/sdk';
|
||||
import type { DiaryInfo } from '@wulkanowy/sdk/dist/diary/interfaces/diary/diary-info';
|
||||
import _ from 'lodash';
|
||||
import { UserInputError } from 'apollo-server-fastify';
|
||||
import {
|
||||
Arg, Ctx, Mutation, Resolver,
|
||||
} from 'type-graphql';
|
||||
import type { SerializedSDK } from '../../../types';
|
||||
import {
|
||||
createKey,
|
||||
encryptSymmetrical, encryptWithPublicKey, generatePrivatePublicPair, isObject, verifyCaptchaResponse,
|
||||
} from '../../../utils';
|
||||
import { CaptchaError, InvalidVulcanCredentialsError, UnknownPromptError } from '../errors';
|
||||
import LoginResult from '../models/login-result';
|
||||
import type LoginResultStudent from '../models/login-result-student';
|
||||
import type { WebsiteAPIContext } from '../types';
|
||||
|
||||
@Resolver()
|
||||
|
@ -26,6 +23,8 @@ export default class LoginResolver {
|
|||
@Arg('captchaResponse') captchaResponse: string,
|
||||
@Ctx() { sessionData, reply }: WebsiteAPIContext,
|
||||
): Promise<LoginResult> {
|
||||
if (username !== username.trim()) throw new UserInputError('Username should be trimmed');
|
||||
if (host !== host.trim()) throw new UserInputError('Host should be trimmed');
|
||||
const prompt = sessionData.prompts.get(promptId);
|
||||
if (!prompt) throw new UnknownPromptError();
|
||||
if (!await verifyCaptchaResponse(captchaResponse)) throw new CaptchaError();
|
||||
|
@ -33,28 +32,17 @@ export default class LoginResolver {
|
|||
username,
|
||||
password,
|
||||
}));
|
||||
let symbols: string[];
|
||||
try {
|
||||
await client.login();
|
||||
symbols = await client.login();
|
||||
} catch (error) {
|
||||
if (isObject(error) && error.name === 'InvalidCredentialsError') throw new InvalidVulcanCredentialsError();
|
||||
throw error;
|
||||
}
|
||||
const diaryList = await client.getDiaryList();
|
||||
const diaryStudents = _.groupBy(diaryList.map((e) => e.serialized.info), 'studentId');
|
||||
const students = _.toPairs(diaryStudents)
|
||||
.map(([, diaryInfoList]: [string, DiaryInfo[]]) => diaryInfoList[0])
|
||||
.map<LoginResultStudent>((diaryInfo) => ({
|
||||
name: `${diaryInfo.studentFirstName} ${diaryInfo.studentSurname}`,
|
||||
studentId: diaryInfo.studentId,
|
||||
}));
|
||||
const serializedSDK: SerializedSDK = {
|
||||
client: client.serialize(),
|
||||
diaries: diaryList.map(({ serialized }) => serialized),
|
||||
};
|
||||
|
||||
const { privateKey, publicKey } = await generatePrivatePublicPair();
|
||||
const tokenKey = createKey();
|
||||
const encryptedSDK = encryptSymmetrical(JSON.stringify(serializedSDK), tokenKey);
|
||||
const encryptedClient = encryptSymmetrical(JSON.stringify(client.serialize()), tokenKey);
|
||||
const encryptedPassword = encryptWithPublicKey(password, publicKey);
|
||||
const encryptedPrivateKey = encryptSymmetrical(privateKey, tokenKey);
|
||||
const encryptedTokenKey = encryptSymmetrical(tokenKey, prompt.promptSecret);
|
||||
|
@ -62,11 +50,10 @@ export default class LoginResolver {
|
|||
prompt.loginInfo = {
|
||||
encryptedPassword,
|
||||
encryptedPrivateKey,
|
||||
encryptedSDK,
|
||||
encryptedClient,
|
||||
publicKey,
|
||||
host,
|
||||
username,
|
||||
availableStudentIds: students.map(({ studentId }) => studentId),
|
||||
};
|
||||
// TODO: Find why the promise never resolves
|
||||
reply.setCookie(`etk-${promptId}`, encryptedTokenKey, {
|
||||
|
@ -79,7 +66,7 @@ export default class LoginResolver {
|
|||
// TODO: Remove
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
return {
|
||||
students,
|
||||
symbols,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,96 @@
|
|||
/* eslint-disable class-methods-use-this */
|
||||
import { Client } from '@wulkanowy/sdk';
|
||||
import type { DiaryInfo } from '@wulkanowy/sdk/dist/diary/interfaces/diary/diary-info';
|
||||
import type { SerializedClient } from '@wulkanowy/sdk/dist/diary/interfaces/serialized-client';
|
||||
import { UserInputError } from 'apollo-server-fastify';
|
||||
import _ from 'lodash';
|
||||
import {
|
||||
Arg, Ctx, Mutation, Resolver,
|
||||
} from 'type-graphql';
|
||||
import database from '../../../database/database';
|
||||
import User from '../../../database/entities/user';
|
||||
import { decryptSymmetrical, decryptWithPrivateKey, encryptSymmetrical } from '../../../utils';
|
||||
import { InvalidSymbolError, UnknownPromptError } from '../errors';
|
||||
import type LoginStudent from '../models/login-student';
|
||||
import SetSymbolResult from '../models/set-symbol-result';
|
||||
import type { WebsiteAPIContext } from '../types';
|
||||
|
||||
@Resolver()
|
||||
export default class SetSymbolResolver {
|
||||
@Mutation(() => SetSymbolResult)
|
||||
public async setSymbol(
|
||||
@Arg('promptId') promptId: string,
|
||||
@Arg('symbol') symbol: string,
|
||||
@Ctx() { sessionData, request }: WebsiteAPIContext,
|
||||
): Promise<SetSymbolResult> {
|
||||
if (symbol !== symbol.trim()) throw new UserInputError('Symbol should be trimmed');
|
||||
if (symbol !== symbol.toLowerCase()) throw new UserInputError('Symbol should be lowercase');
|
||||
const prompt = sessionData.prompts.get(promptId);
|
||||
if (!prompt) throw new UnknownPromptError();
|
||||
if (!prompt.loginInfo) throw new UserInputError('Login data not provided');
|
||||
const { loginInfo } = prompt;
|
||||
|
||||
const encryptedTokenKey: string | undefined = request.cookies[`etk-${promptId}`];
|
||||
if (!encryptedTokenKey) throw new UserInputError('Missing etk cookie'); // TODO: Add standard error
|
||||
|
||||
const tokenKey = decryptSymmetrical(encryptedTokenKey, prompt.promptSecret);
|
||||
const privateKey = decryptSymmetrical(loginInfo.encryptedPrivateKey, tokenKey);
|
||||
const password = decryptWithPrivateKey(loginInfo.encryptedPassword, privateKey);
|
||||
const serializedClient = JSON.parse(decryptSymmetrical(loginInfo.encryptedClient, tokenKey)) as SerializedClient;
|
||||
|
||||
const client = Client.deserialize(serializedClient, () => ({
|
||||
username: loginInfo.username,
|
||||
password,
|
||||
}));
|
||||
|
||||
try {
|
||||
await client.setSymbol(symbol);
|
||||
} catch (error) {
|
||||
throw new InvalidSymbolError();
|
||||
}
|
||||
|
||||
const diaryList = await client.getDiaryList();
|
||||
const diaryStudents = _.groupBy(diaryList.map((e) => e.serialized.info), 'studentId');
|
||||
const students = _.values(diaryStudents)
|
||||
.map((diaryInfoList: DiaryInfo[]) => diaryInfoList[0])
|
||||
.map<LoginStudent>((diaryInfo) => ({
|
||||
name: `${diaryInfo.studentFirstName} ${diaryInfo.studentSurname}`,
|
||||
studentId: diaryInfo.studentId,
|
||||
}));
|
||||
|
||||
const reportingUnits = await client.getReportingUnits();
|
||||
const loginIds = reportingUnits.map((unit) => User.getLoginId(unit.senderId, unit.unitId));
|
||||
|
||||
const encryptedClient = encryptSymmetrical(JSON.stringify(client.serialize()), tokenKey);
|
||||
const encryptedDiaries = encryptSymmetrical(JSON.stringify(diaryList.map(({ serialized }) => serialized)), tokenKey);
|
||||
|
||||
const user = await database.userRepo.findOne({
|
||||
where: {
|
||||
host: prompt.loginInfo.host,
|
||||
symbol,
|
||||
loginIds: {
|
||||
$in: loginIds,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (user) {
|
||||
user.loginIds = loginIds;
|
||||
user.username = prompt.loginInfo.username;
|
||||
await user.save();
|
||||
}
|
||||
|
||||
loginInfo.encryptedClient = encryptedClient;
|
||||
loginInfo.symbolInfo = {
|
||||
symbol,
|
||||
encryptedDiaries,
|
||||
loginIds,
|
||||
availableStudentIds: students.map(({ studentId }) => studentId),
|
||||
userId: user?._id,
|
||||
};
|
||||
return {
|
||||
students,
|
||||
registered: user !== undefined,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -9,6 +9,7 @@ import type {
|
|||
RawServerDefault,
|
||||
} from 'fastify';
|
||||
import { registerEnumType } from 'type-graphql';
|
||||
import type { ObjectID } from 'typeorm';
|
||||
import type SessionData from './session-data';
|
||||
|
||||
export enum StudentsMode {
|
||||
|
@ -38,10 +39,16 @@ export interface Prompt {
|
|||
host: string;
|
||||
username: string;
|
||||
encryptedPassword: string;
|
||||
encryptedSDK: string;
|
||||
encryptedPrivateKey: string;
|
||||
publicKey: string;
|
||||
availableStudentIds: number[];
|
||||
encryptedClient: string;
|
||||
symbolInfo?: {
|
||||
symbol: string;
|
||||
encryptedDiaries: string;
|
||||
availableStudentIds: number[];
|
||||
loginIds: string[];
|
||||
userId?: ObjectID,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -70,6 +77,7 @@ export interface CodeInfo {
|
|||
expires: Date;
|
||||
scopes: string[];
|
||||
clientId: string;
|
||||
userId: ObjectID,
|
||||
studentIds: number[];
|
||||
tokenSecret: string;
|
||||
publicKey: string;
|
||||
|
|
|
@ -5,6 +5,7 @@ import _ from 'lodash';
|
|||
import { ParamError } from '../errors';
|
||||
import SessionData from '../session-data';
|
||||
import type { SerializedSDK, Session } from '../types';
|
||||
import { sha256 } from './crypto';
|
||||
|
||||
export * from './crypto';
|
||||
|
||||
|
|
0
backend/src/utils/token.ts
Normal file
0
backend/src/utils/token.ts
Normal file
|
@ -14,7 +14,11 @@ module.exports = {
|
|||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'import/prefer-default-export': ['warn']
|
||||
'import/prefer-default-export': ['warn'],
|
||||
"prefer-destructuring": ["error", {
|
||||
"array": false,
|
||||
"object": true
|
||||
}]
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
|
|
|
@ -2,6 +2,8 @@ schema: http://localhost:3000/api/website/graphql
|
|||
documents:
|
||||
- src/graphql/queries/get-prompt-info.ts
|
||||
- src/graphql/mutations/login.ts
|
||||
- src/graphql/mutations/set-symbol.ts
|
||||
- src/graphql/mutations/create-user.ts
|
||||
generates:
|
||||
./src/graphql/generated.ts:
|
||||
plugins:
|
||||
|
|
11
website/package-lock.json
generated
11
website/package-lock.json
generated
|
@ -10362,6 +10362,14 @@
|
|||
"integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
|
||||
"dev": true
|
||||
},
|
||||
"isemail": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/isemail/-/isemail-3.2.0.tgz",
|
||||
"integrity": "sha512-zKqkK+O+dGqevc93KNsbZ/TqTUFd46MwWjYOoMrjIMZ51eU7DtQG3Wmd9SQQT7i7RVnuTPEiYEWHU3MSbxC1Tg==",
|
||||
"requires": {
|
||||
"punycode": "2.x.x"
|
||||
}
|
||||
},
|
||||
"isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
|
@ -13255,8 +13263,7 @@
|
|||
"punycode": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
|
||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
|
||||
"dev": true
|
||||
"integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
|
||||
},
|
||||
"q": {
|
||||
"version": "1.5.1",
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
"graphql": "^15.4.0",
|
||||
"graphql-request": "^3.4.0",
|
||||
"graphql-tag": "^2.11.0",
|
||||
"isemail": "^3.2.0",
|
||||
"vue": "^2.6.11",
|
||||
"vue-class-component": "^7.2.3",
|
||||
"vue-property-decorator": "^9.1.2",
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-form v-model="valid" ref="form" @submit.prevent="submit">
|
||||
<v-card-title>Dodaj adres email</v-card-title>
|
||||
<v-card-text>
|
||||
<p>
|
||||
Dodaj kontaktowy adres email, aby nie utracić dostępu do konta.<br>
|
||||
<b>Na ten adres będą wysyłane alerty bezpieczeństwa.</b>
|
||||
</p>
|
||||
<p>
|
||||
Twój email <b>nie</b> będzie używany w celach marketingowych
|
||||
i <b>nie</b> zostanie udostępniony aplikacji <b>{{ promptInfo.application.name }}</b>,
|
||||
ani żadnym podmiotom trzecim.
|
||||
</p>
|
||||
<p>
|
||||
Podany adres może być inny od adresu używanego w systemie VULCAN.
|
||||
</p>
|
||||
</v-card-text>
|
||||
<v-text-field
|
||||
label="Adres email"
|
||||
outlined
|
||||
type="email"
|
||||
class="mx-4"
|
||||
v-model="email"
|
||||
:rules="emailRules"
|
||||
/>
|
||||
<v-alert type="error" class="mx-2" :value="error">
|
||||
Podczas dodawania adresu email wystąpił błąd
|
||||
</v-alert>
|
||||
<v-card-actions>
|
||||
<v-btn text outlined color="primary" @click="back" :disabled="loading">Wróć</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
color="primary"
|
||||
type="submit"
|
||||
:disabled="!valid"
|
||||
:loading="loading"
|
||||
>
|
||||
Zapisz email
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
Component, Prop, Ref, Vue,
|
||||
} from 'vue-property-decorator';
|
||||
import { InputValidationRules } from 'vuetify';
|
||||
import IsEmail from 'isemail';
|
||||
import { PromptInfo, VForm } from '@/types';
|
||||
import { sdk } from '@/pages/authenticate-prompt/sdk';
|
||||
|
||||
@Component({
|
||||
name: 'EmailWindow',
|
||||
})
|
||||
export default class EmailWindow extends Vue {
|
||||
@Ref('form') form!: VForm;
|
||||
|
||||
@Prop({
|
||||
type: Object,
|
||||
required: true,
|
||||
})
|
||||
promptInfo!: PromptInfo
|
||||
|
||||
valid = false;
|
||||
|
||||
loading = false;
|
||||
|
||||
email = '';
|
||||
|
||||
error = false;
|
||||
|
||||
readonly emailRules: InputValidationRules = [
|
||||
(v) => v !== '' || 'To pole jest wymagane',
|
||||
(v) => (IsEmail.validate(v) && v.trim() === v) || 'Email jest niepoprawny',
|
||||
];
|
||||
|
||||
reset(defaultValue?: string) {
|
||||
this.email = defaultValue ?? '';
|
||||
this.form.resetValidation();
|
||||
}
|
||||
|
||||
back() {
|
||||
this.$emit('back');
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (!this.valid || this.loading) return;
|
||||
this.loading = true;
|
||||
this.error = false;
|
||||
try {
|
||||
await sdk.CreateUser({
|
||||
promptId: this.promptInfo.id,
|
||||
email: this.email.trim(),
|
||||
});
|
||||
this.$emit('create');
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.error = true;
|
||||
}
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -53,17 +53,12 @@
|
|||
import {
|
||||
Component, Prop, Ref, Vue,
|
||||
} from 'vue-property-decorator';
|
||||
import { PromptInfo } from '@/types';
|
||||
import { PromptInfo, VForm } from '@/types';
|
||||
import { hasErrorCode, sdk } from '@/pages/authenticate-prompt/sdk';
|
||||
import { InputValidationRules } from 'vuetify';
|
||||
import VueRecaptcha from 'vue-recaptcha';
|
||||
import { requireEnv } from '@/utils';
|
||||
|
||||
interface VForm extends HTMLFormElement {
|
||||
validate(): boolean;
|
||||
resetValidation(): void;
|
||||
}
|
||||
|
||||
@Component({
|
||||
name: 'LoginWindow',
|
||||
components: {
|
||||
|
@ -133,18 +128,21 @@ export default class LoginWindow extends Vue {
|
|||
if (this.loading || !this.formValid || !this.captchaResponse) return;
|
||||
this.error = null;
|
||||
this.loading = true;
|
||||
const username = this.username.trim();
|
||||
const host = this.host.trim();
|
||||
try {
|
||||
const { login } = await sdk.Login({
|
||||
promptId: this.promptInfo.id,
|
||||
host: this.host,
|
||||
username: this.username,
|
||||
host,
|
||||
username,
|
||||
password: this.password,
|
||||
captchaResponse: this.captchaResponse,
|
||||
});
|
||||
const { students } = login;
|
||||
this.$emit('login', { students });
|
||||
const { symbols } = login;
|
||||
this.$emit('login', { symbols, username });
|
||||
this.reset();
|
||||
} catch (error) {
|
||||
this.recaptcha.reset();
|
||||
console.error(error);
|
||||
if (hasErrorCode(error, 'INVALID_VULCAN_CREDENTIALS')) this.error = 'invalid-credentials';
|
||||
if (hasErrorCode(error, 'CAPTCHA_ERROR')) this.error = 'captcha';
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { PromptInfo } from '@/types';
|
||||
import AppInfoDialog from '@/compontents/authenticate-prompt-windows/app-info-dialog.vue';
|
||||
import AppInfoDialog from '@/compontents/app-info-dialog.vue';
|
||||
|
||||
@Component({
|
||||
name: 'OverviewWindow',
|
||||
|
|
|
@ -18,37 +18,33 @@
|
|||
>
|
||||
Aplikacja nie wymaga dostępu do dzienników uczniów
|
||||
</v-alert>
|
||||
<template v-else>
|
||||
<v-divider />
|
||||
<v-list>
|
||||
<v-list-item-group
|
||||
v-model="studentsValue"
|
||||
:multiple="mode === StudentsMode.Many"
|
||||
color="primary"
|
||||
<v-list v-else subheader>
|
||||
<v-list-item-group
|
||||
v-model="studentsValue"
|
||||
:multiple="mode === StudentsMode.Many"
|
||||
color="primary"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="student in students"
|
||||
:key="student.studentId"
|
||||
:value="student.studentId"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="student in students"
|
||||
:key="student.studentId"
|
||||
:value="student.studentId"
|
||||
>
|
||||
<template v-slot:default="{ active }">
|
||||
<v-list-item-action>
|
||||
<v-icon v-if="active">
|
||||
{{ mode === StudentsMode.Many ? '$checkboxOn' : '$radioOn' }}
|
||||
</v-icon>
|
||||
<v-icon v-else>
|
||||
{{ mode === StudentsMode.Many ? '$checkboxOff' : '$radioOff' }}
|
||||
</v-icon>
|
||||
</v-list-item-action>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ student.name }}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list-item-group>
|
||||
</v-list>
|
||||
<v-divider />
|
||||
</template>
|
||||
<template v-slot:default="{ active }">
|
||||
<v-list-item-action>
|
||||
<v-icon v-if="active">
|
||||
{{ mode === StudentsMode.Many ? '$checkboxOn' : '$radioOn' }}
|
||||
</v-icon>
|
||||
<v-icon v-else>
|
||||
{{ mode === StudentsMode.Many ? '$checkboxOff' : '$radioOff' }}
|
||||
</v-icon>
|
||||
</v-list-item-action>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ student.name }}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list-item-group>
|
||||
</v-list>
|
||||
<v-card-actions>
|
||||
<v-btn color="primary" text outlined @click="back">
|
||||
Wróć
|
||||
|
|
|
@ -0,0 +1,165 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-form ref="form" @submit.prevent="submit">
|
||||
<v-card-title>Wybierz symbol</v-card-title>
|
||||
<v-list subheader>
|
||||
<v-list-item-group
|
||||
v-model="selectedSymbol"
|
||||
color="primary"
|
||||
:mandatory="selectedSymbol !== null"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="symbol in symbols"
|
||||
:key="symbol"
|
||||
:value="symbol"
|
||||
>
|
||||
<template v-slot:default="{ active }">
|
||||
<v-list-item-action>
|
||||
<v-icon v-if="active">$radioOn</v-icon>
|
||||
<v-icon v-else>$radioOff</v-icon>
|
||||
</v-list-item-action>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
{{ symbol }}
|
||||
<span class="text--secondary" v-if="symbols.length === 1">(domyślny)</span>
|
||||
</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-item :value="customSymbolMode">
|
||||
<template v-slot:default="{ active }">
|
||||
<v-list-item-action>
|
||||
<v-icon v-if="active">$radioOn</v-icon>
|
||||
<v-icon v-else>$radioOff</v-icon>
|
||||
</v-list-item-action>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>Własny symbol</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-pencil</v-icon>
|
||||
</v-list-item-icon>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list-item-group>
|
||||
</v-list>
|
||||
<v-expand-transition>
|
||||
<div v-if="selectedSymbol === customSymbolMode">
|
||||
<v-text-field
|
||||
outlined
|
||||
label="Własny symbol"
|
||||
v-model="customSymbol"
|
||||
class="mx-4 mt-2"
|
||||
:readonly="loading"
|
||||
/>
|
||||
</div>
|
||||
</v-expand-transition>
|
||||
<v-alert type="error" class="mx-2" :value="errorMessage !== null">
|
||||
{{ errorMessage }}
|
||||
</v-alert>
|
||||
<v-card-actions>
|
||||
<v-btn color="primary" text outlined @click="back" :disabled="loading">
|
||||
Wróć
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn color="primary" type="submit" :loading="loading" :disabled="!valid">
|
||||
Wybierz
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import {
|
||||
Component, Prop, Ref, Vue,
|
||||
} from 'vue-property-decorator';
|
||||
import { PromptInfo, VForm } from '@/types';
|
||||
import { hasErrorCode, sdk } from '@/pages/authenticate-prompt/sdk';
|
||||
|
||||
// This is a JS Symbol, not a diary symbol
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol)
|
||||
const customSymbolMode = Symbol('Custom symbol mode');
|
||||
|
||||
@Component({
|
||||
name: 'SymbolsWindow',
|
||||
})
|
||||
export default class SymbolsWindow extends Vue {
|
||||
@Prop({
|
||||
type: Array,
|
||||
required: true,
|
||||
})
|
||||
symbols!: string[];
|
||||
|
||||
@Prop({
|
||||
required: true,
|
||||
type: Object,
|
||||
})
|
||||
promptInfo!: PromptInfo;
|
||||
|
||||
@Ref('form') form!: VForm;
|
||||
|
||||
selectedSymbol: string | typeof customSymbolMode | null = null;
|
||||
|
||||
readonly customSymbolMode: typeof customSymbolMode = customSymbolMode;
|
||||
|
||||
customSymbol = '';
|
||||
|
||||
loading = false;
|
||||
|
||||
error: 'invalid-symbol' | 'other' | null = null;
|
||||
|
||||
get valid() {
|
||||
if (!this.selectedSymbol) return false;
|
||||
if (this.selectedSymbol === this.customSymbolMode) {
|
||||
if (this.customSymbol.trim() === '') return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.customSymbol = '';
|
||||
if (this.symbols?.length === 1) this.selectedSymbol = this.symbols[0];
|
||||
else if (this.symbols?.length === 0) this.selectedSymbol = this.customSymbolMode;
|
||||
else this.selectedSymbol = null;
|
||||
if (this.form) this.form.resetValidation();
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (!this.valid || this.loading || !this.selectedSymbol) return;
|
||||
this.loading = true;
|
||||
this.error = null;
|
||||
try {
|
||||
const symbol = (this.selectedSymbol === this.customSymbolMode
|
||||
? this.customSymbol
|
||||
: this.selectedSymbol
|
||||
).trim();
|
||||
const { setSymbol } = await sdk.SetSymbol({
|
||||
promptId: this.promptInfo.id,
|
||||
symbol,
|
||||
});
|
||||
this.$emit('set-symbol', {
|
||||
students: setSymbol.students,
|
||||
registered: setSymbol.registered,
|
||||
});
|
||||
} catch (error) {
|
||||
if (hasErrorCode(error, 'INVALID_SYMBOL')) this.error = 'invalid-symbol';
|
||||
else this.error = 'other';
|
||||
}
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
back() {
|
||||
this.$emit('back');
|
||||
}
|
||||
|
||||
created() {
|
||||
this.reset();
|
||||
}
|
||||
|
||||
get errorMessage() {
|
||||
if (this.error === null) return null;
|
||||
if (this.error === 'invalid-symbol') return 'Błędny symbol';
|
||||
return 'Podczas wybierania symbolu wystąpił błąd';
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -58,7 +58,14 @@ export type GitHubUser = {
|
|||
|
||||
export type Mutation = {
|
||||
__typename?: 'Mutation';
|
||||
createUser: CreateUserResult;
|
||||
login: LoginResult;
|
||||
setSymbol: SetSymbolResult;
|
||||
};
|
||||
|
||||
export type MutationCreateUserArgs = {
|
||||
email: Scalars['String'];
|
||||
promptId: Scalars['String'];
|
||||
};
|
||||
|
||||
export type MutationLoginArgs = {
|
||||
|
@ -69,17 +76,46 @@ export type MutationLoginArgs = {
|
|||
promptId: Scalars['String'];
|
||||
};
|
||||
|
||||
export type LoginResult = {
|
||||
__typename?: 'LoginResult';
|
||||
students: Array<LoginResultStudent>;
|
||||
export type MutationSetSymbolArgs = {
|
||||
symbol: Scalars['String'];
|
||||
promptId: Scalars['String'];
|
||||
};
|
||||
|
||||
export type LoginResultStudent = {
|
||||
__typename?: 'LoginResultStudent';
|
||||
export type CreateUserResult = {
|
||||
__typename?: 'CreateUserResult';
|
||||
success: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type LoginResult = {
|
||||
__typename?: 'LoginResult';
|
||||
symbols: Array<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type SetSymbolResult = {
|
||||
__typename?: 'SetSymbolResult';
|
||||
students: Array<LoginStudent>;
|
||||
registered: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type LoginStudent = {
|
||||
__typename?: 'LoginStudent';
|
||||
studentId: Scalars['Int'];
|
||||
name: Scalars['String'];
|
||||
};
|
||||
|
||||
export type CreateUserMutationVariables = Exact<{
|
||||
promptId: Scalars['String'];
|
||||
email: Scalars['String'];
|
||||
}>;
|
||||
|
||||
export type CreateUserMutation = (
|
||||
{ __typename?: 'Mutation' }
|
||||
& { createUser: (
|
||||
{ __typename?: 'CreateUserResult' }
|
||||
& Pick<CreateUserResult, 'success'>
|
||||
); }
|
||||
);
|
||||
|
||||
export type LoginMutationVariables = Exact<{
|
||||
promptId: Scalars['String'];
|
||||
host: Scalars['String'];
|
||||
|
@ -92,9 +128,23 @@ export type LoginMutation = (
|
|||
{ __typename?: 'Mutation' }
|
||||
& { login: (
|
||||
{ __typename?: 'LoginResult' }
|
||||
& Pick<LoginResult, 'symbols'>
|
||||
); }
|
||||
);
|
||||
|
||||
export type SetSymbolMutationVariables = Exact<{
|
||||
promptId: Scalars['String'];
|
||||
symbol: Scalars['String'];
|
||||
}>;
|
||||
|
||||
export type SetSymbolMutation = (
|
||||
{ __typename?: 'Mutation' }
|
||||
& { setSymbol: (
|
||||
{ __typename?: 'SetSymbolResult' }
|
||||
& Pick<SetSymbolResult, 'registered'>
|
||||
& { students: Array<(
|
||||
{ __typename?: 'LoginResultStudent' }
|
||||
& Pick<LoginResultStudent, 'studentId' | 'name'>
|
||||
{ __typename?: 'LoginStudent' }
|
||||
& Pick<LoginStudent, 'studentId' | 'name'>
|
||||
)>; }
|
||||
); }
|
||||
);
|
||||
|
@ -119,6 +169,13 @@ export type GetPromptInfoQuery = (
|
|||
); }
|
||||
);
|
||||
|
||||
export const CreateUserDocument = gql`
|
||||
mutation CreateUser($promptId: String!, $email: String!) {
|
||||
createUser(promptId: $promptId, email: $email) {
|
||||
success
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const LoginDocument = gql`
|
||||
mutation Login($promptId: String!, $host: String!, $username: String!, $password: String!, $captchaResponse: String!) {
|
||||
login(
|
||||
|
@ -128,10 +185,18 @@ export const LoginDocument = gql`
|
|||
promptId: $promptId
|
||||
captchaResponse: $captchaResponse
|
||||
) {
|
||||
symbols
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const SetSymbolDocument = gql`
|
||||
mutation SetSymbol($promptId: String!, $symbol: String!) {
|
||||
setSymbol(promptId: $promptId, symbol: $symbol) {
|
||||
students {
|
||||
studentId
|
||||
name
|
||||
}
|
||||
registered
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
@ -162,9 +227,15 @@ export type SdkFunctionWrapper = <T>(action: () => Promise<T>) => Promise<T>;
|
|||
const defaultWrapper: SdkFunctionWrapper = (sdkFunction) => sdkFunction();
|
||||
export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = defaultWrapper) {
|
||||
return {
|
||||
CreateUser(variables: CreateUserMutationVariables, requestHeaders?: Headers): Promise<CreateUserMutation> {
|
||||
return withWrapper(() => client.request<CreateUserMutation>(print(CreateUserDocument), variables, requestHeaders));
|
||||
},
|
||||
Login(variables: LoginMutationVariables, requestHeaders?: Headers): Promise<LoginMutation> {
|
||||
return withWrapper(() => client.request<LoginMutation>(print(LoginDocument), variables, requestHeaders));
|
||||
},
|
||||
SetSymbol(variables: SetSymbolMutationVariables, requestHeaders?: Headers): Promise<SetSymbolMutation> {
|
||||
return withWrapper(() => client.request<SetSymbolMutation>(print(SetSymbolDocument), variables, requestHeaders));
|
||||
},
|
||||
GetPromptInfo(variables: GetPromptInfoQueryVariables, requestHeaders?: Headers): Promise<GetPromptInfoQuery> {
|
||||
return withWrapper(() => client.request<GetPromptInfoQuery>(print(GetPromptInfoDocument), variables, requestHeaders));
|
||||
},
|
||||
|
|
8
website/src/graphql/mutations/create-user.ts
Normal file
8
website/src/graphql/mutations/create-user.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
import gql from 'graphql-tag';
|
||||
|
||||
export default gql`mutation CreateUser($promptId: String!, $email: String!) {
|
||||
createUser(promptId: $promptId, email: $email) {
|
||||
success
|
||||
}
|
||||
}
|
||||
`;
|
|
@ -2,10 +2,7 @@ import gql from 'graphql-tag';
|
|||
|
||||
export default gql`mutation Login($promptId: String!, $host: String!, $username: String!, $password: String!, $captchaResponse: String!) {
|
||||
login(host: $host, password: $password, username: $username, promptId: $promptId, captchaResponse: $captchaResponse) {
|
||||
students {
|
||||
studentId
|
||||
name
|
||||
}
|
||||
symbols
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
12
website/src/graphql/mutations/set-symbol.ts
Normal file
12
website/src/graphql/mutations/set-symbol.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import gql from 'graphql-tag';
|
||||
|
||||
export default gql`mutation SetSymbol($promptId: String!, $symbol: String!) {
|
||||
setSymbol(promptId: $promptId, symbol: $symbol) {
|
||||
students {
|
||||
studentId
|
||||
name
|
||||
}
|
||||
registered
|
||||
}
|
||||
}
|
||||
`;
|
|
@ -15,9 +15,6 @@
|
|||
<v-progress-circular indeterminate :size="96" color="primary" />
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="pb-1 text--secondary">
|
||||
Krok <span class="primary--text">{{ step }}/3</span>
|
||||
</div>
|
||||
<v-card outlined>
|
||||
<div class="d-flex justify-center mn-16 avatar__wrapper">
|
||||
<v-badge
|
||||
|
@ -26,11 +23,11 @@
|
|||
offset-y="16"
|
||||
bottom
|
||||
:content="promptInfo.application.verified ? 'Zweryfikowana' : 'Niezweryfikowana'"
|
||||
:value="step === 1"
|
||||
:value="step === 'overview'"
|
||||
>
|
||||
<transition name="scale">
|
||||
<v-sheet
|
||||
v-if="step === 1"
|
||||
v-if="step === 'overview'"
|
||||
width="128"
|
||||
height="128"
|
||||
class="avatar-sheet mx-4 overflow-hidden"
|
||||
|
@ -61,13 +58,13 @@
|
|||
</v-badge>
|
||||
</div>
|
||||
<v-window :value="step">
|
||||
<v-window-item :value="1">
|
||||
<v-window-item value="overview">
|
||||
<overview-window
|
||||
:promptInfo="promptInfo"
|
||||
@next="toLoginWindow"
|
||||
/>
|
||||
</v-window-item>
|
||||
<v-window-item :value="2" eager>
|
||||
<v-window-item value="login" eager>
|
||||
<login-window
|
||||
ref="loginWindow"
|
||||
:prompt-info="promptInfo"
|
||||
|
@ -75,12 +72,30 @@
|
|||
@back="loginBack"
|
||||
/>
|
||||
</v-window-item>
|
||||
<v-window-item :value="3">
|
||||
<v-window-item value="symbols" eager>
|
||||
<symbols-window
|
||||
v-if="symbols !== null"
|
||||
ref="symbolsWindow"
|
||||
:prompt-info="promptInfo"
|
||||
:symbols="symbols"
|
||||
@back="toLoginWindow"
|
||||
@set-symbol="setSymbol"
|
||||
/>
|
||||
</v-window-item>
|
||||
<v-window-item value="email" eager>
|
||||
<email-window
|
||||
ref="emailWindow"
|
||||
:prompt-info="promptInfo"
|
||||
@back="toSymbolsWindow"
|
||||
@create="createUser"
|
||||
/>
|
||||
</v-window-item>
|
||||
<v-window-item value="students">
|
||||
<students-window
|
||||
v-if="students !== null"
|
||||
:prompt-info="promptInfo"
|
||||
:students="students"
|
||||
@back="toLoginWindow"
|
||||
@back="toSymbolsWindow"
|
||||
/>
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
|
@ -122,16 +137,28 @@ import LoginWindow from '@/compontents/authenticate-prompt-windows/login-window.
|
|||
import StudentsWindow from '@/compontents/authenticate-prompt-windows/students-window.vue';
|
||||
import { sdk } from '@/pages/authenticate-prompt/sdk';
|
||||
import DialogApp from '@/compontents/dialog-app.vue';
|
||||
import EmailWindow from '@/compontents/authenticate-prompt-windows/email-window.vue';
|
||||
import SymbolsWindow from '@/compontents/authenticate-prompt-windows/symbols-window.vue';
|
||||
import IsEmail from 'isemail';
|
||||
|
||||
@Component({
|
||||
name: 'AuthenticatePromptApp',
|
||||
components: {
|
||||
LoginWindow, OverviewWindow, StudentsWindow, DialogApp,
|
||||
SymbolsWindow,
|
||||
EmailWindow,
|
||||
LoginWindow,
|
||||
OverviewWindow,
|
||||
StudentsWindow,
|
||||
DialogApp,
|
||||
},
|
||||
})
|
||||
export default class AuthenticatePromptApp extends Vue {
|
||||
@Ref() readonly loginWindow!: LoginWindow
|
||||
|
||||
@Ref() readonly emailWindow!: EmailWindow
|
||||
|
||||
@Ref() readonly symbolsWindow!: SymbolsWindow
|
||||
|
||||
promptInfo: PromptInfo | null = null;
|
||||
|
||||
promptId: string | null = null;
|
||||
|
@ -140,7 +167,11 @@ export default class AuthenticatePromptApp extends Vue {
|
|||
|
||||
students: Student[] | null = null;
|
||||
|
||||
step = 1;
|
||||
username: string | null = null;
|
||||
|
||||
symbols: string[] | null = null;
|
||||
|
||||
step = 'overview';
|
||||
|
||||
async loadPromptInfo() {
|
||||
this.promptInfoError = false;
|
||||
|
@ -167,18 +198,46 @@ export default class AuthenticatePromptApp extends Vue {
|
|||
}
|
||||
|
||||
toLoginWindow() {
|
||||
this.step = 2;
|
||||
this.step = 'login';
|
||||
this.loginWindow.reset();
|
||||
this.symbols = null;
|
||||
this.username = null;
|
||||
}
|
||||
|
||||
toSymbolsWindow() {
|
||||
this.students = null;
|
||||
this.step = 'symbols';
|
||||
if (this.symbolsWindow) this.symbolsWindow.reset();
|
||||
}
|
||||
|
||||
loginBack() {
|
||||
this.step = 1;
|
||||
this.step = 'overview';
|
||||
}
|
||||
|
||||
login({ students }: { students: Student[] }) {
|
||||
async login(
|
||||
{ username, symbols }: {
|
||||
symbols: string[];
|
||||
username: string;
|
||||
},
|
||||
) {
|
||||
this.username = username;
|
||||
this.symbols = symbols;
|
||||
this.step = 'symbols';
|
||||
if (this.symbolsWindow) this.symbolsWindow.reset();
|
||||
}
|
||||
|
||||
async setSymbol({ students, registered }: {students: Student[]; registered: boolean}) {
|
||||
this.students = students;
|
||||
this.step = 3;
|
||||
if (registered) this.step = 'students';
|
||||
else {
|
||||
if (this.username && IsEmail.validate(this.username)) this.emailWindow.reset(this.username);
|
||||
else this.emailWindow.reset();
|
||||
this.step = 'email';
|
||||
}
|
||||
}
|
||||
|
||||
async createUser() {
|
||||
this.step = 'students';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
export interface VForm extends HTMLFormElement {
|
||||
validate(): boolean;
|
||||
resetValidation(): void;
|
||||
}
|
||||
|
||||
export enum StudentsMode {
|
||||
None = 'None',
|
||||
One = 'One',
|
||||
|
|
Loading…
Reference in a new issue