mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
Working on the profile module.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, {ReactNode} from "react";
|
||||
import {redirect} from "next/navigation";
|
||||
|
||||
import {SidebarInset, SidebarProvider} from "@/components/ui/sidebar";
|
||||
@@ -6,21 +6,19 @@ import {AppSidebar} from "@/components/wrappers/dashboard/common/sidebar/app-sid
|
||||
import {Header} from "@/features/layout/Header";
|
||||
import {currentUser} from "@/lib/auth/current-user";
|
||||
|
||||
export default async function Layout({children}: { children: React.ReactNode }) {
|
||||
export default async function Layout({children}: { children: ReactNode }) {
|
||||
const user = await currentUser();
|
||||
if (!user) redirect("/login");
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidebarProvider>
|
||||
<div className="flex flex-col lg:flex-row w-full">
|
||||
<AppSidebar/>
|
||||
<SidebarInset>
|
||||
<Header/>
|
||||
<main className="h-full">{children}</main>
|
||||
</SidebarInset>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
</>
|
||||
<SidebarProvider>
|
||||
<div className="flex flex-col lg:flex-row w-full">
|
||||
<AppSidebar/>
|
||||
<SidebarInset>
|
||||
<Header/>
|
||||
<main className="h-full">{children}</main>
|
||||
</SidebarInset>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
||||
|
||||
+10
-5
@@ -5,6 +5,7 @@ import {Providers} from "./providers";
|
||||
import {cn} from "@/lib/utils";
|
||||
import {ConsoleSilencer} from "@/components/wrappers/common/console-silencer";
|
||||
import {inter} from "@/fonts/fonts";
|
||||
import {currentUser} from "@/lib/auth/current-user";
|
||||
|
||||
const title = process.env.PROJECT_NAME ?? "App Title";
|
||||
|
||||
@@ -16,19 +17,23 @@ export const metadata: Metadata = {
|
||||
description: process.env.PROJECT_DESCRIPTION ?? undefined,
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
|
||||
const user = await currentUser();
|
||||
console.info("useerrrrr",user)
|
||||
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
<meta name="apple-mobile-web-app-title" content="Portabase"/>
|
||||
<meta name="apple-mobile-web-app-title" content={title}/>
|
||||
</head>
|
||||
<body className={cn(inter.className, "h-full")}>
|
||||
<ConsoleSilencer/>
|
||||
<Providers>{children}</Providers>
|
||||
<Providers >{children}</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
+10
-3
@@ -5,14 +5,21 @@ import { ThemeProvider } from "@/features/theme/theme-provider";
|
||||
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import {ThemeMetaUpdater} from "@/features/browser/theme-meta-updater";
|
||||
|
||||
export type ProviderProps = PropsWithChildren<{}>;
|
||||
export type ProviderProps = PropsWithChildren<{
|
||||
}>;
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
export const Providers = (props: ProviderProps) => {
|
||||
return (
|
||||
<Suspense fallback={null}>
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
|
||||
<Suspense fallback={null} >
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
defaultTheme="system"
|
||||
enableSystem
|
||||
>
|
||||
<ThemeMetaUpdater />
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<Toaster />
|
||||
{props.children}
|
||||
|
||||
@@ -15,7 +15,7 @@ import {LoggedInButton} from "@/components/wrappers/dashboard/common/logged-in/l
|
||||
export function AppSidebar() {
|
||||
const projectName = env.PROJECT_NAME;
|
||||
return (
|
||||
<Sidebar collapsible="icon">
|
||||
<Sidebar collapsible="icon" >
|
||||
<SidebarHeader>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import {useState} from "react";
|
||||
import React, {useState} from "react";
|
||||
import {Button} from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -24,6 +24,7 @@ import z from "zod";
|
||||
import {zPassword} from "@/lib/zod";
|
||||
import {BackupCodesList} from "../components/backup-codes-list";
|
||||
import {PasswordInput} from "@/components/ui/password-input";
|
||||
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
|
||||
|
||||
const PasswordSchema = z.object({
|
||||
password: zPassword(),
|
||||
@@ -34,9 +35,10 @@ type Password = z.infer<typeof PasswordSchema>;
|
||||
type Setup2FAModalProps = {
|
||||
onOpenChange: (open: boolean) => void;
|
||||
open: boolean;
|
||||
disabled: boolean;
|
||||
};
|
||||
|
||||
export function Setup2FAProfileProviderModal({onOpenChange, open}: Setup2FAModalProps) {
|
||||
export function Setup2FAProfileProviderModal({onOpenChange, open, disabled}: Setup2FAModalProps) {
|
||||
|
||||
const router = useRouter();
|
||||
const [step, setStep] = useState<"PASSWORD" | "QR" | "BACKUP">("PASSWORD");
|
||||
@@ -113,10 +115,9 @@ export function Setup2FAProfileProviderModal({onOpenChange, open}: Setup2FAModal
|
||||
};
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(v) => (!v ? handleClose() : onOpenChange(v))}>
|
||||
<DialogTrigger asChild>
|
||||
<DialogTrigger asChild disabled={disabled}>
|
||||
<Button variant="outline" size="sm">
|
||||
<ShieldCheck className="w-4 h-4 mr-2"/>
|
||||
Enable Two-Factor
|
||||
@@ -177,7 +178,8 @@ export function Setup2FAProfileProviderModal({onOpenChange, open}: Setup2FAModal
|
||||
</div>
|
||||
|
||||
<div className="w-full space-y-2">
|
||||
<p className="text-xs text-muted-foreground text-center">If you are unable to scan the QR code, you can manually enter the secret key into your authentication app :</p>
|
||||
<p className="text-xs text-muted-foreground text-center">If you are unable to scan the
|
||||
QR code, you can manually enter the secret key into your authentication app :</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<code
|
||||
className="flex-1 bg-muted p-2 rounded text-xs font-mono break-all text-center">{secret}</code>
|
||||
@@ -237,7 +239,8 @@ export function Setup2FAProfileProviderModal({onOpenChange, open}: Setup2FAModal
|
||||
className="border-green-200 bg-green-50 dark:bg-green-900/20 dark:border-green-900">
|
||||
<CheckCircle2 className="h-4 w-4 text-green-600 dark:text-green-400"/>
|
||||
<AlertDescription
|
||||
className="text-green-700 dark:text-green-400">Two Factor Authentication is now enabled on your account.</AlertDescription>
|
||||
className="text-green-700 dark:text-green-400">Two Factor Authentication is now enabled
|
||||
on your account.</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<BackupCodesList codes={backupCodes}/>
|
||||
|
||||
@@ -40,7 +40,7 @@ function ThemeSelector() {
|
||||
)}
|
||||
onClick={async () => {
|
||||
// setTheme(item.value)
|
||||
// await authClient.updateUser({theme: item.value});
|
||||
await authClient.updateUser({theme: item.value});
|
||||
}}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -74,11 +74,9 @@ export function ProfileSecurity({user, sessions, credentialAccount, currentSessi
|
||||
<div className="space-y-1">
|
||||
<div className="font-medium">Password</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{/*{user.lastChangedPasswordAt*/}
|
||||
{/* ? t("sections.authentication.password.description.last_changed", {*/}
|
||||
{/* date: timeAgo(new Date(user.lastChangedPasswordAt), locale),*/}
|
||||
{/* })*/}
|
||||
{/* : "Never changed"}*/}
|
||||
{user.lastChangedPasswordAt
|
||||
? `Last changed ${timeAgo(new Date(user.lastChangedPasswordAt))}`
|
||||
: "Never changed"}
|
||||
</div>
|
||||
</div>
|
||||
{credentialAccount ? (
|
||||
@@ -116,8 +114,11 @@ export function ProfileSecurity({user, sessions, credentialAccount, currentSessi
|
||||
onOpenChange={setIsDisable2FADialogOpen}/>
|
||||
</div>
|
||||
) : (
|
||||
<Setup2FAProfileProviderModal open={isSetup2FADialogOpen}
|
||||
onOpenChange={setIsSetup2FADialogOpen}/>
|
||||
<Setup2FAProfileProviderModal
|
||||
disabled={!credentialAccount}
|
||||
open={isSetup2FADialogOpen}
|
||||
onOpenChange={setIsSetup2FADialogOpen}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
CREATE TABLE "passkey" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"name" text,
|
||||
"publicKey" text NOT NULL,
|
||||
"userId" uuid NOT NULL,
|
||||
"credentialId" text NOT NULL,
|
||||
"counter" integer NOT NULL,
|
||||
"deviceType" text NOT NULL,
|
||||
"backedUp" boolean NOT NULL,
|
||||
"transports" text,
|
||||
"updated_at" timestamp,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"deleted_at" timestamp
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "two_factor" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"secret" text NOT NULL,
|
||||
"backup_codes" text NOT NULL,
|
||||
"user_id" uuid NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "passkey" ADD CONSTRAINT "passkey_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "two_factor" ADD CONSTRAINT "two_factor_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
|
||||
@@ -0,0 +1,2 @@
|
||||
CREATE TYPE "public"."user_themes" AS ENUM('light', 'dark', 'system');--> statement-breakpoint
|
||||
ALTER TABLE "user" ADD COLUMN "theme" "user_themes" DEFAULT 'light' NOT NULL;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -99,6 +99,20 @@
|
||||
"when": 1766327760039,
|
||||
"tag": "0013_past_logan",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 14,
|
||||
"version": "7",
|
||||
"when": 1766424611387,
|
||||
"tag": "0014_strong_galactus",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 15,
|
||||
"version": "7",
|
||||
"when": 1766426190521,
|
||||
"tag": "0015_absurd_next_avengers",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import {relations} from "drizzle-orm";
|
||||
import {boolean, pgTable, text, timestamp, uuid} from "drizzle-orm/pg-core";
|
||||
import {boolean, integer, pgEnum, pgTable, text, timestamp, uuid} from "drizzle-orm/pg-core";
|
||||
import {createSelectSchema} from "drizzle-zod";
|
||||
import {z} from "zod";
|
||||
import {project} from "./06_project";
|
||||
@@ -9,6 +9,9 @@ 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(),
|
||||
@@ -16,6 +19,7 @@ export const user = pgTable("user", {
|
||||
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"),
|
||||
@@ -66,6 +70,31 @@ export const verification = pgTable("verification", {
|
||||
|
||||
});
|
||||
|
||||
export const passkey = pgTable("passkey", {
|
||||
id: uuid().defaultRandom().primaryKey(),
|
||||
name: text(),
|
||||
publicKey: text().notNull(),
|
||||
userId: uuid()
|
||||
.notNull()
|
||||
.references(() => user.id, {onDelete: "cascade"}),
|
||||
credentialId: text().notNull(),
|
||||
counter: integer().notNull(),
|
||||
deviceType: text().notNull(),
|
||||
backedUp: boolean().notNull(),
|
||||
transports: text(),
|
||||
|
||||
...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"}),
|
||||
});
|
||||
|
||||
export const userRelations = relations(user, ({many}) => ({
|
||||
sessions: many(session),
|
||||
accounts: many(account),
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
|
||||
import {useTheme} from "next-themes";
|
||||
import {useEffect} from "react";
|
||||
import {authClient} from "@/lib/auth/auth-client";
|
||||
|
||||
export function ThemeMetaUpdater() {
|
||||
const {resolvedTheme, setTheme} = useTheme();
|
||||
const {data: session} = authClient.useSession();
|
||||
|
||||
useEffect(() => {
|
||||
if (!resolvedTheme) return;
|
||||
|
||||
const color = resolvedTheme === "dark" ? "#000000" : "#ffffff";
|
||||
|
||||
let tag = document.querySelector<HTMLMetaElement>('meta[name="theme-color"]');
|
||||
if (!tag) {
|
||||
tag = document.createElement("meta");
|
||||
tag.setAttribute("name", "theme-color");
|
||||
document.head.appendChild(tag);
|
||||
}
|
||||
tag.setAttribute("content", color);
|
||||
}, [resolvedTheme]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!session?.user?.theme) return;
|
||||
setTheme(session.user.theme);
|
||||
}, [session, resolvedTheme, setTheme]);
|
||||
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
//
|
||||
// "use client"
|
||||
// import {useEffect, useState} from "react";
|
||||
// import {useTheme} from "next-themes";
|
||||
// import {authClient} from "@/lib/auth/auth-client";
|
||||
//
|
||||
// export function ThemeMetaUpdater() {
|
||||
// const [mounted, setMounted] = useState(false);
|
||||
// const {resolvedTheme, setTheme} = useTheme();
|
||||
// const {data: session} = authClient.useSession();
|
||||
//
|
||||
// useEffect(() => setMounted(true), []);
|
||||
//
|
||||
// useEffect(() => {
|
||||
// if (!mounted || !resolvedTheme) return;
|
||||
// const color = resolvedTheme === "dark" ? "#000000" : "#ffffff";
|
||||
// let tag = document.querySelector<HTMLMetaElement>('meta[name="theme-color"]');
|
||||
// if (!tag) {
|
||||
// tag = document.createElement("meta");
|
||||
// tag.setAttribute("name", "theme-color");
|
||||
// document.head.appendChild(tag);
|
||||
// }
|
||||
// tag.setAttribute("content", color);
|
||||
// }, [mounted, resolvedTheme]);
|
||||
//
|
||||
// useEffect(() => {
|
||||
// if (!mounted || !session?.user?.theme) return;
|
||||
// if (resolvedTheme !== session.user.theme) {
|
||||
// setTheme(session.user.theme);
|
||||
// }
|
||||
// }, [mounted, session, setTheme]);
|
||||
//
|
||||
// return null;
|
||||
// }
|
||||
@@ -1,7 +1,7 @@
|
||||
import {notFound} from "next/navigation";
|
||||
|
||||
import {SidebarTrigger} from "@/components/ui/sidebar";
|
||||
import {ModeToggle} from "@/features/theme/ModeToggle";
|
||||
import {ModeToggle} from "@/features/theme/mode-toggle";
|
||||
import {currentUser} from "@/lib/auth/current-user";
|
||||
import {BreadCrumbsWrapper} from "@/components/wrappers/common/bread-crumbs/bread-crumbs";
|
||||
import GitHubStarsButtonCustom from "@/components/wrappers/common/github/github-button";
|
||||
|
||||
@@ -2,13 +2,17 @@
|
||||
|
||||
import * as React from "react"
|
||||
import {Moon, Sun} from "lucide-react"
|
||||
import {useTheme} from "next-themes"
|
||||
|
||||
import {Button} from "@/components/ui/button"
|
||||
import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger} from "@/components/ui/dropdown-menu"
|
||||
import {authClient} from "@/lib/auth/auth-client";
|
||||
import {toast} from "sonner";
|
||||
|
||||
export function ModeToggle() {
|
||||
const {setTheme} = useTheme()
|
||||
|
||||
const setTheme = async (theme: "light" | "dark" | "system") => {
|
||||
toast.success("Your theme preference has been updated.");
|
||||
await authClient.updateUser({theme: theme});
|
||||
};
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
@@ -33,3 +37,5 @@ export function ModeToggle() {
|
||||
</DropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ export const auth = betterAuth({
|
||||
provider: "pg",
|
||||
schema: drizzleDb.schemas,
|
||||
}),
|
||||
appName: env.PROJECT_NAME!,
|
||||
baseURL: env.PROJECT_URL,
|
||||
secret: env.PROJECT_SECRET,
|
||||
emailAndPassword: {
|
||||
@@ -131,6 +132,15 @@ export const auth = betterAuth({
|
||||
nullable: true,
|
||||
required: false,
|
||||
},
|
||||
theme: {
|
||||
type: "string",
|
||||
},
|
||||
lastConnectedAt: {
|
||||
type: "date",
|
||||
},
|
||||
lastChangedPasswordAt: {
|
||||
type: "date",
|
||||
},
|
||||
},
|
||||
},
|
||||
databaseHooks: {
|
||||
|
||||
Reference in New Issue
Block a user