Add login window (not finished)
This commit is contained in:
parent
be486b6da6
commit
c1c6812180
11 changed files with 325 additions and 129 deletions
|
@ -1,3 +1,5 @@
|
|||
import { ApolloError } from 'apollo-server-fastify';
|
||||
|
||||
export class UnknownPromptError extends Error {
|
||||
public name = 'UnknownPromptError';
|
||||
|
||||
|
@ -6,10 +8,10 @@ export class UnknownPromptError extends Error {
|
|||
}
|
||||
}
|
||||
|
||||
export class InvalidCredentialsError extends Error {
|
||||
public name = 'InvalidCredentialsError';
|
||||
export class InvalidVulcanCredentialsError extends ApolloError {
|
||||
public name = 'InvalidVulcanCredentialsError';
|
||||
|
||||
public constructor() {
|
||||
super('User with provided credentials not found');
|
||||
super('Invalid vulcan credentials', 'INVALID_VULCAN_CREDENTIALS');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,9 +6,9 @@ import {
|
|||
Arg, Ctx, Mutation, Resolver,
|
||||
} from 'type-graphql';
|
||||
import {
|
||||
encryptSymmetrical, encryptWithPublicKey, generatePrivatePublicPair, requireEnvHex,
|
||||
encryptSymmetrical, encryptWithPublicKey, generatePrivatePublicPair, isObject, requireEnvHex,
|
||||
} from '../../../utils';
|
||||
import { UnknownPromptError } from '../errors';
|
||||
import { InvalidVulcanCredentialsError, UnknownPromptError } from '../errors';
|
||||
import LoginResult from '../models/login-result';
|
||||
import type LoginResultStudent from '../models/login-result-student';
|
||||
import type { WebsiteAPIContext } from '../types';
|
||||
|
@ -29,7 +29,12 @@ export default class LoginResolver {
|
|||
username,
|
||||
password,
|
||||
}));
|
||||
await client.login();
|
||||
try {
|
||||
await client.login();
|
||||
} catch (error) {
|
||||
if (isObject(error) && error.name === 'InvalidCredentialsError') throw new InvalidVulcanCredentialsError();
|
||||
throw error;
|
||||
}
|
||||
const diaryList = await client.getDiaryList();
|
||||
const { privateKey, publicKey } = await generatePrivatePublicPair();
|
||||
const encryptedPrivateKey = encryptSymmetrical(
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
schema: http://localhost:3000/api/website/graphql
|
||||
documents:
|
||||
- src/graphql/queries/get-prompt-info.ts
|
||||
- src/graphql/mutations/login.ts
|
||||
generates:
|
||||
./src/graphql/generated.ts:
|
||||
plugins:
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-form @submit.prevent="submit">
|
||||
<v-card-title class="d-block">Zaloguj się do konta VULCAN UONET+</v-card-title>
|
||||
<div class="mx-4">
|
||||
<v-select v-model="host" label="Odmiana dziennika" :items="hosts" outlined />
|
||||
<v-text-field v-model="username" label="Nazwa użytkownika" outlined />
|
||||
<v-text-field v-model="password" type="password" label="Hasło" outlined />
|
||||
</div>
|
||||
<v-alert type="error" class="mx-2" :value="error === 'invalid-credentials'">
|
||||
Dane logowania są nieprawidłowe
|
||||
</v-alert>
|
||||
<v-alert type="error" class="mx-2" :value="error === 'other'">
|
||||
Podczas logowania wystąpił błąd
|
||||
</v-alert>
|
||||
<v-card-actions>
|
||||
<v-btn color="primary" text outlined @click="back" :disabled="loading">
|
||||
Wróć
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn color="primary" :loading="loading" type="submit">
|
||||
Zaloguj się
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { PromptInfo } from '@/types';
|
||||
import { hasErrorCode, sdk } from '@/pages/authenticate-prompt/sdk';
|
||||
|
||||
@Component({
|
||||
name: 'LoginWindow',
|
||||
})
|
||||
export default class LoginWindow extends Vue {
|
||||
@Prop({
|
||||
required: true,
|
||||
type: Object,
|
||||
})
|
||||
promptInfo!: PromptInfo;
|
||||
|
||||
readonly hosts = [
|
||||
{
|
||||
text: 'Vulcan',
|
||||
value: 'vulcan.net.pl',
|
||||
},
|
||||
{
|
||||
text: 'Fakelog',
|
||||
value: 'fakelog.cf',
|
||||
},
|
||||
]
|
||||
|
||||
host = '';
|
||||
|
||||
username = '';
|
||||
|
||||
password = '';
|
||||
|
||||
loading = false;
|
||||
|
||||
error: 'invalid-credentials' | 'other' | null = null;
|
||||
|
||||
reset() {
|
||||
this.host = 'fakelog.cf';
|
||||
this.username = '';
|
||||
this.password = '';
|
||||
}
|
||||
|
||||
async submit() {
|
||||
if (this.loading) return;
|
||||
this.error = null;
|
||||
this.loading = true;
|
||||
try {
|
||||
const { login } = await sdk.Login({
|
||||
promptId: this.promptInfo.id,
|
||||
host: this.host,
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
});
|
||||
const { students, encryptedPrivateKey } = login;
|
||||
console.log(students, encryptedPrivateKey);
|
||||
this.reset();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
this.error = hasErrorCode(error, 'INVALID_VULCAN_CREDENTIALS') ? 'invalid-credentials' : 'other';
|
||||
}
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
back() {
|
||||
if (this.loading) return;
|
||||
this.$emit('back');
|
||||
}
|
||||
|
||||
created() {
|
||||
this.reset();
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,102 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="pt-16">
|
||||
<h2 class="text-subtitle-1 text--secondary mt-6 mb-2 px-4">
|
||||
Aplikacja
|
||||
<span class="text--primary">{{ promptInfo.application.name }}</span>
|
||||
chce uzyskać dostęp do twojego konta VULCAN UONET+ przez Wulkanowy Bridge
|
||||
</h2>
|
||||
<v-subheader>Uprawnienia aplikacji</v-subheader>
|
||||
<v-list subheader>
|
||||
<v-list-item v-for="item in scopeItems" :key="item.key">
|
||||
<v-list-item-icon>
|
||||
<v-icon>{{ item.icon }}</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
{{ item.title }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle v-if="item.subtitle !== undefined">
|
||||
{{ item.subtitle }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<v-alert color="info" text class="mb-2 mx-2">
|
||||
<span class="font-weight-medium">{{ promptInfo.application.name }}</span>
|
||||
nie zobaczy twojego hasła
|
||||
<template #append>
|
||||
<!-- TODO: Implement -->
|
||||
<v-btn text color="info">Więcej</v-btn>
|
||||
</template>
|
||||
</v-alert>
|
||||
</div>
|
||||
<v-card-actions>
|
||||
<v-btn color="primary" text outlined :href="denyUrl">
|
||||
Odmów
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn color="primary" @click="next">
|
||||
Dalej
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Prop, Vue } from 'vue-property-decorator';
|
||||
import { PromptInfo } from '@/types';
|
||||
|
||||
@Component({
|
||||
name: 'OverviewWindow',
|
||||
})
|
||||
export default class OverviewWindow extends Vue {
|
||||
@Prop({
|
||||
required: true,
|
||||
type: Object,
|
||||
})
|
||||
promptInfo!: PromptInfo;
|
||||
|
||||
readonly scopeDescriptions: {
|
||||
key: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
icon: string;
|
||||
}[] = [
|
||||
{
|
||||
key: 'timetable',
|
||||
title: 'Plan lekcji',
|
||||
icon: 'mdi-timetable',
|
||||
},
|
||||
{
|
||||
key: 'grades',
|
||||
title: 'Oceny i punkty',
|
||||
subtitle: 'Oceny cząstkowe, końcowe, opisowe oraz punkty',
|
||||
icon: 'mdi-numeric-6-box-multiple-outline',
|
||||
},
|
||||
{
|
||||
key: 'notes',
|
||||
title: 'Uwagi i pochwały',
|
||||
icon: 'mdi-note-text-outline',
|
||||
},
|
||||
{
|
||||
key: 'achievements',
|
||||
title: 'Osiągnięcia',
|
||||
icon: 'mdi-trophy-outline',
|
||||
},
|
||||
]
|
||||
|
||||
get scopeItems() {
|
||||
return this.scopeDescriptions
|
||||
.filter(({ key }) => this.promptInfo.scopes.includes(key));
|
||||
}
|
||||
|
||||
get denyUrl() {
|
||||
return `/api/website/deny?prompt_id=${this.promptInfo.id}`;
|
||||
}
|
||||
|
||||
next() {
|
||||
this.$emit('next');
|
||||
}
|
||||
}
|
||||
</script>
|
|
@ -71,6 +71,25 @@ export type LoginResultStudent = {
|
|||
name: Scalars['String'];
|
||||
};
|
||||
|
||||
export type LoginMutationVariables = Exact<{
|
||||
promptId: Scalars['String'];
|
||||
host: Scalars['String'];
|
||||
username: Scalars['String'];
|
||||
password: Scalars['String'];
|
||||
}>;
|
||||
|
||||
export type LoginMutation = (
|
||||
{ __typename?: 'Mutation' }
|
||||
& { login: (
|
||||
{ __typename?: 'LoginResult' }
|
||||
& Pick<LoginResult, 'encryptedPrivateKey'>
|
||||
& { students: Array<(
|
||||
{ __typename?: 'LoginResultStudent' }
|
||||
& Pick<LoginResultStudent, 'studentId' | 'name'>
|
||||
)>; }
|
||||
); }
|
||||
);
|
||||
|
||||
export type GetPromptInfoQueryVariables = Exact<{
|
||||
promptId: Scalars['String'];
|
||||
}>;
|
||||
|
@ -79,7 +98,7 @@ export type GetPromptInfoQuery = (
|
|||
{ __typename?: 'Query' }
|
||||
& { promptInfo: (
|
||||
{ __typename?: 'PromptInfo' }
|
||||
& Pick<PromptInfo, 'scopes' | 'studentsMode'>
|
||||
& Pick<PromptInfo, 'id' | 'scopes' | 'studentsMode'>
|
||||
& { application: (
|
||||
{ __typename?: 'PromptInfoApplication' }
|
||||
& Pick<PromptInfoApplication, 'name' | 'iconUrl' | 'iconColor' | 'verified'>
|
||||
|
@ -87,9 +106,26 @@ export type GetPromptInfoQuery = (
|
|||
); }
|
||||
);
|
||||
|
||||
export const LoginDocument = gql`
|
||||
mutation Login($promptId: String!, $host: String!, $username: String!, $password: String!) {
|
||||
login(
|
||||
host: $host
|
||||
password: $password
|
||||
username: $username
|
||||
promptId: $promptId
|
||||
) {
|
||||
encryptedPrivateKey
|
||||
students {
|
||||
studentId
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
export const GetPromptInfoDocument = gql`
|
||||
query GetPromptInfo($promptId: String!) {
|
||||
promptInfo(promptId: $promptId) {
|
||||
id
|
||||
scopes
|
||||
studentsMode
|
||||
application {
|
||||
|
@ -107,6 +143,9 @@ export type SdkFunctionWrapper = <T>(action: () => Promise<T>) => Promise<T>;
|
|||
const defaultWrapper: SdkFunctionWrapper = (sdkFunction) => sdkFunction();
|
||||
export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = defaultWrapper) {
|
||||
return {
|
||||
Login(variables: LoginMutationVariables, requestHeaders?: Headers): Promise<LoginMutation> {
|
||||
return withWrapper(() => client.request<LoginMutation>(print(LoginDocument), variables, requestHeaders));
|
||||
},
|
||||
GetPromptInfo(variables: GetPromptInfoQueryVariables, requestHeaders?: Headers): Promise<GetPromptInfoQuery> {
|
||||
return withWrapper(() => client.request<GetPromptInfoQuery>(print(GetPromptInfoDocument), variables, requestHeaders));
|
||||
},
|
||||
|
|
12
website/src/graphql/mutations/login.ts
Normal file
12
website/src/graphql/mutations/login.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import gql from 'graphql-tag';
|
||||
|
||||
export default gql`mutation Login($promptId: String!, $host: String!, $username: String!, $password: String!) {
|
||||
login(host: $host, password: $password, username: $username, promptId: $promptId) {
|
||||
encryptedPrivateKey
|
||||
students {
|
||||
studentId
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
|
@ -2,6 +2,7 @@ import gql from 'graphql-tag';
|
|||
|
||||
export default gql`query GetPromptInfo($promptId: String!) {
|
||||
promptInfo(promptId: $promptId) {
|
||||
id
|
||||
scopes
|
||||
studentsMode
|
||||
application {
|
||||
|
@ -11,4 +12,5 @@ export default gql`query GetPromptInfo($promptId: String!) {
|
|||
verified
|
||||
}
|
||||
}
|
||||
}`;
|
||||
}
|
||||
`;
|
||||
|
|
|
@ -69,62 +69,13 @@
|
|||
</div>
|
||||
<v-window :value="step">
|
||||
<v-window-item :value="1">
|
||||
<div>
|
||||
<div class="pt-16">
|
||||
<h2 class="text-subtitle-1 text--secondary mt-6 mb-2 px-4">
|
||||
Aplikacja
|
||||
<span class="text--primary">{{ promptInfo.application.name }}</span>
|
||||
chce uzyskać dostęp do twojego konta VULCAN UONET+ przez Wulkanowy Bridge
|
||||
</h2>
|
||||
<v-subheader>Uprawnienia aplikacji</v-subheader>
|
||||
<v-list subheader>
|
||||
<v-list-item v-for="item in scopeItems" :key="item.key">
|
||||
<v-list-item-icon>
|
||||
<v-icon>{{ item.icon }}</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>
|
||||
{{ item.title }}
|
||||
</v-list-item-title>
|
||||
<v-list-item-subtitle v-if="item.subtitle !== undefined">
|
||||
{{ item.subtitle }}
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
<v-alert color="info" text class="mb-2 mx-2">
|
||||
<span class="font-weight-medium">{{ promptInfo.application.name }}</span>
|
||||
nie zobaczy twojego hasła
|
||||
<template #append>
|
||||
<!-- TODO: Implement -->
|
||||
<v-btn text color="info">Więcej</v-btn>
|
||||
</template>
|
||||
</v-alert>
|
||||
<v-divider />
|
||||
</div>
|
||||
<v-card-actions>
|
||||
<v-btn color="primary" text outlined :href="denyUrl">
|
||||
Odmów
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn color="primary" @click="beginLogin">
|
||||
Dalej
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</div>
|
||||
<overview-window
|
||||
@next="overviewNext"
|
||||
:promptInfo="promptInfo"
|
||||
/>
|
||||
</v-window-item>
|
||||
<v-window-item :value="2">
|
||||
<div>
|
||||
<v-card-actions>
|
||||
<v-btn color="primary" text outlined @click="goBack">
|
||||
Wróć
|
||||
</v-btn>
|
||||
<v-spacer />
|
||||
<v-btn color="primary">
|
||||
Zaloguj się
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</div>
|
||||
<v-window-item :value="2" eager>
|
||||
<login-window @back="loginBack" ref="loginWindow" :prompt-info="promptInfo" />
|
||||
</v-window-item>
|
||||
</v-window>
|
||||
</v-card>
|
||||
|
@ -162,31 +113,19 @@
|
|||
</style>
|
||||
|
||||
<script lang="ts">
|
||||
import { Component, Vue } from 'vue-property-decorator';
|
||||
import { GraphQLClient } from 'graphql-request';
|
||||
import { getSdk } from '@/graphql/generated';
|
||||
|
||||
export enum StudentsMode {
|
||||
None = 'none',
|
||||
One = 'one',
|
||||
Many = 'many',
|
||||
}
|
||||
|
||||
export interface PromptInfo {
|
||||
scopes: string[];
|
||||
studentsMode: StudentsMode;
|
||||
application: {
|
||||
name: string;
|
||||
iconUrl: string | null;
|
||||
iconColor: string;
|
||||
verified: boolean;
|
||||
};
|
||||
}
|
||||
import { Component, Ref, Vue } from 'vue-property-decorator';
|
||||
import OverviewWindow from '@/compontents/authenticate-prompt-windows/overview-window.vue';
|
||||
import { PromptInfo } from '@/types';
|
||||
import LoginWindow from '@/compontents/authenticate-prompt-windows/login-window.vue';
|
||||
import { sdk } from '@/pages/authenticate-prompt/sdk';
|
||||
|
||||
@Component({
|
||||
name: 'AuthenticatePromptApp',
|
||||
components: { LoginWindow, OverviewWindow },
|
||||
})
|
||||
export default class AuthenticatePromptApp extends Vue {
|
||||
@Ref() readonly loginWindow!: LoginWindow
|
||||
|
||||
promptInfo: PromptInfo | null = null;
|
||||
|
||||
promptId: string | null = null;
|
||||
|
@ -195,55 +134,12 @@ export default class AuthenticatePromptApp extends Vue {
|
|||
|
||||
step = 1;
|
||||
|
||||
readonly scopeDescriptions: {
|
||||
key: string;
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
icon: string;
|
||||
}[] = [
|
||||
{
|
||||
key: 'timetable',
|
||||
title: 'Plan lekcji',
|
||||
icon: 'mdi-timetable',
|
||||
},
|
||||
{
|
||||
key: 'grades',
|
||||
title: 'Oceny i punkty',
|
||||
subtitle: 'Oceny cząstkowe, końcowe, opisowe oraz punkty',
|
||||
icon: 'mdi-numeric-6-box-multiple-outline',
|
||||
},
|
||||
{
|
||||
key: 'notes',
|
||||
title: 'Uwagi i pochwały',
|
||||
icon: 'mdi-note-text-outline',
|
||||
},
|
||||
{
|
||||
key: 'achievements',
|
||||
title: 'Osiągnięcia',
|
||||
icon: 'mdi-trophy-outline',
|
||||
},
|
||||
]
|
||||
|
||||
get scopeItems() {
|
||||
if (this.promptInfo === null) return undefined;
|
||||
return this.scopeDescriptions
|
||||
.filter(({ key }) => this.promptInfo?.scopes?.includes(key) ?? false);
|
||||
}
|
||||
|
||||
get denyUrl() {
|
||||
if (!this.promptId) return undefined;
|
||||
return `/api/website/deny?prompt_id=${this.promptId}`;
|
||||
}
|
||||
|
||||
async loadPromptInfo() {
|
||||
this.promptInfoError = false;
|
||||
this.promptInfo = null;
|
||||
|
||||
if (!this.promptId) return;
|
||||
|
||||
const client = new GraphQLClient('/api/website/graphql');
|
||||
const sdk = getSdk(client);
|
||||
|
||||
try {
|
||||
const { promptInfo } = await sdk.GetPromptInfo({
|
||||
promptId: this.promptId,
|
||||
|
@ -262,12 +158,13 @@ export default class AuthenticatePromptApp extends Vue {
|
|||
await this.loadPromptInfo();
|
||||
}
|
||||
|
||||
goBack() {
|
||||
this.step -= 1;
|
||||
loginBack() {
|
||||
this.step = 1;
|
||||
}
|
||||
|
||||
beginLogin() {
|
||||
overviewNext() {
|
||||
this.step = 2;
|
||||
this.loginWindow.reset();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
18
website/src/pages/authenticate-prompt/sdk.ts
Normal file
18
website/src/pages/authenticate-prompt/sdk.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { ClientError, GraphQLClient } from 'graphql-request';
|
||||
import { getSdk } from '@/graphql/generated';
|
||||
import { GraphQLError } from 'graphql-request/dist/types';
|
||||
|
||||
const client = new GraphQLClient('/api/website/graphql');
|
||||
export const sdk = getSdk(client);
|
||||
|
||||
export interface GraphQLErrorFull extends GraphQLError {
|
||||
extensions?: {
|
||||
code?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function hasErrorCode(error: unknown, code: string): boolean {
|
||||
if (!(error instanceof ClientError)) return false;
|
||||
return error.response.errors
|
||||
?.some((resError: GraphQLErrorFull) => resError.extensions?.code === code) ?? false;
|
||||
}
|
17
website/src/types.ts
Normal file
17
website/src/types.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
export enum StudentsMode {
|
||||
None = 'none',
|
||||
One = 'one',
|
||||
Many = 'many',
|
||||
}
|
||||
|
||||
export interface PromptInfo {
|
||||
id: string;
|
||||
scopes: string[];
|
||||
studentsMode: StudentsMode;
|
||||
application: {
|
||||
name: string;
|
||||
iconUrl: string | null;
|
||||
iconColor: string;
|
||||
verified: boolean;
|
||||
};
|
||||
}
|
Loading…
Reference in a new issue