mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-02 03:26:03 +01:00
chore: add logs for credentials login
This commit is contained in:
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user