For release.

This commit is contained in:
charlesgauthereau
2025-07-28 16:53:21 +02:00
parent 85047e7509
commit 17469696ee
13 changed files with 172 additions and 65 deletions
+4 -2
View File
@@ -1,2 +1,4 @@
migrate: CLUSTER_SCRIPT=docker/entrypoints/app-dev-entrypoint.sh
docker compose run --rm app
up:
@bash $(CLUSTER_SCRIPT)
+1 -1
View File
@@ -1,7 +1,7 @@
<br /> <br />
<div align="center"> <div align="center">
<a href="https://portabase.io"> <a href="https://portabase.io">
<img src="images/logo.png" alt="Logo" width="80" height="80"> <img src="/public/images/logo.png" alt="Logo" width="80" height="80">
</a> </a>
<h3 align="center">Portabase</h3> <h3 align="center">Portabase</h3>
+2
View File
@@ -15,6 +15,8 @@ export default function Layout({children}: { children: React.ReactNode }) {
const router = useRouter(); const router = useRouter();
const { data: session } = useSession(); const { data: session } = useSession();
if (session && session.user && !session.user.banned && session.user.role !== "pending") { if (session && session.user && !session.user.banned && session.user.role !== "pending") {
router.replace("/dashboard/home"); router.replace("/dashboard/home");
} }
@@ -1,7 +1,7 @@
import {PageParams} from "@/types/next"; import {PageParams} from "@/types/next";
import {Page, PageActions, PageContent, PageDescription, PageHeader, PageTitle} from "@/features/layout/page"; import {Page, PageActions, PageContent, PageDescription, PageHeader, PageTitle} from "@/features/layout/page";
import {currentUser} from "@/lib/auth/current-user"; import {currentUser} from "@/lib/auth/current-user";
import {getOrganization} from "@/lib/auth/auth"; import {getActiveMember, getOrganization} from "@/lib/auth/auth";
import {notFound} from "next/navigation"; import {notFound} from "next/navigation";
import { import {
DeleteOrganizationButton DeleteOrganizationButton
@@ -15,27 +15,33 @@ import {
export default async function RoutePage(props: PageParams<{ slug: string }>) { export default async function RoutePage(props: PageParams<{ slug: string }>) {
const organization = await getOrganization({}); const organization = await getOrganization({});
const user = await currentUser(); const user = await currentUser();
const activeMember = await getActiveMember()
if (!organization) { if (!organization) {
notFound(); notFound();
} }
const isMember = activeMember?.role === "member";
if (isMember) {
notFound();
}
console.log(organization);
return ( return (
<Page> <Page>
<PageHeader> <PageHeader>
<PageTitle className="flex items-center"> <PageTitle className="flex items-center">
Organization settings Organization settings
{organization.slug != "default" ? {!isMember && organization.slug !== "default" && (
<EditButtonSettings /> <EditButtonSettings />
: null} )}
</PageTitle> </PageTitle>
<PageActions> <PageActions>
{organization.slug != "default" ? {!isMember && organization.slug !== "default" && (
<DeleteOrganizationButton organizationSlug={organization.slug} /> <DeleteOrganizationButton organizationSlug={organization.slug} />
: null} )}
</PageActions> </PageActions>
</PageHeader> </PageHeader>
{/*<PageDescription>*/} {/*<PageDescription>*/}
+4 -7
View File
@@ -1,13 +1,11 @@
import {PageParams} from "@/types/next"; import {PageParams} from "@/types/next";
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; import {Page, PageContent, PageTitle} from "@/features/layout/page";
import {notFound} from "next/navigation"; import {notFound} from "next/navigation";
import {UserForm} from "@/components/wrappers/dashboard/profile/user-form/user-form"; import {UserForm} from "@/components/wrappers/dashboard/profile/user-form/user-form";
import {Badge} from "@/components/ui/badge"; import {Badge} from "@/components/ui/badge";
import {ButtonDeleteAccount} from "@/components/wrappers/dashboard/profile/button-delete-account/button-delete-account"; import {ButtonDeleteAccount} from "@/components/wrappers/dashboard/profile/button-delete-account/button-delete-account";
import {AvatarWithUpload} from "@/components/wrappers/dashboard/profile/avatar/avatar-with-upload"; import {AvatarWithUpload} from "@/components/wrappers/dashboard/profile/avatar/avatar-with-upload";
import {currentUser} from "@/lib/auth/current-user"; import {currentUser} from "@/lib/auth/current-user";
import {getAccounts, getSessions} from "@/lib/auth/auth";
//import { getAccounts, getSessions } from "@/lib/auth/auth";
export default async function RoutePage(props: PageParams<{}>) { export default async function RoutePage(props: PageParams<{}>) {
const user = await currentUser(); const user = await currentUser();
@@ -16,7 +14,6 @@ export default async function RoutePage(props: PageParams<{}>) {
return notFound(); return notFound();
} }
if (user.role !== "user" && user.role !== "admin" && user.role !== "superadmin") { if (user.role !== "user" && user.role !== "admin" && user.role !== "superadmin") {
return notFound(); return notFound();
} }
@@ -42,9 +39,9 @@ export default async function RoutePage(props: PageParams<{}>) {
{user.name} {user.name}
<Badge className="ml-3 hidden lg:block">{user.role}</Badge> <Badge className="ml-3 hidden lg:block">{user.role}</Badge>
</PageTitle> </PageTitle>
<PageActions className="mt-2 hidden sm:block"> {/*<PageActions className="mt-2 hidden sm:block">*/}
<ButtonDeleteAccount text="Delete my account"/> {/* <ButtonDeleteAccount text="Delete my account"/>*/}
</PageActions> {/*</PageActions>*/}
</div> </div>
<PageContent> <PageContent>
<UserForm <UserForm
+8 -5
View File
@@ -1,10 +1,13 @@
import { redirect } from "next/navigation";
export default async function NotFound() { export default async function NotFound() {
// redirect("/dashboard/profile");
return( return(
<> <div className="flex items-center min-h-screen px-4 py-12 sm:px-6 md:px-8 lg:px-12 xl:px-16">
Error <div className="w-full space-y-6 text-center">
</> <div className="space-y-3">
<h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">Not found</h1>
<p className="leading-7 [&:not(:first-child)]:mt-6">The content you are trying to view is not available.</p>
</div>
</div>
</div>
) )
} }
@@ -10,7 +10,7 @@ import {Trash2} from "lucide-react";
import {deleteUserAction} from "@/components/wrappers/dashboard/profile/button-delete-account/delete-account.action"; import {deleteUserAction} from "@/components/wrappers/dashboard/profile/button-delete-account/delete-account.action";
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading"; import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
import {User} from "@/db/schema/01_user"; import {User} from "@/db/schema/01_user";
import {useSession} from "@/lib/auth/auth-client"; import {authClient, useSession} from "@/lib/auth/auth-client";
import {formatFrenchDate} from "@/utils/date-formatting"; import {formatFrenchDate} from "@/utils/date-formatting";
export const usersColumnsAdmin: ColumnDef<User>[] = [ export const usersColumnsAdmin: ColumnDef<User>[] = [
@@ -19,7 +19,12 @@ export const usersColumnsAdmin: ColumnDef<User>[] = [
header: "Role", header: "Role",
cell: ({row}) => { cell: ({row}) => {
const [role, setRole] = useState<string>(row.getValue("role")); const [role, setRole] = useState<string>(row.getValue("role"));
const { data: session } = useSession();
const {data: session, isPending, error} = authClient.useSession();
const isCurrentUser = session?.user.email === row.original.email; const isCurrentUser = session?.user.email === row.original.email;
const updateMutation = useMutation({ const updateMutation = useMutation({
@@ -38,6 +43,15 @@ export const usersColumnsAdmin: ColumnDef<User>[] = [
await updateMutation.mutateAsync(); await updateMutation.mutateAsync();
}; };
if (isPending) return null;
if (error || !session) {
return null;
}
return ( return (
<Badge <Badge
className={isCurrentUser ? "cursor-not-allowed opacity-50" : "cursor-pointer"} className={isCurrentUser ? "cursor-not-allowed opacity-50" : "cursor-pointer"}
@@ -22,6 +22,7 @@ export const SidebarMenuCustomMain = () => {
return null; return null;
} }
const groupContent: SidebarGroupItem["group_content"] = [ const groupContent: SidebarGroupItem["group_content"] = [
{ title: "Dashboard", url: "/home", icon: Home }, { title: "Dashboard", url: "/home", icon: Home },
{ title: "Projects", url: "/projects", icon: Layers, details:true }, { title: "Projects", url: "/projects", icon: Layers, details:true },
@@ -4,7 +4,9 @@ import { userAction } from "@/safe-actions";
import {OrganizationSchema} from "@/components/wrappers/dashboard/organization/organization.schema"; import {OrganizationSchema} from "@/components/wrappers/dashboard/organization/organization.schema";
import {ServerActionResult} from "@/types/action-type"; import {ServerActionResult} from "@/types/action-type";
import {z} from "zod"; import {z} from "zod";
import { OrganizationFormSchema } from "@/components/wrappers/dashboard/organization/organization-form/organization-form.schema"; import {
OrganizationFormSchema
} from "@/components/wrappers/dashboard/organization/organization-form/organization-form.schema";
import {db} from "@/db"; import {db} from "@/db";
import {and, eq, inArray} from "drizzle-orm"; import {and, eq, inArray} from "drizzle-orm";
import {auth, checkSlugOrganization, createOrganization, deleteOrganization} from "@/lib/auth/auth"; import {auth, checkSlugOrganization, createOrganization, deleteOrganization} from "@/lib/auth/auth";
@@ -127,7 +129,7 @@ export const updateOrganizationAction = userAction
} }
if (usersToRemove.length > 0) { if (usersToRemove.length > 0) {
await db.delete(drizzleDb.schemas.member).where(inArray(drizzleDb.schemas.member.userId, usersToRemove)).execute(); await db.delete(drizzleDb.schemas.member).where(and(inArray(drizzleDb.schemas.member.userId, usersToRemove), eq(drizzleDb.schemas.member.organizationId, organization.id))).execute();
// TODO : Do not delete, go permission error with better auth // TODO : Do not delete, go permission error with better auth
// for (const userToRemove of usersToRemove) { // for (const userToRemove of usersToRemove) {
// //
@@ -3,11 +3,13 @@
import {ColumnDef} from "@tanstack/react-table"; import {ColumnDef} from "@tanstack/react-table";
import {MemberWithUser} from "@/db/schema/02_organization"; import {MemberWithUser} from "@/db/schema/02_organization";
import {useState} from "react"; import {useState} from "react";
import {useSession} from "@/lib/auth/auth-client"; import {authClient, useSession} from "@/lib/auth/auth-client";
import {useMutation} from "@tanstack/react-query"; import {useMutation} from "@tanstack/react-query";
import {updateUserAction} from "@/components/wrappers/dashboard/profile/user-form/user-form.action";
import {toast} from "sonner"; import {toast} from "sonner";
import {Badge} from "@/components/ui/badge"; import {Badge} from "@/components/ui/badge";
import {updateMemberRoleAction} from "@/components/wrappers/dashboard/settings/update-member.action";
import {RoleSchemaMember} from "@/components/wrappers/dashboard/settings/member.schema";
export const organizationMemberColumns: ColumnDef<MemberWithUser>[] = [ export const organizationMemberColumns: ColumnDef<MemberWithUser>[] = [
{ {
@@ -16,28 +18,41 @@ export const organizationMemberColumns: ColumnDef<MemberWithUser>[] = [
cell: ({ row }) => { cell: ({ row }) => {
const [role, setRole] = useState<string>(row.getValue("role")); const [role, setRole] = useState<string>(row.getValue("role"));
const { data: session } = useSession(); const { data: session } = useSession();
const isCurrentUser = session?.user.email === row.original.user.email;
// const updateMutation = useMutation({
// mutationFn: () => updateUserAction({id: row.original.id, data: {role: role}}), const updateMutation = useMutation({
// onSuccess: () => { mutationFn: () =>
// toast.success(`User updated successfully.`); updateMemberRoleAction({
// }, memberId: row.original.id,
// onError: () => { organizationId: row.original.organizationId,
// toast.error(`An error occurred while updating user information.`); role: RoleSchemaMember.parse(role),
// }, }),
// }); onSuccess: () => {
toast.success(`User updated successfully.`);
},
onError: () => {
toast.error(`An error occurred while updating user information.`);
},
});
const handleUpdateRole = async () => { const handleUpdateRole = async () => {
// const nextRole = role === "admin" ? "pending" : role === "pending" ? "user" : "admin"; const nextRole =
// setRole(nextRole); role === "owner" ? "admin" : role === "admin" ? "member" : "owner";
// await updateMutation.mutateAsync(); setRole(nextRole);
await updateMutation.mutateAsync();
}; };
const isCurrentUser = session?.user.email === row.original.user.email;
const isMember = session?.user.role === "member";
const isDisabled = isMember || isCurrentUser;
return ( return (
<Badge <Badge
className={isCurrentUser ? "cursor-not-allowed opacity-50" : "cursor-pointer"} className={isDisabled ? "cursor-not-allowed opacity-50" : "cursor-pointer"}
onClick={isCurrentUser ? undefined : () => handleUpdateRole()} onClick={isDisabled ? undefined : handleUpdateRole}
variant="outline" variant="outline"
> >
{role} {role}
@@ -0,0 +1,8 @@
import {z} from "zod";
const RoleEnum = z.enum(["member", "owner", "admin"]);
export const RoleSchemaMember = z.union([
RoleEnum,
z.array(RoleEnum)
]);
@@ -0,0 +1,51 @@
"use server";
import {userAction} from "@/safe-actions";
import {z} from "zod";
import {auth} from "@/lib/auth/auth";
import {ServerActionResult} from "@/types/action-type";
import {Member} from "better-auth/plugins";
import {RoleSchemaMember} from "@/components/wrappers/dashboard/settings/member.schema";
import {headers} from "next/headers";
export const updateMemberRoleAction = userAction.schema(
z.object({
memberId: z.string(),
organizationId: z.string(),
role: RoleSchemaMember,
})
).action(async ({parsedInput}): Promise<ServerActionResult<Member>> => {
try {
console.log(parsedInput);
const updatedMember = await auth.api.updateMemberRole({
body: {
role: parsedInput.role,
memberId: parsedInput.memberId,
organizationId: parsedInput.organizationId,
},
headers: await headers(),
});
console.log(updatedMember);
return {
success: true,
value: updatedMember,
actionSuccess: {
message: "Member has been successfully updated.",
messageParams: {},
},
};
} catch (error) {
return {
success: false,
actionError: {
message: "Failed to update member role.",
status: 500,
cause: error instanceof Error ? error.message : "Unknown error",
messageParams: {},
},
};
}
});
+8 -2
View File
@@ -64,6 +64,9 @@ export const auth = betterAuth({
}, },
}, },
user: { user: {
deleteUser: {
enabled: true,
},
additionalFields: { additionalFields: {
deletedAt: { deletedAt: {
type: "number", //pg timestamp type: "number", //pg timestamp
@@ -88,7 +91,9 @@ export const auth = betterAuth({
}; };
}, },
async after(user, context) { async after(user, context) {
// const userCount = (await db.select({count: count()}).from(drizzleDb.schemas.user))[0].count; const userCount = (await db.select({count: count()}).from(drizzleDb.schemas.user))[0].count;
const role = userCount === 0 ? "owner" : "admin";
const defaultOrgSlug = "default"; // change this if your default org has a different slug const defaultOrgSlug = "default"; // change this if your default org has a different slug
const defaultOrg = await db.query.organization.findFirst({ const defaultOrg = await db.query.organization.findFirst({
@@ -96,10 +101,11 @@ export const auth = betterAuth({
}); });
if (defaultOrg) { if (defaultOrg) {
console.log(user)
await db.insert(drizzleDb.schemas.member).values({ await db.insert(drizzleDb.schemas.member).values({
userId: user.id, userId: user.id,
organizationId: defaultOrg.id, organizationId: defaultOrg.id,
role: "owner", role: role,
}); });
} else { } else {
console.warn("Default organization not found. Cannot assign member."); console.warn("Default organization not found. Cannot assign member.");