From 26817d5fbc9654f1068afc419a8f2d100ca34ec9 Mon Sep 17 00:00:00 2001 From: niko <57009359+hauntii@users.noreply.github.com> Date: Fri, 25 Apr 2025 21:26:27 -0400 Subject: [PATCH 1/4] feat: registration and css separation --- package-lock.json | 12 ++ package.json | 1 + src/assets/css/auth.css | 137 +++++++++++++++++++++ src/pages/account/login/index.vue | 147 +---------------------- src/pages/account/register/index.vue | 140 +++++++++++++++++++++ src/server/api/account/register/index.ts | 37 +++++- 6 files changed, 327 insertions(+), 147 deletions(-) create mode 100644 src/assets/css/auth.css create mode 100644 src/pages/account/register/index.vue diff --git a/package-lock.json b/package-lock.json index 6ed2e47..312f5e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "AGPL-3.0-only", "dependencies": { + "@hcaptcha/vue3-hcaptcha": "^1.3.0", "@nuxt/content": "^3.4.0", "@nuxt/eslint": "^1.3.0", "@nuxt/fonts": "^0.11.1", @@ -1270,6 +1271,17 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@hcaptcha/vue3-hcaptcha": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@hcaptcha/vue3-hcaptcha/-/vue3-hcaptcha-1.3.0.tgz", + "integrity": "sha512-IEonS6JiYdU7uy6aeib8cYtMO4nj8utwStbA9bWHyYbOvOvhpkV+AW8vfSKh6SntYxqle/TRwhv+kU9p92CfsA==", + "dependencies": { + "vue": "^3.2.19" + }, + "peerDependencies": { + "vue": "^3.0.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", diff --git a/package.json b/package.json index 6f25454..0f5e89a 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "lint:fix": "eslint . --fix" }, "dependencies": { + "@hcaptcha/vue3-hcaptcha": "^1.3.0", "@nuxt/content": "^3.4.0", "@nuxt/eslint": "^1.3.0", "@nuxt/fonts": "^0.11.1", diff --git a/src/assets/css/auth.css b/src/assets/css/auth.css new file mode 100644 index 0000000..36bd61e --- /dev/null +++ b/src/assets/css/auth.css @@ -0,0 +1,137 @@ +header { + margin: 35px 0; +} + +.account-form-wrapper { + height: 80vh; + display: flex; + justify-content: center; + align-content: center; + flex-direction: column; + margin: 15vh 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 p, +form.account.register div.email, +form.account.register div.buttons { + grid-column: 1 / span 2; +} + +@media screen and (max-width: 720px) { + form.account.register { + grid-template-columns: 1fr; + } + + form.account.register p, + form.account.register div.email, + form.account.register div.buttons { + grid-column: unset; + } +} \ No newline at end of file diff --git a/src/pages/account/login/index.vue b/src/pages/account/login/index.vue index f2a0e88..66da032 100644 --- a/src/pages/account/login/index.vue +++ b/src/pages/account/login/index.vue @@ -85,150 +85,5 @@ async function loginSubmission() { - - diff --git a/src/pages/account/register/index.vue b/src/pages/account/register/index.vue new file mode 100644 index 0000000..be309cf --- /dev/null +++ b/src/pages/account/register/index.vue @@ -0,0 +1,140 @@ + + + + + diff --git a/src/server/api/account/register/index.ts b/src/server/api/account/register/index.ts index 7a1488f..5536ca5 100644 --- a/src/server/api/account/register/index.ts +++ b/src/server/api/account/register/index.ts @@ -1 +1,36 @@ -// TODO: registration +import { FetchError } from 'ofetch'; + +// TODO: Drop to interface from type, type is unnecessary for this +type RegisterCCResponse = { // TODO: this is the same as the login response. figure out where we should put types and merge into AuthCCReponse! + 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(`/v1/register`, { + method: 'POST', + baseURL: useRuntimeConfig(event).apiBase, + body: body + }); + + 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(); + } +}); From 1e3d27db2965bc28990c64c8d72c9bb3539f3b8b Mon Sep 17 00:00:00 2001 From: niko <57009359+hauntii@users.noreply.github.com> Date: Fri, 25 Apr 2025 21:32:17 -0400 Subject: [PATCH 2/4] fix: clean up some api things --- src/pages/account/login/index.vue | 33 ++++++++++++++---------- src/server/api/account/login/index.ts | 4 +-- src/server/api/account/register/index.ts | 5 ++-- 3 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/pages/account/login/index.vue b/src/pages/account/login/index.vue index 66da032..5fd037b 100644 --- a/src/pages/account/login/index.vue +++ b/src/pages/account/login/index.vue @@ -9,22 +9,27 @@ const loginForm = reactive({ username: '', password: '' }); const errorMessage = ref(); 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 + try { + await $fetch('/api/account/login', { + method: 'POST', + body: loginForm + }); + + if (typeof redirect.value === 'string') { + await navigateTo(redirect.value); + } else { + await navigateTo('/account'); + } + } catch (error: unknown) { + if (error instanceof FetchError) { + errorMessage.value = error.statusText; + } else { + errorMessage.value = `Error during registration: ${error}`; // TODO: localize + } + + setTimeout(() => { // TODO: replace this toast errorMessage.value = null; }, 5000); - - return; - }); - - if (typeof redirect.value === 'string') { - await navigateTo(redirect.value); - } else { - await navigateTo('/account'); } } diff --git a/src/server/api/account/login/index.ts b/src/server/api/account/login/index.ts index 61c2407..25749ca 100644 --- a/src/server/api/account/login/index.ts +++ b/src/server/api/account/login/index.ts @@ -1,11 +1,11 @@ import { FetchError } from 'ofetch'; -type LoginCCResponse = { +interface LoginCCResponse { refresh_token: string; access_token: string; token_type: string; expires_in: number; -}; +} export default defineEventHandler(async (event) => { const body = await readBody(event); diff --git a/src/server/api/account/register/index.ts b/src/server/api/account/register/index.ts index 5536ca5..f27c9bd 100644 --- a/src/server/api/account/register/index.ts +++ b/src/server/api/account/register/index.ts @@ -1,12 +1,11 @@ import { FetchError } from 'ofetch'; -// TODO: Drop to interface from type, type is unnecessary for this -type RegisterCCResponse = { // TODO: this is the same as the login response. figure out where we should put types and merge into AuthCCReponse! +interface RegisterCCResponse { // TODO: this is the same as the login response. figure out where we should put types and merge into AuthCCReponse! refresh_token: string; access_token: string; token_type: string; expires_in: number; -}; +} export default defineEventHandler(async (event) => { const body = await readBody(event); From f6ac4332f98cf31d8002add8b95a318892b3818c Mon Sep 17 00:00:00 2001 From: niko <57009359+hauntii@users.noreply.github.com> Date: Fri, 25 Apr 2025 21:35:50 -0400 Subject: [PATCH 3/4] other: stub forgotten password and add TODO labels to stubs --- src/pages/account/forgot-password/index.vue | 5 +++++ src/pages/account/index.vue | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 src/pages/account/forgot-password/index.vue diff --git a/src/pages/account/forgot-password/index.vue b/src/pages/account/forgot-password/index.vue new file mode 100644 index 0000000..3021bc0 --- /dev/null +++ b/src/pages/account/forgot-password/index.vue @@ -0,0 +1,5 @@ + diff --git a/src/pages/account/index.vue b/src/pages/account/index.vue index 45a26af..2ee8adc 100644 --- a/src/pages/account/index.vue +++ b/src/pages/account/index.vue @@ -1,5 +1,5 @@ From 106370836fba44928273e47f907e442282c9e80b Mon Sep 17 00:00:00 2001 From: niko <57009359+hauntii@users.noreply.github.com> Date: Fri, 25 Apr 2025 22:28:50 -0400 Subject: [PATCH 4/4] feat: configurable and optional hCaptcha sitekey --- nuxt.config.ts | 6 +++- src/pages/account/register/index.vue | 53 ++++++++++++++-------------- 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/nuxt.config.ts b/nuxt.config.ts index 574b3f9..9d1a71a 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -24,7 +24,11 @@ export default defineNuxtConfig({ }, runtimeConfig: { - apiBase: 'https://api.pretendo.cc' + apiBase: 'https://api.pretendo.cc', + + public: { + hCaptchaSitekey: '' + } }, css: ['~/assets/css/main.css'], diff --git a/src/pages/account/register/index.vue b/src/pages/account/register/index.vue index be309cf..3f19ce1 100644 --- a/src/pages/account/register/index.vue +++ b/src/pages/account/register/index.vue @@ -12,35 +12,33 @@ const errorMessage = ref(); const invisibleHcaptcha = ref(null); async function registerSubmission() { - if (invisibleHcaptcha.value) { - try { - const hCaptchaResponse = (await invisibleHcaptcha.value.executeAsync()).response; + try { + const hCaptchaResponse = invisibleHcaptcha.value ? (await invisibleHcaptcha.value.executeAsync()).response : null; - await $fetch('/api/account/register', { - method: 'POST', - body: { ...registerForm, hCaptchaResponse } - }); + await $fetch('/api/account/register', { + method: 'POST', + body: { ...registerForm, hCaptchaResponse } + }); - if (typeof redirect.value === 'string') { - await navigateTo(redirect.value); - } else { - await navigateTo('/account'); - } - } catch (error: unknown) { - if (error instanceof FetchError) { - errorMessage.value = error.statusText; - } else { - if (error === 'challenge-closed') { // Thrown if the captcha is closed, can be safely ignored - return; - } - - errorMessage.value = `Error during registration: ${error}`; // TODO: localize - } - - setTimeout(() => { // TODO: replace this toast - errorMessage.value = null; - }, 5000); + if (typeof redirect.value === 'string') { + await navigateTo(redirect.value); + } else { + await navigateTo('/account'); } + } catch (error: unknown) { + if (error instanceof FetchError) { + errorMessage.value = error.statusText; + } else { + if (error === 'challenge-closed') { // Thrown if the captcha is closed, can be safely ignored + return; + } + + errorMessage.value = `Error during registration: ${error}`; // TODO: localize + } + + setTimeout(() => { // TODO: replace this toast + errorMessage.value = null; + }, 5000); } } @@ -119,8 +117,9 @@ async function registerSubmission() {