mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
For release.
This commit is contained in:
@@ -1,2 +1,4 @@
|
|||||||
migrate:
|
CLUSTER_SCRIPT=docker/entrypoints/app-dev-entrypoint.sh
|
||||||
docker compose run --rm app
|
|
||||||
|
up:
|
||||||
|
@bash $(CLUSTER_SCRIPT)
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>*/}
|
||||||
|
|||||||
@@ -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
@@ -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: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -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.");
|
||||||
|
|||||||
Reference in New Issue
Block a user