Integrate graphql in website

This commit is contained in:
Dominik Korsa 2021-01-18 23:08:39 +01:00
parent 577e144d0b
commit be486b6da6
No known key found for this signature in database
GPG key ID: 546F986F71A6FE6E
7 changed files with 4162 additions and 82 deletions

View file

@ -15,4 +15,12 @@ module.exports = {
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
},
overrides: [
{
files: './src/graphql/generated.ts',
rules: {
'max-len': ['off']
},
},
],
};

14
website/codegen.yml Normal file
View file

@ -0,0 +1,14 @@
schema: http://localhost:3000/api/website/graphql
documents:
- src/graphql/queries/get-prompt-info.ts
generates:
./src/graphql/generated.ts:
plugins:
- typescript
- typescript-operations
- typescript-graphql-request
config:
avoidOptionals: true
hooks:
afterAllFileWrite:
- eslint --fix

3919
website/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -5,16 +5,24 @@
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
"lint": "vue-cli-service lint",
"generate": "graphql-codegen"
},
"dependencies": {
"core-js": "^3.6.5",
"graphql": "^15.4.0",
"graphql-request": "^3.4.0",
"graphql-tag": "^2.11.0",
"vue": "^2.6.11",
"vue-class-component": "^7.2.3",
"vue-property-decorator": "^9.1.2",
"vuetify": "^2.2.11"
},
"devDependencies": {
"@graphql-codegen/cli": "^1.20.0",
"@graphql-codegen/typescript": "^1.20.0",
"@graphql-codegen/typescript-graphql-request": "^3.0.0",
"@graphql-codegen/typescript-operations": "^1.17.13",
"@typescript-eslint/eslint-plugin": "^2.33.0",
"@typescript-eslint/parser": "^2.33.0",
"@vue/cli-plugin-babel": "~4.5.0",

View file

@ -0,0 +1,115 @@
import { GraphQLClient } from 'graphql-request';
import { print } from 'graphql';
import gql from 'graphql-tag';
export type Maybe<T> = T | null;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
export type MakeOptional<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]?: Maybe<T[SubKey]> };
export type MakeMaybe<T, K extends keyof T> = Omit<T, K> & { [SubKey in K]: Maybe<T[SubKey]> };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
ID: string;
String: string;
Boolean: boolean;
Int: number;
Float: number;
};
export type Query = {
__typename?: 'Query';
promptInfo: PromptInfo;
};
export type QueryPromptInfoArgs = {
promptId: Scalars['String'];
};
export type PromptInfo = {
__typename?: 'PromptInfo';
id: Scalars['String'];
scopes: Array<Scalars['String']>;
clientId: Scalars['String'];
studentsMode: StudentsMode;
application: PromptInfoApplication;
};
export enum StudentsMode {
None = 'None',
One = 'One',
Many = 'Many'
}
export type PromptInfoApplication = {
__typename?: 'PromptInfoApplication';
name: Scalars['String'];
iconUrl: Maybe<Scalars['String']>;
iconColor: Scalars['String'];
verified: Scalars['Boolean'];
};
export type Mutation = {
__typename?: 'Mutation';
login: LoginResult;
};
export type MutationLoginArgs = {
host: Scalars['String'];
password: Scalars['String'];
username: Scalars['String'];
promptId: Scalars['String'];
};
export type LoginResult = {
__typename?: 'LoginResult';
encryptedPrivateKey: Scalars['String'];
students: Array<LoginResultStudent>;
};
export type LoginResultStudent = {
__typename?: 'LoginResultStudent';
studentId: Scalars['Int'];
name: Scalars['String'];
};
export type GetPromptInfoQueryVariables = Exact<{
promptId: Scalars['String'];
}>;
export type GetPromptInfoQuery = (
{ __typename?: 'Query' }
& { promptInfo: (
{ __typename?: 'PromptInfo' }
& Pick<PromptInfo, 'scopes' | 'studentsMode'>
& { application: (
{ __typename?: 'PromptInfoApplication' }
& Pick<PromptInfoApplication, 'name' | 'iconUrl' | 'iconColor' | 'verified'>
); }
); }
);
export const GetPromptInfoDocument = gql`
query GetPromptInfo($promptId: String!) {
promptInfo(promptId: $promptId) {
scopes
studentsMode
application {
name
iconUrl
iconColor
verified
}
}
}
`;
export type SdkFunctionWrapper = <T>(action: () => Promise<T>) => Promise<T>;
const defaultWrapper: SdkFunctionWrapper = (sdkFunction) => sdkFunction();
export function getSdk(client: GraphQLClient, withWrapper: SdkFunctionWrapper = defaultWrapper) {
return {
GetPromptInfo(variables: GetPromptInfoQueryVariables, requestHeaders?: Headers): Promise<GetPromptInfoQuery> {
return withWrapper(() => client.request<GetPromptInfoQuery>(print(GetPromptInfoDocument), variables, requestHeaders));
},
};
}
export type Sdk = ReturnType<typeof getSdk>;

View file

@ -0,0 +1,14 @@
import gql from 'graphql-tag';
export default gql`query GetPromptInfo($promptId: String!) {
promptInfo(promptId: $promptId) {
scopes
studentsMode
application {
name
iconUrl
iconColor
verified
}
}
}`;

View file

@ -67,47 +67,66 @@
</transition>
</v-badge>
</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">
Zaloguj się
</v-btn>
</v-card-actions>
<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>
</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>
</v-window>
</v-card>
</template>
</v-sheet>
@ -144,6 +163,8 @@
<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',
@ -174,36 +195,39 @@ export default class AuthenticatePromptApp extends Vue {
step = 1;
readonly scopeDescriptions: Record<string, {
readonly scopeDescriptions: {
key: string;
title: string;
subtitle?: string;
icon: string;
}> = {
timetable: {
}[] = [
{
key: 'timetable',
title: 'Plan lekcji',
icon: 'mdi-timetable',
},
grades: {
{
key: 'grades',
title: 'Oceny i punkty',
subtitle: 'Oceny cząstkowe, końcowe, opisowe oraz punkty',
icon: 'mdi-numeric-6-box-multiple-outline',
},
notes: {
title: 'Uwagi',
{
key: 'notes',
title: 'Uwagi i pochwały',
icon: 'mdi-note-text-outline',
},
achievements: {
{
key: 'achievements',
title: 'Osiągnięcia',
icon: 'mdi-trophy-outline',
},
}
]
get scopeItems() {
if (this.promptInfo === null) return undefined;
return this.promptInfo.scopes.map((key: string) => ({
key,
...this.scopeDescriptions[key],
}));
return this.scopeDescriptions
.filter(({ key }) => this.promptInfo?.scopes?.includes(key) ?? false);
}
get denyUrl() {
@ -214,20 +238,20 @@ export default class AuthenticatePromptApp extends Vue {
async loadPromptInfo() {
this.promptInfoError = false;
this.promptInfo = null;
await new Promise((resolve) => setTimeout(resolve, 1000));
if (Math.random() < 0.5) {
if (!this.promptId) return;
const client = new GraphQLClient('/api/website/graphql');
const sdk = getSdk(client);
try {
const { promptInfo } = await sdk.GetPromptInfo({
promptId: this.promptId,
});
this.promptInfo = promptInfo;
} catch (error) {
console.error(error);
this.promptInfoError = true;
} else {
this.promptInfo = {
scopes: ['grades', 'timetable', 'notes', 'achievements'],
studentsMode: StudentsMode.Many,
application: {
iconColor: '#f00',
iconUrl: null,
name: 'Not a fancy app',
verified: false,
},
};
}
}
@ -237,5 +261,13 @@ export default class AuthenticatePromptApp extends Vue {
if (!this.promptId) return;
await this.loadPromptInfo();
}
goBack() {
this.step -= 1;
}
beginLogin() {
this.step = 2;
}
}
</script>