Merge pull request #398 from PretendoNetwork/nuxt-refactor-login
Some checks failed
Build and Publish Docker Image / Build and Publish (amd64) (push) Has been cancelled
Build and Publish Docker Image / Build and Publish (arm64) (push) Has been cancelled

Add login & update configurations
This commit is contained in:
mrjvs
2025-04-22 08:46:37 +02:00
committed by GitHub
8 changed files with 284 additions and 2 deletions

2
.gitattributes vendored
View File

@@ -1 +1 @@
* text=auto
* text=auto eol=lf

View File

@@ -23,6 +23,10 @@ export default defineNuxtConfig({
}
},
runtimeConfig: {
apiBase: 'https://api.pretendo.cc'
},
css: ['~/assets/css/main.css'],
fonts: {

View File

@@ -0,0 +1,5 @@
<template>
<div>
<p>account stub page</p>
</div>
</template>

View File

@@ -0,0 +1,234 @@
<script setup lang="ts">
import { FetchError } from 'ofetch';
const route = useRoute();
const redirect = computed(() => route.query.redirect);
const registerURI = computed(() => `/account/register${redirect.value ? `?redirect=${redirect.value}` : ''}`);
const loginForm = reactive({ username: '', password: '' });
const errorMessage = ref<string | null>();
async function loginSubmission() {
await $fetch('/api/account/login', {
method: 'POST',
body: loginForm
}).catch((error: FetchError) => {
errorMessage.value = error.statusText;
setTimeout(() => { // TODO: this is not the best way to clear this out, but this is temporary! replace all toasts with input alerts in the future
errorMessage.value = null;
}, 5000);
return;
});
if (typeof redirect.value === 'string') {
await navigateTo(redirect.value);
} else {
await navigateTo('/account');
}
}
</script>
<template>
<div>
<div class="account-form-wrapper">
<form
class="account"
@submit.prevent="loginSubmission"
>
<h2>{{ $t("account.loginForm.login") }}</h2>
<p>{{ $t("account.loginForm.detailsPrompt") }}</p>
<div>
<label for="username">{{ $t("account.loginForm.username") }}</label>
<input
id="username"
v-model="loginForm.username"
name="username"
required
>
</div>
<div>
<label for="password">{{ $t("account.loginForm.password") }}</label>
<input
id="password"
v-model="loginForm.password"
name="password"
type="password"
required
>
<a
href="/account/forgot-password"
class="pwdreset"
>{{ $t("account.loginForm.forgotPassword") }}</a>
</div>
<div class="buttons">
<button type="submit">
<!-- TODO: loading state on button. ... style maybe? -->
{{ $t("account.loginForm.login") }}
</button>
<a
:href="registerURI"
class="register"
>{{ $t("account.loginForm.registerPrompt") }}</a>
</div>
</form>
</div>
<div
v-if="errorMessage"
class="banner-notice error"
>
<div>
<p>{{ errorMessage }}</p>
</div>
</div>
</div>
</template>
<style scoped>
header {
margin: 35px 0;
}
.account-form-wrapper {
height: 75vh;
display: flex;
justify-content: center;
align-content: center;
flex-direction: column;
margin: auto;
width: fit-content;
overflow: hidden;
}
form.account {
height: fit-content;
display: block;
padding: 40px 48px;
background-color: var(--bg-shade-2);
color: var(--text-shade-1);
border-radius: 12px;
width: min(480px, 90vw);
box-sizing: border-box;
}
form.account h2 {
margin: 0;
color: var(--text-shade-3);
}
form.account p {
margin: 12px 0;
}
form.account div {
margin-top: 24px;
}
form.account label {
display: block;
margin-bottom: 6px;
text-transform: uppercase;
font-size: 12px;
}
form.account button {
width: 100%;
background: var(--accent-shade-0);
}
form.account a {
text-decoration: none;
display: block;
color: var(--text-shade-1);
text-align: right;
margin: 6px 0;
width: fit-content;
}
form.account a:hover {
color: var(--text-shade-3);
}
form.account a.pwdreset {
margin-left: auto;
font-size: 14px;
}
form.account a.register {
margin: auto;
margin-top: 18px;
}
@keyframes banner-notice {
0% {
top: -150px;
}
20% {
top: 35px;
}
80% {
top: 35px;
}
100% {
top: -150px;
}
}
.banner-notice {
display: flex;
justify-content: center;
position: absolute;
left: 0;
top: -150px;
width: 100%;
animation: banner-notice 5s;
}
.banner-notice div {
padding: 4px 36px;
border-radius: 5px;
z-index: 3;
}
.banner-notice.error div {
background: var(--red-shade-1);
}
form.account.register {
display: grid;
grid-template-columns: repeat(2, 1fr);
width: min(780px, 90vw);
column-gap: 24px;
margin-bottom: 48px;
}
form.account.register div.h-captcha {
grid-column: 1 / span 2;
display: flex;
justify-content: center;
}
form.account.register p,
form.account.register div.email,
form.account.register div.buttons {
grid-column: 1 / span 2;
}
</style>
<style lang="scss">
@media screen and (max-width: 720px) {
form.account.register {
grid-template-columns: 1fr;
}
form.account.register div.h-captcha,
form.account.register p,
form.account.register div.email,
form.account.register div.buttons {
grid-column: unset;
}
}
</style>

View File

@@ -0,0 +1,37 @@
import { FetchError } from 'ofetch';
type LoginCCResponse = {
refresh_token: string;
access_token: string;
token_type: string;
expires_in: number;
};
export default defineEventHandler(async (event) => {
const body = await readBody(event);
try {
const apiResponse = await $fetch<LoginCCResponse>(`/v1/login`, {
method: 'POST',
baseURL: useRuntimeConfig(event).apiBase,
body: { ...body, grant_type: 'password' }
});
setCookie(event, 'refresh_token', apiResponse.refresh_token);
setCookie(event, 'access_token', apiResponse.access_token);
setCookie(event, 'token_type', apiResponse.token_type);
setResponseStatus(event, 200);
event.node.res.end();
} catch (error: unknown) {
if (error instanceof FetchError) {
setResponseStatus(event, error.status, error.data.error);
} else {
setResponseStatus(event, 500);
}
event.node.res.end();
return;
}
});

View File

@@ -0,0 +1 @@
// TODO: endpoint for refreshing tokens

View File

@@ -0,0 +1 @@
// TODO: registration

View File

@@ -1,3 +1,3 @@
{
"extends": "../.nuxt/tsconfig.server.json"
"extends": "../../.nuxt/tsconfig.server.json"
}