diff --git a/src/pages/api/auth/[...nextauth].ts b/src/pages/api/auth/[...nextauth].ts index 8adfc426b..bc8d7536d 100644 --- a/src/pages/api/auth/[...nextauth].ts +++ b/src/pages/api/auth/[...nextauth].ts @@ -1,7 +1,10 @@ +import Consola from 'consola'; import { NextApiRequest, NextApiResponse } from 'next'; import NextAuth from 'next-auth'; import { constructAuthOptions } from '~/server/auth'; export default async function auth(req: NextApiRequest, res: NextApiResponse) { + const sanitizedUrl = req.url?.split('?')[0]; + Consola.info(`Authentication endpoint called method=${req.method} url=${sanitizedUrl}`); return await NextAuth(req, res, await constructAuthOptions(req, res)); } diff --git a/src/server/api/routers/user.ts b/src/server/api/routers/user.ts index eda464487..daae117d2 100644 --- a/src/server/api/routers/user.ts +++ b/src/server/api/routers/user.ts @@ -1,19 +1,14 @@ import { TRPCError } from '@trpc/server'; - import bcrypt from 'bcryptjs'; - +import Consola from 'consola'; import { randomUUID } from 'crypto'; - import { and, eq, like, sql } from 'drizzle-orm'; - +import { createSelectSchema } from 'drizzle-zod'; import { z } from 'zod'; - -import { COOKIE_COLOR_SCHEME_KEY, COOKIE_LOCALE_KEY } from '../../../../data/constants'; -import { adminProcedure, createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc'; - +import { PossibleRoleFilter } from '~/pages/manage/users'; import { db } from '~/server/db'; import { getTotalUserCountAsync } from '~/server/db/queries/user'; -import { invites, sessions, users, userSettings, UserSettings } from '~/server/db/schema'; +import { UserSettings, invites, sessions, userSettings, users } from '~/server/db/schema'; import { hashPassword } from '~/utils/security'; import { colorSchemeParser, @@ -21,8 +16,9 @@ import { signUpFormSchema, updateSettingsValidationSchema, } from '~/validations/user'; -import { PossibleRoleFilter } from '~/pages/manage/users'; -import { createSelectSchema } from 'drizzle-zod'; + +import { COOKIE_COLOR_SCHEME_KEY, COOKIE_LOCALE_KEY } from '../../../../data/constants'; +import { adminProcedure, createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc'; export const userRouter = createTRPCRouter({ createOwnerAccount: publicProcedure.input(signUpFormSchema).mutation(async ({ ctx, input }) => { @@ -33,13 +29,17 @@ export const userRouter = createTRPCRouter({ }); } - await createUserIfNotPresent(input, { + Consola.info('Creating owner account'); + + const creationId = await createUserIfNotPresent(input, { defaultSettings: { colorScheme: colorSchemeParser.parse(ctx.cookies[COOKIE_COLOR_SCHEME_KEY]), language: ctx.cookies[COOKIE_LOCALE_KEY] ?? 'en', }, isOwner: true, }); + + Consola.info(`Owner account created userId=${creationId}`); }), updatePassword: adminProcedure .meta({ openapi: { method: 'PUT', path: '/users/password', tags: ['user'] } }) @@ -48,7 +48,7 @@ export const userRouter = createTRPCRouter({ userId: z.string(), newPassword: z.string().min(3), terminateExistingSessions: z.boolean(), - }), + }) ) .output(z.void()) .mutation(async ({ input, ctx }) => { @@ -96,8 +96,8 @@ export const userRouter = createTRPCRouter({ signUpFormSchema.and( z.object({ inviteToken: z.string(), - }), - ), + }) + ) ) .mutation(async ({ ctx, input }) => { const invite = await db.query.invites.findFirst({ @@ -129,7 +129,7 @@ export const userRouter = createTRPCRouter({ .input( z.object({ colorScheme: colorSchemeParser, - }), + }) ) .mutation(async ({ ctx, input }) => { await db @@ -179,7 +179,7 @@ export const userRouter = createTRPCRouter({ .input( z.object({ language: z.string(), - }), + }) ) .output(z.void()) .mutation(async ({ ctx, input }) => { @@ -254,23 +254,26 @@ export const userRouter = createTRPCRouter({ .transform((value) => (value.length > 0 ? value : undefined)) .optional(), }), - }), + }) + ) + .output( + z.object({ + users: z.array( + z.object({ + id: z.string(), + name: z.string(), + email: z.string().or(z.null()).optional(), + isAdmin: z.boolean(), + isOwner: z.boolean(), + }) + ), + countPages: z.number().min(0), + stats: z.object({ + roles: z.record(z.number()), + }), + }) ) - .output(z.object({ - users: z.array(z.object({ - id: z.string(), - name: z.string(), - email: z.string().or(z.null()).optional(), - isAdmin: z.boolean(), - isOwner: z.boolean(), - })), - countPages: z.number().min(0), - stats: z.object({ - roles: z.record(z.number()), - }), - })) .query(async ({ input }) => { - const roleFilter = () => { if (input.search.role === PossibleRoleFilter[1].id) { return eq(users.isOwner, true); @@ -291,13 +294,22 @@ export const userRouter = createTRPCRouter({ const dbUsers = await db.query.users.findMany({ limit: limit + 1, offset: limit * input.page, - where: and(input.search.fullTextSearch ? like(users.name, `%${input.search.fullTextSearch}%`) : undefined, roleFilter()), + where: and( + input.search.fullTextSearch + ? like(users.name, `%${input.search.fullTextSearch}%`) + : undefined, + roleFilter() + ), }); const countUsers = await db .select({ count: sql`count(*)` }) .from(users) - .where(input.search.fullTextSearch ? like(users.name, `%${input.search.fullTextSearch}%`) : undefined) + .where( + input.search.fullTextSearch + ? like(users.name, `%${input.search.fullTextSearch}%`) + : undefined + ) .where(roleFilter()) .then((rows) => rows[0].count); @@ -351,7 +363,8 @@ export const userRouter = createTRPCRouter({ password: true, salt: true, }) - .optional()) + .optional() + ) .query(async ({ input }) => { return db.query.users.findFirst({ where: eq(users.id, input.userId), @@ -363,24 +376,32 @@ export const userRouter = createTRPCRouter({ }), updateDetails: adminProcedure .meta({ openapi: { method: 'PUT', path: '/users/details', tags: ['user'] } }) - .input(z.object({ - userId: z.string(), - username: z.string(), - eMail: z.string().optional().transform(value => value?.length === 0 ? null : value), - })) + .input( + z.object({ + userId: z.string(), + username: z.string(), + eMail: z + .string() + .optional() + .transform((value) => (value?.length === 0 ? null : value)), + }) + ) .output(z.void()) .mutation(async ({ input }) => { - await db.update(users).set({ - name: input.username, - email: input.eMail as string | null, - }).where(eq(users.id, input.userId)); + await db + .update(users) + .set({ + name: input.username, + email: input.eMail as string | null, + }) + .where(eq(users.id, input.userId)); }), deleteUser: adminProcedure .meta({ openapi: { method: 'DELETE', path: '/users', tags: ['user'] } }) .input( z.object({ id: z.string(), - }), + }) ) .output(z.void()) .mutation(async ({ ctx, input }) => { @@ -417,7 +438,7 @@ const createUserIfNotPresent = async ( options: { defaultSettings?: Partial; isOwner?: boolean; - } | void, + } | void ) => { const existingUser = await db.query.users.findFirst({ where: eq(users.name, input.username), diff --git a/src/server/auth.ts b/src/server/auth.ts index 9b7e19a93..d7e532cab 100644 --- a/src/server/auth.ts +++ b/src/server/auth.ts @@ -1,3 +1,4 @@ +import Consola from 'consola'; import Cookies from 'cookies'; import { eq } from 'drizzle-orm'; import { type GetServerSidePropsContext, type NextApiRequest, type NextApiResponse } from 'next'; @@ -62,18 +63,27 @@ export const constructAuthOptions = async ( return session; }, async signIn({ user }) { + Consola.info('User sign in'); + // Check if this sign in callback is being called in the credentials authentication flow. // If so, use the next-auth adapter to create a session entry in the database // (SignIn is called after authorize so we can safely assume the user is valid and already authenticated). - if (!isCredentialsRequest(req)) return true; + if (!isCredentialsRequest(req)) { + Consola.error('Not credentials request'); + return true; + } - if (!user) return true; + if (!user) { + Consola.error('No user'); + return true; + } const sessionToken = generateSessionToken(); const sessionExpiry = fromDate(sessionMaxAgeInSeconds); // https://github.com/nextauthjs/next-auth/issues/6106 if (!adapter?.createSession) { + Consola.error('Adapter does not have createSession method'); return false; } @@ -88,11 +98,14 @@ export const constructAuthOptions = async ( expires: sessionExpiry, }); + Consola.info('Session created'); + return true; }, async redirect({ url, baseUrl }) { const pathname = new URL(url, baseUrl).pathname; const redirectUrl = createRedirectUri(req.headers, pathname); + Consola.info(`Redirecting to ${redirectUrl}`); return redirectUrl; }, }, diff --git a/src/utils/auth/credentials.ts b/src/utils/auth/credentials.ts index 199c1e239..330fe1f58 100644 --- a/src/utils/auth/credentials.ts +++ b/src/utils/auth/credentials.ts @@ -2,7 +2,7 @@ import bcrypt from 'bcryptjs'; import Consola from 'consola'; import { eq } from 'drizzle-orm'; import Credentials from 'next-auth/providers/credentials'; -import { colorSchemeParser, signInSchema } from '~/validations/user'; +import { signInSchema } from '~/validations/user'; import { db } from '../../server/db'; import { users } from '../../server/db/schema'; @@ -17,8 +17,11 @@ export default Credentials({ password: { label: 'Password', type: 'password' }, }, async authorize(credentials) { + Consola.info("Authorizing user's credentials..."); const data = await signInSchema.parseAsync(credentials); + Consola.info(`Checking if user ${data.name} exists...`); + const user = await db.query.users.findFirst({ with: { settings: { @@ -33,6 +36,7 @@ export default Credentials({ }); if (!user || !user.password) { + Consola.info(`user ${data.name} does not exist`); return null; }