From d233f2c764fc213d8c411391a4ac60f59116791a Mon Sep 17 00:00:00 2001 From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com> Date: Sat, 15 Feb 2025 19:12:44 -0600 Subject: [PATCH] feat: adjust roles --- .../components/dashboard/projects/show.tsx | 6 +- apps/dokploy/pages/index.tsx | 1 - apps/dokploy/server/api/routers/auth.ts | 13 +- apps/dokploy/server/api/trpc.ts | 5 +- packages/server/src/db/schema/account.ts | 4 +- packages/server/src/db/schema/user.ts | 5 +- packages/server/src/lib/auth.ts | 167 ++++++---- packages/server/src/services/admin.ts | 299 +++++++++--------- 8 files changed, 273 insertions(+), 227 deletions(-) diff --git a/apps/dokploy/components/dashboard/projects/show.tsx b/apps/dokploy/components/dashboard/projects/show.tsx index 98ef14a4b..206137f56 100644 --- a/apps/dokploy/components/dashboard/projects/show.tsx +++ b/apps/dokploy/components/dashboard/projects/show.tsx @@ -57,7 +57,7 @@ export const ShowProjects = () => { authId: auth?.id || "", }, { - enabled: !!auth?.id && auth?.role === "user", + enabled: !!auth?.id && auth?.role === "member", }, ); const { mutateAsync } = api.project.remove.useMutation(); @@ -91,7 +91,7 @@ export const ShowProjects = () => { - {(auth?.role === "admin" || user?.canCreateProjects) && ( + {(auth?.role === "owner" || user?.canCreateProjects) && (
@@ -293,7 +293,7 @@ export const ShowProjects = () => {
e.stopPropagation()} > - {(auth?.role === "admin" || + {(auth?.role === "owner" || user?.canDeleteProjects) && ( diff --git a/apps/dokploy/pages/index.tsx b/apps/dokploy/pages/index.tsx index 0fecc209c..b85b1c7e1 100644 --- a/apps/dokploy/pages/index.tsx +++ b/apps/dokploy/pages/index.tsx @@ -261,7 +261,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { } const { user } = await validateRequest(context.req); - console.log("Response", user); if (user) { return { diff --git a/apps/dokploy/server/api/routers/auth.ts b/apps/dokploy/server/api/routers/auth.ts index 7f1382b49..ccb6b5ad7 100644 --- a/apps/dokploy/server/api/routers/auth.ts +++ b/apps/dokploy/server/api/routers/auth.ts @@ -7,6 +7,7 @@ import { apiVerify2FA, apiVerifyLogin2FA, auth, + member, } from "@/server/db/schema"; import { WEBSITE_URL } from "@/server/utils/stripe"; import { @@ -32,7 +33,7 @@ import { import { TRPCError } from "@trpc/server"; import * as bcrypt from "bcrypt"; import { isBefore } from "date-fns"; -import { eq } from "drizzle-orm"; +import { and, eq } from "drizzle-orm"; import { nanoid } from "nanoid"; import { z } from "zod"; import { db } from "../../db"; @@ -170,8 +171,14 @@ export const authRouter = createTRPCRouter({ }), get: protectedProcedure.query(async ({ ctx }) => { - const auth = await findAuthById(ctx.user.id); - return auth; + const memberResult = await db.query.member.findFirst({ + where: and( + eq(member.userId, ctx.user.id), + eq(member.organizationId, ctx.session?.activeOrganizationId || ""), + ), + }); + + return memberResult; }), logout: protectedProcedure.mutation(async ({ ctx }) => { diff --git a/apps/dokploy/server/api/trpc.ts b/apps/dokploy/server/api/trpc.ts index 9f373ad3f..c88158b85 100644 --- a/apps/dokploy/server/api/trpc.ts +++ b/apps/dokploy/server/api/trpc.ts @@ -32,7 +32,7 @@ import { ZodError } from "zod"; interface CreateContextOptions { user: (User & { rol: "admin" | "user"; ownerId: string }) | null; - session: Session | null; + session: (Session & { activeOrganizationId: string }) | null; req: CreateNextContextOptions["req"]; res: CreateNextContextOptions["res"]; } @@ -75,12 +75,15 @@ export const createTRPCContext = async (opts: CreateNextContextOptions) => { user = cookieResult.user; } + console.log("session", { session, user }); + return createInnerTRPCContext({ req, res, session: session, ...((user && { user: { + ...user, email: user.email, rol: user.role, id: user.id, diff --git a/packages/server/src/db/schema/account.ts b/packages/server/src/db/schema/account.ts index 432753fde..0b5ef2707 100644 --- a/packages/server/src/db/schema/account.ts +++ b/packages/server/src/db/schema/account.ts @@ -77,7 +77,7 @@ export const member = pgTable("member", { userId: text("user_id") .notNull() .references(() => users_temp.id), - role: text("role").notNull(), + role: text("role").notNull().$type<"owner" | "member" | "admin">(), createdAt: timestamp("created_at").notNull(), }); @@ -98,7 +98,7 @@ export const invitation = pgTable("invitation", { .notNull() .references(() => organization.id), email: text("email").notNull(), - role: text("role"), + role: text("role").$type<"owner" | "member" | "admin">(), status: text("status").notNull(), expiresAt: timestamp("expires_at").notNull(), inviterId: text("inviter_id") diff --git a/packages/server/src/db/schema/user.ts b/packages/server/src/db/schema/user.ts index f22967d59..c05b734a7 100644 --- a/packages/server/src/db/schema/user.ts +++ b/packages/server/src/db/schema/user.ts @@ -10,7 +10,7 @@ import { import { createInsertSchema } from "drizzle-zod"; import { nanoid } from "nanoid"; import { z } from "zod"; -import { account } from "./account"; +import { account, organization } from "./account"; import { admins } from "./admin"; import { auth } from "./auth"; import { certificateType } from "./shared"; @@ -185,7 +185,7 @@ export const users_temp = pgTable("user_temp", { serversQuantity: integer("serversQuantity").notNull().default(0), }); -export const usersRelations = relations(users_temp, ({ one }) => ({ +export const usersRelations = relations(users_temp, ({ one, many }) => ({ // auth: one(auth, { // fields: [users.authId], // references: [auth.id], @@ -194,6 +194,7 @@ export const usersRelations = relations(users_temp, ({ one }) => ({ fields: [users_temp.id], references: [account.userId], }), + organizations: many(organization), // admin: one(admins, { // fields: [users.adminId], // references: [admins.adminId], diff --git a/packages/server/src/lib/auth.ts b/packages/server/src/lib/auth.ts index 226ab0a8c..ce636c134 100644 --- a/packages/server/src/lib/auth.ts +++ b/packages/server/src/lib/auth.ts @@ -3,85 +3,116 @@ import * as bcrypt from "bcrypt"; import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { createAuthMiddleware, organization } from "better-auth/plugins"; -import { eq } from "drizzle-orm"; +import { desc, eq } from "drizzle-orm"; import { db } from "../db"; import * as schema from "../db/schema"; export const auth = betterAuth({ - database: drizzleAdapter(db, { - provider: "pg", - schema: schema, - }), + database: drizzleAdapter(db, { + provider: "pg", + schema: schema, + }), - emailAndPassword: { - enabled: true, + emailAndPassword: { + enabled: true, - password: { - async hash(password) { - return bcrypt.hashSync(password, 10); - }, - async verify({ hash, password }) { - return bcrypt.compareSync(password, hash); - }, - }, - }, - hooks: { - after: createAuthMiddleware(async (ctx) => { - if (ctx.path.startsWith("/sign-up")) { - const newSession = ctx.context.newSession; - await db - .update(schema.users_temp) - .set({ - role: "admin", - }) - .where(eq(schema.users_temp.id, newSession?.user?.id || "")); - } - }), - }, - user: { - modelName: "users_temp", - additionalFields: { - role: { - type: "string", - }, - ownerId: { - type: "string", - }, - }, - }, - plugins: [organization()], + password: { + async hash(password) { + return bcrypt.hashSync(password, 10); + }, + async verify({ hash, password }) { + return bcrypt.compareSync(password, hash); + }, + }, + }, + hooks: { + after: createAuthMiddleware(async (ctx) => { + if (ctx.path.startsWith("/sign-up")) { + const newSession = ctx.context.newSession; + const organization = await db + .insert(schema.organization) + .values({ + name: "My Organization", + ownerId: newSession?.user?.id || "", + createdAt: new Date(), + }) + .returning() + .then((res) => res[0]); + + await db.insert(schema.member).values({ + userId: newSession?.user?.id || "", + organizationId: organization?.id || "", + role: "owner", + createdAt: new Date(), + }); + } + }), + }, + databaseHooks: { + session: { + create: { + before: async (session) => { + const member = await db.query.member.findFirst({ + where: eq(schema.member.userId, session.userId), + orderBy: desc(schema.member.createdAt), + with: { + organization: true, + }, + }); + + return { + data: { + ...session, + activeOrganizationId: member?.organization.id, + }, + }; + }, + }, + }, + }, + user: { + modelName: "users_temp", + additionalFields: { + role: { + type: "string", + }, + ownerId: { + type: "string", + }, + }, + }, + plugins: [organization()], }); export const validateRequest = async (request: IncomingMessage) => { - const session = await auth.api.getSession({ - headers: new Headers({ - cookie: request.headers.cookie || "", - }), - }); + const session = await auth.api.getSession({ + headers: new Headers({ + cookie: request.headers.cookie || "", + }), + }); - if (!session?.session || !session.user) { - return { - session: null, - user: null, - }; - } + if (!session?.session || !session.user) { + return { + session: null, + user: null, + }; + } - if (session?.user) { - if (session?.user.role === "user") { - const owner = await db.query.member.findFirst({ - where: eq(schema.member.userId, session.user.id), - with: { - organization: true, - }, - }); + if (session?.user) { + const member = await db.query.member.findFirst({ + where: eq(schema.member.userId, session.user.id), + with: { + organization: true, + }, + }); - if (owner) { - session.user.ownerId = owner.organization.ownerId; - } - } else { - session.user.ownerId = session?.user?.id || ""; - } - } + session.user.role = member?.role || "member"; + if (member) { + session.user.ownerId = member.organization.ownerId; + } else { + session.user.ownerId = session.user.id; + } + } - return session; + return session; }; diff --git a/packages/server/src/services/admin.ts b/packages/server/src/services/admin.ts index e8a60498f..78a0375a8 100644 --- a/packages/server/src/services/admin.ts +++ b/packages/server/src/services/admin.ts @@ -1,10 +1,13 @@ import { randomBytes } from "node:crypto"; import { db } from "@dokploy/server/db"; import { - admins, - type apiCreateUserInvitation, - auth, - users_temp, + account, + admins, + type apiCreateUserInvitation, + auth, + member, + organization, + users_temp, } from "@dokploy/server/db/schema"; import { TRPCError } from "@trpc/server"; import * as bcrypt from "bcrypt"; @@ -13,188 +16,190 @@ import { IS_CLOUD } from "../constants"; export type Admin = typeof users_temp.$inferSelect; export const createInvitation = async ( - input: typeof apiCreateUserInvitation._type, - adminId: string + input: typeof apiCreateUserInvitation._type, + adminId: string, ) => { - await db.transaction(async (tx) => { - const result = await tx - .insert(auth) - .values({ - email: input.email.toLowerCase(), - rol: "user", - password: bcrypt.hashSync("01231203012312", 10), - }) - .returning() - .then((res) => res[0]); + await db.transaction(async (tx) => { + const result = await tx + .insert(auth) + .values({ + email: input.email.toLowerCase(), + rol: "user", + password: bcrypt.hashSync("01231203012312", 10), + }) + .returning() + .then((res) => res[0]); - if (!result) { - throw new TRPCError({ - code: "BAD_REQUEST", - message: "Error creating the user", - }); - } - const expiresIn24Hours = new Date(); - expiresIn24Hours.setDate(expiresIn24Hours.getDate() + 1); - const token = randomBytes(32).toString("hex"); - await tx - .insert(users) - .values({ - adminId: adminId, - authId: result.id, - token, - expirationDate: expiresIn24Hours.toISOString(), - }) - .returning(); - }); + if (!result) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Error creating the user", + }); + } + const expiresIn24Hours = new Date(); + expiresIn24Hours.setDate(expiresIn24Hours.getDate() + 1); + const token = randomBytes(32).toString("hex"); + // await tx + // .insert(users) + // .values({ + // adminId: adminId, + // authId: result.id, + // token, + // expirationDate: expiresIn24Hours.toISOString(), + // }) + // .returning(); + }); }; export const findUserById = async (userId: string) => { - const user = await db.query.users_temp.findFirst({ - where: eq(users_temp.id, userId), - // with: { - // account: true, - // }, - }); - if (!user) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "User not found", - }); - } - return user; + const user = await db.query.users_temp.findFirst({ + where: eq(users_temp.id, userId), + // with: { + // account: true, + // }, + }); + if (!user) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "User not found", + }); + } + return user; }; export const updateUser = async (userId: string, userData: Partial) => { - const user = await db - .update(users_temp) - .set({ - ...userData, - }) - .where(eq(users_temp.id, userId)) - .returning() - .then((res) => res[0]); + const user = await db + .update(users_temp) + .set({ + ...userData, + }) + .where(eq(users_temp.id, userId)) + .returning() + .then((res) => res[0]); - return user; + return user; }; export const updateAdminById = async ( - adminId: string, - adminData: Partial + adminId: string, + adminData: Partial, ) => { - const admin = await db - .update(admins) - .set({ - ...adminData, - }) - .where(eq(admins.adminId, adminId)) - .returning() - .then((res) => res[0]); - - return admin; + // const admin = await db + // .update(admins) + // .set({ + // ...adminData, + // }) + // .where(eq(admins.adminId, adminId)) + // .returning() + // .then((res) => res[0]); + // return admin; }; export const findAdminById = async (userId: string) => { - const admin = await db.query.admins.findFirst({ - where: eq(admins.userId, userId), - }); - return admin; + const admin = await db.query.admins.findFirst({ + // where: eq(admins.userId, userId), + }); + return admin; }; export const isAdminPresent = async () => { - const admin = await db.query.users_temp.findFirst({ - where: eq(users_temp.role, "admin"), - }); - if (!admin) { - return false; - } - return true; + const admin = await db.query.member.findFirst({ + where: eq(member.role, "owner"), + }); + + console.log("admin", admin); + + if (!admin) { + return false; + } + return true; }; export const findAdminByAuthId = async (authId: string) => { - const admin = await db.query.admins.findFirst({ - where: eq(admins.authId, authId), - with: { - users: true, - }, - }); - if (!admin) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Admin not found", - }); - } - return admin; + const admin = await db.query.admins.findFirst({ + where: eq(admins.authId, authId), + with: { + users: true, + }, + }); + if (!admin) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Admin not found", + }); + } + return admin; }; export const findAdmin = async () => { - const admin = await db.query.admins.findFirst({}); - if (!admin) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Admin not found", - }); - } - return admin; + const admin = await db.query.admins.findFirst({}); + if (!admin) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Admin not found", + }); + } + return admin; }; export const getUserByToken = async (token: string) => { - const user = await db.query.users.findFirst({ - where: eq(users.token, token), - with: { - auth: { - columns: { - password: false, - }, - }, - }, - }); + const user = await db.query.users.findFirst({ + where: eq(users.token, token), + with: { + auth: { + columns: { + password: false, + }, + }, + }, + }); - if (!user) { - throw new TRPCError({ - code: "NOT_FOUND", - message: "Invitation not found", - }); - } - return { - ...user, - isExpired: user.isRegistered, - }; + if (!user) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Invitation not found", + }); + } + return { + ...user, + isExpired: user.isRegistered, + }; }; export const removeUserById = async (userId: string) => { - await db - .delete(users_temp) - .where(eq(users_temp.id, userId)) - .returning() - .then((res) => res[0]); + await db + .delete(users_temp) + .where(eq(users_temp.id, userId)) + .returning() + .then((res) => res[0]); }; export const removeAdminByAuthId = async (authId: string) => { - const admin = await findAdminByAuthId(authId); - if (!admin) return null; + const admin = await findAdminByAuthId(authId); + if (!admin) return null; - // First delete all associated users - const users = admin.users; + // First delete all associated users + const users = admin.users; - for (const user of users) { - await removeUserById(user.id); - } - // Then delete the auth record which will cascade delete the admin - return await db - .delete(auth) - .where(eq(auth.id, authId)) - .returning() - .then((res) => res[0]); + for (const user of users) { + await removeUserById(user.id); + } + // Then delete the auth record which will cascade delete the admin + return await db + .delete(auth) + .where(eq(auth.id, authId)) + .returning() + .then((res) => res[0]); }; export const getDokployUrl = async () => { - if (IS_CLOUD) { - return "https://app.dokploy.com"; - } - const admin = await findAdmin(); + if (IS_CLOUD) { + return "https://app.dokploy.com"; + } + const admin = await findAdmin(); - if (admin.host) { - return `https://${admin.host}`; - } - return `http://${admin.serverIp}:${process.env.PORT}`; + if (admin.host) { + return `https://${admin.host}`; + } + return `http://${admin.serverIp}:${process.env.PORT}`; };