diff --git a/src/components/wrappers/dashboard/organization/organization.action.ts b/src/components/wrappers/dashboard/organization/organization.action.ts
index 5a9b587b..10aad8db 100644
--- a/src/components/wrappers/dashboard/organization/organization.action.ts
+++ b/src/components/wrappers/dashboard/organization/organization.action.ts
@@ -7,13 +7,16 @@ import { z } from "zod";
import { v4 as uuidv4 } from "uuid";
import { OrganizationFormSchema } from "@/components/wrappers/dashboard/organization/OrganizationForm/organization-form.schema";
import { db } from "@/db";
-import { Organization, organization as drizzleOrganization, organizationMember as drizzleOrganizationMember } from "@/db/schema";
import { eq } from "drizzle-orm";
-import { checkSlugOrganization, createOrganization } from "@/lib/auth/auth";
+import {checkSlugOrganization, createOrganization, deleteOrganization} from "@/lib/auth/auth";
+import {slugify} from "@/utils/slugify";
+import {Organization} from "@/db/schema/02_organization";
+import * as drizzleDb from "@/db";
export const createOrganizationAction = userAction.schema(OrganizationSchema).action(async ({ parsedInput }): Promise> => {
try {
- if (!checkSlugOrganization(parsedInput.slug)) {
+ const slug = slugify(parsedInput.name);
+ if (!await checkSlugOrganization(slug)) {
return {
success: false,
actionError: {
@@ -24,14 +27,29 @@ export const createOrganizationAction = userAction.schema(OrganizationSchema).ac
};
}
- const organization = await createOrganization(parsedInput.name, parsedInput.slug);
+ let createdOrganization: Organization;
+
+ try {
+ createdOrganization = await createOrganization(parsedInput.name, slug) as unknown as Organization;
+ } catch (authError: any) {
+ console.error("Auth deletion failed:", authError);
+ return {
+ success: false,
+ actionError: {
+ message: authError.message || "Authentication service error.",
+ status: authError.status || 500,
+ cause: "auth_error",
+ messageParams: { message: authError.message },
+ },
+ };
+ }
return {
success: true,
- value: organization!,
+ value: createdOrganization,
actionSuccess: {
message: "Organization has been successfully created.",
- messageParams: { organizationId: organization!.id },
+ messageParams: { organizationId: createdOrganization!.id },
},
};
} catch (error) {
@@ -58,7 +76,7 @@ export const updateOrganizationAction = userAction
try {
const newUserList = parsedInput.data.users;
- const organization = await db.select().from(organization).where(eq(organization.id, parsedInput.organizationId)).execute();
+ const organization = await db.select().from(drizzleDb.schemas.organization).where(eq(drizzleDb.schemas.organization.id, parsedInput.organizationId)).execute();
if (organization.length === 0) {
throw new Error("Organization not found.");
@@ -70,7 +88,7 @@ export const updateOrganizationAction = userAction
if (usersToAdd.length > 0) {
await db
- .insert(userOrganization)
+ .insert(drizzleDb.schemas.organization)
.values(
usersToAdd.map((userId) => ({
userId,
@@ -117,44 +135,60 @@ export const updateOrganizationAction = userAction
}
});
-export const deleteOrganizationAction = userAction.schema(z.string()).action(async ({ parsedInput, ctx }): Promise> => {
- try {
- const uuid = uuidv4();
- const organization = await db.select().from(organization).where(eq(organization.slug, parsedInput)).execute();
+export const deleteOrganizationAction = userAction.schema(z.string()).action(
+ async ({ parsedInput, ctx }): Promise> => {
+ try {
+ const org = await db.query.organization.findFirst({
+ where: eq(drizzleDb.schemas.organization.slug, parsedInput),
+ });
- if (organization.length === 0) {
- throw new Error("Organization not found.");
+ if (!org) {
+ return {
+ success: false,
+ actionError: {
+ message: "Organization not found.",
+ status: 404,
+ cause: "not_found",
+ },
+ };
+ }
+
+ let deletedOrganization: Organization;
+
+ try {
+ deletedOrganization = await deleteOrganization(org.id) as Organization;
+ } catch (authError: any) {
+ console.error("Auth deletion failed:", authError);
+ return {
+ success: false,
+ actionError: {
+ message: authError.message || "Authentication service error.",
+ status: authError.status || 500,
+ cause: "auth_error",
+ messageParams: { message: authError.message },
+ },
+ };
+ }
+
+ return {
+ success: true,
+ value: deletedOrganization,
+ actionSuccess: {
+ message: "Organization has been successfully deleted.",
+ messageParams: { organizationId: deletedOrganization.id },
+ },
+ };
+ } catch (error) {
+ console.error("Unexpected error in deleteOrganizationAction:", error);
+ return {
+ success: false,
+ actionError: {
+ message: "Failed to delete organization due to a server error.",
+ status: 500,
+ cause: "server_error",
+ messageParams: { message: "Internal server error while deleting the organization" },
+ },
+ };
}
-
- const updatedOrganization = await db
- .update(organization)
- .set({
- name: `${organization[0].name}-${uuid}`,
- slug: `${organization[0].slug}-${uuid}`,
- deleted: true,
- })
- .where(eq(organization.id, organization[0].id))
- .returning()
- .execute();
-
- return {
- success: true,
- value: updatedOrganization[0],
- actionSuccess: {
- message: "Organization has been successfully deleted.",
- messageParams: { organizationId: updatedOrganization[0].id },
- },
- };
- } catch (error) {
- console.error("Error deleting organization:", error);
- return {
- success: false,
- actionError: {
- message: "Failed to delete organization.",
- status: 500,
- cause: error.message ?? "Unknown error",
- messageParams: { message: "Error deleting the organization" },
- },
- };
}
-});
+);
diff --git a/src/components/wrappers/dashboard/organization/organization.schema.ts b/src/components/wrappers/dashboard/organization/organization.schema.ts
index 328cdd52..6e2db975 100644
--- a/src/components/wrappers/dashboard/organization/organization.schema.ts
+++ b/src/components/wrappers/dashboard/organization/organization.schema.ts
@@ -2,11 +2,11 @@ import { z } from "zod";
export const OrganizationSchema = z.object({
name: z.string().min(5, "Name must be at least 5 characters long").max(40, "Name must be at most 40 characters long"),
- slug: z
- .string()
- .regex(/^[a-zA-Z0-9_-]*$/, "Slug can only contain letters, numbers, underscores, and hyphens")
- .min(5, "Slug must be at least 5 characters long")
- .max(20, "Slug must be at most 20 characters long"),
+ // slug: z
+ // .string()
+ // .regex(/^[a-zA-Z0-9_-]*$/, "Slug can only contain letters, numbers, underscores, and hyphens")
+ // .min(5, "Slug must be at least 5 characters long")
+ // .max(20, "Slug must be at most 20 characters long"),
});
export type OrganizationSchema = z.infer;
diff --git a/src/components/wrappers/dashboard/sideBar/app-sidebar.tsx b/src/components/wrappers/dashboard/sideBar/app-sidebar.tsx
index 938af7e8..455ced0b 100644
--- a/src/components/wrappers/dashboard/sideBar/app-sidebar.tsx
+++ b/src/components/wrappers/dashboard/sideBar/app-sidebar.tsx
@@ -23,9 +23,9 @@ export async function AppSidebar() {
return notFound();
}
- const organization = await getOrganization({organizationId: member.organizationId});
+ // const organization = await getOrganization({organizationId: member.organizationId});
- console.log("organization", organization);
+ // console.log("organization", organization);
return (
diff --git a/src/db/schema/02_organization.ts b/src/db/schema/02_organization.ts
index 0fdc3a9c..cc6ad586 100644
--- a/src/db/schema/02_organization.ts
+++ b/src/db/schema/02_organization.ts
@@ -3,8 +3,9 @@ import { relations } from "drizzle-orm";
import { project } from "./05_project";
import { createSelectSchema } from "drizzle-zod";
import { z } from "zod";
-import {invitation} from "@/db/schema/04_invitation";
-import {member} from "@/db/schema/03_member";
+import {invitation, OrganizationInvitation} from "@/db/schema/04_invitation";
+import {member, OrganizationMember} from "@/db/schema/03_member";
+import {User} from "@/db/schema/01_user";
export const organization = pgTable("organization", {
id: uuid("id").defaultRandom().primaryKey(),
@@ -28,3 +29,15 @@ export const organizationRelations = relations(organization, ({ many }) => ({
export const organizationSchema = createSelectSchema(organization);
export type Organization = z.infer;
+export type OrganizationWithMembers = Organization & {
+ members: OrganizationMember[];
+};
+
+export type MemberWithUser = OrganizationMember & {
+ user: User;
+};
+
+export type OrganizationWithMembersAndUsers = Organization & {
+ members: MemberWithUser[];
+ invitations: OrganizationInvitation[];
+};
\ No newline at end of file
diff --git a/src/lib/auth/auth.ts b/src/lib/auth/auth.ts
index 32f370d5..eadbc7aa 100644
--- a/src/lib/auth/auth.ts
+++ b/src/lib/auth/auth.ts
@@ -1,13 +1,14 @@
import {betterAuth} from "better-auth";
import {drizzleAdapter} from "better-auth/adapters/drizzle";
+import * as drizzleDb from "@/db";
import {db} from "@/db";
import {env} from "@/env.mjs";
import {nextCookies} from "better-auth/next-js";
import {admin as adminPlugin, openAPI, organization} from "better-auth/plugins";
import {ac, admin, orgAdmin, orgMember, orgOwner, pending, superadmin, user} from "@/lib/auth/permissions";
-import * as drizzleDb from "@/db";
import {headers} from "next/headers";
import {count, eq} from "drizzle-orm";
+import {OrganizationWithMembers, OrganizationWithMembersAndUsers} from "@/db/schema/02_organization";
export const auth = betterAuth({
database: drizzleAdapter(db, {
@@ -40,9 +41,9 @@ export const auth = betterAuth({
organization({
ac,
roles: {
- orgOwner,
- orgAdmin,
- orgMember,
+ owner: orgOwner,
+ admin: orgAdmin,
+ member: orgMember,
},
}),
adminPlugin({
@@ -98,7 +99,7 @@ export const auth = betterAuth({
await db.insert(drizzleDb.schemas.member).values({
userId: user.id,
organizationId: defaultOrg.id,
- role: "owner",
+ role: "orgOwner",
});
} else {
console.warn("Default organization not found. Cannot assign member.");
@@ -277,13 +278,39 @@ export const unlinkAccount = async (provider: string, account: string) => {
// return null;
// }
// };
+// export const getOrganization = async ({
+// organizationId,
+// organizationSlug,
+// }: {
+// organizationId?: string;
+// organizationSlug?: string;
+// } = {}) => {
+// const query =
+// organizationId != null
+// ? {organizationId}
+// : organizationSlug != null
+// ? {organizationSlug}
+// : undefined;
+//
+// console.log(query);
+//
+// try {
+// return await auth.api.getFullOrganization({
+// headers: await headers(),
+// ...(query ? {query} : {}),
+// });
+// } catch (e) {
+// console.error(e);
+// return null;
+// }
+// };
export const getOrganization = async ({
organizationId,
organizationSlug,
}: {
organizationId?: string;
organizationSlug?: string;
-} = {}) => {
+} = {}): Promise => {
const query =
organizationId != null
? { organizationId }
@@ -291,13 +318,13 @@ export const getOrganization = async ({
? { organizationSlug }
: undefined;
- console.log(query);
-
try {
- return await auth.api.getFullOrganization({
+ const response = await auth.api.getFullOrganization({
headers: await headers(),
...(query ? { query } : {}),
});
+
+ return response as OrganizationWithMembersAndUsers;
} catch (e) {
console.error(e);
return null;
@@ -328,22 +355,63 @@ export const getLastOrganizationOrFirst = async (userId: string) => {
return null;
}
};
+
export const createOrganization = async (name: string, slug: string) => {
try {
- const organization = await auth.api.createOrganization({
- headers: await headers(),
+ return await auth.api.createOrganization({
+ headers: await headers(),
body: {
name,
slug,
},
});
+ } catch (e: any) {
+ const errorMessage = e?.response?.data?.message || e?.message || "Unknown auth error";
+ const status = e?.response?.status || 500;
- return organization;
- } catch (e) {
- console.log("err", e);
+ console.error("Auth API createOrganization error:", {
+ message: errorMessage,
+ status,
+ raw: e,
+ });
+
+ throw {
+ name: "AuthCreateOrganizationError",
+ message: errorMessage,
+ status,
+ cause: e,
+ };
}
};
+export const deleteOrganization = async (organizationId: string) => {
+ try {
+ return await auth.api.deleteOrganization({
+ body: {
+ organizationId,
+ },
+ headers: await headers(),
+ });
+ } catch (e: any) {
+ const errorMessage = e?.response?.data?.message || e?.message || "Unknown auth error";
+ const status = e?.response?.status || 500;
+
+ console.error("Auth API deleteOrganization error:", {
+ message: errorMessage,
+ status,
+ raw: e,
+ });
+
+ throw {
+ name: "AuthDeleteOrganizationError",
+ message: errorMessage,
+ status,
+ cause: e,
+ };
+ }
+};
+
+
export const checkSlugOrganization = async (slug: string) => {
try {
const {status} = await auth.api.checkOrganizationSlug({
diff --git a/src/lib/auth/permissions.ts b/src/lib/auth/permissions.ts
index be29bdb2..a8b5be7c 100644
--- a/src/lib/auth/permissions.ts
+++ b/src/lib/auth/permissions.ts
@@ -1,6 +1,6 @@
import { createAccessControl } from "better-auth/plugins/access";
import { defaultStatements, adminAc } from "better-auth/plugins/admin/access";
-import { defaultStatements as orgDefaultStatements, adminAc as orgAdminAc } from "better-auth/plugins/organization/access";
+import { defaultStatements as orgDefaultStatements, adminAc as orgAdminAc, ownerAc as orgOwnerAc } from "better-auth/plugins/organization/access";
const statement = {
...defaultStatements,
@@ -18,6 +18,7 @@ const superadmin = ac.newRole({
agent: ["create", "list", "update", "delete"],
...adminAc.statements,
...orgAdminAc.statements,
+ ...orgOwnerAc.statements,
});
const admin = ac.newRole({
@@ -54,6 +55,7 @@ const orgAdmin = ac.newRole({
const orgOwner = ac.newRole({
project: ["create", "update", "delete"],
...orgAdminAc.statements,
+ ...orgOwnerAc.statements,
});
export { ac, admin, superadmin, user, pending, orgAdmin, orgMember, orgOwner };
diff --git a/src/utils/slugify.ts b/src/utils/slugify.ts
new file mode 100644
index 00000000..ac669d60
--- /dev/null
+++ b/src/utils/slugify.ts
@@ -0,0 +1,14 @@
+export const slugify = (text: string) => {
+ return text
+ .toString()
+ .normalize('NFKD') // Normalize accents
+ .replace(/[\u0300-\u036f]/g, '') // Remove diacritics
+ .toLowerCase()
+ .trim()
+ .replace(/\_/g, '-') // _ → dash
+ .replace(/\s+/g, '-') // spaces → dash
+ .replace(/[^\w\-]+/g, '') // Remove non-word chars
+ .replace(/\-\-+/g, '-') // multiple dashes → one
+ .replace(/^-+/, '') // Remove leading dash
+ .replace(/-+$/, ''); // Remove trailing dash
+};
\ No newline at end of file