chore: add logs for credentials login

This commit is contained in:
Meier Lukas
2024-10-27 11:00:48 +01:00
parent 5b23f7d13a
commit 0f7e661c31
4 changed files with 90 additions and 49 deletions

View File

@@ -1,7 +1,10 @@
import Consola from 'consola';
import { NextApiRequest, NextApiResponse } from 'next'; import { NextApiRequest, NextApiResponse } from 'next';
import NextAuth from 'next-auth'; import NextAuth from 'next-auth';
import { constructAuthOptions } from '~/server/auth'; import { constructAuthOptions } from '~/server/auth';
export default async function auth(req: NextApiRequest, res: NextApiResponse) { 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)); return await NextAuth(req, res, await constructAuthOptions(req, res));
} }

View File

@@ -1,19 +1,14 @@
import { TRPCError } from '@trpc/server'; import { TRPCError } from '@trpc/server';
import bcrypt from 'bcryptjs'; import bcrypt from 'bcryptjs';
import Consola from 'consola';
import { randomUUID } from 'crypto'; import { randomUUID } from 'crypto';
import { and, eq, like, sql } from 'drizzle-orm'; import { and, eq, like, sql } from 'drizzle-orm';
import { createSelectSchema } from 'drizzle-zod';
import { z } from 'zod'; import { z } from 'zod';
import { PossibleRoleFilter } from '~/pages/manage/users';
import { COOKIE_COLOR_SCHEME_KEY, COOKIE_LOCALE_KEY } from '../../../../data/constants';
import { adminProcedure, createTRPCRouter, protectedProcedure, publicProcedure } from '../trpc';
import { db } from '~/server/db'; import { db } from '~/server/db';
import { getTotalUserCountAsync } from '~/server/db/queries/user'; 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 { hashPassword } from '~/utils/security';
import { import {
colorSchemeParser, colorSchemeParser,
@@ -21,8 +16,9 @@ import {
signUpFormSchema, signUpFormSchema,
updateSettingsValidationSchema, updateSettingsValidationSchema,
} from '~/validations/user'; } 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({ export const userRouter = createTRPCRouter({
createOwnerAccount: publicProcedure.input(signUpFormSchema).mutation(async ({ ctx, input }) => { 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: { defaultSettings: {
colorScheme: colorSchemeParser.parse(ctx.cookies[COOKIE_COLOR_SCHEME_KEY]), colorScheme: colorSchemeParser.parse(ctx.cookies[COOKIE_COLOR_SCHEME_KEY]),
language: ctx.cookies[COOKIE_LOCALE_KEY] ?? 'en', language: ctx.cookies[COOKIE_LOCALE_KEY] ?? 'en',
}, },
isOwner: true, isOwner: true,
}); });
Consola.info(`Owner account created userId=${creationId}`);
}), }),
updatePassword: adminProcedure updatePassword: adminProcedure
.meta({ openapi: { method: 'PUT', path: '/users/password', tags: ['user'] } }) .meta({ openapi: { method: 'PUT', path: '/users/password', tags: ['user'] } })
@@ -48,7 +48,7 @@ export const userRouter = createTRPCRouter({
userId: z.string(), userId: z.string(),
newPassword: z.string().min(3), newPassword: z.string().min(3),
terminateExistingSessions: z.boolean(), terminateExistingSessions: z.boolean(),
}), })
) )
.output(z.void()) .output(z.void())
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
@@ -96,8 +96,8 @@ export const userRouter = createTRPCRouter({
signUpFormSchema.and( signUpFormSchema.and(
z.object({ z.object({
inviteToken: z.string(), inviteToken: z.string(),
}), })
), )
) )
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
const invite = await db.query.invites.findFirst({ const invite = await db.query.invites.findFirst({
@@ -129,7 +129,7 @@ export const userRouter = createTRPCRouter({
.input( .input(
z.object({ z.object({
colorScheme: colorSchemeParser, colorScheme: colorSchemeParser,
}), })
) )
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
await db await db
@@ -179,7 +179,7 @@ export const userRouter = createTRPCRouter({
.input( .input(
z.object({ z.object({
language: z.string(), language: z.string(),
}), })
) )
.output(z.void()) .output(z.void())
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
@@ -254,23 +254,26 @@ export const userRouter = createTRPCRouter({
.transform((value) => (value.length > 0 ? value : undefined)) .transform((value) => (value.length > 0 ? value : undefined))
.optional(), .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 }) => { .query(async ({ input }) => {
const roleFilter = () => { const roleFilter = () => {
if (input.search.role === PossibleRoleFilter[1].id) { if (input.search.role === PossibleRoleFilter[1].id) {
return eq(users.isOwner, true); return eq(users.isOwner, true);
@@ -291,13 +294,22 @@ export const userRouter = createTRPCRouter({
const dbUsers = await db.query.users.findMany({ const dbUsers = await db.query.users.findMany({
limit: limit + 1, limit: limit + 1,
offset: limit * input.page, 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 const countUsers = await db
.select({ count: sql<number>`count(*)` }) .select({ count: sql<number>`count(*)` })
.from(users) .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()) .where(roleFilter())
.then((rows) => rows[0].count); .then((rows) => rows[0].count);
@@ -351,7 +363,8 @@ export const userRouter = createTRPCRouter({
password: true, password: true,
salt: true, salt: true,
}) })
.optional()) .optional()
)
.query(async ({ input }) => { .query(async ({ input }) => {
return db.query.users.findFirst({ return db.query.users.findFirst({
where: eq(users.id, input.userId), where: eq(users.id, input.userId),
@@ -363,24 +376,32 @@ export const userRouter = createTRPCRouter({
}), }),
updateDetails: adminProcedure updateDetails: adminProcedure
.meta({ openapi: { method: 'PUT', path: '/users/details', tags: ['user'] } }) .meta({ openapi: { method: 'PUT', path: '/users/details', tags: ['user'] } })
.input(z.object({ .input(
userId: z.string(), z.object({
username: z.string(), userId: z.string(),
eMail: z.string().optional().transform(value => value?.length === 0 ? null : value), username: z.string(),
})) eMail: z
.string()
.optional()
.transform((value) => (value?.length === 0 ? null : value)),
})
)
.output(z.void()) .output(z.void())
.mutation(async ({ input }) => { .mutation(async ({ input }) => {
await db.update(users).set({ await db
name: input.username, .update(users)
email: input.eMail as string | null, .set({
}).where(eq(users.id, input.userId)); name: input.username,
email: input.eMail as string | null,
})
.where(eq(users.id, input.userId));
}), }),
deleteUser: adminProcedure deleteUser: adminProcedure
.meta({ openapi: { method: 'DELETE', path: '/users', tags: ['user'] } }) .meta({ openapi: { method: 'DELETE', path: '/users', tags: ['user'] } })
.input( .input(
z.object({ z.object({
id: z.string(), id: z.string(),
}), })
) )
.output(z.void()) .output(z.void())
.mutation(async ({ ctx, input }) => { .mutation(async ({ ctx, input }) => {
@@ -417,7 +438,7 @@ const createUserIfNotPresent = async (
options: { options: {
defaultSettings?: Partial<UserSettings>; defaultSettings?: Partial<UserSettings>;
isOwner?: boolean; isOwner?: boolean;
} | void, } | void
) => { ) => {
const existingUser = await db.query.users.findFirst({ const existingUser = await db.query.users.findFirst({
where: eq(users.name, input.username), where: eq(users.name, input.username),

View File

@@ -1,3 +1,4 @@
import Consola from 'consola';
import Cookies from 'cookies'; import Cookies from 'cookies';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import { type GetServerSidePropsContext, type NextApiRequest, type NextApiResponse } from 'next'; import { type GetServerSidePropsContext, type NextApiRequest, type NextApiResponse } from 'next';
@@ -62,18 +63,27 @@ export const constructAuthOptions = async (
return session; return session;
}, },
async signIn({ user }) { async signIn({ user }) {
Consola.info('User sign in');
// Check if this sign in callback is being called in the credentials authentication flow. // 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 // 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). // (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 sessionToken = generateSessionToken();
const sessionExpiry = fromDate(sessionMaxAgeInSeconds); const sessionExpiry = fromDate(sessionMaxAgeInSeconds);
// https://github.com/nextauthjs/next-auth/issues/6106 // https://github.com/nextauthjs/next-auth/issues/6106
if (!adapter?.createSession) { if (!adapter?.createSession) {
Consola.error('Adapter does not have createSession method');
return false; return false;
} }
@@ -88,11 +98,14 @@ export const constructAuthOptions = async (
expires: sessionExpiry, expires: sessionExpiry,
}); });
Consola.info('Session created');
return true; return true;
}, },
async redirect({ url, baseUrl }) { async redirect({ url, baseUrl }) {
const pathname = new URL(url, baseUrl).pathname; const pathname = new URL(url, baseUrl).pathname;
const redirectUrl = createRedirectUri(req.headers, pathname); const redirectUrl = createRedirectUri(req.headers, pathname);
Consola.info(`Redirecting to ${redirectUrl}`);
return redirectUrl; return redirectUrl;
}, },
}, },

View File

@@ -2,7 +2,7 @@ import bcrypt from 'bcryptjs';
import Consola from 'consola'; import Consola from 'consola';
import { eq } from 'drizzle-orm'; import { eq } from 'drizzle-orm';
import Credentials from 'next-auth/providers/credentials'; import Credentials from 'next-auth/providers/credentials';
import { colorSchemeParser, signInSchema } from '~/validations/user'; import { signInSchema } from '~/validations/user';
import { db } from '../../server/db'; import { db } from '../../server/db';
import { users } from '../../server/db/schema'; import { users } from '../../server/db/schema';
@@ -17,8 +17,11 @@ export default Credentials({
password: { label: 'Password', type: 'password' }, password: { label: 'Password', type: 'password' },
}, },
async authorize(credentials) { async authorize(credentials) {
Consola.info("Authorizing user's credentials...");
const data = await signInSchema.parseAsync(credentials); const data = await signInSchema.parseAsync(credentials);
Consola.info(`Checking if user ${data.name} exists...`);
const user = await db.query.users.findFirst({ const user = await db.query.users.findFirst({
with: { with: {
settings: { settings: {
@@ -33,6 +36,7 @@ export default Credentials({
}); });
if (!user || !user.password) { if (!user || !user.password) {
Consola.info(`user ${data.name} does not exist`);
return null; return null;
} }