mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Merge branch 'dev' into feat/oidc
This commit is contained in:
+2
-2
@@ -26,5 +26,5 @@ keywords:
|
||||
- web-ui
|
||||
- agent
|
||||
license: Apache-2.0
|
||||
version: 1.2.9
|
||||
date-released: "2026-02-19"
|
||||
version: 1.3.0
|
||||
date-released: "2026-02-24"
|
||||
|
||||
+39
-32
@@ -10,42 +10,49 @@ import { CardAuth } from "@/features/layout/card-auth";
|
||||
import { env } from "@/env.mjs";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Login",
|
||||
title: "Login",
|
||||
};
|
||||
|
||||
export default async function SignInPage() {
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<CardAuth className="w-full">
|
||||
<CardHeader>
|
||||
<div className="grid gap-2 text-center mb-2">
|
||||
<h1 className="text-3xl font-bold">Login</h1>
|
||||
<p className="text-balance text-muted-foreground">Fill your login informations</p>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{env.AUTH_EMAIL_PASSWORD_ENABLED === "true" && <LoginForm isPasskeyEnabled={env.AUTH_PASSKEY_ENABLED === "true"} />}
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<CardAuth className="w-full">
|
||||
<CardHeader>
|
||||
<div className="grid gap-2 text-center mb-2">
|
||||
<h1 className="text-3xl font-bold">Login</h1>
|
||||
<p className="text-balance text-muted-foreground">
|
||||
Fill your login informations
|
||||
</p>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{env.AUTH_EMAIL_PASSWORD_ENABLED === "true" && (
|
||||
<LoginForm isPasskeyEnabled={env.AUTH_PASSKEY_ENABLED === "true"} />
|
||||
)}
|
||||
|
||||
{env.AUTH_EMAIL_PASSWORD_ENABLED === "true" && SUPPORTED_PROVIDERS.filter((p) => !p.isManual && p.isActive).length > 0 && (
|
||||
<div className="relative my-4 flex items-center justify-center overflow-hidden">
|
||||
<Separator />
|
||||
<div className="px-2 text-center text-sm">OR</div>
|
||||
<Separator />
|
||||
</div>
|
||||
)}
|
||||
{env.AUTH_EMAIL_PASSWORD_ENABLED === "true" &&
|
||||
SUPPORTED_PROVIDERS.filter((p) => !p.isManual && p.isActive)
|
||||
.length > 0 && (
|
||||
<div className="relative my-4 flex items-center justify-center overflow-hidden">
|
||||
<Separator />
|
||||
<div className="px-2 text-center text-sm">OR</div>
|
||||
<Separator />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{SUPPORTED_PROVIDERS.filter((p) => !p.isManual && p.isActive).length > 0 && <SocialAuthButtons providers={SUPPORTED_PROVIDERS} />}
|
||||
{SUPPORTED_PROVIDERS.filter((p) => !p.isManual && p.isActive).length >
|
||||
0 && <SocialAuthButtons providers={SUPPORTED_PROVIDERS} />}
|
||||
|
||||
{env.AUTH_SIGNUP_ENABLED === "true" && (
|
||||
<div className="mt-4 text-center text-sm">
|
||||
Don't have an account ?{" "}
|
||||
<Link href="/register" className="underline">
|
||||
Sign up
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</CardAuth>
|
||||
</TooltipProvider>
|
||||
);
|
||||
{env.AUTH_SIGNUP_ENABLED === "true" && (
|
||||
<div className="mt-4 text-center text-sm">
|
||||
Don't have an account ?{" "}
|
||||
<Link href="/register" className="underline">
|
||||
Sign up
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</CardAuth>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {db} from "@/db";
|
||||
import {desc, isNull} from "drizzle-orm";
|
||||
import {AdminUserList} from "@/components/wrappers/dashboard/admin/users/admin-user-list";
|
||||
import {AdminUserAddModal} from "@/components/wrappers/dashboard/admin/users/admin-user-add-modal";
|
||||
import {SUPPORTED_PROVIDERS} from "@/lib/auth/config";
|
||||
|
||||
export default async function RoutePage(props: PageParams<{}>) {
|
||||
|
||||
@@ -21,6 +22,9 @@ export default async function RoutePage(props: PageParams<{}>) {
|
||||
},
|
||||
});
|
||||
|
||||
const credentialProvider = SUPPORTED_PROVIDERS.find(p => p.id === 'credential');
|
||||
const isPasswordAuthEnabled = credentialProvider?.isActive || false;
|
||||
|
||||
return (
|
||||
<Page>
|
||||
<PageHeader className="flex flex-col">
|
||||
@@ -32,7 +36,7 @@ export default async function RoutePage(props: PageParams<{}>) {
|
||||
</div>
|
||||
</PageHeader>
|
||||
<PageContent className="flex flex-col gap-5">
|
||||
<AdminUserList users={users}/>
|
||||
<AdminUserList users={users} isPasswordAuthEnabled={isPasswordAuthEnabled}/>
|
||||
</PageContent>
|
||||
</Page>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import {NextResponse} from "next/server";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
return NextResponse.json({
|
||||
PROJECT_URL: process.env.PROJECT_URL,
|
||||
PROJECT_NAME: process.env.PROJECT_NAME,
|
||||
PROJECT_DESCRIPTION: process.env.PROJECT_DESCRIPTION,
|
||||
});
|
||||
return NextResponse.json({
|
||||
PROJECT_URL: process.env.PROJECT_URL,
|
||||
PROJECT_NAME: process.env.PROJECT_NAME,
|
||||
PROJECT_DESCRIPTION: process.env.PROJECT_DESCRIPTION,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
name: portabase-dev-func
|
||||
|
||||
services:
|
||||
keycloak:
|
||||
image: quay.io/keycloak/keycloak:latest
|
||||
command: start-dev
|
||||
environment:
|
||||
KC_BOOTSTRAP_ADMIN_USERNAME: admin
|
||||
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- keycloak-data:/opt/keycloak/data
|
||||
pocket-id:
|
||||
image: ghcr.io/pocket-id/pocket-id
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- APP_URL=http://localhost:3055
|
||||
- ENCRYPTION_KEY=QwHyjbZvSsDUAcjpdmSPsuYxaH6vET6OeBaeLwXccCb43L6Om3W1AoU5pKIJTzYr
|
||||
ports:
|
||||
- 3055:1411
|
||||
volumes:
|
||||
- pocket-id-data:/app/data
|
||||
healthcheck:
|
||||
test: "curl -f http://localhost:1411/healthz"
|
||||
interval: 1m30s
|
||||
timeout: 5s
|
||||
retries: 2
|
||||
start_period: 10s
|
||||
volumes:
|
||||
keycloak-data:
|
||||
pocket-id-data:
|
||||
@@ -32,33 +32,5 @@ services:
|
||||
- ./private/uploads/tmp:/data/uploads/tmp
|
||||
user: "1000:1000"
|
||||
|
||||
keycloak:
|
||||
image: quay.io/keycloak/keycloak:latest
|
||||
command: start-dev
|
||||
environment:
|
||||
KC_BOOTSTRAP_ADMIN_USERNAME: admin
|
||||
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- keycloak-data:/opt/keycloak/data
|
||||
pocket-id:
|
||||
image: ghcr.io/pocket-id/pocket-id
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- APP_URL=http://localhost:3055
|
||||
- ENCRYPTION_KEY=QwHyjbZvSsDUAcjpdmSPsuYxaH6vET6OeBaeLwXccCb43L6Om3W1AoU5pKIJTzYr
|
||||
ports:
|
||||
- 3055:1411
|
||||
volumes:
|
||||
- pocket-id-data:/app/data
|
||||
healthcheck:
|
||||
test: "curl -f http://localhost:1411/healthz"
|
||||
interval: 1m30s
|
||||
timeout: 5s
|
||||
retries: 2
|
||||
start_period: 10s
|
||||
volumes:
|
||||
postgres-data:
|
||||
keycloak-data:
|
||||
pocket-id-data:
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "portabase",
|
||||
"version": "1.2.9",
|
||||
"version": "1.3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack -p 8887",
|
||||
@@ -129,5 +129,5 @@
|
||||
"typescript": "^5.9.3",
|
||||
"zenstack": "2.14.2"
|
||||
},
|
||||
"packageManager": "pnpm@10.29.2+sha512.bef43fa759d91fd2da4b319a5a0d13ef7a45bb985a3d7342058470f9d2051a3ba8674e629672654686ef9443ad13a82da2beb9eeb3e0221c87b8154fff9d74b8"
|
||||
"packageManager": "pnpm@10.30.1+sha512.3590e550d5384caa39bd5c7c739f72270234b2f6059e13018f975c313b1eb9fefcc09714048765d4d9efe961382c312e624572c0420762bdc5d5940cdf9be73a"
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
@@ -6,7 +6,7 @@ import {env} from "@/env.mjs";
|
||||
|
||||
interface EmailCreateUserProps {
|
||||
email: string;
|
||||
password: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export const EmailCreateUser = ({email, password}: EmailCreateUserProps) => {
|
||||
@@ -23,9 +23,15 @@ export const EmailCreateUser = ({email, password}: EmailCreateUserProps) => {
|
||||
<strong>Email: </strong>{email}
|
||||
</Text>
|
||||
|
||||
<Text className="text-[14px] text-black leading-[24px]">
|
||||
<strong>Default password: </strong>{password}
|
||||
</Text>
|
||||
{password ? (
|
||||
<Text className="text-[14px] text-black leading-[24px]">
|
||||
<strong>Default password: </strong>{password}
|
||||
</Text>
|
||||
) : (
|
||||
<Text className="text-[14px] text-black leading-[24px]">
|
||||
You can log in using one of the single sign-on (SSO) providers configured by your administrator.
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Section className="mt-[32px] mb-[32px] text-center">
|
||||
<Button
|
||||
|
||||
@@ -39,7 +39,8 @@ export function SocialAuthButtons({ providers }: { providers: AuthProviderConfig
|
||||
});
|
||||
} else {
|
||||
result = await authClient.signIn.social({
|
||||
provider: provider.id as "google" | "github",
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
provider: provider.id as any,
|
||||
callbackURL: "/dashboard",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,8 +5,9 @@ import {User} from "@/db/schema/02_user";
|
||||
|
||||
type AdminUserListProps = {
|
||||
users: User[];
|
||||
isPasswordAuthEnabled: boolean;
|
||||
};
|
||||
|
||||
export const AdminUserList = ({ users }: AdminUserListProps) => {
|
||||
return <DataTable columns={usersListColumns()} data={users} enablePagination={true} enableSelect={false} />;
|
||||
export const AdminUserList = ({ users, isPasswordAuthEnabled }: AdminUserListProps) => {
|
||||
return <DataTable columns={usersListColumns({ isPasswordAuthEnabled })} data={users} enablePagination={true} enableSelect={false} />;
|
||||
};
|
||||
|
||||
@@ -9,7 +9,11 @@ import {Info} from "lucide-react";
|
||||
import {Table, TableBody, TableCell, TableRow} from "@/components/ui/table";
|
||||
import {UserActionsCell} from "@/components/wrappers/dashboard/admin/users/user-actions-cell";
|
||||
|
||||
export function usersListColumns(): ColumnDef<User>[] {
|
||||
type UsersListColumnsProps = {
|
||||
isPasswordAuthEnabled: boolean;
|
||||
}
|
||||
|
||||
export function usersListColumns({ isPasswordAuthEnabled }: UsersListColumnsProps): ColumnDef<User>[] {
|
||||
|
||||
return [
|
||||
{
|
||||
@@ -85,7 +89,7 @@ export function usersListColumns(): ColumnDef<User>[] {
|
||||
{
|
||||
id: "actions",
|
||||
header: "Actions",
|
||||
cell: ({row}) => <UserActionsCell user={row.original}/>,
|
||||
cell: ({row}) => <UserActionsCell user={row.original} isPasswordAuthEnabled={isPasswordAuthEnabled} />,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
@@ -20,9 +20,10 @@ import {AdminDeleteUserModal} from "@/components/wrappers/dashboard/admin/users/
|
||||
|
||||
interface UserActionsCellProps {
|
||||
user: User;
|
||||
isPasswordAuthEnabled: boolean;
|
||||
}
|
||||
|
||||
export function UserActionsCell({user}: UserActionsCellProps) {
|
||||
export function UserActionsCell({user, isPasswordAuthEnabled}: UserActionsCellProps) {
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isModalChangePasswordOpen, setIsModalChangePasswordOpen] = useState(false);
|
||||
@@ -43,9 +44,11 @@ export function UserActionsCell({user}: UserActionsCellProps) {
|
||||
onOpenChange={setIsModalChangePasswordOpen}/>
|
||||
<AdminUserEdit user={user} open={isModalEditUserOpen} onOpenChange={setIsModalEditUserOpen}/>
|
||||
<div className={cn("flex items-center space-x-2")}>
|
||||
<Button variant="outline" size="icon" onClick={() => setIsModalChangePasswordOpen(true)}>
|
||||
<RotateCcwKey className="w-4 h-4"/>
|
||||
</Button>
|
||||
{isPasswordAuthEnabled && (
|
||||
<Button variant="outline" size="icon" onClick={() => setIsModalChangePasswordOpen(true)}>
|
||||
<RotateCcwKey className="w-4 h-4"/>
|
||||
</Button>
|
||||
)}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon">
|
||||
|
||||
@@ -1,191 +1,211 @@
|
||||
"use server";
|
||||
|
||||
import * as drizzleDb from "@/db";
|
||||
import {ServerActionResult} from "@/types/action-type";
|
||||
import {render} from "@react-email/render";
|
||||
import {UserSchema} from "@/components/wrappers/dashboard/admin/users/user.schema";
|
||||
import {extractNameFromEmail} from "@/utils/name-from-email";
|
||||
import {generateValidPassword} from "@/utils/password";
|
||||
import {auth} from "@/lib/auth/auth";
|
||||
import {z} from "zod";
|
||||
import {Organization} from "@/db/schema/03_organization";
|
||||
import {db} from "@/db";
|
||||
import {and, eq} from "drizzle-orm";
|
||||
import { ServerActionResult } from "@/types/action-type";
|
||||
import { render } from "@react-email/render";
|
||||
import { UserSchema } from "@/components/wrappers/dashboard/admin/users/user.schema";
|
||||
import { extractNameFromEmail } from "@/utils/name-from-email";
|
||||
import { generateValidPassword } from "@/utils/password";
|
||||
import { auth } from "@/lib/auth/auth";
|
||||
import { z } from "zod";
|
||||
import { Organization } from "@/db/schema/03_organization";
|
||||
import { db } from "@/db";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
|
||||
import {zEmail, zString} from "@/lib/zod";
|
||||
import {withUpdatedAt} from "@/db/utils";
|
||||
import {userAction} from "@/lib/safe-actions/actions";
|
||||
import {
|
||||
addMemberOrganizationAction
|
||||
} from "@/components/wrappers/dashboard/admin/organizations/organization/details/add-member.action";
|
||||
import {sendEmail} from "@/lib/email";
|
||||
import { zEmail, zString } from "@/lib/zod";
|
||||
import { withUpdatedAt } from "@/db/utils";
|
||||
import { userAction } from "@/lib/safe-actions/actions";
|
||||
import { addMemberOrganizationAction } from "@/components/wrappers/dashboard/admin/organizations/organization/details/add-member.action";
|
||||
import { sendEmail } from "@/lib/email";
|
||||
import EmailCreateUser from "@/components/emails/email-create-user";
|
||||
import {SignUpUser} from "@/types/auth";
|
||||
import {createUserDb} from "@/db/services/user";
|
||||
import {User} from "@/db/schema/02_user";
|
||||
import { SignUpUser } from "@/types/auth";
|
||||
import { createUserDb } from "@/db/services/user";
|
||||
import { User } from "@/db/schema/02_user";
|
||||
import { env } from "@/env.mjs";
|
||||
|
||||
export const createUserAction = userAction.schema(UserSchema).action(async ({parsedInput}): Promise<ServerActionResult<User>> => {
|
||||
export const createUserAction = userAction
|
||||
.schema(UserSchema)
|
||||
.action(async ({ parsedInput }): Promise<ServerActionResult<User>> => {
|
||||
try {
|
||||
const password = generateValidPassword();
|
||||
const isPasswordAuthEnabled = env.AUTH_EMAIL_PASSWORD_ENABLED === "true";
|
||||
let password;
|
||||
|
||||
const userData: SignUpUser = {
|
||||
name: parsedInput.name || extractNameFromEmail(parsedInput.email),
|
||||
email: parsedInput.email,
|
||||
password: password,
|
||||
theme: "dark",
|
||||
role: "user",
|
||||
};
|
||||
const userData: SignUpUser = {
|
||||
name: parsedInput.name || extractNameFromEmail(parsedInput.email),
|
||||
email: parsedInput.email,
|
||||
theme: "dark",
|
||||
role: "user",
|
||||
};
|
||||
|
||||
const newUser = await createUserDb(userData);
|
||||
if (isPasswordAuthEnabled) {
|
||||
password = generateValidPassword();
|
||||
userData.password = password;
|
||||
}
|
||||
|
||||
if (newUser) {
|
||||
const newUser = await createUserDb(userData);
|
||||
|
||||
await sendEmail({
|
||||
to: parsedInput.email,
|
||||
subject: "Your account is created",
|
||||
html: await render(EmailCreateUser({
|
||||
password: password,
|
||||
email: parsedInput.email,
|
||||
})),
|
||||
});
|
||||
if (newUser) {
|
||||
await sendEmail({
|
||||
to: parsedInput.email,
|
||||
subject: "Your account is created",
|
||||
html: await render(
|
||||
EmailCreateUser({
|
||||
password: password,
|
||||
email: parsedInput.email,
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
const defaultOrganization = await db.query.organization.findFirst({
|
||||
where: eq(drizzleDb.schemas.organization.slug, "default"),
|
||||
});
|
||||
const defaultOrganization = await db.query.organization.findFirst({
|
||||
where: eq(drizzleDb.schemas.organization.slug, "default"),
|
||||
});
|
||||
|
||||
if (defaultOrganization) {
|
||||
await auth.api.addMember({
|
||||
body: {
|
||||
userId: newUser.id,
|
||||
organizationId: defaultOrganization.id,
|
||||
role: "admin",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: newUser,
|
||||
actionSuccess: {
|
||||
message: "user_created",
|
||||
},
|
||||
};
|
||||
if (defaultOrganization) {
|
||||
await auth.api.addMember({
|
||||
body: {
|
||||
userId: newUser.id,
|
||||
organizationId: defaultOrganization.id,
|
||||
role: "admin",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "user_created",
|
||||
cause: "Unknown error",
|
||||
},
|
||||
success: true,
|
||||
value: newUser,
|
||||
actionSuccess: {
|
||||
message: "user_created",
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "user_created",
|
||||
cause: "Unknown error",
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "user_created",
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
},
|
||||
};
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "user_created",
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
export const updateUserAction = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
id: zString(),
|
||||
name: zString().optional(),
|
||||
email: zEmail(),
|
||||
})
|
||||
)
|
||||
.action(async ({parsedInput, ctx}): Promise<ServerActionResult<{}>> => {
|
||||
try {
|
||||
.schema(
|
||||
z.object({
|
||||
id: zString(),
|
||||
name: zString().optional(),
|
||||
email: zEmail(),
|
||||
}),
|
||||
)
|
||||
.action(async ({ parsedInput, ctx }): Promise<ServerActionResult<{}>> => {
|
||||
try {
|
||||
const [updatedUser] = await db
|
||||
.update(drizzleDb.schemas.user)
|
||||
.set(
|
||||
withUpdatedAt({
|
||||
name: parsedInput.name
|
||||
? parsedInput.name
|
||||
: extractNameFromEmail(parsedInput.email),
|
||||
email: parsedInput.email,
|
||||
emailVerified: false,
|
||||
}),
|
||||
)
|
||||
.where(eq(drizzleDb.schemas.user.id, parsedInput.id))
|
||||
.returning();
|
||||
|
||||
if (updatedUser) {
|
||||
return {
|
||||
success: true,
|
||||
actionSuccess: {
|
||||
message: "user_updated",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const [updatedUser] = await db.update(drizzleDb.schemas.user).set(withUpdatedAt({
|
||||
name: parsedInput.name ? parsedInput.name : extractNameFromEmail(parsedInput.email),
|
||||
email: parsedInput.email,
|
||||
emailVerified: false
|
||||
})).where(eq(drizzleDb.schemas.user.id, parsedInput.id)).returning();
|
||||
|
||||
|
||||
if (updatedUser) {
|
||||
return {
|
||||
success: true,
|
||||
actionSuccess: {
|
||||
message: "user_updated",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "user_updated",
|
||||
cause: "Unknown error",
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "user_updated",
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "user_updated",
|
||||
cause: "Unknown error",
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "user_updated",
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
export const setSuperAdminOwnerOfOrganizationsOwnedByUser = userAction
|
||||
.schema(
|
||||
z.object({
|
||||
userId: z.string(),
|
||||
})
|
||||
)
|
||||
.action(async ({parsedInput}): Promise<ServerActionResult<Organization[]>> => {
|
||||
try {
|
||||
const organizationsWhereUserIsMemberAndOwner = await db.query.member.findMany({
|
||||
where: and(eq(drizzleDb.schemas.member.role, "owner"), eq(drizzleDb.schemas.member.userId, parsedInput.userId)),
|
||||
with: {
|
||||
organization: true,
|
||||
},
|
||||
});
|
||||
.schema(
|
||||
z.object({
|
||||
userId: z.string(),
|
||||
}),
|
||||
)
|
||||
.action(
|
||||
async ({ parsedInput }): Promise<ServerActionResult<Organization[]>> => {
|
||||
try {
|
||||
const organizationsWhereUserIsMemberAndOwner =
|
||||
await db.query.member.findMany({
|
||||
where: and(
|
||||
eq(drizzleDb.schemas.member.role, "owner"),
|
||||
eq(drizzleDb.schemas.member.userId, parsedInput.userId),
|
||||
),
|
||||
with: {
|
||||
organization: true,
|
||||
},
|
||||
});
|
||||
|
||||
const superAdminUser = await db.query.user.findFirst();
|
||||
if (!superAdminUser) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "set_super_admin_owner_of_organizations_owned_by_user",
|
||||
cause: "Unknown error",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
for (let {organization} of organizationsWhereUserIsMemberAndOwner) {
|
||||
await addMemberOrganizationAction({
|
||||
userId: superAdminUser.id,
|
||||
organizationId: organization.id,
|
||||
role: "owner",
|
||||
});
|
||||
}
|
||||
const organizations = organizationsWhereUserIsMemberAndOwner.map(
|
||||
(organizationWhereUserIsMemberAndOwner) => organizationWhereUserIsMemberAndOwner.organization
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: organizations as unknown as Organization[],
|
||||
actionSuccess: {
|
||||
message: "set_super_admin_owner_of_organizations_owned_by_user",
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "error_set_super_admin_owner_of_organizations_owned_by_user",
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
},
|
||||
};
|
||||
const superAdminUser = await db.query.user.findFirst();
|
||||
if (!superAdminUser) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message: "set_super_admin_owner_of_organizations_owned_by_user",
|
||||
cause: "Unknown error",
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
for (let { organization } of organizationsWhereUserIsMemberAndOwner) {
|
||||
await addMemberOrganizationAction({
|
||||
userId: superAdminUser.id,
|
||||
organizationId: organization.id,
|
||||
role: "owner",
|
||||
});
|
||||
}
|
||||
const organizations = organizationsWhereUserIsMemberAndOwner.map(
|
||||
(organizationWhereUserIsMemberAndOwner) =>
|
||||
organizationWhereUserIsMemberAndOwner.organization,
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
value: organizations as unknown as Organization[],
|
||||
actionSuccess: {
|
||||
message: "set_super_admin_owner_of_organizations_owned_by_user",
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
actionError: {
|
||||
message:
|
||||
"error_set_super_admin_owner_of_organizations_owned_by_user",
|
||||
cause: error instanceof Error ? error.message : "Unknown error",
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -8,7 +8,11 @@ import { authClient } from "@/lib/auth/auth-client";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { SetPasswordProfileProviderModal } from "./modal/set-password-modal";
|
||||
import { Icon } from "@iconify/react";
|
||||
@@ -17,164 +21,217 @@ import type { AuthProviderConfig } from "@/lib/auth/config";
|
||||
import { Account } from "@/db/schema/02_user";
|
||||
|
||||
interface ProfileProviderProps {
|
||||
accounts: Account[];
|
||||
providers: AuthProviderConfig[];
|
||||
accounts: Account[];
|
||||
providers: AuthProviderConfig[];
|
||||
}
|
||||
|
||||
export function ProfileProviders({ accounts, providers }: ProfileProviderProps) {
|
||||
const router = useRouter();
|
||||
const totalConnected = accounts.length;
|
||||
const [loadingProvider, setLoadingProvider] = useState<string | null>(null);
|
||||
export function ProfileProviders({
|
||||
accounts,
|
||||
providers,
|
||||
}: ProfileProviderProps) {
|
||||
const router = useRouter();
|
||||
const totalConnected = accounts.length;
|
||||
const [loadingProvider, setLoadingProvider] = useState<string | null>(null);
|
||||
|
||||
const [isPasswordDialogOpen, setIsPasswordDialogOpen] = useState(false);
|
||||
const [isPasswordDialogOpen, setIsPasswordDialogOpen] = useState(false);
|
||||
|
||||
const { mutate: unlinkAccount } = useMutation({
|
||||
mutationFn: async (providerId: string) => {
|
||||
setLoadingProvider(providerId);
|
||||
const { error } = await authClient.unlinkAccount({ providerId });
|
||||
if (error) throw error;
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success("Provider successfully unlinked!");
|
||||
setLoadingProvider(null);
|
||||
router.refresh();
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("An error occurred while unlinking provider.");
|
||||
setLoadingProvider(null);
|
||||
},
|
||||
});
|
||||
const { mutate: unlinkAccount } = useMutation({
|
||||
mutationFn: async (providerId: string) => {
|
||||
setLoadingProvider(providerId);
|
||||
const { error } = await authClient.unlinkAccount({ providerId });
|
||||
if (error) throw error;
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.success("Provider successfully unlinked!");
|
||||
setLoadingProvider(null);
|
||||
router.refresh();
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("An error occurred while unlinking provider.");
|
||||
setLoadingProvider(null);
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: linkAccount } = useMutation({
|
||||
mutationFn: async (provider: AuthProviderConfig) => {
|
||||
setLoadingProvider(provider.id);
|
||||
let result;
|
||||
if (provider.type === "sso") {
|
||||
result = await authClient.signIn.sso({
|
||||
providerId: provider.id,
|
||||
providerType: "oidc",
|
||||
callbackURL: "/dashboard",
|
||||
});
|
||||
} else {
|
||||
result = await authClient.signIn.social({
|
||||
provider: provider.id as "google" | "github" | "credential",
|
||||
callbackURL: "/dashboard",
|
||||
});
|
||||
const { mutate: linkAccount } = useMutation({
|
||||
mutationFn: async (provider: AuthProviderConfig) => {
|
||||
setLoadingProvider(provider.id);
|
||||
let result;
|
||||
if (provider.type === "sso") {
|
||||
result = await authClient.signIn.sso({
|
||||
providerId: provider.id,
|
||||
providerType: "oidc",
|
||||
callbackURL: "/dashboard",
|
||||
});
|
||||
} else {
|
||||
result = await authClient.signIn.social({
|
||||
provider: provider.id as any,
|
||||
callbackURL: "/dashboard",
|
||||
});
|
||||
}
|
||||
if (result.error) throw result.error;
|
||||
},
|
||||
onSuccess: () => {
|
||||
setLoadingProvider(null);
|
||||
router.refresh();
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("An error occurred while linking provider.");
|
||||
setLoadingProvider(null);
|
||||
},
|
||||
});
|
||||
|
||||
const enterpriseProviders = providers.filter(
|
||||
(p) => p.type === "sso" && p.id !== "passkey" && p.type !== "credential",
|
||||
);
|
||||
const otherProviders = providers.filter(
|
||||
(p) => p.type !== "sso" && p.id !== "passkey" && p.type !== "credential",
|
||||
);
|
||||
|
||||
const renderProvider = (provider: AuthProviderConfig) => {
|
||||
const linkedAccount = accounts.find(
|
||||
(acc) => acc.providerId === provider.id,
|
||||
);
|
||||
const isConnected = !!linkedAccount;
|
||||
const canUnlink =
|
||||
totalConnected > 1 || (totalConnected === 1 && !provider.isManual);
|
||||
const isLoading = loadingProvider === provider.id;
|
||||
|
||||
const isUnlinkDisabled = !canUnlink || provider.allowUnlinking === false;
|
||||
|
||||
const unlinkButton = (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => unlinkAccount(provider.id)}
|
||||
disabled={isUnlinkDisabled || isLoading || provider.isManual}
|
||||
className={isUnlinkDisabled ? "opacity-50 cursor-not-allowed" : ""}
|
||||
>
|
||||
{isLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : "Unlink"}
|
||||
</Button>
|
||||
);
|
||||
|
||||
let actionElement;
|
||||
|
||||
if (!isConnected) {
|
||||
actionElement =
|
||||
provider.id === "credential" ? (
|
||||
<SetPasswordProfileProviderModal
|
||||
open={isPasswordDialogOpen}
|
||||
onOpenChange={setIsPasswordDialogOpen}
|
||||
/>
|
||||
) : (
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={() => linkAccount(provider)}
|
||||
disabled={
|
||||
isLoading || provider.isManual || provider.allowLinking === false
|
||||
}
|
||||
if (result.error) throw result.error;
|
||||
},
|
||||
onSuccess: () => {
|
||||
setLoadingProvider(null);
|
||||
router.refresh();
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("An error occurred while linking provider.");
|
||||
setLoadingProvider(null);
|
||||
},
|
||||
});
|
||||
|
||||
const enterpriseProviders = providers.filter((p) => p.type === "sso" && p.id !== "passkey" && p.type !== "credential");
|
||||
const otherProviders = providers.filter((p) => p.type !== "sso" && p.id !== "passkey" && p.type !== "credential");
|
||||
|
||||
const renderProvider = (provider: AuthProviderConfig) => {
|
||||
const linkedAccount = accounts.find((acc) => acc.providerId === provider.id);
|
||||
const isConnected = !!linkedAccount;
|
||||
const canUnlink = totalConnected > 1 || (totalConnected === 1 && !provider.isManual);
|
||||
const isLoading = loadingProvider === provider.id;
|
||||
|
||||
return (
|
||||
<div key={provider.id} className="flex items-center justify-between p-4 border rounded-lg hover:bg-muted/30 transition-colors">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 rounded-full bg-muted flex items-center justify-center">
|
||||
{provider.icon.startsWith("/") || provider.icon.startsWith("http") ? (
|
||||
<Image
|
||||
src={provider.icon}
|
||||
alt={provider.id}
|
||||
width={20}
|
||||
height={20}
|
||||
className="w-5 h-5"
|
||||
unoptimized={provider.icon.startsWith("http")}
|
||||
/>
|
||||
) : (
|
||||
<Icon icon={provider.icon} className="w-5 h-5" />
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
<div className="font-medium flex items-center gap-2">
|
||||
{provider.title || provider.name}
|
||||
{isConnected && (
|
||||
<Badge variant="secondary" className="text-[10px] h-5 px-1.5 text-green-600 bg-green-500/10 border-0">
|
||||
Active
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">{provider.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{isConnected ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span tabIndex={0}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => unlinkAccount(provider.id)}
|
||||
disabled={!canUnlink || isLoading || provider.isManual || provider.allowUnlinking === false}
|
||||
className={!canUnlink || provider.allowUnlinking === false ? "opacity-50 cursor-not-allowed" : ""}
|
||||
>
|
||||
{isLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : "Unlink"}
|
||||
</Button>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
{(!canUnlink || provider.allowUnlinking === false) && (
|
||||
<TooltipContent>
|
||||
<p>{provider.allowUnlinking === false ? "Unlinking is disabled for this provider." : "You cannot unlink your last authentication provider."}</p>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
<>
|
||||
{provider.id === "credential" ? (
|
||||
<SetPasswordProfileProviderModal open={isPasswordDialogOpen} onOpenChange={setIsPasswordDialogOpen} />
|
||||
) : (
|
||||
<Button variant="default" size="sm" onClick={() => linkAccount(provider)} disabled={isLoading || provider.isManual || provider.allowLinking === false}>
|
||||
{isLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : "Link"}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
>
|
||||
{isLoading ? <Loader2 className="w-4 h-4 animate-spin" /> : "Link"}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
} else if (isUnlinkDisabled) {
|
||||
actionElement = (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div tabIndex={0} className="inline-block">
|
||||
{unlinkButton}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
{provider.allowUnlinking === false
|
||||
? "Unlinking is disabled for this provider."
|
||||
: "You cannot unlink your last authentication provider."}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
} else {
|
||||
actionElement = unlinkButton;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-in fade-in-50 duration-300">
|
||||
<div className="mb-6 space-y-1">
|
||||
<h2 className="text-2xl font-semibold tracking-tight">Authentication</h2>
|
||||
<p className="text-sm text-muted-foreground">Manage how you access your account.</p>
|
||||
</div>
|
||||
|
||||
{enterpriseProviders.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wider">Enterprise Connection</h3>
|
||||
<div className="grid gap-4">{enterpriseProviders.map(renderProvider)}</div>
|
||||
</div>
|
||||
<div
|
||||
key={provider.id}
|
||||
className="flex items-center justify-between p-4 border rounded-lg hover:bg-muted/30 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 rounded-full bg-muted flex items-center justify-center">
|
||||
{provider.icon.startsWith("/") ||
|
||||
provider.icon.startsWith("http") ? (
|
||||
<Image
|
||||
src={provider.icon}
|
||||
alt={provider.id}
|
||||
width={20}
|
||||
height={20}
|
||||
className="w-5 h-5"
|
||||
unoptimized={provider.icon.startsWith("http")}
|
||||
/>
|
||||
) : (
|
||||
<Icon icon={provider.icon} className="w-5 h-5" />
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wider">Standard Connections</h3>
|
||||
<div className="grid gap-4">{otherProviders.map(renderProvider)}</div>
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
<div className="font-medium flex items-center gap-2">
|
||||
{provider.title || provider.name}
|
||||
{isConnected && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="text-[10px] h-5 px-1.5 text-green-600 bg-green-500/10 border-0"
|
||||
>
|
||||
Active
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Alert variant={"default"}>
|
||||
<AlertTriangle className="w-5 h-5 shrink-0 mt-0.5" />
|
||||
<AlertDescription>Linked providers allow you to log in to your account using any of these methods.</AlertDescription>
|
||||
</Alert>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{provider.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">{actionElement}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-in fade-in-50 duration-300 pb-10">
|
||||
<div className="mb-6 space-y-1">
|
||||
<h2 className="text-2xl font-semibold tracking-tight">
|
||||
Authentication
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Manage how you access your account.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{enterpriseProviders.length > 0 && (
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wider">
|
||||
Enterprise Connection
|
||||
</h3>
|
||||
<div className="grid gap-4">
|
||||
{enterpriseProviders.map(renderProvider)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
<h3 className="text-sm font-medium text-muted-foreground uppercase tracking-wider">
|
||||
Standard Connections
|
||||
</h3>
|
||||
<div className="grid gap-4">{otherProviders.map(renderProvider)}</div>
|
||||
</div>
|
||||
|
||||
<Alert variant={"default"}>
|
||||
<AlertTriangle className="w-5 h-5 shrink-0 mt-0.5" />
|
||||
<AlertDescription>
|
||||
Linked providers allow you to log in to your account using any of
|
||||
these methods.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -162,77 +162,81 @@ export function ProfileSecurity({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-lg font-medium">Authentication</h3>
|
||||
<div className="border rounded-lg p-4 space-y-4">
|
||||
{isPasswordEnabled && (
|
||||
<>
|
||||
{isPasskeyEnabled && (
|
||||
<div className="space-y-6">
|
||||
<h3 className="text-lg font-medium">Authentication</h3>
|
||||
<div className="border rounded-lg p-4 space-y-4">
|
||||
{isPasswordEnabled && (
|
||||
<>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div className="space-y-1">
|
||||
<div className="font-medium">Password</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{user.lastChangedPasswordAt
|
||||
? `Last changed ${timeAgo(new Date(user.lastChangedPasswordAt))}`
|
||||
: "Never changed"}
|
||||
</div>
|
||||
</div>
|
||||
{credentialAccount ? (
|
||||
<ResetPasswordProfileProviderModal
|
||||
open={isPasswordDialogOpen}
|
||||
onOpenChange={setIsPasswordDialogOpen}
|
||||
/>
|
||||
) : (
|
||||
<SetPasswordProfileProviderModal
|
||||
open={isPasswordDialogOpen}
|
||||
onOpenChange={setIsPasswordDialogOpen}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
</>
|
||||
)}
|
||||
|
||||
{isPasswordEnabled && (
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div className="space-y-1">
|
||||
<div className="font-medium">Password</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="font-medium">Two-Factor Authentication</div>
|
||||
{user.twoFactorEnabled && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="text-[10px] h-5 px-1.5 text-green-600 bg-green-500/10 border-0"
|
||||
>
|
||||
Active
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{user.lastChangedPasswordAt
|
||||
? `Last changed ${timeAgo(new Date(user.lastChangedPasswordAt))}`
|
||||
: "Never changed"}
|
||||
Enhance the security of your account by requiring a second
|
||||
form of verification during login.
|
||||
</div>
|
||||
</div>
|
||||
{credentialAccount ? (
|
||||
<ResetPasswordProfileProviderModal
|
||||
open={isPasswordDialogOpen}
|
||||
onOpenChange={setIsPasswordDialogOpen}
|
||||
/>
|
||||
|
||||
{user.twoFactorEnabled ? (
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<ViewBackupCodesModal
|
||||
open={isBackupCodesDialogOpen}
|
||||
onOpenChange={setIsBackupCodesDialogOpen}
|
||||
/>
|
||||
<Disable2FAProfileProviderModal
|
||||
open={isDisable2FADialogOpen}
|
||||
onOpenChange={setIsDisable2FADialogOpen}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<SetPasswordProfileProviderModal
|
||||
open={isPasswordDialogOpen}
|
||||
onOpenChange={setIsPasswordDialogOpen}
|
||||
<Setup2FAProfileProviderModal
|
||||
disabled={!credentialAccount}
|
||||
open={isSetup2FADialogOpen}
|
||||
onOpenChange={setIsSetup2FADialogOpen}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="font-medium">Two-Factor Authentication</div>
|
||||
{user.twoFactorEnabled && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="text-[10px] h-5 px-1.5 text-green-600 bg-green-500/10 border-0"
|
||||
>
|
||||
Active
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Enhance the security of your account by requiring a second form
|
||||
of verification during login.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{user.twoFactorEnabled ? (
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<ViewBackupCodesModal
|
||||
open={isBackupCodesDialogOpen}
|
||||
onOpenChange={setIsBackupCodesDialogOpen}
|
||||
/>
|
||||
<Disable2FAProfileProviderModal
|
||||
open={isDisable2FADialogOpen}
|
||||
onOpenChange={setIsDisable2FADialogOpen}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Setup2FAProfileProviderModal
|
||||
disabled={!credentialAccount}
|
||||
open={isSetup2FADialogOpen}
|
||||
onOpenChange={setIsSetup2FADialogOpen}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isPasskeyEnabled && (
|
||||
<div className="space-y-6">
|
||||
@@ -377,20 +381,21 @@ function SessionRow({
|
||||
<div className="w-10 h-10 rounded-full bg-muted flex items-center justify-center text-muted-foreground relative">
|
||||
<deviceInfo.Icon className="w-5 h-5" />
|
||||
{provider && (
|
||||
<div className="absolute -bottom-1 -right-1 w-5 h-5 rounded-full bg-background border flex items-center justify-center overflow-hidden">
|
||||
{provider.icon.startsWith("/") || provider.icon.startsWith("http") ? (
|
||||
<Image
|
||||
src={provider.icon}
|
||||
alt={provider.id}
|
||||
width={12}
|
||||
height={12}
|
||||
className="w-3 h-3"
|
||||
unoptimized={provider.icon.startsWith("http")}
|
||||
/>
|
||||
) : (
|
||||
<Icon icon={provider.icon} className="w-3 h-3" />
|
||||
)}
|
||||
</div>
|
||||
<div className="absolute -bottom-1 -right-1 w-5 h-5 rounded-full bg-background border flex items-center justify-center overflow-hidden">
|
||||
{provider.icon.startsWith("/") ||
|
||||
provider.icon.startsWith("http") ? (
|
||||
<Image
|
||||
src={provider.icon}
|
||||
alt={provider.id}
|
||||
width={12}
|
||||
height={12}
|
||||
className="w-3 h-3"
|
||||
unoptimized={provider.icon.startsWith("http")}
|
||||
/>
|
||||
) : (
|
||||
<Icon icon={provider.icon} className="w-3 h-3" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
@@ -400,9 +405,9 @@ function SessionRow({
|
||||
• {deviceInfo.browser}
|
||||
</span>
|
||||
{provider && (
|
||||
<span className="text-muted-foreground font-normal">
|
||||
• {provider.title || provider.name}
|
||||
</span>
|
||||
<span className="text-muted-foreground font-normal">
|
||||
• {provider.title || provider.name}
|
||||
</span>
|
||||
)}
|
||||
{session.id === currentSession.id && (
|
||||
<Badge
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TYPE "public"."dbms_status" ADD VALUE 'sqlite';
|
||||
@@ -1,13 +0,0 @@
|
||||
CREATE TABLE "sso_provider" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"issuer" text NOT NULL,
|
||||
"oidc_config" json,
|
||||
"saml_config" json,
|
||||
"user_id" uuid,
|
||||
"provider_id" text NOT NULL,
|
||||
"organization_id" text,
|
||||
"domain" text NOT NULL,
|
||||
CONSTRAINT "sso_provider_provider_id_unique" UNIQUE("provider_id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "sso_provider" ADD CONSTRAINT "sso_provider_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
|
||||
+15
-1
@@ -1,11 +1,25 @@
|
||||
CREATE TABLE "sso_provider" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"issuer" text NOT NULL,
|
||||
"oidc_config" json,
|
||||
"saml_config" json,
|
||||
"user_id" uuid,
|
||||
"provider_id" text NOT NULL,
|
||||
"organization_id" text,
|
||||
"domain" text NOT NULL,
|
||||
CONSTRAINT "sso_provider_provider_id_unique" UNIQUE("provider_id")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "passkey" DROP CONSTRAINT "passkey_userId_user_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "passkey" ADD COLUMN "public_key" text NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "passkey" ADD COLUMN "user_id" uuid NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "passkey" ADD COLUMN "credential_i_d" text NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "passkey" ADD COLUMN "credential_id" text NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "passkey" ADD COLUMN "device_type" text NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "passkey" ADD COLUMN "backed_up" boolean NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "passkey" ADD COLUMN "aaguid" text;--> statement-breakpoint
|
||||
ALTER TABLE "session" ADD COLUMN "provider_id" text;--> statement-breakpoint
|
||||
ALTER TABLE "sso_provider" ADD CONSTRAINT "sso_provider_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "passkey" ADD CONSTRAINT "passkey_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "passkey" DROP COLUMN "publicKey";--> statement-breakpoint
|
||||
ALTER TABLE "passkey" DROP COLUMN "userId";--> statement-breakpoint
|
||||
@@ -1 +0,0 @@
|
||||
ALTER TABLE "session" ADD COLUMN "provider_id" text;
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"id": "9106b694-50a6-4b6b-b33a-26d958844097",
|
||||
"id": "90c26bd4-552c-445a-8491-487bcf705fa6",
|
||||
"prevId": "29b53ca7-d603-4433-96f0-44d2df48ebdc",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
@@ -96,12 +96,8 @@
|
||||
"name": "settings_default_storage_channel_id_storage_channel_id_fk",
|
||||
"tableFrom": "settings",
|
||||
"tableTo": "storage_channel",
|
||||
"columnsFrom": [
|
||||
"default_storage_channel_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["default_storage_channel_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "set null",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -111,9 +107,7 @@
|
||||
"settings_name_unique": {
|
||||
"name": "settings_name_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"name"
|
||||
]
|
||||
"columns": ["name"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
@@ -217,12 +211,8 @@
|
||||
"name": "account_user_id_user_id_fk",
|
||||
"tableFrom": "account",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -318,12 +308,8 @@
|
||||
"name": "passkey_userId_user_id_fk",
|
||||
"tableFrom": "passkey",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"userId"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["userId"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -413,12 +399,8 @@
|
||||
"name": "session_user_id_user_id_fk",
|
||||
"tableFrom": "session",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -428,93 +410,7 @@
|
||||
"session_token_unique": {
|
||||
"name": "session_token_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"token"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.sso_provider": {
|
||||
"name": "sso_provider",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"issuer": {
|
||||
"name": "issuer",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"oidc_config": {
|
||||
"name": "oidc_config",
|
||||
"type": "json",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"saml_config": {
|
||||
"name": "saml_config",
|
||||
"type": "json",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"provider_id": {
|
||||
"name": "provider_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"organization_id": {
|
||||
"name": "organization_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"domain": {
|
||||
"name": "domain",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"sso_provider_user_id_user_id_fk": {
|
||||
"name": "sso_provider_user_id_user_id_fk",
|
||||
"tableFrom": "sso_provider",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"sso_provider_provider_id_unique": {
|
||||
"name": "sso_provider_provider_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"provider_id"
|
||||
]
|
||||
"columns": ["token"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
@@ -557,12 +453,8 @@
|
||||
"name": "two_factor_user_id_user_id_fk",
|
||||
"tableFrom": "two_factor",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -686,9 +578,7 @@
|
||||
"user_email_unique": {
|
||||
"name": "user_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"email"
|
||||
]
|
||||
"columns": ["email"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
@@ -814,9 +704,7 @@
|
||||
"organization_slug_unique": {
|
||||
"name": "organization_slug_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"slug"
|
||||
]
|
||||
"columns": ["slug"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
@@ -878,12 +766,8 @@
|
||||
"name": "member_organization_id_organization_id_fk",
|
||||
"tableFrom": "member",
|
||||
"tableTo": "organization",
|
||||
"columnsFrom": [
|
||||
"organization_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["organization_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
@@ -891,12 +775,8 @@
|
||||
"name": "member_user_id_user_id_fk",
|
||||
"tableFrom": "member",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["user_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -980,12 +860,8 @@
|
||||
"name": "invitation_organization_id_organization_id_fk",
|
||||
"tableFrom": "invitation",
|
||||
"tableTo": "organization",
|
||||
"columnsFrom": [
|
||||
"organization_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["organization_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
@@ -993,12 +869,8 @@
|
||||
"name": "invitation_inviter_id_user_id_fk",
|
||||
"tableFrom": "invitation",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"inviter_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["inviter_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -1071,12 +943,8 @@
|
||||
"name": "projects_organization_id_organization_id_fk",
|
||||
"tableFrom": "projects",
|
||||
"tableTo": "organization",
|
||||
"columnsFrom": [
|
||||
"organization_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["organization_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -1086,9 +954,7 @@
|
||||
"projects_slug_unique": {
|
||||
"name": "projects_slug_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"slug"
|
||||
]
|
||||
"columns": ["slug"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
@@ -1165,12 +1031,8 @@
|
||||
"name": "backups_database_id_databases_id_fk",
|
||||
"tableFrom": "backups",
|
||||
"tableTo": "databases",
|
||||
"columnsFrom": [
|
||||
"database_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["database_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -1281,12 +1143,8 @@
|
||||
"name": "databases_agent_id_agents_id_fk",
|
||||
"tableFrom": "databases",
|
||||
"tableTo": "agents",
|
||||
"columnsFrom": [
|
||||
"agent_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["agent_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
@@ -1294,12 +1152,8 @@
|
||||
"name": "databases_project_id_projects_id_fk",
|
||||
"tableFrom": "databases",
|
||||
"tableTo": "projects",
|
||||
"columnsFrom": [
|
||||
"project_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["project_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -1373,12 +1227,8 @@
|
||||
"name": "restorations_backup_storage_id_backup_storage_id_fk",
|
||||
"tableFrom": "restorations",
|
||||
"tableTo": "backup_storage",
|
||||
"columnsFrom": [
|
||||
"backup_storage_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["backup_storage_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
@@ -1386,12 +1236,8 @@
|
||||
"name": "restorations_backup_id_backups_id_fk",
|
||||
"tableFrom": "restorations",
|
||||
"tableTo": "backups",
|
||||
"columnsFrom": [
|
||||
"backup_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["backup_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
@@ -1399,12 +1245,8 @@
|
||||
"name": "restorations_database_id_databases_id_fk",
|
||||
"tableFrom": "restorations",
|
||||
"tableTo": "databases",
|
||||
"columnsFrom": [
|
||||
"database_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["database_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -1507,12 +1349,8 @@
|
||||
"name": "retention_policies_database_id_databases_id_fk",
|
||||
"tableFrom": "retention_policies",
|
||||
"tableTo": "databases",
|
||||
"columnsFrom": [
|
||||
"database_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["database_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -1598,9 +1436,7 @@
|
||||
"agents_slug_unique": {
|
||||
"name": "agents_slug_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"slug"
|
||||
]
|
||||
"columns": ["slug"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
@@ -1676,12 +1512,8 @@
|
||||
"name": "notification_channel_organization_id_organization_id_fk",
|
||||
"tableFrom": "notification_channel",
|
||||
"tableTo": "organization",
|
||||
"columnsFrom": [
|
||||
"organization_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["organization_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -1715,12 +1547,8 @@
|
||||
"name": "organization_notification_channels_organization_id_organization_id_fk",
|
||||
"tableFrom": "organization_notification_channels",
|
||||
"tableTo": "organization",
|
||||
"columnsFrom": [
|
||||
"organization_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["organization_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
@@ -1728,12 +1556,8 @@
|
||||
"name": "organization_notification_channels_notification_channel_id_notification_channel_id_fk",
|
||||
"tableFrom": "organization_notification_channels",
|
||||
"tableTo": "notification_channel",
|
||||
"columnsFrom": [
|
||||
"notification_channel_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["notification_channel_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -1743,10 +1567,7 @@
|
||||
"organization_notification_channels_organization_id_notification_channel_id_unique": {
|
||||
"name": "organization_notification_channels_organization_id_notification_channel_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"organization_id",
|
||||
"notification_channel_id"
|
||||
]
|
||||
"columns": ["organization_id", "notification_channel_id"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
@@ -1816,12 +1637,8 @@
|
||||
"name": "alert_policy_notification_channel_id_notification_channel_id_fk",
|
||||
"tableFrom": "alert_policy",
|
||||
"tableTo": "notification_channel",
|
||||
"columnsFrom": [
|
||||
"notification_channel_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["notification_channel_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
@@ -1829,12 +1646,8 @@
|
||||
"name": "alert_policy_database_id_databases_id_fk",
|
||||
"tableFrom": "alert_policy",
|
||||
"tableTo": "databases",
|
||||
"columnsFrom": [
|
||||
"database_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["database_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -1993,12 +1806,8 @@
|
||||
"name": "organization_storage_channels_organization_id_organization_id_fk",
|
||||
"tableFrom": "organization_storage_channels",
|
||||
"tableTo": "organization",
|
||||
"columnsFrom": [
|
||||
"organization_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["organization_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
@@ -2006,12 +1815,8 @@
|
||||
"name": "organization_storage_channels_storage_channel_id_storage_channel_id_fk",
|
||||
"tableFrom": "organization_storage_channels",
|
||||
"tableTo": "storage_channel",
|
||||
"columnsFrom": [
|
||||
"storage_channel_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["storage_channel_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -2021,10 +1826,7 @@
|
||||
"organization_storage_channels_organization_id_storage_channel_id_unique": {
|
||||
"name": "organization_storage_channels_organization_id_storage_channel_id_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"organization_id",
|
||||
"storage_channel_id"
|
||||
]
|
||||
"columns": ["organization_id", "storage_channel_id"]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
@@ -2100,12 +1902,8 @@
|
||||
"name": "storage_channel_organization_id_organization_id_fk",
|
||||
"tableFrom": "storage_channel",
|
||||
"tableTo": "organization",
|
||||
"columnsFrom": [
|
||||
"organization_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["organization_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -2172,12 +1970,8 @@
|
||||
"name": "storage_policy_storage_channel_id_storage_channel_id_fk",
|
||||
"tableFrom": "storage_policy",
|
||||
"tableTo": "storage_channel",
|
||||
"columnsFrom": [
|
||||
"storage_channel_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["storage_channel_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
@@ -2185,12 +1979,8 @@
|
||||
"name": "storage_policy_database_id_databases_id_fk",
|
||||
"tableFrom": "storage_policy",
|
||||
"tableTo": "databases",
|
||||
"columnsFrom": [
|
||||
"database_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["database_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -2276,12 +2066,8 @@
|
||||
"name": "backup_storage_backup_id_backups_id_fk",
|
||||
"tableFrom": "backup_storage",
|
||||
"tableTo": "backups",
|
||||
"columnsFrom": [
|
||||
"backup_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["backup_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
},
|
||||
@@ -2289,12 +2075,8 @@
|
||||
"name": "backup_storage_storage_channel_id_storage_channel_id_fk",
|
||||
"tableFrom": "backup_storage",
|
||||
"tableTo": "storage_channel",
|
||||
"columnsFrom": [
|
||||
"storage_channel_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"columnsFrom": ["storage_channel_id"],
|
||||
"columnsTo": ["id"],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
@@ -2310,20 +2092,12 @@
|
||||
"public.user_themes": {
|
||||
"name": "user_themes",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"light",
|
||||
"dark",
|
||||
"system"
|
||||
]
|
||||
"values": ["light", "dark", "system"]
|
||||
},
|
||||
"public.retention_policy_type": {
|
||||
"name": "retention_policy_type",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"count",
|
||||
"days",
|
||||
"gfs"
|
||||
]
|
||||
"values": ["count", "days", "gfs"]
|
||||
},
|
||||
"public.provider_kind": {
|
||||
"name": "provider_kind",
|
||||
@@ -2352,56 +2126,32 @@
|
||||
"public.level": {
|
||||
"name": "level",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"critical",
|
||||
"warning",
|
||||
"info"
|
||||
]
|
||||
"values": ["critical", "warning", "info"]
|
||||
},
|
||||
"public.provider_storage_kind": {
|
||||
"name": "provider_storage_kind",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"local",
|
||||
"s3",
|
||||
"google-drive"
|
||||
]
|
||||
"values": ["local", "s3", "google-drive"]
|
||||
},
|
||||
"public.backup_storage_status": {
|
||||
"name": "backup_storage_status",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"pending",
|
||||
"success",
|
||||
"failed"
|
||||
]
|
||||
"values": ["pending", "success", "failed"]
|
||||
},
|
||||
"public.dbms_status": {
|
||||
"name": "dbms_status",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"postgresql",
|
||||
"mysql",
|
||||
"mongodb"
|
||||
]
|
||||
"values": ["postgresql", "mysql", "mongodb", "sqlite"]
|
||||
},
|
||||
"public.status": {
|
||||
"name": "status",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"waiting",
|
||||
"ongoing",
|
||||
"failed",
|
||||
"success"
|
||||
]
|
||||
"values": ["waiting", "ongoing", "failed", "success"]
|
||||
},
|
||||
"public.type_storage": {
|
||||
"name": "type_storage",
|
||||
"schema": "public",
|
||||
"values": [
|
||||
"local",
|
||||
"s3"
|
||||
]
|
||||
"values": ["local", "s3"]
|
||||
}
|
||||
},
|
||||
"schemas": {},
|
||||
@@ -2414,4 +2164,4 @@
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"id": "fc5d3d01-2097-4498-b5b0-525eb5f8417c",
|
||||
"prevId": "9106b694-50a6-4b6b-b33a-26d958844097",
|
||||
"id": "d26e1e78-ce2d-4084-bceb-c9e058a2b610",
|
||||
"prevId": "90c26bd4-552c-445a-8491-487bcf705fa6",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
@@ -262,8 +262,8 @@
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"credential_i_d": {
|
||||
"name": "credential_i_d",
|
||||
"credential_id": {
|
||||
"name": "credential_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
@@ -381,6 +381,12 @@
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"provider_id": {
|
||||
"name": "provider_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"impersonated_by": {
|
||||
"name": "impersonated_by",
|
||||
"type": "text",
|
||||
@@ -2388,7 +2394,8 @@
|
||||
"values": [
|
||||
"postgresql",
|
||||
"mysql",
|
||||
"mongodb"
|
||||
"mongodb",
|
||||
"sqlite"
|
||||
]
|
||||
},
|
||||
"public.status": {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -243,22 +243,15 @@
|
||||
{
|
||||
"idx": 34,
|
||||
"version": "7",
|
||||
"when": 1770991368921,
|
||||
"tag": "0034_vengeful_blacklash",
|
||||
"when": 1771790675240,
|
||||
"tag": "0034_lush_speed",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 35,
|
||||
"version": "7",
|
||||
"when": 1770993283219,
|
||||
"tag": "0035_windy_shockwave",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 36,
|
||||
"version": "7",
|
||||
"when": 1771842940506,
|
||||
"tag": "0036_left_longshot",
|
||||
"when": 1771922367910,
|
||||
"tag": "0035_late_young_avengers",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
|
||||
+126
-123
@@ -1,175 +1,178 @@
|
||||
import {relations} from "drizzle-orm";
|
||||
import {boolean, integer, pgEnum, json, pgTable, text, timestamp, uuid} from "drizzle-orm/pg-core";
|
||||
import {createSelectSchema} from "drizzle-zod";
|
||||
import {z} from "zod";
|
||||
import {project} from "./06_project";
|
||||
import {member} from "@/db/schema/04_member";
|
||||
import {invitation} from "@/db/schema/05_invitation";
|
||||
import {organization} from "@/db/schema/03_organization";
|
||||
import {Account as BetterAuthAccount} from "better-auth";
|
||||
import {timestamps} from "@/db/schema/00_common";
|
||||
import { relations } from "drizzle-orm";
|
||||
import {
|
||||
boolean,
|
||||
integer,
|
||||
pgEnum,
|
||||
json,
|
||||
pgTable,
|
||||
text,
|
||||
timestamp,
|
||||
uuid,
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { createSelectSchema } from "drizzle-zod";
|
||||
import { z } from "zod";
|
||||
import { project } from "./06_project";
|
||||
import { member } from "@/db/schema/04_member";
|
||||
import { invitation } from "@/db/schema/05_invitation";
|
||||
import { organization } from "@/db/schema/03_organization";
|
||||
import { Account as BetterAuthAccount } from "better-auth";
|
||||
import { timestamps } from "@/db/schema/00_common";
|
||||
|
||||
export const userThemeEnum = pgEnum("user_themes", ["light", "dark", "system"]);
|
||||
|
||||
|
||||
export const user = pgTable("user", {
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
email: text("email").notNull().unique(),
|
||||
emailVerified: boolean("email_verified").notNull(),
|
||||
image: text("image"),
|
||||
role: text("role"),
|
||||
theme: userThemeEnum().notNull().default("light"),
|
||||
banned: boolean("banned"),
|
||||
banReason: text("ban_reason"),
|
||||
banExpires: timestamp("ban_expires"),
|
||||
lastConnectedAt: timestamp(),
|
||||
lastChangedPasswordAt: timestamp(),
|
||||
twoFactorEnabled: boolean("two_factor_enabled").default(false),
|
||||
...timestamps
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
name: text("name").notNull(),
|
||||
email: text("email").notNull().unique(),
|
||||
emailVerified: boolean("email_verified").notNull(),
|
||||
image: text("image"),
|
||||
role: text("role"),
|
||||
theme: userThemeEnum().notNull().default("light"),
|
||||
banned: boolean("banned"),
|
||||
banReason: text("ban_reason"),
|
||||
banExpires: timestamp("ban_expires"),
|
||||
lastConnectedAt: timestamp(),
|
||||
lastChangedPasswordAt: timestamp(),
|
||||
twoFactorEnabled: boolean("two_factor_enabled").default(false),
|
||||
...timestamps,
|
||||
});
|
||||
|
||||
export const session = pgTable("session", {
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
expiresAt: timestamp("expires_at").notNull(),
|
||||
token: text("token").notNull().unique(),
|
||||
ipAddress: text("ip_address"),
|
||||
userAgent: text("user_agent"),
|
||||
userId: uuid("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, {onDelete: "cascade"}),
|
||||
providerId: text("provider_id"),
|
||||
impersonatedBy: text("impersonated_by"), //id or name ????
|
||||
activeOrganizationId: text("active_organization_id"),
|
||||
...timestamps
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
expiresAt: timestamp("expires_at").notNull(),
|
||||
token: text("token").notNull().unique(),
|
||||
ipAddress: text("ip_address"),
|
||||
userAgent: text("user_agent"),
|
||||
userId: uuid("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
providerId: text("provider_id"),
|
||||
impersonatedBy: text("impersonated_by"), //id or name ????
|
||||
activeOrganizationId: text("active_organization_id"),
|
||||
...timestamps,
|
||||
});
|
||||
|
||||
export const account = pgTable("account", {
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
accountId: text("account_id").notNull(),
|
||||
providerId: text("provider_id").notNull(),
|
||||
userId: uuid("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, {onDelete: "cascade"}),
|
||||
accessToken: text("access_token"),
|
||||
refreshToken: text("refresh_token"),
|
||||
idToken: text("id_token"),
|
||||
accessTokenExpiresAt: timestamp("access_token_expires_at"),
|
||||
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
|
||||
scope: text("scope"),
|
||||
password: text("password"),
|
||||
...timestamps
|
||||
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
accountId: text("account_id").notNull(),
|
||||
providerId: text("provider_id").notNull(),
|
||||
userId: uuid("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
accessToken: text("access_token"),
|
||||
refreshToken: text("refresh_token"),
|
||||
idToken: text("id_token"),
|
||||
accessTokenExpiresAt: timestamp("access_token_expires_at"),
|
||||
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
|
||||
scope: text("scope"),
|
||||
password: text("password"),
|
||||
...timestamps,
|
||||
});
|
||||
|
||||
export const verification = pgTable("verification", {
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
identifier: text("identifier").notNull(),
|
||||
value: text("value").notNull(),
|
||||
expiresAt: timestamp("expires_at").notNull(),
|
||||
...timestamps
|
||||
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
identifier: text("identifier").notNull(),
|
||||
value: text("value").notNull(),
|
||||
expiresAt: timestamp("expires_at").notNull(),
|
||||
...timestamps,
|
||||
});
|
||||
|
||||
export const passkey = pgTable("passkey", {
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
name: text("name"),
|
||||
publicKey: text("public_key").notNull(),
|
||||
userId: uuid("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
credentialID: text("credential_i_d").notNull(),
|
||||
counter: integer("counter").notNull(),
|
||||
deviceType: text("device_type").notNull(),
|
||||
backedUp: boolean("backed_up").notNull(),
|
||||
transports: text("transports"),
|
||||
aaguid: text("aaguid"),
|
||||
...timestamps
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
name: text("name"),
|
||||
publicKey: text("public_key").notNull(),
|
||||
userId: uuid("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
credentialId: text("credential_id").notNull(),
|
||||
counter: integer("counter").notNull(),
|
||||
deviceType: text("device_type").notNull(),
|
||||
backedUp: boolean("backed_up").notNull(),
|
||||
transports: text("transports"),
|
||||
aaguid: text("aaguid"),
|
||||
...timestamps,
|
||||
});
|
||||
|
||||
export const twoFactor = pgTable("two_factor", {
|
||||
id: uuid().defaultRandom().primaryKey(),
|
||||
secret: text("secret").notNull(),
|
||||
backupCodes: text("backup_codes").notNull(),
|
||||
userId: uuid("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, {onDelete: "cascade"}),
|
||||
id: uuid().defaultRandom().primaryKey(),
|
||||
secret: text("secret").notNull(),
|
||||
backupCodes: text("backup_codes").notNull(),
|
||||
userId: uuid("user_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
});
|
||||
|
||||
export const ssoProvider = pgTable("sso_provider", {
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
issuer: text("issuer").notNull(),
|
||||
oidcConfig: json("oidc_config"),
|
||||
samlConfig: json("saml_config"),
|
||||
userId: uuid("user_id").references(() => user.id, { onDelete: "cascade" }),
|
||||
providerId: text("provider_id").notNull().unique(),
|
||||
organizationId: text("organization_id"),
|
||||
domain: text("domain").notNull(),
|
||||
id: uuid("id").defaultRandom().primaryKey(),
|
||||
issuer: text("issuer").notNull(),
|
||||
oidcConfig: json("oidc_config"),
|
||||
samlConfig: json("saml_config"),
|
||||
userId: uuid("user_id").references(() => user.id, { onDelete: "cascade" }),
|
||||
providerId: text("provider_id").notNull().unique(),
|
||||
organizationId: text("organization_id"),
|
||||
domain: text("domain").notNull(),
|
||||
});
|
||||
|
||||
export const userRelations = relations(user, ({many}) => ({
|
||||
sessions: many(session),
|
||||
accounts: many(account),
|
||||
ssoProviders: many(ssoProvider),
|
||||
memberships: many(member),
|
||||
invitations: many(invitation),
|
||||
passkeys: many(passkey),
|
||||
export const userRelations = relations(user, ({ many }) => ({
|
||||
sessions: many(session),
|
||||
accounts: many(account),
|
||||
ssoProviders: many(ssoProvider),
|
||||
memberships: many(member),
|
||||
invitations: many(invitation),
|
||||
passkeys: many(passkey),
|
||||
}));
|
||||
|
||||
export const sessionRelations = relations(session, ({one}) => ({
|
||||
user: one(user, {
|
||||
fields: [session.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
export const sessionRelations = relations(session, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [session.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const accountRelations = relations(account, ({one}) => ({
|
||||
user: one(user, {
|
||||
fields: [account.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
export const accountRelations = relations(account, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [account.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const ssoProviderRelations = relations(ssoProvider, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [ssoProvider.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
user: one(user, {
|
||||
fields: [ssoProvider.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const projectRelations = relations(project, ({one}) => ({
|
||||
organization: one(organization, {
|
||||
fields: [project.organizationId],
|
||||
references: [organization.id],
|
||||
}),
|
||||
export const projectRelations = relations(project, ({ one }) => ({
|
||||
organization: one(organization, {
|
||||
fields: [project.organizationId],
|
||||
references: [organization.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
export const passkeyRelations = relations(passkey, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [passkey.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
user: one(user, {
|
||||
fields: [passkey.userId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
|
||||
export const userSchema = createSelectSchema(user);
|
||||
export type User = z.infer<typeof userSchema>;
|
||||
|
||||
export const userThemeEnumSchema = createSelectSchema(userThemeEnum)
|
||||
export const userThemeEnumSchema = createSelectSchema(userThemeEnum);
|
||||
export type UserThemeEnum = z.infer<typeof userThemeEnumSchema>;
|
||||
|
||||
|
||||
export const sessionSchema = createSelectSchema(session);
|
||||
export type Session = z.infer<typeof sessionSchema>;
|
||||
|
||||
export const accountSchema = createSelectSchema(account);
|
||||
export type Account = z.infer<typeof accountSchema>;
|
||||
|
||||
|
||||
type FixedAccount = Omit<BetterAuthAccount, 'updatedAt'> & {
|
||||
updatedAt: Date | null;
|
||||
type FixedAccount = Omit<BetterAuthAccount, "updatedAt"> & {
|
||||
updatedAt: Date | null;
|
||||
};
|
||||
|
||||
export type UserWithAccounts = User & {
|
||||
accounts: FixedAccount[];
|
||||
};
|
||||
accounts: FixedAccount[];
|
||||
};
|
||||
|
||||
@@ -9,7 +9,6 @@ import {timestamps} from "@/db/schema/00_common";
|
||||
import {AlertPolicy, alertPolicy} from "@/db/schema/10_alert-policy";
|
||||
import {StoragePolicy, storagePolicy} from "@/db/schema/13_storage-policy";
|
||||
import {BackupStorage, backupStorage} from "@/db/schema/14_storage-backup";
|
||||
import {storageChannel} from "@/db/schema/12_storage-channel";
|
||||
|
||||
export const database = pgTable("databases", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
|
||||
@@ -2,7 +2,7 @@ import {pgEnum} from "drizzle-orm/pg-core";
|
||||
import {createSelectSchema} from "drizzle-zod";
|
||||
import {z} from "zod";
|
||||
|
||||
export const dbmsEnum = pgEnum("dbms_status", ["postgresql", "mysql", "mongodb"]);
|
||||
export const dbmsEnum = pgEnum("dbms_status", ["postgresql", "mysql", "mongodb", "sqlite"]);
|
||||
export const statusEnum = pgEnum("status", ["waiting", "ongoing", "failed", "success"]);
|
||||
export const typeStorageEnum = pgEnum("type_storage", ["local", "s3"]);
|
||||
|
||||
|
||||
+11
-9
@@ -7,7 +7,6 @@ import {User, UserThemeEnum} from "@/db/schema/02_user";
|
||||
|
||||
export async function createUserDb(data: SignUpUser): Promise<User> {
|
||||
const now = new Date();
|
||||
const hashedPassword = await hashPassword(data.password);
|
||||
const userId = crypto.randomUUID();
|
||||
|
||||
const [newUser] = await db.insert(drizzleDb.schemas.user).values({
|
||||
@@ -21,14 +20,17 @@ export async function createUserDb(data: SignUpUser): Promise<User> {
|
||||
theme: data.theme as UserThemeEnum,
|
||||
}).returning();
|
||||
|
||||
await db.insert(drizzleDb.schemas.account).values({
|
||||
providerId: "credential",
|
||||
accountId: userId,
|
||||
userId: userId,
|
||||
password: hashedPassword,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
if (data.password) {
|
||||
const hashedPassword = await hashPassword(data.password);
|
||||
await db.insert(drizzleDb.schemas.account).values({
|
||||
providerId: "credential",
|
||||
accountId: userId,
|
||||
userId: userId,
|
||||
password: hashedPassword,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
});
|
||||
}
|
||||
|
||||
return newUser
|
||||
}
|
||||
|
||||
+24
-7
@@ -52,6 +52,15 @@ export const env = createEnv({
|
||||
AUTH_OIDC_DISCOVERY_ENDPOINT: z.string().optional(),
|
||||
AUTH_OIDC_JWKS_ENDPOINT: z.string().optional(),
|
||||
AUTH_OIDC_PKCE: z.string().optional(),
|
||||
|
||||
AUTH_SOCIAL_ID: z.string().optional().default("social"),
|
||||
AUTH_SOCIAL_TITLE: z.string().optional(),
|
||||
AUTH_SOCIAL_DESC: z.string().optional(),
|
||||
AUTH_SOCIAL_ICON: z.string().optional(),
|
||||
AUTH_SOCIAL_CLIENT: z.string().optional(),
|
||||
AUTH_SOCIAL_SECRET: z.string().optional(),
|
||||
AUTH_SOCIAL_APPLE_APP_BUNDLE_IDENTIFIER: z.string().optional(),
|
||||
|
||||
ALLOWED_GROUP: z.string().optional(),
|
||||
|
||||
AUTH_EMAIL_PASSWORD_ENABLED: z.string().optional().default("true"),
|
||||
@@ -59,6 +68,10 @@ export const env = createEnv({
|
||||
AUTH_PASSKEY_ENABLED: z.string().optional().default("true"),
|
||||
|
||||
AUTH_SYNC_OIDC_ROLES_ON_LOGIN: z.enum(["true", "false"]).default("true"),
|
||||
AUTH_ROLE_MAP: z.string().optional(),
|
||||
AUTH_DEFAULT_ROLE: z.string().optional(),
|
||||
AUTH_ALLOW_LINKING: z.enum(["true", "false"]).default("false"),
|
||||
AUTH_ALLOW_UNLINKING: z.enum(["true", "false"]).default("false"),
|
||||
|
||||
PRIVATE_PATH: z.string().optional(),
|
||||
},
|
||||
@@ -82,13 +95,6 @@ export const env = createEnv({
|
||||
SMTP_USER: process.env.SMTP_USER,
|
||||
SMTP_SECURE: process.env.SMTP_SECURE,
|
||||
|
||||
AUTH_GOOGLE_ID: process.env.AUTH_GOOGLE_ID,
|
||||
AUTH_GOOGLE_SECRET: process.env.AUTH_GOOGLE_SECRET,
|
||||
AUTH_GOOGLE_METHOD: process.env.AUTH_GOOGLE_METHOD === "true",
|
||||
|
||||
AUTH_GITHUB_ID: process.env.AUTH_GITHUB_ID,
|
||||
AUTH_GITHUB_SECRET: process.env.AUTH_GITHUB_SECRET,
|
||||
|
||||
RETENTION_CRON: process.env.RETENTION_CRON,
|
||||
|
||||
AUTH_OIDC_ID: process.env.AUTH_OIDC_ID,
|
||||
@@ -103,6 +109,14 @@ export const env = createEnv({
|
||||
AUTH_OIDC_DISCOVERY_ENDPOINT: process.env.AUTH_OIDC_DISCOVERY_ENDPOINT,
|
||||
AUTH_OIDC_JWKS_ENDPOINT: process.env.AUTH_OIDC_JWKS_ENDPOINT,
|
||||
AUTH_OIDC_PKCE: process.env.AUTH_OIDC_PKCE,
|
||||
|
||||
AUTH_SOCIAL_ID: process.env.AUTH_SOCIAL_ID,
|
||||
AUTH_SOCIAL_TITLE: process.env.AUTH_SOCIAL_TITLE,
|
||||
AUTH_SOCIAL_DESC: process.env.AUTH_SOCIAL_DESC,
|
||||
AUTH_SOCIAL_ICON: process.env.AUTH_SOCIAL_ICON,
|
||||
AUTH_SOCIAL_CLIENT: process.env.AUTH_SOCIAL_CLIENT,
|
||||
AUTH_SOCIAL_SECRET: process.env.AUTH_SOCIAL_SECRET,
|
||||
|
||||
ALLOWED_GROUP: process.env.ALLOWED_GROUP,
|
||||
|
||||
AUTH_EMAIL_PASSWORD_ENABLED: process.env.AUTH_EMAIL_PASSWORD_ENABLED,
|
||||
@@ -111,6 +125,9 @@ export const env = createEnv({
|
||||
|
||||
AUTH_SYNC_OIDC_ROLES_ON_LOGIN: process.env.AUTH_SYNC_OIDC_ROLES_ON_LOGIN,
|
||||
|
||||
AUTH_ALLOW_LINKING: process.env.AUTH_ALLOW_LINKING,
|
||||
AUTH_ALLOW_UNLINKING: process.env.AUTH_ALLOW_UNLINKING,
|
||||
|
||||
PRIVATE_PATH:
|
||||
process.env.PRIVATE_PATH || path.join(process.cwd(), "private"),
|
||||
},
|
||||
|
||||
@@ -1,141 +1,161 @@
|
||||
"use server"
|
||||
import {mkdir, unlink} from 'fs/promises';
|
||||
import path from 'path';
|
||||
import {StorageDeleteInput, StorageGetInput, StorageMetaData, StorageResult, StorageUploadInput} from '../types';
|
||||
"use server";
|
||||
import { mkdir, unlink } from "fs/promises";
|
||||
import path from "path";
|
||||
import {
|
||||
StorageDeleteInput,
|
||||
StorageGetInput,
|
||||
StorageMetaData,
|
||||
StorageResult,
|
||||
StorageUploadInput,
|
||||
} from "../types";
|
||||
import fs from "node:fs";
|
||||
import {generateFileUrl} from "@/features/storages/helpers";
|
||||
import {Readable} from "node:stream";
|
||||
import {env} from "@/env.mjs";
|
||||
import { generateFileUrl } from "@/features/storages/helpers";
|
||||
import { Readable } from "node:stream";
|
||||
import { env } from "@/env.mjs";
|
||||
|
||||
const BASE_DIR = path.join(env.PRIVATE_PATH, '/uploads')
|
||||
const BASE_DIR = path.join(env.PRIVATE_PATH, "/uploads");
|
||||
|
||||
export async function uploadLocal(
|
||||
config: { baseDir?: string },
|
||||
input: { data: StorageUploadInput; metadata?: StorageMetaData }
|
||||
config: { baseDir?: string },
|
||||
input: { data: StorageUploadInput; metadata?: StorageMetaData },
|
||||
): Promise<StorageResult> {
|
||||
const base = config.baseDir ? path.join(process.cwd(), config.baseDir ?? "") : BASE_DIR;
|
||||
const base = config.baseDir
|
||||
? path.join(process.cwd(), config.baseDir ?? "")
|
||||
: BASE_DIR;
|
||||
|
||||
const fullPath = path.join(base, input.data.path);
|
||||
const dir = path.dirname(fullPath);
|
||||
const fullPath = path.join(base, input.data.path);
|
||||
const dir = path.dirname(fullPath);
|
||||
|
||||
await mkdir(dir, { recursive: true });
|
||||
await mkdir(dir, { recursive: true });
|
||||
|
||||
try {
|
||||
const file = input.data.file;
|
||||
if (Buffer.isBuffer(file)) {
|
||||
await fs.promises.writeFile(fullPath, input.data.file);
|
||||
} else if (file instanceof Readable) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const writable = fs.createWriteStream(fullPath);
|
||||
file.pipe(writable);
|
||||
writable.on("finish", resolve);
|
||||
writable.on("error", reject);
|
||||
});
|
||||
} else {
|
||||
return { success: false, provider: "local", error: "Unsupported file type. Must be Buffer or ReadableStream" };
|
||||
}
|
||||
|
||||
if (input.data.url) {
|
||||
const url = await generateFileUrl(input);
|
||||
if (!url) {
|
||||
return { success: false, provider: "local", response: "Unable to get URL" };
|
||||
}
|
||||
return { success: true, provider: "local", url };
|
||||
}
|
||||
|
||||
return { success: true, provider: "local" };
|
||||
} catch (err: any) {
|
||||
try { await unlink(fullPath); } catch {}
|
||||
return { success: false, provider: "local", error: err.message || "Upload failed" };
|
||||
try {
|
||||
const file = input.data.file;
|
||||
if (Buffer.isBuffer(file)) {
|
||||
await fs.promises.writeFile(fullPath, input.data.file);
|
||||
} else if (file instanceof Readable) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const writable = fs.createWriteStream(fullPath);
|
||||
file.pipe(writable);
|
||||
writable.on("finish", resolve);
|
||||
writable.on("error", reject);
|
||||
});
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
provider: "local",
|
||||
error: "Unsupported file type. Must be Buffer or ReadableStream",
|
||||
};
|
||||
}
|
||||
|
||||
if (input.data.url) {
|
||||
const url = await generateFileUrl(input);
|
||||
if (!url) {
|
||||
return {
|
||||
success: false,
|
||||
provider: "local",
|
||||
response: "Unable to get URL",
|
||||
};
|
||||
}
|
||||
return { success: true, provider: "local", url };
|
||||
}
|
||||
|
||||
return { success: true, provider: "local" };
|
||||
} catch (err: any) {
|
||||
try {
|
||||
await unlink(fullPath);
|
||||
} catch {}
|
||||
return {
|
||||
success: false,
|
||||
provider: "local",
|
||||
error: err.message || "Upload failed",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function getLocal(
|
||||
config: { baseDir?: string },
|
||||
input: { data: StorageGetInput; metadata: StorageMetaData }
|
||||
config: { baseDir?: string },
|
||||
input: { data: StorageGetInput; metadata: StorageMetaData },
|
||||
): Promise<StorageResult> {
|
||||
const base = config.baseDir
|
||||
? path.join(process.cwd(), config.baseDir ?? "")
|
||||
: BASE_DIR;
|
||||
const filePath = path.join(base, input.data.path);
|
||||
|
||||
const base = config.baseDir ? path.join(process.cwd(), config.baseDir ?? "") : BASE_DIR;
|
||||
const filePath = path.join(base, input.data.path);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.error("File not found at:", filePath);
|
||||
return {
|
||||
success: false,
|
||||
provider: "local",
|
||||
error: "File not found",
|
||||
};
|
||||
}
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.error("File not found at:", filePath);
|
||||
return {
|
||||
success: false,
|
||||
provider: "local",
|
||||
error: "File not found",
|
||||
};
|
||||
}
|
||||
let fileStream: fs.ReadStream | undefined;
|
||||
|
||||
let fileStream: fs.ReadStream | undefined;
|
||||
try {
|
||||
fileStream = fs.createReadStream(filePath);
|
||||
} catch (err: any) {
|
||||
console.error("Error creating read stream:", err);
|
||||
return {
|
||||
success: false,
|
||||
provider: "local",
|
||||
error: err.message,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
fileStream = fs.createReadStream(filePath);
|
||||
} catch (err: any) {
|
||||
console.error("Error creating read stream:", err);
|
||||
return {
|
||||
success: false,
|
||||
provider: "local",
|
||||
error: err.message,
|
||||
};
|
||||
}
|
||||
|
||||
if (input.data.signedUrl) {
|
||||
const url = await generateFileUrl(input);
|
||||
if (!url) {
|
||||
return {
|
||||
success: false,
|
||||
provider: "local",
|
||||
error: "Unable to generate signed URL",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
provider: "local",
|
||||
file: fileStream,
|
||||
url,
|
||||
};
|
||||
if (input.data.signedUrl) {
|
||||
const url = await generateFileUrl(input);
|
||||
if (!url) {
|
||||
return {
|
||||
success: false,
|
||||
provider: "local",
|
||||
error: "Unable to generate signed URL",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
provider: "local",
|
||||
file: fileStream,
|
||||
success: true,
|
||||
provider: "local",
|
||||
file: fileStream,
|
||||
url,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
provider: "local",
|
||||
file: fileStream,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
export async function deleteLocal(
|
||||
config: { baseDir?: string },
|
||||
input: { data: StorageDeleteInput, metadata?: StorageMetaData }
|
||||
config: { baseDir?: string },
|
||||
input: { data: StorageDeleteInput; metadata?: StorageMetaData },
|
||||
): Promise<StorageResult> {
|
||||
const base = config.baseDir
|
||||
? path.join(process.cwd(), config.baseDir ?? "")
|
||||
: BASE_DIR;
|
||||
const fullPath = path.join(base, input.data.path);
|
||||
|
||||
const base = config.baseDir ? path.join(process.cwd(), config.baseDir ?? "") : BASE_DIR;
|
||||
const fullPath = path.join(base, input.data.path);
|
||||
|
||||
await unlink(fullPath);
|
||||
return {
|
||||
success: true,
|
||||
provider: 'local',
|
||||
};
|
||||
await unlink(fullPath);
|
||||
return {
|
||||
success: true,
|
||||
provider: "local",
|
||||
};
|
||||
}
|
||||
|
||||
export async function pingLocal(
|
||||
config: { baseDir?: string }
|
||||
): Promise<StorageResult> {
|
||||
export async function pingLocal(config: {
|
||||
baseDir?: string;
|
||||
}): Promise<StorageResult> {
|
||||
const base = path.join(process.cwd(), config.baseDir ?? "") || BASE_DIR;
|
||||
const fullPath = path.join(base, "ping.txt");
|
||||
|
||||
const base = path.join(process.cwd(), config.baseDir ?? "") || BASE_DIR;
|
||||
const fullPath = path.join(base, "ping.txt");
|
||||
|
||||
await fs.promises.writeFile(fullPath, "ping");
|
||||
await fs.promises.readFile(fullPath);
|
||||
await fs.promises.unlink(fullPath);
|
||||
return {
|
||||
success: true,
|
||||
provider: 'local',
|
||||
response: "Local storage OK"
|
||||
};
|
||||
|
||||
}
|
||||
await fs.promises.writeFile(fullPath, "ping");
|
||||
await fs.promises.readFile(fullPath);
|
||||
await fs.promises.unlink(fullPath);
|
||||
return {
|
||||
success: true,
|
||||
provider: "local",
|
||||
response: "Local storage OK",
|
||||
};
|
||||
}
|
||||
|
||||
+51
-58
@@ -35,10 +35,11 @@ import EmailForgotPassword from "@/components/emails/auth/email-forgot-password"
|
||||
import { getDeviceDetails } from "@/utils/detection";
|
||||
import EmailNewLogin from "@/components/emails/auth/email-new-login";
|
||||
import { sso } from "@better-auth/sso";
|
||||
import { AuthProviderConfig, SUPPORTED_PROVIDERS } from "@/lib/auth/config";
|
||||
import { SUPPORTED_PROVIDERS } from "@/lib/auth/config";
|
||||
import { passkey } from "@better-auth/passkey";
|
||||
import { getOidcProviders } from "./oidc";
|
||||
import { APIError } from "better-auth/api";
|
||||
import { getOAuthProviders } from "./oauth";
|
||||
|
||||
const oidcProviders = getOidcProviders();
|
||||
|
||||
@@ -54,6 +55,11 @@ export const auth = betterAuth({
|
||||
enabled: env.AUTH_EMAIL_PASSWORD_ENABLED === "true",
|
||||
requireEmailVerification: false,
|
||||
sendResetPassword: async ({ user, token }, request) => {
|
||||
if (env.AUTH_EMAIL_PASSWORD_ENABLED !== "true") {
|
||||
throw new APIError("FORBIDDEN", {
|
||||
message: "Password reset is disabled.",
|
||||
});
|
||||
}
|
||||
await db
|
||||
.update(drizzleDb.schemas.user)
|
||||
.set(
|
||||
@@ -104,37 +110,33 @@ export const auth = betterAuth({
|
||||
});
|
||||
},
|
||||
},
|
||||
socialProviders: SUPPORTED_PROVIDERS.reduce(
|
||||
(acc: any, provider: AuthProviderConfig) => {
|
||||
if (!provider.isActive) return acc;
|
||||
if (provider.id === "credential") return acc;
|
||||
if (provider.type === "sso") return acc;
|
||||
if (provider.id === "google") {
|
||||
acc.google = {
|
||||
clientId: env.AUTH_GOOGLE_ID! as string,
|
||||
clientSecret: env.AUTH_GOOGLE_SECRET! as string,
|
||||
};
|
||||
}
|
||||
if (provider.id === "github") {
|
||||
acc.github = {
|
||||
// clientId: provider.credentials?.clientId,
|
||||
// clientSecret: provider.credentials?.clientSecret,
|
||||
};
|
||||
}
|
||||
socialProviders: getOAuthProviders().reduce<
|
||||
Record<string, { clientId: string; clientSecret: string }>
|
||||
>((acc, provider) => {
|
||||
const configEntry = SUPPORTED_PROVIDERS.find((p) => p.id === provider.id);
|
||||
|
||||
if (!configEntry?.isActive) {
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
),
|
||||
}
|
||||
|
||||
acc[provider.id] = {
|
||||
clientId: provider.client,
|
||||
clientSecret: provider.secret,
|
||||
...(provider.id === "apple" && provider.appleBundleIdentifier
|
||||
? { appBundleIdentifier: provider.appleBundleIdentifier }
|
||||
: {}),
|
||||
};
|
||||
return acc;
|
||||
}, {}),
|
||||
account: {
|
||||
accountLinking: {
|
||||
enabled: true,
|
||||
trustedProviders: [
|
||||
"google",
|
||||
"github",
|
||||
"credential",
|
||||
...getOAuthProviders().map((p) => p.id),
|
||||
...oidcProviders.map((p) => p.id),
|
||||
],
|
||||
allowDifferentEmails: false,
|
||||
allowDifferentEmails: true,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -288,6 +290,14 @@ export const auth = betterAuth({
|
||||
database: {
|
||||
generateId: false,
|
||||
},
|
||||
cookies: {
|
||||
state: {
|
||||
attributes: {
|
||||
sameSite: "none",
|
||||
secure: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
deleteUser: {
|
||||
@@ -320,6 +330,7 @@ export const auth = betterAuth({
|
||||
const provider = SUPPORTED_PROVIDERS.find(
|
||||
(p) => p.id === account.providerId,
|
||||
);
|
||||
|
||||
if (provider && provider.allowLinking === false) {
|
||||
throw new APIError("FORBIDDEN", {
|
||||
message: "Linking is disabled for this provider.",
|
||||
@@ -413,50 +424,28 @@ export const auth = betterAuth({
|
||||
const url =
|
||||
context?.request?.url || context?.headers?.get("referer") || "";
|
||||
|
||||
let providerId: string;
|
||||
let providerId: string | undefined;
|
||||
|
||||
if (url.includes("/sso/callback")) {
|
||||
const urlObj = new URL(url, "http://localhost");
|
||||
providerId = urlObj.searchParams.get("providerId") || "sso";
|
||||
console.log(`Found provider: ${providerId}`);
|
||||
if (url) {
|
||||
const urlPath = new URL(url).pathname;
|
||||
const pathParts = urlPath.split("/");
|
||||
const lastPathPart = pathParts[pathParts.length - 1];
|
||||
|
||||
if (urlPath.startsWith("/api/auth/sso/callback/")) {
|
||||
providerId = lastPathPart;
|
||||
} else if (urlPath.startsWith("/api/auth/callback/")) {
|
||||
providerId = lastPathPart;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
activeOrganizationId: memberships[0].organizationId,
|
||||
activeOrganizationId: memberships[0]?.organizationId,
|
||||
providerId: providerId,
|
||||
},
|
||||
};
|
||||
},
|
||||
// after: async (session) => {
|
||||
// const user = await db.query.user.findFirst({
|
||||
// where: eq(drizzleDb.schemas.user.id, session.userId),
|
||||
// });
|
||||
//
|
||||
// if (user && user.role != "pending") {
|
||||
// const deviceInfo = getDeviceDetails(session.userAgent);
|
||||
// await sendEmail({
|
||||
// to: user.email,
|
||||
// subject: "New login to your account",
|
||||
// html: await render(
|
||||
// EmailNewLogin({
|
||||
// firstname: user.name!,
|
||||
// os: deviceInfo.os,
|
||||
// browser: deviceInfo.browser,
|
||||
// ipAddress: session.ipAddress!,
|
||||
// }),
|
||||
// {}
|
||||
// ),
|
||||
// });
|
||||
//
|
||||
// (await auth.$context).internalAdapter.updateUser(user.id, {
|
||||
// lastConnectedAt: new Date(),
|
||||
// });
|
||||
// }
|
||||
// },
|
||||
after: async (session) => {
|
||||
console.log("session", session);
|
||||
|
||||
const user = await db.query.user.findFirst({
|
||||
where: eq(drizzleDb.schemas.user.id, session.userId),
|
||||
});
|
||||
@@ -512,6 +501,10 @@ export const auth = betterAuth({
|
||||
type: "string",
|
||||
required: false,
|
||||
},
|
||||
providerId: {
|
||||
type: "string",
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
/* databaseHooks: {
|
||||
|
||||
+26
-23
@@ -1,5 +1,7 @@
|
||||
import { env } from "@/env.mjs";
|
||||
import { getOidcProviders } from "./oidc";
|
||||
import { getOAuthProviders } from "./oauth";
|
||||
import * as BetterAuthSocialProviders from "better-auth/social-providers";
|
||||
|
||||
export interface AuthProviderConfig {
|
||||
id: string;
|
||||
@@ -15,6 +17,9 @@ export interface AuthProviderConfig {
|
||||
}
|
||||
|
||||
const oidcProviders = getOidcProviders();
|
||||
const oauthProviders = getOAuthProviders();
|
||||
|
||||
const availableSocialProviders = Object.keys(BetterAuthSocialProviders);
|
||||
|
||||
export const SUPPORTED_PROVIDERS: AuthProviderConfig[] = [
|
||||
{
|
||||
@@ -29,28 +34,26 @@ export const SUPPORTED_PROVIDERS: AuthProviderConfig[] = [
|
||||
allowLinking: true,
|
||||
allowUnlinking: true,
|
||||
},
|
||||
{
|
||||
id: "google",
|
||||
isActive: !!env.AUTH_GOOGLE_ID,
|
||||
name: "Google",
|
||||
icon: "logos:google-icon",
|
||||
title: "Google",
|
||||
description: "Sign in with your Google account.",
|
||||
type: "social",
|
||||
allowLinking: true,
|
||||
allowUnlinking: true,
|
||||
},
|
||||
{
|
||||
id: "github",
|
||||
isActive: !!env.AUTH_GITHUB_ID,
|
||||
name: "GitHub",
|
||||
icon: "logos:github-icon",
|
||||
title: "GitHub",
|
||||
description: "Sign in with your GitHub account.",
|
||||
type: "social",
|
||||
allowLinking: true,
|
||||
allowUnlinking: true,
|
||||
},
|
||||
...oauthProviders.map((p) => {
|
||||
const isSupported = availableSocialProviders.includes(p.id.toLowerCase());
|
||||
|
||||
if (!isSupported) {
|
||||
console.warn(`Provider ${p.id} is not supported. Skipping...`);
|
||||
}
|
||||
|
||||
return {
|
||||
id: p.id,
|
||||
isActive: isSupported,
|
||||
name: p.title,
|
||||
icon: p.icon,
|
||||
title: p.title,
|
||||
description: p.description,
|
||||
isManual: false,
|
||||
type: "social" as const,
|
||||
allowLinking: p.allowLinking,
|
||||
allowUnlinking: p.allowUnlinking,
|
||||
};
|
||||
}),
|
||||
...oidcProviders.map((p) => ({
|
||||
id: p.id,
|
||||
isActive: true,
|
||||
@@ -58,7 +61,7 @@ export const SUPPORTED_PROVIDERS: AuthProviderConfig[] = [
|
||||
icon: p.icon,
|
||||
title: p.title,
|
||||
description: p.description,
|
||||
isManual: true,
|
||||
isManual: false,
|
||||
type: "sso" as const,
|
||||
allowLinking: p.allowLinking,
|
||||
allowUnlinking: p.allowUnlinking,
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import { env } from "@/env.mjs";
|
||||
|
||||
export interface OAuthProvider {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: string;
|
||||
client: string;
|
||||
secret: string;
|
||||
allowedGroup?: string;
|
||||
roleMap?: string;
|
||||
defaultRole?: string;
|
||||
allowLinking: boolean;
|
||||
allowUnlinking: boolean;
|
||||
appleBundleIdentifier?: string;
|
||||
}
|
||||
|
||||
const PROVIDER_ICONS: Record<string, string> = {
|
||||
google: "logos:google-icon",
|
||||
github: "logos:github-icon",
|
||||
gitlab: "logos:gitlab-icon",
|
||||
discord: "logos:discord-icon",
|
||||
facebook: "logos:facebook",
|
||||
twitter: "logos:twitter",
|
||||
x: "logos:x",
|
||||
linkedin: "logos:linkedin-icon",
|
||||
apple: "logos:apple",
|
||||
microsoft: "logos:microsoft-icon",
|
||||
twitch: "logos:twitch",
|
||||
spotify: "logos:spotify-icon",
|
||||
slack: "logos:slack-icon",
|
||||
tiktok: "logos:tiktok-icon",
|
||||
figma: "logos:figma",
|
||||
dropbox: "logos:dropbox",
|
||||
notion: "logos:notion-icon",
|
||||
paypal: "logos:paypal",
|
||||
reddit: "logos:reddit-icon",
|
||||
salesforce: "logos:salesforce",
|
||||
vercel: "logos:vercel-icon",
|
||||
zoom: "logos:zoom-icon",
|
||||
altassian: "logos:jira",
|
||||
};
|
||||
|
||||
function getProviderIcon(providerId: string, envIcon?: string): string {
|
||||
if (envIcon) return envIcon;
|
||||
|
||||
const normalizedId = providerId.toLowerCase();
|
||||
|
||||
if (PROVIDER_ICONS[normalizedId]) {
|
||||
return PROVIDER_ICONS[normalizedId];
|
||||
}
|
||||
|
||||
return `lucide:building`;
|
||||
}
|
||||
|
||||
export function getOAuthProviders(): OAuthProvider[] {
|
||||
const providers: OAuthProvider[] = [];
|
||||
|
||||
if (env.AUTH_SOCIAL_CLIENT && env.AUTH_SOCIAL_ID) {
|
||||
providers.push({
|
||||
id: env.AUTH_SOCIAL_ID,
|
||||
title: env.AUTH_SOCIAL_TITLE || "OAuth",
|
||||
description: env.AUTH_SOCIAL_DESC || "Sign in with your OAuth account.",
|
||||
icon: env.AUTH_SOCIAL_ICON || "lucide:building",
|
||||
client: env.AUTH_SOCIAL_CLIENT,
|
||||
secret: env.AUTH_SOCIAL_SECRET || "",
|
||||
allowedGroup: env.ALLOWED_GROUP,
|
||||
roleMap: env.AUTH_ROLE_MAP,
|
||||
defaultRole: env.AUTH_DEFAULT_ROLE,
|
||||
allowLinking: env.AUTH_ALLOW_LINKING !== "false",
|
||||
allowUnlinking: env.AUTH_ALLOW_UNLINKING !== "false",
|
||||
appleBundleIdentifier: env.AUTH_SOCIAL_APPLE_APP_BUNDLE_IDENTIFIER,
|
||||
});
|
||||
}
|
||||
|
||||
const prefixes = new Set<string>();
|
||||
Object.keys(process.env).forEach((key) => {
|
||||
const match = key.match(/^AUTH_SOCIAL_(.+)_CLIENT$/);
|
||||
if (match) {
|
||||
prefixes.add(match[1]);
|
||||
}
|
||||
});
|
||||
|
||||
prefixes.forEach((prefix) => {
|
||||
const client = process.env[`AUTH_SOCIAL_${prefix}_CLIENT`];
|
||||
if (!client) return;
|
||||
|
||||
const providerId = prefix.toLowerCase();
|
||||
|
||||
const envTitle = process.env[`AUTH_SOCIAL_${prefix}_TITLE`];
|
||||
const envDesc = process.env[`AUTH_SOCIAL_${prefix}_DESC`];
|
||||
const envIcon = process.env[`AUTH_SOCIAL_${prefix}_ICON`];
|
||||
const envSecret = process.env[`AUTH_SOCIAL_${prefix}_SECRET`];
|
||||
const envAllowedGroup = process.env[`AUTH_SOCIAL_${prefix}_ALLOWED_GROUP`];
|
||||
const envRoleMap = process.env[`AUTH_SOCIAL_${prefix}_ROLE_MAP`];
|
||||
const envDefaultRole = process.env[`AUTH_SOCIAL_${prefix}_DEFAULT_ROLE`];
|
||||
|
||||
const envAppleBundleId =
|
||||
process.env[`AUTH_SOCIAL_APPLE_APP_BUNDLE_IDENTIFIER`];
|
||||
|
||||
providers.push({
|
||||
id: providerId,
|
||||
title:
|
||||
envTitle ||
|
||||
prefix.charAt(0).toUpperCase() + prefix.slice(1).toLowerCase(),
|
||||
description:
|
||||
envDesc ||
|
||||
`Sign in with ${prefix.charAt(0).toUpperCase() + prefix.slice(1).toLowerCase()}`,
|
||||
icon: getProviderIcon(providerId, envIcon),
|
||||
client: client,
|
||||
secret: envSecret || "",
|
||||
allowedGroup: envAllowedGroup || env.ALLOWED_GROUP,
|
||||
roleMap: envRoleMap,
|
||||
defaultRole: envDefaultRole,
|
||||
allowLinking: env.AUTH_ALLOW_LINKING !== "false",
|
||||
allowUnlinking: env.AUTH_ALLOW_UNLINKING !== "false",
|
||||
appleBundleIdentifier: envAppleBundleId,
|
||||
});
|
||||
});
|
||||
|
||||
return providers;
|
||||
}
|
||||
@@ -41,10 +41,10 @@ export function getOidcProviders(): OIDCProvider[] {
|
||||
jwksEndpoint: env.AUTH_OIDC_JWKS_ENDPOINT,
|
||||
pkce: env.AUTH_OIDC_PKCE === "true",
|
||||
allowedGroup: env.ALLOWED_GROUP,
|
||||
roleMap: process.env.AUTH_OIDC_ROLE_MAP,
|
||||
defaultRole: process.env.AUTH_OIDC_DEFAULT_ROLE,
|
||||
allowLinking: process.env.AUTH_OIDC_ALLOW_LINKING !== "false",
|
||||
allowUnlinking: process.env.AUTH_OIDC_ALLOW_UNLINKING !== "false",
|
||||
roleMap: env.AUTH_ROLE_MAP,
|
||||
defaultRole: env.AUTH_DEFAULT_ROLE,
|
||||
allowLinking: env.AUTH_ALLOW_LINKING !== "false",
|
||||
allowUnlinking: env.AUTH_ALLOW_UNLINKING !== "false",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
export type SignUpUser = {
|
||||
name: string
|
||||
email: string
|
||||
password: string
|
||||
password?: string
|
||||
callbackURL?: string
|
||||
role?: string
|
||||
theme: string
|
||||
|
||||
Reference in New Issue
Block a user