Merge branch 'dev' into ajnart/fix-duplicate-users

This commit is contained in:
Meier Lukas
2024-05-19 23:08:04 +02:00
288 changed files with 11536 additions and 5631 deletions

View File

@@ -11,60 +11,58 @@ import { validation, z } from "@homarr/validation";
import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
export const userRouter = createTRPCRouter({
initUser: publicProcedure
.input(validation.user.init)
.mutation(async ({ ctx, input }) => {
const firstUser = await ctx.db.query.users.findFirst({
columns: {
id: true,
},
initUser: publicProcedure.input(validation.user.init).mutation(async ({ ctx, input }) => {
const firstUser = await ctx.db.query.users.findFirst({
columns: {
id: true,
},
});
if (firstUser) {
throw new TRPCError({
code: "FORBIDDEN",
message: "User already exists",
});
}
if (firstUser) {
throw new TRPCError({
code: "FORBIDDEN",
message: "User already exists",
});
}
await createUserAsync(ctx.db, input);
}),
register: publicProcedure.input(validation.user.registrationApi).mutation(async ({ ctx, input }) => {
const inviteWhere = and(eq(invites.id, input.inviteId), eq(invites.token, input.token));
const dbInvite = await ctx.db.query.invites.findFirst({
columns: {
id: true,
expirationDate: true,
},
where: inviteWhere,
});
await createUserAsync(ctx.db, input);
}),
register: publicProcedure
.input(validation.user.registrationApi)
.mutation(async ({ ctx, input }) => {
const inviteWhere = and(
eq(invites.id, input.inviteId),
eq(invites.token, input.token),
);
const dbInvite = await ctx.db.query.invites.findFirst({
columns: {
id: true,
expirationDate: true,
},
where: inviteWhere,
if (!dbInvite || dbInvite.expirationDate < new Date()) {
throw new TRPCError({
code: "FORBIDDEN",
message: "Invalid invite",
});
}
if (!dbInvite || dbInvite.expirationDate < new Date()) {
throw new TRPCError({
code: "FORBIDDEN",
message: "Invalid invite",
});
}
if (!dbInvite || dbInvite.expirationDate < new Date()) {
throw new TRPCError({
code: "FORBIDDEN",
message: "Invalid invite",
});
}
await checkUsernameAlreadyTakenAndThrowAsync(ctx.db, input.username);
await checkUsernameAlreadyTakenAndThrowAsync(ctx.db, input.username);
await createUserAsync(ctx.db, input);
await createUserAsync(ctx.db, input);
// Delete invite as it's used
await ctx.db.delete(invites).where(inviteWhere);
}),
create: publicProcedure
.input(validation.user.create)
.mutation(async ({ ctx, input }) => {
await checkUsernameAlreadyTakenAndThrowAsync(ctx.db, input.username);
// Delete invite as it's used
await ctx.db.delete(invites).where(inviteWhere);
}),
create: publicProcedure.input(validation.user.create).mutation(async ({ ctx, input }) => {
await checkUsernameAlreadyTakenAndThrowAsync(ctx.db, input.username);
await createUserAsync(ctx.db, input);
}),
await createUserAsync(ctx.db, input);
}),
setProfileImage: protectedProcedure
.input(
z.object({
@@ -79,10 +77,7 @@ export const userRouter = createTRPCRouter({
)
.mutation(async ({ input, ctx }) => {
// Only admins can change other users profile images
if (
ctx.session.user.id !== input.userId &&
!ctx.session.user.permissions.includes("admin")
) {
if (ctx.session.user.id !== input.userId && !ctx.session.user.permissions.includes("admin")) {
throw new TRPCError({
code: "FORBIDDEN",
message: "You are not allowed to change other users profile images",
@@ -131,118 +126,105 @@ export const userRouter = createTRPCRouter({
},
});
}),
getById: publicProcedure
.input(z.object({ userId: z.string() }))
.query(async ({ input, ctx }) => {
const user = await ctx.db.query.users.findFirst({
getById: publicProcedure.input(z.object({ userId: z.string() })).query(async ({ input, ctx }) => {
const user = await ctx.db.query.users.findFirst({
columns: {
id: true,
name: true,
email: true,
emailVerified: true,
image: true,
},
where: eq(users.id, input.userId),
});
if (!user) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
});
}
return user;
}),
editProfile: publicProcedure.input(validation.user.editProfile).mutation(async ({ input, ctx }) => {
const user = await ctx.db.query.users.findFirst({
columns: { email: true },
where: eq(users.id, input.id),
});
if (!user) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
});
}
await checkUsernameAlreadyTakenAndThrowAsync(ctx.db, input.name, input.id);
const emailDirty = input.email && user.email !== input.email;
await ctx.db
.update(users)
.set({
name: input.name,
email: emailDirty === true ? input.email : undefined,
emailVerified: emailDirty === true ? null : undefined,
})
.where(eq(users.id, input.id));
}),
delete: publicProcedure.input(z.string()).mutation(async ({ input, ctx }) => {
await ctx.db.delete(users).where(eq(users.id, input));
}),
changePassword: protectedProcedure.input(validation.user.changePasswordApi).mutation(async ({ ctx, input }) => {
const user = ctx.session.user;
// Only admins can change other users' passwords
if (!user.permissions.includes("admin") && user.id !== input.userId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
});
}
// Admins can change the password of other users without providing the previous password
const isPreviousPasswordRequired = ctx.session.user.id === input.userId;
if (isPreviousPasswordRequired) {
const dbUser = await ctx.db.query.users.findFirst({
columns: {
id: true,
name: true,
email: true,
emailVerified: true,
image: true,
password: true,
salt: true,
},
where: eq(users.id, input.userId),
});
if (!user) {
if (!dbUser) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
});
}
return user;
}),
editProfile: publicProcedure
.input(validation.user.editProfile)
.mutation(async ({ input, ctx }) => {
const user = await ctx.db.query.users.findFirst({
columns: { email: true },
where: eq(users.id, input.id),
});
const previousPasswordHash = await hashPasswordAsync(input.previousPassword, dbUser.salt ?? "");
const isValid = previousPasswordHash === dbUser.password;
if (!user) {
if (!isValid) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
code: "FORBIDDEN",
message: "Invalid password",
});
}
}
await checkUsernameAlreadyTakenAndThrowAsync(
ctx.db,
input.name,
input.id,
);
const emailDirty = input.email && user.email !== input.email;
await ctx.db
.update(users)
.set({
name: input.name,
email: emailDirty === true ? input.email : undefined,
emailVerified: emailDirty === true ? null : undefined,
})
.where(eq(users.id, input.id));
}),
delete: publicProcedure.input(z.string()).mutation(async ({ input, ctx }) => {
await ctx.db.delete(users).where(eq(users.id, input));
const salt = await createSaltAsync();
const hashedPassword = await hashPasswordAsync(input.password, salt);
await ctx.db
.update(users)
.set({
password: hashedPassword,
})
.where(eq(users.id, input.userId));
}),
changePassword: protectedProcedure
.input(validation.user.changePasswordApi)
.mutation(async ({ ctx, input }) => {
const user = ctx.session.user;
// Only admins can change other users' passwords
if (!user.permissions.includes("admin") && user.id !== input.userId) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
});
}
// Admins can change the password of other users without providing the previous password
const isPreviousPasswordRequired = ctx.session.user.id === input.userId;
if (isPreviousPasswordRequired) {
const dbUser = await ctx.db.query.users.findFirst({
columns: {
id: true,
password: true,
salt: true,
},
where: eq(users.id, input.userId),
});
if (!dbUser) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
});
}
const previousPasswordHash = await hashPasswordAsync(
input.previousPassword,
dbUser.salt ?? "",
);
const isValid = previousPasswordHash === dbUser.password;
if (!isValid) {
throw new TRPCError({
code: "FORBIDDEN",
message: "Invalid password",
});
}
}
const salt = await createSaltAsync();
const hashedPassword = await hashPasswordAsync(input.password, salt);
await ctx.db
.update(users)
.set({
password: hashedPassword,
})
.where(eq(users.id, input.userId));
}),
setMessage: publicProcedure.input(z.string()).mutation(async ({ input }) => {
await exampleChannel.publishAsync({ message: input });
}),
@@ -255,10 +237,7 @@ export const userRouter = createTRPCRouter({
}),
});
const createUserAsync = async (
db: Database,
input: z.infer<typeof validation.user.create>,
) => {
const createUserAsync = async (db: Database, input: z.infer<typeof validation.user.create>) => {
const salt = await createSaltAsync();
const hashedPassword = await hashPasswordAsync(input.password, salt);
@@ -268,16 +247,13 @@ const createUserAsync = async (
await db.insert(schema.users).values({
id: userId,
name: username,
email: input.email,
password: hashedPassword,
salt,
});
};
const checkUsernameAlreadyTakenAndThrowAsync = async (
db: Database,
username: string,
ignoreId?: string,
) => {
const checkUsernameAlreadyTakenAndThrowAsync = async (db: Database, username: string, ignoreId?: string) => {
const user = await db.query.users.findFirst({
where: eq(users.name, username.toLowerCase()),
});