diff --git a/content/blog/12-25-25.md b/content/blog/12-25-25.md index 351422a..216fb2e 100644 --- a/content/blog/12-25-25.md +++ b/content/blog/12-25-25.md @@ -116,7 +116,7 @@ Thanks to the dedicated work by developer [Arian Kordi](https://github.com/arian While the last few months had a lot of focus on internal changes and Juxtaposition, game servers also saw a lot of attention! - [Dani](https://github.com/DaniElectra) has begun implementing the [`MessageDelivery` and `Messaging` protocols into our "common" module](https://github.com/PretendoNetwork/nex-protocols-common-go/pull/54). Once complete, these protocols should be widely available to all games -- [Dani](https://github.com/DaniElectra) Has begun [implementing the `MatchmakeExtension::GetPlayingSession`](https://github.com/PretendoNetwork/nex-protocols-common-go/pull/55) NEX method for use in multiplayer games +- [Dani](https://github.com/DaniElectra) Has begun [implementing the `MatchmakeExtension::​GetPlayingSession`](https://github.com/PretendoNetwork/nex-protocols-common-go/pull/55) NEX method for use in multiplayer games ![The Lobby area in Splatoon 1.](/assets/images/blogposts/december-25-2025/image6.png) *Splatoon, for its part, calls GetPlayingSession every time you enter the multiplayer lobby.* - [Trace](https://github.com/TraceEntertains) updated [Yo Kai Watch Blasters](https://github.com/PretendoNetwork/yo-kai-watch-blasters/pull/5) to the latest server libraries, including the above mentioned changes by [Dani](https://github.com/DaniElectra). With this update friend rooms and trading are now functional diff --git a/nuxt.config.ts b/nuxt.config.ts index 3833db3..6a786fa 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -156,6 +156,7 @@ export default defineNuxtConfig({ { name: 'application-name', content: 'Pretendo Network' }, { name: 'msapplication-TileColor', content: '#1b1f3b' }, { name: 'theme-color', content: '#1b1f3b' }, + { property: 'og:title', content: 'Pretendo Network' }, { property: 'og:description', content: diff --git a/server/api/auth/register.post.ts b/server/api/auth/register.post.ts index bfa81d4..e443307 100644 --- a/server/api/auth/register.post.ts +++ b/server/api/auth/register.post.ts @@ -21,11 +21,12 @@ const errors: Record = { 'INVALID_ARGUMENT: Two or more punctuation characters cannot be used in a row': 'USERNAME_INVALID_CHARS', 'INVALID_ARGUMENT: PNID already in use': 'USERNAME_IN_USE', 'INVALID_ARGUMENT: Mii name too long': 'MIINAME_TOO_LONG', - 'INVALID_ARGUMENT: Password must be between 6 and 16 characters long': 'INVALID_PASSWORD_LENGTH', - 'INVALID_ARGUMENT: Password cannot be the same as username': 'INVALID_PASSWORD_USERNAME', - 'INVALID_ARGUMENT: Password must have combination of letters, numbers, and/or punctuation characters': 'INVALID_PASSWORD_COMBOS', - 'INVALID_ARGUMENT: Password may not have 3 repeating characters': 'INVALID_PASSWORD_REPEATING', - 'INVALID_ARGUMENT: Passwords do not match': 'INVALID_PASSWORD_NO_MATCH' + 'INVALID_ARGUMENT: Password must be between 6 and 16 characters long': 'PASSWORD_INVALID_LENGTH', + 'INVALID_ARGUMENT: Password cannot be the same as username': 'PASSWORD_NOT_USERNAME', + 'INVALID_ARGUMENT: Password must have combination of letters, numbers, and/or punctuation characters': 'PASSWORD_NEEDS_CHARS', + 'INVALID_ARGUMENT: Password may not have 3 repeating characters': 'PASSWORD_REPEATED_CHARS', + 'INVALID_ARGUMENT: Passwords do not match': 'PASSWORDS_DO_NOT_MATCH' + }; function getCutoffDateForAge(today: Date, age: number) { diff --git a/server/api/auth/reset-password.post.ts b/server/api/auth/reset-password.post.ts index ea77ebd..b2a6492 100644 --- a/server/api/auth/reset-password.post.ts +++ b/server/api/auth/reset-password.post.ts @@ -1,19 +1,45 @@ import { ResetPasswordSchema } from '~~/shared/api-types'; +import type { ApiErrorCodes } from '~~/shared/errors'; + +const errors: Record = { + 'Missing token': 'INVALID_INPUT', + 'Invalid token': 'INVALID_INPUT', + 'Token expired': 'INVALID_INPUT', + 'Invalid token. No user found': 'INVALID_INPUT', + 'Must enter a password': 'PASSWORD_INVALID_LENGTH', + 'Password is too long': 'PASSWORD_INVALID_LENGTH', + 'Password is too short': 'PASSWORD_INVALID_LENGTH', + 'Password cannot be the same as username': 'PASSWORD_NOT_USERNAME', + 'Password must have combination of letters, numbers, and/or punctuation characters': 'PASSWORD_NEEDS_CHARS', + 'Password may not have 3 repeating characters': 'PASSWORD_REPEATED_CHARS', + 'Passwords do not match': 'PASSWORDS_DO_NOT_MATCH' +}; export default defineEventHandler(async (event): Promise => { const body = await readZodBody(event, ResetPasswordSchema); const apiFetch = useHttpApi(event); // The GRPC version requires a login token, which the user doesnt have when resetting password, so we're using the HTTP api - await apiFetch('/v1/reset-password', { - method: 'POST', - body: JSON.stringify({ - password: body.password, - password_confirm: body.passwordConfirm, - token: body.resetToken - }), - headers: { - 'Content-type': 'application/json' + try { + await apiFetch('/v1/reset-password', { + method: 'POST', + body: JSON.stringify({ + password: body.password, + password_confirm: body.passwordConfirm, + token: body.resetToken + }), + headers: { + 'Content-type': 'application/json' + } + }); + } catch (err: any) { + const data = err?.data; + let errorCode: ApiErrorCodes = 'UNHANDLED_ERROR'; + + if (typeof data === 'object' && data?.error) { + errorCode = errors[data.error] ?? 'UNHANDLED_ERROR'; } - }); + + throw createApiError(errorCode); + } }); diff --git a/server/plugins/log-errors.ts b/server/plugins/log-errors.ts new file mode 100644 index 0000000..ab1b6f6 --- /dev/null +++ b/server/plugins/log-errors.ts @@ -0,0 +1,18 @@ +import type { H3Error } from 'h3'; + +export default defineNitroPlugin((nitroApp) => { + nitroApp.hooks.hook('error', (err, { event }) => { + const error = err as Partial; + const statusCode = error.statusCode ?? 500; + + // Skip expected client errors (404, 401, 422, redirects, ...). + if (statusCode < 500) { + return; + } + + const where = event ? `${event.method} ${event.path}` : 'non-request'; + const tag = error.unhandled ? '[unhandled]' : ''; + + console.error(`[server error] ${tag} ${statusCode} ${where}:`, err); + }); +}); diff --git a/server/utils/httpApi.ts b/server/utils/httpApi.ts index 28f9264..e819396 100644 --- a/server/utils/httpApi.ts +++ b/server/utils/httpApi.ts @@ -27,7 +27,11 @@ export function useHttpApi(event: H3Event, token?: string): HttpApiFetch { if (response.statusCode >= 400) { const err = new Error(`Request failed with ${response.statusCode}`); try { - (err as any).data = await response.body.text(); + if (response.headers['content-type']?.includes('application/json')) { + (err as any).data = await response.body.json(); + } else { + (err as any).data = await response.body.text(); + } } catch { // It's already errored, we don't need to know the body } diff --git a/shared/errors.ts b/shared/errors.ts index d1d9fa6..e8147b6 100644 --- a/shared/errors.ts +++ b/shared/errors.ts @@ -16,11 +16,11 @@ const apiErrorCodes = { USERNAME_INVALID_CHARS: 'Username contains invalid characters', USERNAME_IN_USE: 'PNID already in use', MIINAME_TOO_LONG: 'Mii name too long', - INVALID_PASSWORD_LENGTH: 'Password must be between 6 and 16 characters long', - INVALID_PASSWORD_USERNAME: 'Password cannot be the same as username', - INVALID_PASSWORD_COMBOS: 'Password must have combination of letters, numbers, and/or punctuation characters', - INVALID_PASSWORD_REPEATING: 'Password may not have 3 repeating characters', - INVALID_PASSWORD_NO_MATCH: 'Passwords do not match', + PASSWORD_INVALID_LENGTH: 'Password must be between 6 and 16 characters long', + PASSWORD_NOT_USERNAME: 'Password cannot be the same as your username', + PASSWORD_NEEDS_CHARS: 'Password must have combination of letters, numbers, and/or punctuation characters', + PASSWORDS_DO_NOT_MATCH: 'Passwords do not match', + PASSWORD_REPEATED_CHARS: 'Password may not have 3 repeating characters', ACCOUNT_DELETED: 'Account has been deleted', INVALID_ACCESS_LEVEL: 'Invalid access level', BANNED: 'Account is banned', @@ -51,7 +51,11 @@ export const apiErrorCodeStatus: Record = { UNDER_THIRTEEN: 400, ACCOUNT_DELETED: 400, INVALID_EMAIL: 400, - INVALID_PASSWORD_NO_MATCH: 400, + PASSWORD_INVALID_LENGTH: 400, + PASSWORD_NOT_USERNAME: 400, + PASSWORD_NEEDS_CHARS: 400, + PASSWORDS_DO_NOT_MATCH: 400, + PASSWORD_REPEATED_CHARS: 400, MIINAME_TOO_LONG: 400, USERNAME_IN_USE: 400, USERNAME_INVALID_CHARS: 400, @@ -67,11 +71,7 @@ export const apiErrorCodeStatus: Record = { INVALID_MII_DATA: 400, EMAIL_UNCHANGED: 400, INVALID_EMAIL_TOKEN: 400, - MISSING_EMAIL_TOKEN: 400, - INVALID_PASSWORD_LENGTH: 400, - INVALID_PASSWORD_USERNAME: 400, - INVALID_PASSWORD_COMBOS: 400, - INVALID_PASSWORD_REPEATING: 400 + MISSING_EMAIL_TOKEN: 400 }; export function getTextForApiErrorCode(code: ApiErrorCodes): string { diff --git a/src/composables/customSeoMeta.ts b/src/composables/customSeoMeta.ts new file mode 100644 index 0000000..fa4f897 --- /dev/null +++ b/src/composables/customSeoMeta.ts @@ -0,0 +1,36 @@ +import type { UseSeoMetaInput } from '@unhead/vue'; + +export type CustomSeoMetaOptions = UseSeoMetaInput & { + subsection?: string; +}; + +export default function (args?: CustomSeoMetaOptions) { + let newTitleTemplate = ''; + + switch (args?.subsection) { + case ('account'): { + newTitleTemplate = 'Account | Pretendo Network'; + break; + } + case ('terms'): { + newTitleTemplate = 'Terms | Pretendo Network'; + break; + } + case ('blog'): { + newTitleTemplate = 'Blog | Pretendo Network'; + break; + } + case ('docs'): { + newTitleTemplate = 'Docs | Pretendo Network'; + break; + } + default: { + newTitleTemplate = 'Pretendo Network'; + } + } + + const newTitle = args?.title ? `${args?.title} | ${newTitleTemplate}` : newTitleTemplate; + + useHead({ title: newTitle }); + useSeoMeta({ ...args, title: newTitle, ogTitle: newTitle, twitterTitle: newTitle, ogDescription: args?.description, twitterDescription: args?.description, twitterImage: args?.ogImage }); +} diff --git a/src/error.vue b/src/error.vue index cfb6106..f480546 100644 --- a/src/error.vue +++ b/src/error.vue @@ -3,9 +3,11 @@ import { watchImmediate } from '@vueuse/core'; import DefaultLayout from './layouts/default.vue'; const { error } = defineProps<{ error: any }>(); -watchImmediate([error], () => { - console.error(error); -}); +if (import.meta.client) { + watchImmediate([() => error], () => { + console.error(error); + }); +}