Add error screen

This commit is contained in:
Dominik Korsa 2021-01-22 17:47:26 +01:00
parent 800e545ff5
commit 3c1f07f3b0
No known key found for this signature in database
GPG key ID: 546F986F71A6FE6E
7 changed files with 217 additions and 107 deletions

View file

@ -18,17 +18,28 @@ export default function registerAuthorize(server: MyFastifyInstance): void {
) => { ) => {
if (!isObject(request.query)) { if (!isObject(request.query)) {
server.log.warn('Request query is not an object'); server.log.warn('Request query is not an object');
throw server.httpErrors.badRequest(); await reply.redirect(urlJoin(
websitePrefix,
`/prompt-error?code=invalid_request&description=${
encodeURIComponent('Request query is not an object')
}`,
));
return;
} }
try { try {
validateParam('client_id', request.query.client_id); validateParam('client_id', request.query.client_id);
validateParam('redirect_uri', request.query.redirect_uri); validateParam('redirect_uri', request.query.redirect_uri);
} catch (error) { } catch (error) {
if (error instanceof ParamError) { if (error instanceof ParamError) {
throw server.httpErrors.badRequest(error.message); await reply.redirect(urlJoin(
websitePrefix,
`/prompt-error?code=invalid_request&description=${encodeURIComponent(error.message)}`,
));
return;
} }
server.log.error(error); server.log.error(error);
throw server.httpErrors.internalServerError(); await reply.redirect(urlJoin(websitePrefix, '/prompt-error?code=internal'));
return;
} }
const application = await database.applicationRepo.findOne({ const application = await database.applicationRepo.findOne({
@ -36,8 +47,14 @@ export default function registerAuthorize(server: MyFastifyInstance): void {
clientId: request.query.client_id, clientId: request.query.client_id,
}, },
}); });
if (application === undefined) throw server.httpErrors.badRequest('Unknown application'); if (application === undefined) {
if (!application.redirectUris.includes(request.query.redirect_uri)) throw server.httpErrors.badRequest('Redirect URI not registered'); await reply.redirect(urlJoin(websitePrefix, '/prompt-error?code=unknown_application'));
return;
}
if (!application.redirectUris.includes(request.query.redirect_uri)) {
await reply.redirect(urlJoin(websitePrefix, '/prompt-error?code=unknown_redirect_uri'));
return;
}
try { try {
validateParam('response_type', request.query.response_type); validateParam('response_type', request.query.response_type);

View file

@ -23,5 +23,11 @@ module.exports = {
'max-len': ['off'] 'max-len': ['off']
}, },
}, },
{
files: '**/*.vue',
rules: {
'class-methods-use-this': ['off']
}
}
], ],
}; };

View file

@ -0,0 +1,35 @@
<template>
<v-app class="dialog-app">
<div>
<div class="text-h4 text-center mt-8 mx-2">
<span class="primary--text">Wulkanowy</span> Bridge
</div>
</div>
<v-main class="px-4">
<v-sheet max-width="500" class="mx-auto mt-16" color="transparent">
<slot />
</v-sheet>
</v-main>
</v-app>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
@Component({
name: 'DialogApp',
})
export default class DialogApp extends Vue {
}
</script>
<style lang="scss">
.dialog-app {
background-color: #f7f7f7 !important;
.v-card__text, .v-card__title {
word-break: normal;
}
}
</style>

View file

@ -1,107 +1,96 @@
<template> <template>
<v-app class="authenticate-prompt-app"> <dialog-app class="authenticate-prompt-app">
<div> <v-alert type="error" text v-if="!promptId">
<div class="text-h4 text-center mt-8"> Brak wymaganego parametru <code>prompt_id</code>
<span class="primary--text">Wulkanowy</span> Bridge </v-alert>
</div> <v-alert type="error" text v-else-if="promptInfoError">
Nie udało się wczytać danych
<template #append>
<v-btn text color="error" @click="loadPromptInfo">
Spróbuj ponownie
</v-btn>
</template>
</v-alert>
<div class="d-flex align-center justify-center mt-16 mb-16" v-else-if="!promptInfo">
<v-progress-circular indeterminate :size="96" color="primary" />
</div> </div>
<v-main class="px-4"> <template v-else>
<v-sheet max-width="500" class="mx-auto mt-16" color="transparent"> <div class="pb-1 text--secondary">
<v-alert type="error" text v-if="!promptId"> Krok <span class="primary--text">{{ step }}/3</span>
Brak wymaganego parametru <code>prompt_id</code> </div>
</v-alert> <v-card outlined>
<v-alert type="error" text v-else-if="promptInfoError"> <div class="d-flex justify-center mn-16 avatar__wrapper">
Nie udało się wczytać danych <v-badge
<template #append> :color="promptInfo.application.verified ? 'green' : 'grey'"
<v-btn text color="error" @click="loadPromptInfo"> offset-x="64"
Spróbuj ponownie offset-y="16"
</v-btn> bottom
</template> :content="promptInfo.application.verified ? 'Zweryfikowana' : 'Niezweryfikowana'"
</v-alert> :value="step === 1"
<div class="d-flex align-center justify-center mt-16 mb-16" v-else-if="!promptInfo"> >
<v-progress-circular indeterminate :size="96" color="primary" /> <transition name="scale">
</div> <v-sheet
<template v-else> v-if="step === 1"
<div class="pb-1 text--secondary"> width="128"
Krok <span class="primary--text">{{ step }}/3</span> height="128"
</div> class="avatar-sheet mx-4 overflow-hidden"
<v-card outlined> outlined
<div class="d-flex justify-center mn-16 avatar__wrapper">
<v-badge
:color="promptInfo.application.verified ? 'green' : 'grey'"
offset-x="64"
offset-y="16"
bottom
:content="promptInfo.application.verified ? 'Zweryfikowana' : 'Niezweryfikowana'"
:value="step === 1"
> >
<transition name="scale"> <v-sheet
<v-sheet class="fill-height d-flex align-center justify-center"
v-if="step === 1" :color="promptInfo.application.iconColor"
width="128" >
height="128" <v-img
class="avatar-sheet mx-4 overflow-hidden" :src="promptInfo.application.iconUrl"
outlined width="80"
height="80"
aspect-ratio="1"
contain
> >
<v-sheet <template v-slot:placeholder>
class="fill-height d-flex align-center justify-center" <div class="fill-height d-flex align-center justify-center">
:color="promptInfo.application.iconColor" <v-icon :size="80">
> mdi-help
<v-img </v-icon>
:src="promptInfo.application.iconUrl" </div>
width="80" </template>
height="80" </v-img>
aspect-ratio="1" </v-sheet>
contain </v-sheet>
> </transition>
<template v-slot:placeholder> </v-badge>
<div class="fill-height d-flex align-center justify-center"> </div>
<v-icon :size="80"> <v-window :value="step">
mdi-help <v-window-item :value="1">
</v-icon> <overview-window
</div> :promptInfo="promptInfo"
</template> @next="toLoginWindow"
</v-img> />
</v-sheet> </v-window-item>
</v-sheet> <v-window-item :value="2" eager>
</transition> <login-window
</v-badge> ref="loginWindow"
</div> :prompt-info="promptInfo"
<v-window :value="step"> @login="login"
<v-window-item :value="1"> @back="loginBack"
<overview-window />
:promptInfo="promptInfo" </v-window-item>
@next="toLoginWindow" <v-window-item :value="3">
/> <students-window
</v-window-item> v-if="students !== null"
<v-window-item :value="2" eager> :prompt-info="promptInfo"
<login-window :students="students"
ref="loginWindow" @back="toLoginWindow"
:prompt-info="promptInfo" />
@login="login" </v-window-item>
@back="loginBack" </v-window>
/> </v-card>
</v-window-item> </template>
<v-window-item :value="3"> </dialog-app>
<students-window
v-if="students !== null"
:prompt-info="promptInfo"
:students="students"
@back="toLoginWindow"
/>
</v-window-item>
</v-window>
</v-card>
</template>
</v-sheet>
</v-main>
</v-app>
</template> </template>
<style lang="scss"> <style lang="scss">
.authenticate-prompt-app { .authenticate-prompt-app {
background-color: #f7f7f7 !important;
.avatar-sheet { .avatar-sheet {
border-radius: 50%; border-radius: 50%;
} }
@ -122,10 +111,6 @@
.scale-enter, .scale-leave-to { .scale-enter, .scale-leave-to {
transform: scale(0); transform: scale(0);
} }
.v-card__text, .v-card__title {
word-break: normal;
}
} }
</style> </style>
@ -136,10 +121,13 @@ import { PromptInfo, Student } from '@/types';
import LoginWindow from '@/compontents/authenticate-prompt-windows/login-window.vue'; import LoginWindow from '@/compontents/authenticate-prompt-windows/login-window.vue';
import StudentsWindow from '@/compontents/authenticate-prompt-windows/students-window.vue'; import StudentsWindow from '@/compontents/authenticate-prompt-windows/students-window.vue';
import { sdk } from '@/pages/authenticate-prompt/sdk'; import { sdk } from '@/pages/authenticate-prompt/sdk';
import DialogApp from '@/compontents/dialog-app.vue';
@Component({ @Component({
name: 'AuthenticatePromptApp', name: 'AuthenticatePromptApp',
components: { LoginWindow, OverviewWindow, StudentsWindow }, components: {
LoginWindow, OverviewWindow, StudentsWindow, DialogApp,
},
}) })
export default class AuthenticatePromptApp extends Vue { export default class AuthenticatePromptApp extends Vue {
@Ref() readonly loginWindow!: LoginWindow @Ref() readonly loginWindow!: LoginWindow

View file

@ -0,0 +1,48 @@
<template>
<dialog-app>
<v-card outlined>
<v-card-title>Podczas autoryzacji wystąpił błąd</v-card-title>
<v-card-text>
<v-alert text type="error">
{{ errorMessage }}<br>
<code
v-if="errorDescription !== null"
class="d-block mt-3 py-2 px-sm-3"
>
{{ errorDescription }}
</code>
</v-alert>
Skontaktuj się z twórcą aplikacji w celu rozwiązania problemu
</v-card-text>
</v-card>
</dialog-app>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import DialogApp from '@/compontents/dialog-app.vue';
@Component({
name: 'PromptErrorApp',
components: { DialogApp },
})
export default class PromptErrorApp extends Vue {
errorCode: string | null = null;
errorDescription: string | null = null;
get errorMessage() {
if (this.errorCode === 'invalid_request') return 'Niepoprawny format zapytania';
if (this.errorCode === 'internal') return 'Błąd serwera';
if (this.errorCode === 'unknown_application') return 'Nie znaleziono aplikacji';
if (this.errorCode === 'unknown_redirect_uri') return 'Niepoprawne URI przekierowania';
return 'Nieznany błąd';
}
async created() {
const searchParams = new URLSearchParams(window.location.search);
this.errorCode = searchParams.get('code');
this.errorDescription = searchParams.get('description');
}
}
</script>

View file

@ -0,0 +1,10 @@
import Vue from 'vue';
import vuetify from '@/plugins/vuetify';
import AuthenticatePromptApp from './app.vue';
Vue.config.productionTip = false;
new Vue({
vuetify,
render: (h) => h(AuthenticatePromptApp),
}).$mount('#app');

View file

@ -7,7 +7,13 @@ module.exports = {
entry: 'src/pages/authenticate-prompt/main.ts', entry: 'src/pages/authenticate-prompt/main.ts',
template: 'public/index.html', template: 'public/index.html',
filename: 'authenticate-prompt.html', filename: 'authenticate-prompt.html',
title: 'Authorize application | Wulkanowy Bridge', title: 'Autoryzuj aplikację | Wulkanowy Bridge',
},
'prompt-error': {
entry: 'src/pages/prompt-error/main.ts',
template: 'public/index.html',
filename: 'prompt-error.html',
title: 'Błąd autoryzacji | Wulkanowy Bridge',
}, },
}, },
}; };