From cad9f6960c12e401cce070eb2bd249ed780d5625 Mon Sep 17 00:00:00 2001 From: Manuel <30572287+manuel-rw@users.noreply.github.com> Date: Fri, 11 Oct 2024 23:42:18 +0200 Subject: [PATCH] feat: #1243 add api routes --- packages/api/src/router/app.ts | 75 ++- packages/api/src/router/invite.ts | 43 +- packages/api/src/router/serverSettings.ts | 7 +- packages/api/src/router/user.ts | 466 ++++++++++-------- packages/db/package.json | 4 +- packages/db/validationSchemas/app.ts | 4 + packages/db/validationSchemas/board.ts | 4 + packages/db/validationSchemas/group.ts | 4 + packages/db/validationSchemas/invite.ts | 4 + packages/db/validationSchemas/searchEngine.ts | 4 + .../db/validationSchemas/serverSettings.ts | 4 + packages/db/validationSchemas/user.ts | 4 + pnpm-lock.yaml | 18 +- 13 files changed, 362 insertions(+), 279 deletions(-) create mode 100644 packages/db/validationSchemas/app.ts create mode 100644 packages/db/validationSchemas/board.ts create mode 100644 packages/db/validationSchemas/group.ts create mode 100644 packages/db/validationSchemas/invite.ts create mode 100644 packages/db/validationSchemas/searchEngine.ts create mode 100644 packages/db/validationSchemas/serverSettings.ts create mode 100644 packages/db/validationSchemas/user.ts diff --git a/packages/api/src/router/app.ts b/packages/api/src/router/app.ts index 1987671fd..e260b2d6d 100644 --- a/packages/api/src/router/app.ts +++ b/packages/api/src/router/app.ts @@ -5,19 +5,14 @@ import { apps } from "@homarr/db/schema/sqlite"; import { validation, z } from "@homarr/validation"; import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; +import { selectAppSchema } from "@homarr/db/validationSchemas/app"; export const appRouter = createTRPCRouter({ all: publicProcedure .input(z.void()) .output( z.array( - z.object({ - name: z.string(), - id: z.string(), - description: z.string().nullable(), - iconUrl: z.string(), - href: z.string().nullable(), - }), + selectAppSchema ), ) .meta({ openapi: { method: "GET", path: "/api/apps", tags: ["apps"], protect: true } }) @@ -30,13 +25,7 @@ export const appRouter = createTRPCRouter({ .input(z.object({ query: z.string(), limit: z.number().min(1).max(100).default(10) })) .output( z.array( - z.object({ - name: z.string(), - id: z.string(), - description: z.string().nullable(), - iconUrl: z.string(), - href: z.string().nullable(), - }), + selectAppSchema, ), ) .meta({ openapi: { method: "GET", path: "/api/apps/search", tags: ["apps"], protect: true } }) @@ -51,11 +40,7 @@ export const appRouter = createTRPCRouter({ .input(z.void()) .output( z.array( - z.object({ - name: z.string(), - id: z.string(), - iconUrl: z.string(), - }), + selectAppSchema.pick({ id: true, name: true, iconUrl: true }), ), ) .meta({ @@ -79,13 +64,7 @@ export const appRouter = createTRPCRouter({ byId: publicProcedure .input(validation.common.byId) .output( - z.object({ - name: z.string(), - id: z.string(), - description: z.string().nullable(), - iconUrl: z.string(), - href: z.string().nullable(), - }), + selectAppSchema, ) .meta({ openapi: { method: "GET", path: "/api/apps/{id}", tags: ["apps"], protect: true } }) .query(async ({ ctx, input }) => { @@ -115,28 +94,32 @@ export const appRouter = createTRPCRouter({ href: input.href, }); }), - update: protectedProcedure.input(validation.app.edit).mutation(async ({ ctx, input }) => { - const app = await ctx.db.query.apps.findFirst({ - where: eq(apps.id, input.id), - }); - - if (!app) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "App not found", + update: protectedProcedure + .input(validation.app.edit) + .output(z.void()) + .meta({ openapi: { method: "PATCH", path: "/api/apps/{id}", tags: ["apps"], protect: true } }) + .mutation(async ({ ctx, input }) => { + const app = await ctx.db.query.apps.findFirst({ + where: eq(apps.id, input.id), }); - } - await ctx.db - .update(apps) - .set({ - name: input.name, - description: input.description, - iconUrl: input.iconUrl, - href: input.href, - }) - .where(eq(apps.id, input.id)); - }), + if (!app) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "App not found", + }); + } + + await ctx.db + .update(apps) + .set({ + name: input.name, + description: input.description, + iconUrl: input.iconUrl, + href: input.href, + }) + .where(eq(apps.id, input.id)); + }), delete: protectedProcedure .output(z.void()) .meta({ openapi: { method: "DELETE", path: "/api/apps/{id}", tags: ["apps"], protect: true } }) diff --git a/packages/api/src/router/invite.ts b/packages/api/src/router/invite.ts index 9285ea4d2..f8e48590f 100644 --- a/packages/api/src/router/invite.ts +++ b/packages/api/src/router/invite.ts @@ -7,32 +7,41 @@ import { z } from "@homarr/validation"; import { createTRPCRouter, protectedProcedure } from "../trpc"; import { throwIfCredentialsDisabled } from "./invite/checks"; +import { selectInviteSchema } from "@homarr/db/validationSchemas/invite"; export const inviteRouter = createTRPCRouter({ - getAll: protectedProcedure.query(async ({ ctx }) => { - throwIfCredentialsDisabled(); - const dbInvites = await ctx.db.query.invites.findMany({ - orderBy: asc(invites.expirationDate), - columns: { - token: false, - }, - with: { - creator: { - columns: { - id: true, - name: true, + getAll: protectedProcedure + .output(z.array(selectInviteSchema.pick({ + id: true, + expirationDate: true + }).extend({ creator: z.object({ name: z.string().nullable(), id: z.string() }) }))) + .input(z.undefined()) + .meta({ openapi: { method: "GET", path: "/api/invites", tags: ["invites"], protect: true } }) + .query(async ({ ctx }) => { + throwIfCredentialsDisabled(); + return await ctx.db.query.invites.findMany({ + orderBy: asc(invites.expirationDate), + columns: { + token: false, + }, + with: { + creator: { + columns: { + id: true, + name: true, + }, }, }, - }, - }); - return dbInvites; - }), + }); + }), createInvite: protectedProcedure .input( z.object({ expirationDate: z.date(), }), ) + .output(z.object({ id: z.string(), token: z.string() })) + .meta({ openapi: { method: "GET", path: "/api/invites", tags: ["invites"], protect: true } }) .mutation(async ({ ctx, input }) => { throwIfCredentialsDisabled(); const id = createId(); @@ -56,6 +65,8 @@ export const inviteRouter = createTRPCRouter({ id: z.string(), }), ) + .output(z.undefined()) + .meta({ openapi: { method: "DELETE", path: "/api/invites/{id}", tags: ["invites"], protect: true } }) .mutation(async ({ ctx, input }) => { throwIfCredentialsDisabled(); const dbInvite = await ctx.db.query.invites.findFirst({ diff --git a/packages/api/src/router/serverSettings.ts b/packages/api/src/router/serverSettings.ts index b1f00f057..9905bda74 100644 --- a/packages/api/src/router/serverSettings.ts +++ b/packages/api/src/router/serverSettings.ts @@ -11,7 +11,9 @@ import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc"; export const serverSettingsRouter = createTRPCRouter({ // this must be public so anonymous users also get analytics - getAnalytics: publicProcedure.query(async ({ ctx }) => { + getAnalytics: publicProcedure + .input(z.undefined()) + .query(async ({ ctx }) => { const setting = await ctx.db.query.serverSettings.findFirst({ where: eq(serverSettings.settingKey, "analytics"), }); @@ -30,7 +32,8 @@ export const serverSettingsRouter = createTRPCRouter({ return SuperJSON.parse<(typeof defaultServerSettings)["analytics"]>(setting.value); }), - getAll: protectedProcedure.query(async ({ ctx }) => { + getAll: protectedProcedure + .query(async ({ ctx }) => { const settings = await ctx.db.query.serverSettings.findMany(); const data = {} as ServerSettings; diff --git a/packages/api/src/router/user.ts b/packages/api/src/router/user.ts index 805140540..4e16f5726 100644 --- a/packages/api/src/router/user.ts +++ b/packages/api/src/router/user.ts @@ -10,65 +10,71 @@ import { validation, z } from "@homarr/validation"; import { createTRPCRouter, permissionRequiredProcedure, protectedProcedure, publicProcedure } from "../trpc"; import { throwIfCredentialsDisabled } from "./invite/checks"; +import { selectUserSchema } from "@homarr/db/validationSchemas/user"; export const userRouter = createTRPCRouter({ - initUser: publicProcedure.input(validation.user.init).mutation(async ({ ctx, input }) => { - throwIfCredentialsDisabled(); - - const firstUser = await ctx.db.query.users.findFirst({ - columns: { - id: true, - }, - }); + initUser: publicProcedure + .input(validation.user.init) + .mutation(async ({ ctx, input }) => { + throwIfCredentialsDisabled(); - if (firstUser) { - throw new TRPCError({ - code: "FORBIDDEN", - message: "User already exists", + const firstUser = await ctx.db.query.users.findFirst({ + columns: { + id: true, + }, }); - } - const userId = await createUserAsync(ctx.db, input); - const groupId = createId(); - await ctx.db.insert(groups).values({ - id: groupId, - name: "admin", - ownerId: userId, - }); - await ctx.db.insert(groupPermissions).values({ - groupId, - permission: "admin", - }); - await ctx.db.insert(groupMembers).values({ - groupId, - userId, - }); - }), - register: publicProcedure.input(validation.user.registrationApi).mutation(async ({ ctx, input }) => { - throwIfCredentialsDisabled(); - 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 (firstUser) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "User already exists", + }); + } - if (!dbInvite || dbInvite.expirationDate < new Date()) { - throw new TRPCError({ - code: "FORBIDDEN", - message: "Invalid invite", + const userId = await createUserAsync(ctx.db, input); + const groupId = createId(); + await ctx.db.insert(groups).values({ + id: groupId, + name: "admin", + ownerId: userId, }); - } + await ctx.db.insert(groupPermissions).values({ + groupId, + permission: "admin", + }); + await ctx.db.insert(groupMembers).values({ + groupId, + userId, + }); + }), + register: publicProcedure + .input(validation.user.registrationApi) + .output(z.void()) + .mutation(async ({ ctx, input }) => { + throwIfCredentialsDisabled(); + 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", + }); + } - await checkUsernameAlreadyTakenAndThrowAsync(ctx.db, "credentials", input.username); + await checkUsernameAlreadyTakenAndThrowAsync(ctx.db, "credentials", 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); - }), + // Delete invite as it's used + await ctx.db.delete(invites).where(inviteWhere); + }), create: permissionRequiredProcedure .requiresPermission("admin") .meta({ openapi: { method: "POST", path: "/api/users", tags: ["users"], protect: true } }) @@ -81,6 +87,8 @@ export const userRouter = createTRPCRouter({ await createUserAsync(ctx.db, input); }), setProfileImage: protectedProcedure + .output(z.void()) + .meta({ openapi: { method: "PUT", path: "/api/users/profileImage", tags: ["users"], protect: true } }) .input( z.object({ userId: z.string(), @@ -136,13 +144,7 @@ export const userRouter = createTRPCRouter({ .input(z.void()) .output( z.array( - z.object({ - id: z.string(), - name: z.string().nullable(), - email: z.string().nullable(), - emailVerified: z.date().nullable(), - image: z.string().nullable(), - }), + selectUserSchema.pick({ id: true, name: true, email: true, emailVerified: true, image: true }) ), ) .meta({ openapi: { method: "GET", path: "/api/users", tags: ["users"], protect: true } }) @@ -158,15 +160,19 @@ export const userRouter = createTRPCRouter({ }); }), // Is protected because also used in board access / integration access forms - selectable: protectedProcedure.query(({ ctx }) => { - return ctx.db.query.users.findMany({ - columns: { - id: true, - name: true, - image: true, - }, - }); - }), + selectable: protectedProcedure + .input(z.undefined()) + .output(z.array(selectUserSchema.pick({ id: true, name: true, image: true }))) + .meta({ openapi: { method: "GET", path: "/api/users/selectable", tags: ["users"], protect: true } }) + .query(({ ctx }) => { + return ctx.db.query.users.findMany({ + columns: { + id: true, + name: true, + image: true, + }, + }); + }), search: permissionRequiredProcedure .requiresPermission("admin") .input( @@ -175,6 +181,8 @@ export const userRouter = createTRPCRouter({ limit: z.number().min(1).max(100).default(10), }), ) + .output(z.array(selectUserSchema.pick({ id: true, name: true, image: true }))) + .meta({ openapi: { method: "POST", path: "/api/users/search", tags: ["users"], protect: true } }) .query(async ({ input, ctx }) => { const dbUsers = await ctx.db.query.users.findMany({ columns: { @@ -191,153 +199,179 @@ export const userRouter = createTRPCRouter({ image: user.image, })); }), - getById: protectedProcedure.input(z.object({ userId: z.string() })).query(async ({ input, ctx }) => { - // Only admins can view other users details - if (ctx.session.user.id !== input.userId && !ctx.session.user.permissions.includes("admin")) { - throw new TRPCError({ - code: "FORBIDDEN", - message: "You are not allowed to view other users details", - }); - } - const user = await ctx.db.query.users.findFirst({ - columns: { - id: true, - name: true, - email: true, - emailVerified: true, - image: true, - provider: true, - homeBoardId: true, - firstDayOfWeek: true, - pingIconsEnabled: true, - }, - where: eq(users.id, input.userId), - }); - - if (!user) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "User not found", + getById: protectedProcedure + .input(z.object({ userId: z.string() })) + .output(selectUserSchema.pick({ + id: true, + name: true, + email: true, + emailVerified: true, + image: true, + provider: true, + homeBoardId: true, + firstDayOfWeek: true, + pingIconsEnabled: true, + })) + .meta({ openapi: { method: "GET", path: "/api/users/{userId}", tags: ["users"], protect: true } }) + .query(async ({ input, ctx }) => { + // Only admins can view other users details + if (ctx.session.user.id !== input.userId && !ctx.session.user.permissions.includes("admin")) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "You are not allowed to view other users details", + }); + } + const user = await ctx.db.query.users.findFirst({ + columns: { + id: true, + name: true, + email: true, + emailVerified: true, + image: true, + provider: true, + homeBoardId: true, + firstDayOfWeek: true, + }, + where: eq(users.id, input.userId), }); - } - return user; - }), - editProfile: protectedProcedure.input(validation.user.editProfile).mutation(async ({ input, ctx }) => { - // Only admins can view other users details - if (ctx.session.user.id !== input.id && !ctx.session.user.permissions.includes("admin")) { - throw new TRPCError({ - code: "FORBIDDEN", - message: "You are not allowed to edit other users details", - }); - } + if (!user) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "User not found", + }); + } - const user = await ctx.db.query.users.findFirst({ - columns: { email: true, provider: true }, - where: eq(users.id, input.id), - }); + return user; + }), + editProfile: protectedProcedure + .input(validation.user.editProfile) + .output(z.void()) + .meta({ openapi: { method: "PUT", path: "/api/users/profile", tags: ["users"], protect: true } }) + .mutation(async ({ input, ctx }) => { + // Only admins can view other users details + if (ctx.session.user.id !== input.id && !ctx.session.user.permissions.includes("admin")) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "You are not allowed to edit other users details", + }); + } - if (!user) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "User not found", + const user = await ctx.db.query.users.findFirst({ + columns: { email: true, provider: true }, + where: eq(users.id, input.id), }); - } - if (user.provider !== "credentials") { - throw new TRPCError({ - code: "FORBIDDEN", - message: "Username and email can not be changed for users with external providers", - }); - } + if (!user) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "User not found", + }); + } - await checkUsernameAlreadyTakenAndThrowAsync(ctx.db, "credentials", 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: protectedProcedure.input(z.string()).mutation(async ({ input, ctx }) => { - // Only admins and user itself can delete a user - if (ctx.session.user.id !== input && !ctx.session.user.permissions.includes("admin")) { - throw new TRPCError({ - code: "FORBIDDEN", - message: "You are not allowed to delete other users", - }); - } + if (user.provider !== "credentials") { + throw new TRPCError({ + code: "FORBIDDEN", + message: "Username and email can not be changed for users with external providers", + }); + } - 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", - }); - } + await checkUsernameAlreadyTakenAndThrowAsync(ctx.db, "credentials", input.name, input.id); - const dbUser = await ctx.db.query.users.findFirst({ - columns: { - id: true, - password: true, - salt: true, - provider: true, - }, - where: eq(users.id, input.userId), - }); + 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: protectedProcedure + .input(z.object({ userId: z.string() })) + .output(z.void()) + .meta({ openapi: { method: "DELETE", path: "/api/users/{userId}", tags: ["users"], protect: true } }) + .mutation(async ({ input, ctx }) => { + // Only admins and user itself can delete a user + if (ctx.session.user.id !== input && !ctx.session.user.permissions.includes("admin")) { + throw new TRPCError({ + code: "FORBIDDEN", + message: "You are not allowed to delete other users", + }); + } - if (!dbUser) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "User not found", - }); - } + await ctx.db.delete(users).where(eq(users.id, input)); + }), + changePassword: protectedProcedure + .input(validation.user.changePasswordApi) + .output(z.void()) + .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", + }); + } - if (dbUser.provider !== "credentials") { - throw new TRPCError({ - code: "FORBIDDEN", - message: "Password can not be changed for users with external providers", + const dbUser = await ctx.db.query.users.findFirst({ + columns: { + id: true, + password: true, + salt: true, + provider: true, + }, + where: eq(users.id, input.userId), }); - } - - // Admins can change the password of other users without providing the previous password - const isPreviousPasswordRequired = ctx.session.user.id === input.userId; - logger.info( - `User ${user.id} is changing password for user ${input.userId}, previous password is required: ${isPreviousPasswordRequired}`, - ); - - if (isPreviousPasswordRequired) { - const previousPasswordHash = await hashPasswordAsync(input.previousPassword, dbUser.salt ?? ""); - const isValid = previousPasswordHash === dbUser.password; + if (!dbUser) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "User not found", + }); + } - if (!isValid) { + if (dbUser.provider !== "credentials") { throw new TRPCError({ code: "FORBIDDEN", - message: "Invalid password", + message: "Password can not be changed for users with external providers", }); } - } - 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)); - }), + // Admins can change the password of other users without providing the previous password + const isPreviousPasswordRequired = ctx.session.user.id === input.userId; + + logger.info( + `User ${user.id} is changing password for user ${input.userId}, previous password is required: ${isPreviousPasswordRequired}`, + ); + + if (isPreviousPasswordRequired) { + 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)); + }), changeHomeBoardId: protectedProcedure .input(validation.user.changeHomeBoard.and(z.object({ userId: z.string() }))) + .output(z.void()) + .meta({ openapi: { method: "PATCH", path: "/api/users/changeHome", tags: ["users"], protect: true } }) .mutation(async ({ input, ctx }) => { const user = ctx.session.user; // Only admins can change other users passwords @@ -369,14 +403,18 @@ export const userRouter = createTRPCRouter({ }) .where(eq(users.id, input.userId)); }), - changeColorScheme: protectedProcedure.input(validation.user.changeColorScheme).mutation(async ({ input, ctx }) => { - await ctx.db - .update(users) - .set({ - colorScheme: input.colorScheme, - }) - .where(eq(users.id, ctx.session.user.id)); - }), + changeColorScheme: protectedProcedure + .input(validation.user.changeColorScheme) + .output(z.void()) + .meta({ openapi: { method: "PATCH", path: "/api/users/changeScheme", tags: ["users"], protect: true } }) + .mutation(async ({ input, ctx }) => { + await ctx.db + .update(users) + .set({ + colorScheme: input.colorScheme, + }) + .where(eq(users.id, ctx.session.user.id)); + }), getPingIconsEnabledOrDefault: publicProcedure.query(async ({ ctx }) => { if (!ctx.session?.user) { return false; @@ -410,23 +448,27 @@ export const userRouter = createTRPCRouter({ }) .where(eq(users.id, ctx.session.user.id)); }), - getFirstDayOfWeekForUserOrDefault: publicProcedure.query(async ({ ctx }) => { - if (!ctx.session?.user) { - return 1 as const; - } + getFirstDayOfWeekForUserOrDefault: publicProcedure + .input(z.undefined()) + .query(async ({ ctx }) => { + if (!ctx.session?.user) { + return 1 as const; + } - const user = await ctx.db.query.users.findFirst({ - columns: { - id: true, - firstDayOfWeek: true, - }, - where: eq(users.id, ctx.session.user.id), - }); + const user = await ctx.db.query.users.findFirst({ + columns: { + id: true, + firstDayOfWeek: true, + }, + where: eq(users.id, ctx.session.user.id), + }); - return user?.firstDayOfWeek ?? (1 as const); - }), + return user?.firstDayOfWeek ?? (1 as const); + }), changeFirstDayOfWeek: protectedProcedure .input(validation.user.firstDayOfWeek.and(validation.common.byId)) + .output(z.void()) + .meta({ openapi: { method: "PATCH", path: "/api/users/firstDayOfWeek", tags: ["users"], protect: true } }) .mutation(async ({ input, ctx }) => { // Only admins can change other users first day of week if (!ctx.session.user.permissions.includes("admin") && ctx.session.user.id !== input.id) { diff --git a/packages/db/package.json b/packages/db/package.json index db156f545..63400c478 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -9,7 +9,8 @@ "./client": "./client.ts", "./schema/sqlite": "./schema/sqlite.ts", "./test": "./test/index.ts", - "./queries": "./queries/index.ts" + "./queries": "./queries/index.ts", + "./validationSchemas/*": "./validationSchemas/*.ts" }, "main": "./index.ts", "types": "./index.ts", @@ -43,6 +44,7 @@ "dotenv": "^16.4.5", "drizzle-kit": "^0.26.2", "drizzle-orm": "^0.35.2", + "drizzle-zod": "^0.5.1", "mysql2": "3.11.3" }, "devDependencies": { diff --git a/packages/db/validationSchemas/app.ts b/packages/db/validationSchemas/app.ts new file mode 100644 index 000000000..3a0dad91d --- /dev/null +++ b/packages/db/validationSchemas/app.ts @@ -0,0 +1,4 @@ +import { createSelectSchema } from "drizzle-zod"; +import { apps } from "../schema/sqlite"; + +export const selectAppSchema = createSelectSchema(apps); \ No newline at end of file diff --git a/packages/db/validationSchemas/board.ts b/packages/db/validationSchemas/board.ts new file mode 100644 index 000000000..bfb674a24 --- /dev/null +++ b/packages/db/validationSchemas/board.ts @@ -0,0 +1,4 @@ +import { createSelectSchema } from "drizzle-zod"; +import { boards } from "../schema/sqlite"; + +export const selectBoardSchema = createSelectSchema(boards); \ No newline at end of file diff --git a/packages/db/validationSchemas/group.ts b/packages/db/validationSchemas/group.ts new file mode 100644 index 000000000..d64668a01 --- /dev/null +++ b/packages/db/validationSchemas/group.ts @@ -0,0 +1,4 @@ +import { createSelectSchema } from "drizzle-zod"; +import { groups } from "../schema/sqlite"; + +export const selectGroupSchema = createSelectSchema(groups); \ No newline at end of file diff --git a/packages/db/validationSchemas/invite.ts b/packages/db/validationSchemas/invite.ts new file mode 100644 index 000000000..05dee0f82 --- /dev/null +++ b/packages/db/validationSchemas/invite.ts @@ -0,0 +1,4 @@ +import { createSelectSchema } from "drizzle-zod"; +import { invites } from "../schema/sqlite"; + +export const selectInviteSchema = createSelectSchema(invites); \ No newline at end of file diff --git a/packages/db/validationSchemas/searchEngine.ts b/packages/db/validationSchemas/searchEngine.ts new file mode 100644 index 000000000..7e08fa459 --- /dev/null +++ b/packages/db/validationSchemas/searchEngine.ts @@ -0,0 +1,4 @@ +import { createSelectSchema } from "drizzle-zod"; +import { searchEngines } from "../schema/sqlite"; + +export const selectSearchEnginesSchema = createSelectSchema(searchEngines); \ No newline at end of file diff --git a/packages/db/validationSchemas/serverSettings.ts b/packages/db/validationSchemas/serverSettings.ts new file mode 100644 index 000000000..590bce004 --- /dev/null +++ b/packages/db/validationSchemas/serverSettings.ts @@ -0,0 +1,4 @@ +import { createSelectSchema } from "drizzle-zod"; +import { serverSettings } from "../schema/sqlite"; + +export const selectSeverSettingsSchema = createSelectSchema(serverSettings); \ No newline at end of file diff --git a/packages/db/validationSchemas/user.ts b/packages/db/validationSchemas/user.ts new file mode 100644 index 000000000..9d0f0757b --- /dev/null +++ b/packages/db/validationSchemas/user.ts @@ -0,0 +1,4 @@ +import { createSelectSchema } from "drizzle-zod"; +import { users } from "../schema/sqlite"; + +export const selectUserSchema = createSelectSchema(users); \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 824c48c41..6dc05beda 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,7 +24,7 @@ importers: version: 4.3.3(vite@5.4.5(@types/node@20.16.13)(sass@1.80.3)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0)) '@vitest/coverage-v8': specifier: ^2.1.3 - version: 2.1.3(vitest@2.1.3) + version: 2.1.3(vitest@2.1.3(@types/node@20.16.13)(@vitest/ui@2.1.3)(jsdom@25.0.1)(sass@1.80.3)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0)) '@vitest/ui': specifier: ^2.1.3 version: 2.1.3(vitest@2.1.3) @@ -862,6 +862,9 @@ importers: drizzle-orm: specifier: ^0.35.2 version: 0.35.2(@types/better-sqlite3@7.6.11)(@types/react@18.3.11)(better-sqlite3@11.4.0)(mysql2@3.11.3)(react@18.3.1) + drizzle-zod: + specifier: ^0.5.1 + version: 0.5.1(drizzle-orm@0.35.2(@types/better-sqlite3@7.6.11)(@types/react@18.3.11)(better-sqlite3@11.4.0)(mysql2@3.11.3)(react@18.3.1))(zod@3.23.8) mysql2: specifier: 3.11.3 version: 3.11.3 @@ -4607,6 +4610,12 @@ packages: sqlite3: optional: true + drizzle-zod@0.5.1: + resolution: {integrity: sha512-C/8bvzUH/zSnVfwdSibOgFjLhtDtbKYmkbPbUCq46QZyZCH6kODIMSOgZ8R7rVjoI+tCj3k06MRJMDqsIeoS4A==} + peerDependencies: + drizzle-orm: '>=0.23.13' + zod: '*' + eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} @@ -9939,7 +9948,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@vitest/coverage-v8@2.1.3(vitest@2.1.3)': + '@vitest/coverage-v8@2.1.3(vitest@2.1.3(@types/node@20.16.13)(@vitest/ui@2.1.3)(jsdom@25.0.1)(sass@1.80.3)(sugarss@4.0.1(postcss@8.4.47))(terser@5.32.0))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 0.2.3 @@ -10986,6 +10995,11 @@ snapshots: mysql2: 3.11.3 react: 18.3.1 + drizzle-zod@0.5.1(drizzle-orm@0.35.2(@types/better-sqlite3@7.6.11)(@types/react@18.3.11)(better-sqlite3@11.4.0)(mysql2@3.11.3)(react@18.3.1))(zod@3.23.8): + dependencies: + drizzle-orm: 0.35.2(@types/better-sqlite3@7.6.11)(@types/react@18.3.11)(better-sqlite3@11.4.0)(mysql2@3.11.3)(react@18.3.1) + zod: 3.23.8 + eastasianwidth@0.2.0: {} effect@3.9.2: {}