menampilkan user saat ini ke profile

This commit is contained in:
vergiLgood1 2025-03-02 23:47:02 +07:00
parent f00544cb28
commit dc3c0bebbb
13 changed files with 457 additions and 193 deletions

View File

@ -1,10 +1,12 @@
// src/app/(auth-pages)/actions.ts
"use server";
import db from "@/lib/db";
import { SignInFormData } from "@/src/models/auth/sign-in.model";
import { VerifyOtpFormData } from "@/src/models/auth/verify-otp.model";
import { User } from "@/src/models/users/users.model";
import { authRepository } from "@/src/repositories/authentication.repository";
import { createClient } from "@/utils/supabase/server";
import { redirect } from "next/navigation";
export async function signIn(
@ -15,7 +17,7 @@ export async function signIn(
return {
success: true,
message: "Check your email for the login link!",
redirectTo: result.redirectTo
redirectTo: result.redirectTo,
};
} catch (error) {
console.error("Authentication error:", error);
@ -37,7 +39,7 @@ export async function verifyOtp(
return {
success: true,
message: "Successfully authenticated!",
redirectTo: result.redirectTo
redirectTo: result.redirectTo,
};
} catch (error) {
console.error("OTP verification error:", error);
@ -56,7 +58,7 @@ export async function signOut() {
const result = await authRepository.signOut();
return {
success: true,
redirectTo: result.redirectTo
redirectTo: result.redirectTo,
};
} catch (error) {
console.error("Sign out error:", error);
@ -69,3 +71,33 @@ export async function signOut() {
};
}
}
// get current user
export async function getCurrentUser(): Promise<User> {
const supabase = await createClient();
const {
data: { user },
error,
} = await supabase.auth.getUser();
if (error) {
console.error("Error fetching current user:", error);
throw new Error(error.message);
}
const userDetail = await db.users.findUnique({
where: {
id: user?.id,
},
include: {
profile: true,
},
});
if (!userDetail) {
throw new Error("User not found");
}
return userDetail;
}

View File

@ -1,5 +1,4 @@
export default function DashboardPage() {
return (
<>

View File

@ -1,158 +1,238 @@
"use server"
"use server";
import { CreateUserParams, InviteUserParams, UpdateUserParams, User } from "@/src/models/users/users.model"
import { createClient } from "@supabase/supabase-js"
import db from "@/lib/db";
import {
CreateUserParams,
InviteUserParams,
UpdateUserParams,
User,
UserFromSupabase,
} from "@/src/models/users/users.model";
import { createClient } from "@supabase/supabase-js";
import { createClient as supabaseUser } from "@/utils/supabase/server";
// Initialize Supabase client with admin key
const supabase = createClient(process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.SERVICE_ROLE_SECRET!, {
auth: {
autoRefreshToken: false,
persistSession: false,
},
})
// Fetch all users
export async function fetchUsers(): Promise<User[]> {
const { data, error } = await supabase.auth.admin.listUsers()
// const { data, error } = await supabase.auth.admin.listUsers();
if (error) {
console.error("Error fetching users:", error)
throw new Error(error.message)
// if (error) {
// console.error("Error fetching users:", error);
// throw new Error(error.message);
// }
// return data.users.map((user) => ({
// ...user,
// })) as User[];
const users = await db.users.findMany({
include: {
profile: true,
},
});
if (!users) {
throw new Error("Users not found");
}
return data.users.map(user => ({
...user,
updated_at: user.updated_at || "",
})) as User[]
return users;
}
// get current user
export async function getCurrentUser(): Promise<User> {
const supabase = await supabaseUser();
const {
data: { user },
error,
} = await supabase.auth.getUser();
if (error) {
console.error("Error fetching current user:", error);
throw new Error(error.message);
}
const userDetail = await db.users.findUnique({
where: {
id: user?.id,
},
include: {
profile: true,
},
});
if (!userDetail) {
throw new Error("User not found");
}
return userDetail;
}
// Create a new user
export async function createUser(params: CreateUserParams): Promise<User> {
export async function createUser(
params: CreateUserParams
): Promise<UserFromSupabase> {
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SERVICE_ROLE_SECRET!
);
const { data, error } = await supabase.auth.admin.createUser({
email: params.email,
password: params.password,
phone: params.phone,
user_metadata: params.user_metadata,
email_confirm: params.email_confirm,
})
});
if (error) {
console.error("Error creating user:", error)
throw new Error(error.message)
console.error("Error creating user:", error);
throw new Error(error.message);
}
return {
...data.user,
updated_at: data.user.updated_at || "",
} as User
};
}
// Update an existing user
export async function updateUser(userId: string, params: UpdateUserParams): Promise<User> {
export async function updateUser(
userId: string,
params: UpdateUserParams
): Promise<UserFromSupabase> {
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SERVICE_ROLE_SECRET!
);
const { data, error } = await supabase.auth.admin.updateUserById(userId, {
email: params.email,
phone: params.phone,
password: params.password,
user_metadata: params.user_metadata,
})
});
if (error) {
console.error("Error updating user:", error)
throw new Error(error.message)
console.error("Error updating user:", error);
throw new Error(error.message);
}
return {
...data.user,
updated_at: data.user.updated_at || "",
} as User
};
}
// Delete a user
export async function deleteUser(userId: string): Promise<void> {
const { error } = await supabase.auth.admin.deleteUser(userId)
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SERVICE_ROLE_SECRET!
);
const { error } = await supabase.auth.admin.deleteUser(userId);
if (error) {
console.error("Error deleting user:", error)
throw new Error(error.message)
console.error("Error deleting user:", error);
throw new Error(error.message);
}
}
// Send password recovery email
export async function sendPasswordRecovery(email: string): Promise<void> {
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SERVICE_ROLE_SECRET!
);
const { error } = await supabase.auth.resetPasswordForEmail(email, {
redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/reset-password`,
})
});
if (error) {
console.error("Error sending password recovery:", error)
throw new Error(error.message)
console.error("Error sending password recovery:", error);
throw new Error(error.message);
}
}
// Send magic link
export async function sendMagicLink(email: string): Promise<void> {
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SERVICE_ROLE_SECRET!
);
const { error } = await supabase.auth.signInWithOtp({
email,
options: {
emailRedirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
},
})
});
if (error) {
console.error("Error sending magic link:", error)
throw new Error(error.message)
console.error("Error sending magic link:", error);
throw new Error(error.message);
}
}
// Ban a user
export async function banUser(userId: string): Promise<User> {
export async function banUser(userId: string): Promise<UserFromSupabase> {
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SERVICE_ROLE_SECRET!
);
// Ban for 100 years (effectively permanent)
const banUntil = new Date()
banUntil.setFullYear(banUntil.getFullYear() + 100)
const banUntil = new Date();
banUntil.setFullYear(banUntil.getFullYear() + 100);
const { data, error } = await supabase.auth.admin.updateUserById(userId, {
ban_duration: "100h",
})
});
if (error) {
console.error("Error banning user:", error)
throw new Error(error.message)
console.error("Error banning user:", error);
throw new Error(error.message);
}
return {
...data.user,
updated_at: data.user.updated_at || "",
} as User
};
}
// Unban a user
export async function unbanUser(userId: string): Promise<User> {
export async function unbanUser(userId: string): Promise<UserFromSupabase> {
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SERVICE_ROLE_SECRET!
);
const { data, error } = await supabase.auth.admin.updateUserById(userId, {
ban_duration: "none",
})
});
if (error) {
console.error("Error unbanning user:", error)
throw new Error(error.message)
console.error("Error unbanning user:", error);
throw new Error(error.message);
}
return {
...data.user,
updated_at: data.user.updated_at || "",
} as User
};
}
// Invite a user
export async function inviteUser(params: InviteUserParams): Promise<void> {
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SERVICE_ROLE_SECRET!
);
const { error } = await supabase.auth.admin.inviteUserByEmail(params.email, {
data: params.user_metadata,
redirectTo: `${process.env.NEXT_PUBLIC_SITE_URL}/auth/callback`,
})
});
if (error) {
console.error("Error inviting user:", error)
throw new Error(error.message)
console.error("Error inviting user:", error);
throw new Error(error.message);
}
}

View File

@ -34,6 +34,7 @@ export default async function Layout({
}: {
children: React.ReactNode;
}) {
return (
<>
<SidebarProvider>

View File

@ -1,4 +1,5 @@
import FetchDataSteps from "@/components/tutorial/fetch-data-steps";
import db from "@/lib/db";
import { createClient } from "@/utils/supabase/server";
import { InfoIcon } from "lucide-react";
import { redirect } from "next/navigation";
@ -14,6 +15,13 @@ export default async function ProtectedPage() {
return redirect("/sign-in");
}
const userDetail = await db.users.findUnique({
where: {
id: user.id,
},
});
return (
<div className="flex-1 w-full flex flex-col gap-12">
<div className="w-full">
@ -23,11 +31,15 @@ export default async function ProtectedPage() {
user
</div>
</div>
<div className="flex flex-col gap-2 items-start">
<div className="flex gap-2 items-start">
<h2 className="font-bold text-2xl mb-4">Your user details</h2>
<pre className="text-xs font-mono p-3 rounded border max-h-32 overflow-auto">
<pre className="text-xs font-mono p-3 rounded border overflow-auto">
{JSON.stringify(userDetail, null, 2)}
</pre>
<pre className="text-xs font-mono p-3 rounded border overflow-auto">
{JSON.stringify(user, null, 2)}
</pre>
</div>
<div>
<h2 className="font-bold text-2xl mb-4">Next steps</h2>

View File

@ -6,7 +6,6 @@ import { NavMain } from "@/components/admin/navigations/nav-main";
import { NavReports } from "@/components/admin/navigations/nav-report";
import { NavUser } from "@/components/admin/navigations/nav-user";
import {
Sidebar,
SidebarContent,
@ -18,7 +17,30 @@ import { NavPreMain } from "./navigations/nav-pre-main";
import { navData } from "@/prisma/data/nav";
import { TeamSwitcher } from "../team-switcher";
import { Profile, User } from "@/src/models/users/users.model";
import { getCurrentUser } from "@/app/protected/(admin)/dashboard/user-management/action";
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
const [user, setUser] = React.useState<User | null>(null);
const [isLoading, setIsLoading] = React.useState(true);
React.useEffect(() => {
async function fetchUser() {
try {
setIsLoading(true);
const userData = await getCurrentUser();
setUser(userData);
} catch (error) {
console.error("Failed to fetch user:", error);
} finally {
setIsLoading(false);
}
}
fetchUser();
}, []);
return (
<Sidebar collapsible="icon" {...props}>
<SidebarHeader>
@ -30,7 +52,7 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
<NavReports reports={navData.reports} />
</SidebarContent>
<SidebarFooter>
<NavUser user={navData.user} />
<NavUser user={user} />
</SidebarFooter>
<SidebarRail />
</Sidebar>

View File

@ -1,5 +1,3 @@
"use client";
import {
Folder,
Forward,

View File

@ -1,19 +1,8 @@
"use client";
import {
BadgeCheck,
Bell,
ChevronsUpDown,
CreditCard,
LogOut,
Sparkles,
} from "lucide-react";
import { ChevronsUpDown } from "lucide-react";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
DropdownMenu,
DropdownMenuContent,
@ -36,18 +25,35 @@ import {
IconLogout,
IconSparkles,
} from "@tabler/icons-react";
import { Profile, User } from "@/src/models/users/users.model";
export function NavUser({
user,
}: {
user: {
name: string;
email: string;
avatar: string;
};
}) {
export function NavUser({ user }: { user: User | null }) {
const { isMobile } = useSidebar();
// Use profile data with fallbacks
const firstName = user?.profile?.first_name || "";
const lastName = user?.profile?.last_name || "";
const userEmail = user?.email || "";
const userAvatar = user?.profile?.avatar || "";
const getFullName = () => {
return `${firstName} ${lastName}`.trim() || "User";
};
// Generate initials for avatar fallback
const getInitials = () => {
if (firstName && lastName) {
return `${firstName[0]}${lastName[0]}`.toUpperCase();
}
if (firstName) {
return firstName[0].toUpperCase();
}
if (userEmail) {
return userEmail[0].toUpperCase();
}
return "U";
};
return (
<SidebarMenu>
<SidebarMenuItem>
@ -58,12 +64,14 @@ export function NavUser({
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
<AvatarImage src={userAvatar || ""} alt={getFullName()} />
<AvatarFallback className="rounded-lg">
{getInitials()}
</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
<span className="truncate font-semibold">{getFullName()}</span>
<span className="truncate text-xs">{userEmail}</span>
</div>
<ChevronsUpDown className="ml-auto size-4" />
</SidebarMenuButton>
@ -77,12 +85,16 @@ export function NavUser({
<DropdownMenuLabel className="p-0 font-normal">
<div className="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
<Avatar className="h-8 w-8 rounded-lg">
<AvatarImage src={user.avatar} alt={user.name} />
<AvatarFallback className="rounded-lg">CN</AvatarFallback>
<AvatarImage src={userAvatar || ""} alt={getFullName()} />
<AvatarFallback className="rounded-lg">
{getInitials()}
</AvatarFallback>
</Avatar>
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-semibold">{user.name}</span>
<span className="truncate text-xs">{user.email}</span>
<span className="truncate font-semibold">
{getFullName()}
</span>
<span className="truncate text-xs">{userEmail}</span>
</div>
</div>
</DropdownMenuLabel>

15
sigap-website/lib/db.ts Normal file
View File

@ -0,0 +1,15 @@
import { PrismaClient } from "@prisma/client";
const prismaClientSingleton = () => {
return new PrismaClient();
};
declare const globalThis: {
prismaGlobal: ReturnType<typeof prismaClientSingleton>;
} & typeof global;
const db = globalThis.prismaGlobal ?? prismaClientSingleton();
export default db;
if (process.env.NODE_ENV !== "production") globalThis.prismaGlobal = db;

View File

@ -22,7 +22,7 @@
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-tooltip": "^1.1.8",
"@supabase/ssr": "latest",
"@supabase/ssr": "^0.4.1",
"@supabase/supabase-js": "latest",
"@tabler/icons-react": "^3.30.0",
"@tanstack/react-query": "^5.66.9",
@ -2588,6 +2588,19 @@
"react-dom": "^18.0 || ^19.0 || ^19.0.0-rc"
}
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.34.9",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.9.tgz",
"integrity": "sha512-FwBHNSOjUTQLP4MG7y6rR6qbGw4MFeQnIBrMe161QGaQoBQLqSUEKlHIiVgF3g/mb3lxlxzJOpIBhaP+C+KP2A==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@selderee/plugin-htmlparser2": {
"version": "0.11.0",
"resolved": "https://registry.npmjs.org/@selderee/plugin-htmlparser2/-/plugin-htmlparser2-0.11.0.tgz",
@ -2666,18 +2679,29 @@
}
},
"node_modules/@supabase/ssr": {
"version": "0.5.2",
"resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.5.2.tgz",
"integrity": "sha512-n3plRhr2Bs8Xun1o4S3k1CDv17iH5QY9YcoEvXX3bxV1/5XSasA0mNXYycFmADIdtdE6BG9MRjP5CGIs8qxC8A==",
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.4.1.tgz",
"integrity": "sha512-000i7y4ITXjXU0T1JytZYU33VbUNklX9YN47hCweaLKsTBAEigJJJCeq3G+/IiwEggBt58Vu0KQ3UGXON7OmDQ==",
"license": "MIT",
"dependencies": {
"@types/cookie": "^0.6.0",
"cookie": "^0.7.0"
"cookie": "^0.6.0"
},
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "^4.9.5"
},
"peerDependencies": {
"@supabase/supabase-js": "^2.43.4"
}
},
"node_modules/@supabase/ssr/node_modules/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/@supabase/storage-js": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.7.1.tgz",
@ -2829,12 +2853,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
"integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==",
"license": "MIT"
},
"node_modules/@types/cors": {
"version": "2.8.17",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz",
@ -3429,6 +3447,7 @@
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"

View File

@ -27,7 +27,7 @@
"@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-tooltip": "^1.1.8",
"@supabase/ssr": "latest",
"@supabase/ssr": "^0.4.1",
"@supabase/supabase-js": "latest",
"@tabler/icons-react": "^3.30.0",
"@tanstack/react-query": "^5.66.9",

View File

@ -133,6 +133,7 @@ model profiles {
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
user_id String @unique @db.Uuid
avatar String? @db.VarChar(255)
username String? @unique @db.VarChar(255)
first_name String? @db.VarChar(255)
last_name String? @db.VarChar(255)
bio String? @db.VarChar
@ -141,41 +142,31 @@ model profiles {
users users @relation(fields: [user_id], references: [id])
@@index([user_id])
@@index([username])
}
model users {
id String @id @db.Uuid
email String @unique @db.VarChar(255)
email_verified Boolean @default(false)
first_name String? @db.VarChar(255)
last_name String? @db.VarChar(255)
avatar String? @db.VarChar(255)
id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
role roles @default(user)
created_at DateTime @default(now())
updated_at DateTime
banned_until DateTime?
confirmation_sent_at DateTime?
confirmation_token String? @db.VarChar(255)
deleted_at DateTime?
email_change String? @db.VarChar(255)
email_change_sent_at DateTime?
email_change_token String? @db.VarChar(255)
email_confirmed_at DateTime?
email String @unique @db.VarChar(255)
phone String? @unique @db.VarChar(20)
encrypted_password String? @db.VarChar(255)
is_anonymous Boolean? @default(false)
is_sso_user Boolean? @default(false)
last_sign_in_at DateTime?
phone String? @db.VarChar(20)
phone_confirmed_at DateTime?
raw_app_meta_data Json?
raw_user_meta_data Json?
reauthentication_sent_at DateTime?
reauthentication_token String? @db.VarChar(255)
recovery_sent_at DateTime?
recovery_token String? @db.VarChar(255)
providers Json? @default("[]")
profiles profiles?
invited_at DateTime? @db.Timestamptz(6)
confirmed_at DateTime? @db.Timestamptz(6)
email_confirmed_at DateTime? @db.Timestamptz(6)
recovery_sent_at DateTime? @db.Timestamptz(6)
last_sign_in_at DateTime? @db.Timestamptz(6)
app_metadata Json?
user_metadata Json?
created_at DateTime @default(now()) @db.Timestamptz(6)
updated_at DateTime @default(now()) @db.Timestamptz(6)
banned_until DateTime? @db.Timestamptz(6)
is_anonymous Boolean @default(false)
profile profiles?
@@index([is_anonymous])
@@index([created_at])
@@index([updated_at])
@@index([role])
}

View File

@ -1,53 +1,136 @@
export interface User {
id: string;
email?: string;
phone?: string;
created_at: string;
updated_at: string;
last_sign_in_at?: string;
email_confirmed_at?: string;
phone_confirmed_at?: string;
invited_at?: string;
confirmation_sent_at?: string;
banned_until?: string;
factors?: {
id: string;
factor_type: string;
created_at: string;
updated_at: string;
}[];
raw_user_meta_data?: Record<string, any>;
raw_app_meta_data?: Record<string, any>;
profile?: Profile;
}
import { z } from "zod";
export interface CreateUserParams {
email: string;
password: string;
phone?: string;
user_metadata?: Record<string, any>;
email_confirm?: boolean;
}
import { JsonValue } from "@prisma/client/runtime/library";
import { roles } from "@prisma/client";
import { AuthError } from "@supabase/supabase-js";
export interface UpdateUserParams {
email?: string;
phone?: string;
password?: string;
user_metadata?: Record<string, any>;
}
const timestampSchema = z.date().nullable();
export interface InviteUserParams {
email: string;
user_metadata?: Record<string, any>;
}
const AppMetadataSchema = z
.object({
provider: z.string(),
providers: z.array(z.string()),
})
.nullable();
export interface Profile {
id: string;
user_id: string;
avatar?: string;
first_name?: string;
last_name?: string;
bio: string;
address?: string;
birthdate?: string;
}
const UserMetadataSchema = z.object({
email_verified: z.boolean().optional(),
});
const IdentityDataSchema = z.object({
email: z.string().email(),
email_verified: z.boolean(),
phone_verified: z.boolean(),
sub: z.string(),
});
const IdentitySchema = z.object({
identity_id: z.string(),
id: z.string(),
user_id: z.string(),
identity_data: IdentityDataSchema,
provider: z.string(),
last_sign_in_at: timestampSchema,
created_at: timestampSchema,
updated_at: timestampSchema,
email: z.string().email(),
});
export const UserSchema = z.object({
id: z.string(),
role: z.string(),
email: z.string().email(),
email_confirmed_at: timestampSchema,
invited_at: timestampSchema,
phone: z.string().nullable(),
confirmed_at: timestampSchema,
recovery_sent_at: timestampSchema,
last_sign_in_at: timestampSchema,
created_at: timestampSchema,
updated_at: timestampSchema,
is_anonymous: z.boolean(),
// Keep profile as optional since it's not in the response
profile: z
.object({
id: z.string(),
user_id: z.string(),
avatar: z.string().nullable(),
first_name: z.string().nullable(),
last_name: z.string().nullable(),
bio: z.string().nullable(),
address: z.any().nullable(),
birth_date: timestampSchema,
})
.nullable(),
});
export type User = z.infer<typeof UserSchema>;
export const UserSupabaseSchema = z.object({
id: z.string(),
aud: z.string(),
role: z.string().optional(),
email: z.string().email().optional(),
email_confirmed_at: z.string().optional(),
invited_at: z.string().optional(),
phone: z.string().optional(),
confirmed_at: z.string().optional(),
recovery_sent_at: z.string().optional(),
last_sign_in_at: z.string().optional(),
created_at: z.string(),
updated_at: z.string().optional(),
is_anonymous: z.boolean().optional(),
});
export type UserFromSupabase = z.infer<typeof UserSupabaseSchema>;
// Type helper
export const ProfileSchema = z.object({
id: z.string(),
user_id: z.string(),
avatar: z.string().optional(),
first_name: z.string().optional(),
last_name: z.string().optional(),
bio: z.string(),
address: z.string().optional(),
birthdate: z.string().optional(),
});
export type Profile = z.infer<typeof ProfileSchema>;
export const CreateUserParamsSchema = z.object({
email: z.string().email(),
password: z.string(),
phone: z.string().optional(),
user_metadata: z.record(z.any()).optional(),
email_confirm: z.boolean().optional(),
});
export const UpdateUserParamsSchema = z.object({
email: z.string().email().optional(),
phone: z.string().optional(),
password: z.string().optional(),
});
export const InviteUserParamsSchema = z.object({
email: z.string().email(),
});
export type UserResponse =
| {
data: {
user: User;
};
error: null;
}
| {
data: {
user: null;
};
error: AuthError;
};
export type CreateUserParams = z.infer<typeof CreateUserParamsSchema>;
export type UpdateUserParams = z.infer<typeof UpdateUserParamsSchema>;
export type InviteUserParams = z.infer<typeof InviteUserParamsSchema>;