Membuat use case dan controller untuk users repository

This commit is contained in:
vergiLgood1 2025-03-19 06:08:32 +07:00
parent 009bfc19c3
commit 4750f0055e
36 changed files with 506 additions and 441 deletions

View File

@ -52,11 +52,11 @@ function SubSubItemComponent({ item }: { item: SubSubItem }) {
asChild
className={
isActive
? "font-medium bg-primary/10 before:absolute before:left-0 before:top-1/2 before:-translate-y-1/2 before:bg-primary"
? "bg-primary/10 active text-primary"
: ""
}
>
<a href={formattedUrl} className={isActive ? "text-primary" : ""}>
<a href={formattedUrl}>
<span>{item.title}</span>
</a>
</SidebarMenuButton>
@ -77,13 +77,13 @@ function SubItemComponent({ item }: { item: SubItem }) {
asChild
className={
isActive
? "font-medium bg-primary/10 before:absolute before:left-0 before:top-1/2 before:-translate-y-1/2 before:bg-primary"
? "bg-primary/10 active text-primary"
: ""
}
>
<a href={formattedUrl} className={isActive ? "text-primary" : ""}>
<a href={formattedUrl}>
{item.icon && (
<item.icon className={isActive ? "text-primary" : ""} />
<item.icon />
)}
<span>{item.title}</span>
</a>
@ -99,14 +99,14 @@ function SubItemComponent({ item }: { item: SubItem }) {
<SidebarMenuButton
className={
isActive
? "font-medium bg-primary/10 before:absolute before:left-0 before:top-1/2 before:-translate-y-1/2 before:bg-primary"
? "bg-primary/10 active text-primary"
: ""
}
>
{item.icon && (
<item.icon className={isActive ? "text-primary" : ""} />
<item.icon />
)}
<span className={isActive ? "text-primary" : ""}>{item.title}</span>
<span>{item.title}</span>
<ChevronRight
className={`ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90 ${isActive ? "text-primary" : ""}`}
/>
@ -138,13 +138,13 @@ function RecursiveNavItem({ item, index }: { item: NavItem; index: number }) {
asChild
className={
isActive
? "font-medium bg-primary/10 before:absolute before:left-0 before:top-1/2 before:-translate-y-1/2 before:bg-primary"
? "bg-primary/10 active text-primary"
: ""
}
>
<a href={formattedUrl} className={isActive ? "text-primary" : ""}>
<a href={formattedUrl}>
{item.icon && (
<item.icon className={isActive ? "text-primary" : ""} />
<item.icon />
)}
<span>{item.title}</span>
</a>
@ -166,14 +166,14 @@ function RecursiveNavItem({ item, index }: { item: NavItem; index: number }) {
tooltip={item.title}
className={
isActive
? "font-medium bg-primary/10 before:absolute before:left-0 before:top-1/2 before:-translate-y-1/2 before:bg-primary"
? "bg-primary/10 active text-primary"
: ""
}
>
{item.icon && (
<item.icon className={isActive ? "text-primary" : ""} />
<item.icon />
)}
<span className={isActive ? "text-primary" : ""}>{item.title}</span>
<span>{item.title}</span>
{hasSubItems && (
<ChevronRight
className={`ml-auto transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90 ${isActive ? "text-primary" : ""}`}

View File

@ -25,8 +25,8 @@ function NavItemComponent({ item }: { item: NavItem }) {
const isActive = router.pathname === item.url;
return (
<SidebarMenuItem className={isActive ? "active text-primary" : ""}>
<SidebarMenuButton tooltip={item.title} asChild>
<SidebarMenuItem>
<SidebarMenuButton className={isActive ? "bg-primary/10 text-primary" : ""} tooltip={item.title} asChild>
<a href={item.url}>
<item.icon className="h-4 w-4" />
<span>{item.title}</span>

View File

@ -0,0 +1,9 @@
const WelcomePage = () => {
return (
<>
<h1>Welcome to the dashboard</h1>
</>
);
}
export default WelcomePage;

View File

@ -33,7 +33,7 @@ export function SignInForm({
// setLoading(false);
// };
const { isPending, handleSubmit, error, errors, clearError } = useSignInHandler();
const { isPending, handleSignIn, error, errors, clearError } = useSignInHandler();
return (
<div>
@ -76,7 +76,7 @@ export function SignInForm({
</div>
</div>
<form onSubmit={handleSubmit} className="space-y-4" {...props} noValidate>
<form onSubmit={handleSignIn} className="space-y-4" {...props} noValidate>
<FormField
label="Email"
input={

View File

@ -28,7 +28,7 @@ export function VerifyOtpForm({ className, ...props }: VerifyOtpFormProps) {
const {
register,
control,
handleSubmit,
handleVerifyOtp,
handleOtpChange,
errors,
isPending
@ -44,7 +44,7 @@ export function VerifyOtpForm({ className, ...props }: VerifyOtpFormProps) {
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<form onSubmit={handleVerifyOtp} className="space-y-4">
<input type="hidden" {...register("email")} />
<div className="space-y-6">
<Controller

View File

@ -25,6 +25,7 @@ export async function signIn(formData: FormData) {
// }
return { success: true }
} catch (err) {
if (
err instanceof InputParseError ||

View File

@ -54,7 +54,7 @@ export function useSignInHandler() {
return {
// formData,
// handleChange,
handleSubmit,
handleSignIn: handleSubmit,
error,
isPending: signIn.isPending,
errors: !!error || signIn.error,
@ -123,7 +123,7 @@ export function useVerifyOtpHandler(email: string) {
return {
register,
control,
handleSubmit,
handleVerifyOtp: handleSubmit,
handleOtpChange,
errors: {
...errors,

View File

@ -1,38 +1,34 @@
import { useMutation } from '@tanstack/react-query';
import { queryOptions, useMutation } from '@tanstack/react-query';
import { signIn, signOut, verifyOtp } from './action';
export function useAuthActions() {
// Sign In Mutation
const signInMutation = useMutation({
mutationKey: ["signIn"],
mutationFn: async (formData: FormData) => {
const email = formData.get("email")?.toString()
const response = await signIn(formData);
// If the server action returns an error, treat it as an error for React Query
if (response?.error) {
throw new Error(response.error);
}
return { email };
}
});
const verifyOtpMutation = useMutation({
mutationKey: ["verifyOtp"],
mutationFn: async (formData: FormData) => {
const email = formData.get("email")?.toString()
const token = formData.get("token")?.toString()
const response = await verifyOtp(formData);
// If the server action returns an error, treat it as an error for React Query
if (response?.error) {
throw new Error(response.error);
}
return { email, token };
}
})
const signOutMutation = useMutation({
mutationKey: ["signOut"],
mutationFn: async () => {
const response = await signOut();
@ -40,8 +36,6 @@ export function useAuthActions() {
if (response?.error) {
throw new Error(response.error);
}
return response;
}
})

View File

@ -2,7 +2,7 @@ import { createModule } from '@evyweb/ioctopus';
import { DI_SYMBOLS } from '@/di/types';
import { UsersRepository } from '@/src/infrastructure/repositories/users.repository.impl';
import { UsersRepository } from '@/src/infrastructure/repositories/users.repository';
export function createUsersModule() {

View File

@ -1,23 +1,18 @@
import { createAdminClient } from "@/app/_utils/supabase/admin";
import { createClient } from "@/app/_utils/supabase/client";
import { CreateUser, InviteUser, UpdateUser, User, UserResponse } from "@/src/entities/models/users/users.model";
import db from "@/prisma/db";
import { DatabaseOperationError, NotFoundError } from "@/src/entities/errors/common";
import { AuthenticationError } from "@/src/entities/errors/auth";
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
import { ICrashReporterService } from "@/src/application/services/crash-reporter.service.interface";
import { ITransaction } from "@/src/entities/models/transaction.interface";
export interface IUsersRepository {
listUsers(): Promise<User[]>;
getCurrentUser(): Promise<UserResponse>;
getCurrentUser(): Promise<User>;
getUserById(id: string): Promise<User | undefined>;
getUserByUsername(username: string): Promise<User | undefined>;
getUserByEmail(email: string): Promise<User | undefined>;
createUser(input: CreateUser, tx?: ITransaction): Promise<User | null>;
inviteUser(email: string, tx?: ITransaction): Promise<User | null>;
updateUser(id: string, input: Partial<UpdateUser>, tx?: ITransaction): Promise<UserResponse>;
deleteUser(id: string, tx?: ITransaction): Promise<void>;
createUser(input: CreateUser, tx?: ITransaction): Promise<User>;
inviteUser(email: string, tx?: ITransaction): Promise<User>;
updateUser(id: string, input: Partial<UpdateUser>, tx?: ITransaction): Promise<User>;
deleteUser(id: string, tx?: ITransaction): Promise<User>;
banUser(id: string, ban_duration: string, tx?: ITransaction): Promise<User>;
unbanUser(id: string, tx?: ITransaction): Promise<User>;
}

View File

@ -19,9 +19,7 @@ export const banUserUseCase = (
const bannedUser = await usersRepository.banUser(id, ban_duration)
return {
...bannedUser
}
return bannedUser
}
)
}

View File

@ -14,6 +14,7 @@ export const createUserUseCase = (
) => async (input: CreateUser): Promise<User> => {
return await instrumentationService.startSpan({ name: "createUser Use Case", op: "function" },
async () => {
const existingUser = await usersRepository.getUserByEmail(input.email)
if (existingUser) {
@ -26,13 +27,7 @@ export const createUserUseCase = (
email_confirm: true
})
if (!newUser) {
throw new InputParseError("User not created")
}
return {
...newUser
};
return newUser
}
)
}

View File

@ -1,15 +1,25 @@
import { NotFoundError } from "@/src/entities/errors/common"
import { IUsersRepository } from "../../repositories/users.repository.interface"
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
import { User } from "@/src/entities/models/users/users.model"
export type IDeleteUserUseCase = ReturnType<typeof deleteUserUseCase>
const deleteUserUseCase = (
instrumentationService: IInstrumentationService,
usersRepository: IUsersRepository
) => async (id: string): Promise<void> => {
) => async (id: string): Promise<User> => {
return await instrumentationService.startSpan({ name: "deleteUser Use Case", op: "function" },
async () => {
await usersRepository.deleteUser(id)
const user = await usersRepository.getUserById(id)
if (!user) {
throw new NotFoundError("User not found")
}
const deletedUser = await usersRepository.deleteUser(id)
return deletedUser
}
)
}

View File

@ -1,7 +1,8 @@
import { NotFoundError } from "@/src/entities/errors/common"
import { IUsersRepository } from "../../repositories/users.repository.interface"
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
import { UserResponse } from "@/src/entities/models/users/users.model"
import { User, UserResponse } from "@/src/entities/models/users/users.model"
import { AuthenticationError } from "@/src/entities/errors/auth"
export type IGetCurrentUserUseCase = ReturnType<typeof getCurrentUserUseCase>
@ -9,7 +10,7 @@ export type IGetCurrentUserUseCase = ReturnType<typeof getCurrentUserUseCase>
export const getCurrentUserUseCase = (
instrumentationService: IInstrumentationService,
usersRepository: IUsersRepository
) => async (): Promise<UserResponse> => {
) => async (): Promise<User> => {
return await instrumentationService.startSpan({ name: "getCurrentUser Use Case", op: "function" },
async () => {
@ -19,9 +20,7 @@ export const getCurrentUserUseCase = (
throw new NotFoundError("User not found")
}
return {
...existingUser
}
return existingUser
}
)
}

View File

@ -10,7 +10,9 @@ export const getListUsersUseCase = (
) => async (): Promise<User[]> => {
return await instrumentationService.startSpan({ name: "getListUsers Use Case", op: "function" },
async () => {
return await usersRepository.listUsers()
const users = await usersRepository.listUsers()
return users
}
)
}

View File

@ -12,15 +12,13 @@ const getUserByEmailUseCase = (
return await instrumentationService.startSpan({ name: "getUserByEmail Use Case", op: "function" },
async () => {
const existingUser = await usersRepository.getUserByEmail(email)
const user = await usersRepository.getUserByEmail(email)
if (!existingUser) {
if (!user) {
throw new NotFoundError("User not found")
}
return {
...existingUser
}
return user
}
)
}

View File

@ -14,15 +14,13 @@ export const getUserByIdUseCase = (
return await instrumentationService.startSpan({ name: "getUserById Use Case", op: "function" },
async () => {
const existingUser = await usersRepository.getUserById(id)
const user = await usersRepository.getUserById(id)
if (!existingUser) {
if (!user) {
throw new NotFoundError("User not found")
}
return {
...existingUser
}
return user
}
)
}

View File

@ -12,15 +12,13 @@ const getUserByUsernameUseCase = (
return await instrumentationService.startSpan({ name: "getUserByUsername Use Case", op: "function" },
async () => {
const existingUser = await usersRepository.getUserByUsername(username)
const user = await usersRepository.getUserByUsername(username)
if (!existingUser) {
if (!user) {
throw new NotFoundError("User not found")
}
return {
...existingUser
}
return user
}
)
}

View File

@ -11,24 +11,22 @@ export const inviteUserUseCase = (
instrumentationService: IInstrumentationService,
usersRepository: IUsersRepository,
authenticationService: IAuthenticationService,
) => async (input: { email: string }): Promise<User> => {
) => async (email: string): Promise<User> => {
return await instrumentationService.startSpan({ name: "inviteUser Use Case", op: "function" },
async () => {
const existingUser = await usersRepository.getUserByEmail(input.email)
const existingUser = await usersRepository.getUserByEmail(email)
if (existingUser) {
throw new AuthenticationError("User already exists")
}
const newUser = await usersRepository.inviteUser(input.email)
const invitedUser = await usersRepository.inviteUser(email)
if (!newUser) {
if (!invitedUser) {
throw new AuthenticationError("User not invited")
}
return {
...newUser
}
return invitedUser
}
)
}

View File

@ -0,0 +1,27 @@
import { NotFoundError } from "@/src/entities/errors/common"
import { IUsersRepository } from "../../repositories/users.repository.interface"
import { IInstrumentationService } from "../../services/instrumentation.service.interface"
import { User } from "@/src/entities/models/users/users.model"
export type IUnbanUserUseCase = ReturnType<typeof unbanUserUseCase>
export function unbanUserUseCase(
instrumentationService: IInstrumentationService,
usersRepository: IUsersRepository
) {
return async (id: string): Promise<User> => {
return await instrumentationService.startSpan({ name: "unbanUser Use Case", op: "function" },
async () => {
const existingUser = await usersRepository.getUserById(id)
if (!existingUser) {
throw new NotFoundError("User not found")
}
const unbanUser = await usersRepository.unbanUser(id)
return unbanUser
}
)
}
}

View File

@ -8,7 +8,7 @@ export type IUpdateUserUseCase = ReturnType<typeof updateUserUseCase>
const updateUserUseCase = (
instrumentationService: IInstrumentationService,
usersRepository: IUsersRepository
) => async (id: string, input: UpdateUser): Promise<UserResponse> => {
) => async (id: string, input: UpdateUser): Promise<User> => {
return await instrumentationService.startSpan({ name: "updateUser Use Case", op: "function" },
async () => {
@ -20,9 +20,7 @@ const updateUserUseCase = (
const updatedUser = await usersRepository.updateUser(id, input)
return {
...updatedUser
}
return updatedUser
}
)
}

View File

@ -6,7 +6,7 @@ import { createClient as createServerClient } from "@/app/_utils/supabase/server
import { CreateUser, UpdateUser, User, UserResponse } from "@/src/entities/models/users/users.model";
import { ITransaction } from "@/src/entities/models/transaction.interface";
import db from "@/prisma/db";
import { NotFoundError } from "@/src/entities/errors/common";
import { DatabaseOperationError, NotFoundError } from "@/src/entities/errors/common";
import { AuthenticationError } from "@/src/entities/errors/auth";
export class UsersRepository implements IUsersRepository {
@ -39,6 +39,10 @@ export class UsersRepository implements IUsersRepository {
}
)
if (!users) {
throw new NotFoundError("Users not found");
}
return users;
} catch (err) {
this.crashReporterService.report(err);
@ -71,8 +75,14 @@ export class UsersRepository implements IUsersRepository {
}
)
if (user)
return user;
if (!user) {
throw new NotFoundError("User not found");
}
return {
...user,
id,
};
} catch (err) {
this.crashReporterService.report(err);
throw err;
@ -106,9 +116,13 @@ export class UsersRepository implements IUsersRepository {
}
)
if (user)
return user;
if (!user) {
throw new NotFoundError("User not found");
}
return user;
} catch (err) {
this.crashReporterService.report(err);
throw err;
@ -141,9 +155,11 @@ export class UsersRepository implements IUsersRepository {
}
)
if (user)
return user;
if (!user) {
throw new NotFoundError("User not found");
}
return user;
} catch (err) {
this.crashReporterService.report(err);
throw err;
@ -151,7 +167,7 @@ export class UsersRepository implements IUsersRepository {
})
}
async getCurrentUser(): Promise<UserResponse> {
async getCurrentUser(): Promise<User> {
return await this.instrumentationService.startSpan({
name: "UsersRepository > getCurrentUser",
}, async () => {
@ -160,7 +176,7 @@ export class UsersRepository implements IUsersRepository {
const query = supabase.auth.getUser();
const user = await this.instrumentationService.startSpan({
const { data, error } = await this.instrumentationService.startSpan({
name: "UsersRepository > getCurrentUser > supabase.auth.getUser",
op: "db:query",
attributes: { "system": "supabase.auth" },
@ -170,7 +186,18 @@ export class UsersRepository implements IUsersRepository {
}
)
return user;
if (error) {
throw new AuthenticationError("Failed to get current user");
}
if (!data) {
throw new NotFoundError("User not found");
}
return {
...data,
id: data.user.id,
};
} catch (err) {
this.crashReporterService.report(err);
throw err;
@ -178,18 +205,14 @@ export class UsersRepository implements IUsersRepository {
})
}
async createUser(input: CreateUser, tx?: ITransaction): Promise<User | null> {
async createUser(input: CreateUser, tx?: ITransaction): Promise<User> {
return await this.instrumentationService.startSpan({
name: "UsersRepository > createUser",
}, async () => {
try {
const supabase = this.supabaseAdmin;
const query = supabase.auth.admin.createUser({
email: input.email,
password: input.password,
email_confirm: input.email_confirm,
})
const query = supabase.auth.admin.createUser(input)
const { data: { user } } = await this.instrumentationService.startSpan({
name: "UsersRepository > createUser > supabase.auth.admin.createUser",
@ -201,6 +224,10 @@ export class UsersRepository implements IUsersRepository {
}
)
if (!user) {
throw new DatabaseOperationError("Failed to create user");
}
return user;
} catch (err) {
@ -210,7 +237,7 @@ export class UsersRepository implements IUsersRepository {
})
}
async inviteUser(email: string, tx?: ITransaction): Promise<User | null> {
async inviteUser(email: string, tx?: ITransaction): Promise<User> {
return await this.instrumentationService.startSpan({
name: "UsersRepository > inviteUser",
}, async () => {
@ -229,6 +256,10 @@ export class UsersRepository implements IUsersRepository {
}
)
if (!user) {
throw new DatabaseOperationError("Failed to invite user");
}
return user;
} catch (err) {
this.crashReporterService.report(err);
@ -237,7 +268,7 @@ export class UsersRepository implements IUsersRepository {
})
}
async updateUser(id: string, input: Partial<UpdateUser>, tx?: ITransaction): Promise<UserResponse> {
async updateUser(id: string, input: Partial<UpdateUser>, tx?: ITransaction): Promise<User> {
return await this.instrumentationService.startSpan({
name: "UsersRepository > updateUser",
}, async () => {
@ -267,7 +298,7 @@ export class UsersRepository implements IUsersRepository {
)
if (error) {
throw new AuthenticationError(error.message);
throw new DatabaseOperationError("Failed to update user");
}
const queryGetUser = db.users.findUnique({
@ -332,18 +363,13 @@ export class UsersRepository implements IUsersRepository {
}
)
if (!updatedUser) {
throw new DatabaseOperationError("Failed to update user");
}
return {
data: {
user: {
...data.user,
role: updatedUser.role,
profile: {
user_id: id,
...updatedUser.profile,
},
},
},
error: null,
...updatedUser,
id,
};
} catch (err) {
@ -353,7 +379,7 @@ export class UsersRepository implements IUsersRepository {
})
}
async deleteUser(id: string, tx?: ITransaction): Promise<void> {
async deleteUser(id: string, tx?: ITransaction): Promise<User> {
return await this.instrumentationService.startSpan({
name: "UsersRepository > deleteUser",
}, async () => {
@ -362,7 +388,7 @@ export class UsersRepository implements IUsersRepository {
const query = supabase.auth.admin.deleteUser(id);
await this.instrumentationService.startSpan({
const { data: user, error } = await this.instrumentationService.startSpan({
name: "UsersRepository > deleteUser > supabase.auth.admin.deleteUser",
op: "db:query",
attributes: { "system": "supabase.auth" },
@ -372,7 +398,14 @@ export class UsersRepository implements IUsersRepository {
}
)
return;
if (error) {
throw new DatabaseOperationError("Failed to delete user");
}
return {
...user,
id
};
} catch (err) {
this.crashReporterService.report(err);
@ -392,7 +425,7 @@ export class UsersRepository implements IUsersRepository {
ban_duration: ban_duration ?? "100h",
})
const { data, error } = await this.instrumentationService.startSpan({
const { data: user, error } = await this.instrumentationService.startSpan({
name: "UsersRepository > banUser > supabase.auth.admin.updateUserById",
op: "db:query",
attributes: { "system": "supabase.auth" },
@ -403,10 +436,13 @@ export class UsersRepository implements IUsersRepository {
)
if (error) {
throw new AuthenticationError(error.message);
throw new DatabaseOperationError("Failed to ban user");
}
return data.user;
return {
...user,
id
};
} catch (err) {
this.crashReporterService.report(err);
@ -427,7 +463,7 @@ export class UsersRepository implements IUsersRepository {
ban_duration: "none",
})
const { data, error } = await this.instrumentationService.startSpan({
const { data: user, error } = await this.instrumentationService.startSpan({
name: "UsersRepository > unbanUser > supabase.auth.admin.updateUserById",
op: "db:query",
attributes: { "system": "supabase.auth" },
@ -438,10 +474,13 @@ export class UsersRepository implements IUsersRepository {
)
if (error) {
throw new AuthenticationError(error.message);
throw new DatabaseOperationError("Failed to unban user");
}
return data.user;
return {
...user,
id
};
} catch (err) {
this.crashReporterService.report(err);

View File

@ -1,201 +1,8 @@
// "use client";
import { useRouter } from "next/navigation";
import {
defaultSignInPasswordlessValues,
SignInFormData,
SignInSchema,
} from "@/src/entities/models/auth/sign-in.model";
import { useState, type FormEvent, type ChangeEvent } from "react";
import { toast } from "sonner";
import { z } from "zod";
// import { signIn } from "";
import { useAuthActions } from "./auth-controller";
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { AuthenticationError } from "@/src/entities/errors/auth";
import { ISignInUseCase } from "@/src/application/use-cases/auth/sign-in.use-case";
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
import { InputParseError } from "@/src/entities/errors/common";
type SignInFormErrors = Partial<Record<keyof SignInFormData, string>>;
// export function useSignInForm() {
// const [formData, setFormData] = useState<SignInFormData>(defaultSignInValues);
// const [errors, setErrors] = useState<SignInFormErrors>({});
// const [isSubmitting, setIsSubmitting] = useState(false);
// const [message, setMessage] = useState<string | null>(null);
// const router = useRouter();
// const validateForm = (): boolean => {
// try {
// SignInSchema.parse(formData);
// setErrors({});
// return true;
// } catch (error) {
// if (error instanceof z.ZodError) {
// const formattedErrors: SignInFormErrors = {};
// error.errors.forEach((err) => {
// const path = err.path[0] as keyof SignInFormData;
// formattedErrors[path] = err.message;
// });
// setErrors(formattedErrors);
// }
// return false;
// }
// };
// const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
// const { name, value } = e.target;
// setFormData((prev) => ({
// ...prev,
// [name]: value,
// }));
// };
// const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
// e.preventDefault();
// if (!validateForm()) {
// return;
// }
// setIsSubmitting(true);
// setMessage(null);
// try {
// const result = await signIn(formData);
// if (result.success) {
// setMessage(result.message);
// toast.success(result.message);
// // Handle client-side navigation
// if (result.redirectTo) {
// router.push(result.redirectTo);
// }
// } else {
// setErrors({
// email: result.message || "Sign in failed. Please try again.",
// });
// toast.error(result.message || "Sign in failed. Please try again.");
// }
// } catch (error) {
// console.error("Sign in failed", error);
// setErrors({
// email: "An unexpected error occurred. Please try again.",
// });
// toast.error("An unexpected error occurred. Please try again.");
// } finally {
// setIsSubmitting(false);
// }
// };
// return {
// formData,
// errors,
// isSubmitting,
// message,
// setFormData,
// handleChange,
// handleSubmit,
// };
// }
// export function useSignInController() {
// const [formData, setFormData] = useState<SignInFormData>(defaultSignInValues);
// const [errors, setErrors] = useState<Record<string, string>>({});
// const { signIn } = useAuthActions();
// const form = useForm<SignInFormData>({
// resolver: zodResolver(SignInSchema),
// defaultValues: defaultSignInValues,
// });
// // Handle input changes
// const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
// const { name, value } = e.target;
// setFormData(prev => ({
// ...prev,
// [name]: value
// }));
// // Clear error when user starts typing
// if (errors[name]) {
// setErrors(prev => ({
// ...prev,
// [name]: ''
// }));
// }
// };
// // Direct handleSubmit handler for the form
// const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
// e.preventDefault();
// setErrors({});
// try {
// // Basic email validation before sending to API
// if (!formData.email || !formData.email.includes('@')) {
// setErrors({ email: 'Please enter a valid email address' });
// return;
// }
// await signIn.mutate(formData);
// } catch (error) {
// // This catch block will likely not be used since errors are handled in the mutation
// console.error("Form submission error:", error);
// }
// };
// // Combine form validation errors with API errors
// const formErrors = {
// ...errors,
// // If there's an API error from the mutation, add it to the appropriate field
// ...(signIn.error instanceof AuthenticationError ?
// { email: signIn.error.message } :
// {})
// };
// return {
// formData,
// handleChange,
// handleSubmit,
// isPending: signIn.isPending,
// error: formErrors
// };
// }
// export function useSignInController() {
// const { signIn } = useAuthActions();
// // Gunakan react-hook-form untuk mengelola form state & error handling
// const {
// register,
// handleSubmit,
// formState: { errors },
// } = useForm<SignInFormData>({
// resolver: zodResolver(SignInSchema),
// defaultValues: defaultSignInPasswordlessValues,
// });
// // Handler untuk submit form
// const onSubmit = handleSubmit(async (data) => {
// try {
// signIn.mutate(data);
// } catch (error) {
// console.error("Sign-in submission error:", error);
// }
// });
// return {
// register,
// handleSubmit: onSubmit,
// errors,
// isPending: signIn.isPending,
// };
// }
// Sign In Controller
const signInInputSchema = z.object({
email: z.string().email("Please enter a valid email address"),

View File

@ -3,105 +3,6 @@ import { IVerifyOtpUseCase } from "@/src/application/use-cases/auth/verify-otp.u
import { z } from "zod";
import { InputParseError } from "@/src/entities/errors/common";
// export function useVerifyOtpForm(email: string) {
// const [isSubmitting, setIsSubmitting] = useState(false);
// const [message, setMessage] = useState<string | null>(null);
// const { router } = useNavigations();
// const form = useForm<VerifyOtpFormData>({
// resolver: zodResolver(verifyOtpSchema),
// defaultValues: { ...defaultVerifyOtpValues, email: email },
// });
// const onSubmit = async (data: VerifyOtpFormData) => {
// setIsSubmitting(true);
// setMessage(null);
// try {
// const result = await verifyOtp(data);
// if (result.success) {
// setMessage(result.message);
// // Redirect or update UI state as needed
// toast.success(result.message);
// if (result.redirectTo) {
// router.push(result.redirectTo);
// }
// } else {
// toast.error(result.message);
// form.setError("token", { type: "manual", message: result.message });
// }
// } catch (error) {
// console.error("OTP verification failed", error);
// toast.error("An unexpected error occurred. Please try again.");
// form.setError("token", {
// type: "manual",
// message: "An unexpected error occurred. Please try again.",
// });
// } finally {
// setIsSubmitting(false);
// }
// };
// return {
// form,
// isSubmitting,
// message,
// onSubmit,
// };
// }
// export const useVerifyOtpController = (email: string) => {
// const { verifyOtp } = useAuthActions()
// const {
// control,
// register,
// handleSubmit,
// reset,
// formState: { errors, isSubmitSuccessful },
// } = useForm<VerifyOtpFormData>({
// resolver: zodResolver(verifyOtpSchema),
// defaultValues: { ...defaultVerifyOtpValues, email: email },
// })
// // Clear form after successful submission
// useEffect(() => {
// if (isSubmitSuccessful) {
// reset({ ...defaultVerifyOtpValues, email })
// }
// }, [isSubmitSuccessful, reset, email])
// const onSubmit = handleSubmit(async (data) => {
// try {
// await verifyOtp.mutate(data)
// } catch (error) {
// console.error("OTP verification failed", error)
// }
// })
// // Function to handle auto-submission when all digits are entered
// const handleOtpChange = (value: string, onChange: (value: string) => void) => {
// onChange(value)
// // Auto-submit when all 6 digits are entered
// if (value.length === 6) {
// setTimeout(() => {
// onSubmit()
// }, 300) // Small delay to allow the UI to update
// }
// }
// return {
// control,
// register,
// handleSubmit: onSubmit,
// handleOtpChange,
// errors,
// isPending: verifyOtp.isPending,
// }
// }
// Verify OTP Controller
const verifyOtpInputSchema = z.object({
email: z.string().email("Please enter a valid email address"),

View File

@ -0,0 +1,27 @@
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
import { IBanUserUseCase } from "@/src/application/use-cases/users/ban-user.use-case";
import { InputParseError } from "@/src/entities/errors/common";
import { z } from "zod";
const inputSchema = z.object({
id: z.string(),
ban_duration: z.string()
})
export type IBanUserController = ReturnType<typeof banUserController>
export const banUserController = (
instrumentationService: IInstrumentationService,
banUserUseCase: IBanUserUseCase
) =>
async (input: Partial<z.infer<typeof inputSchema>>) => {
return await instrumentationService.startSpan({ name: "banUser Controller" }, async () => {
const { data, error: inputParseError } = inputSchema.safeParse(input);
if (inputParseError) {
throw new InputParseError("Invalid data", { cause: inputParseError });
}
return await banUserUseCase(data.id, data.ban_duration);
})
}

View File

@ -0,0 +1,33 @@
import { IUsersRepository } from "@/src/application/repositories/users.repository.interface"
import { IAuthenticationService } from "@/src/application/services/authentication.service.interface"
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"
import { ICreateUserUseCase } from "@/src/application/use-cases/users/create-user.use-case"
import { UnauthenticatedError } from "@/src/entities/errors/auth"
import { InputParseError } from "@/src/entities/errors/common"
import { CreateUserSchema } from "@/src/entities/models/users/users.model"
import { z } from "zod"
const inputSchema = CreateUserSchema
export type ICreateUserController = ReturnType<typeof createUserController>
export const createUserController = (
instrumentationService: IInstrumentationService,
createUserUseCase: ICreateUserUseCase,
authenticationService: IAuthenticationService
) => async (input: Partial<z.infer<typeof inputSchema>>) => {
const session = await authenticationService.getSession()
if (!session) {
throw new UnauthenticatedError("Must be logged in to create a todo")
}
const { data, error: inputParseError } = inputSchema.safeParse(input)
if (inputParseError) {
throw new InputParseError("Invalid data", { cause: inputParseError })
}
return await createUserUseCase(data);
}

View File

@ -0,0 +1,25 @@
import { IAuthenticationService } from "@/src/application/services/authentication.service.interface"
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"
import { IDeleteUserUseCase } from "@/src/application/use-cases/users/delete-user.use-case"
import { UnauthenticatedError } from "@/src/entities/errors/auth"
export type IDeleteUserController = ReturnType<typeof deleteUserController>
export const deleteUserController =
(
instrumentationService: IInstrumentationService,
deleteUserUseCase: IDeleteUserUseCase,
authenticationService: IAuthenticationService
) =>
async (id: string) => {
return await instrumentationService.startSpan({ name: "deleteUser Controller" }, async () => {
const session = await authenticationService.getSession()
if (!session) {
throw new UnauthenticatedError("Must be logged in to create a todo")
}
return await deleteUserUseCase(id);
})
}

View File

@ -0,0 +1,18 @@
import { IUsersRepository } from "@/src/application/repositories/users.repository.interface"
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"
import { IGetCurrentUserUseCase } from "@/src/application/use-cases/users/get-current-user.use-case"
import { NotFoundError } from "@/src/entities/errors/common"
export type IGetCurrentUserController = ReturnType<typeof getCurrentUserController>
export const getCurrentUserController =
(
instrumentationService: IInstrumentationService,
getCurrentUserUseCase: IGetCurrentUserUseCase
) =>
async () => {
return await instrumentationService.startSpan({ name: "getCurrentUser Controller" }, async () => {
return await getCurrentUserUseCase();
})
}

View File

@ -0,0 +1,15 @@
import { IUsersRepository } from "@/src/application/repositories/users.repository.interface"
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"
export type IGetListUserController = ReturnType<typeof getListUsersController>
export const getListUsersController =
(
instrumentationService: IInstrumentationService,
usersRepository: IUsersRepository
) =>
async () => {
return await instrumentationService.startSpan({ name: "getListUsers Controller" }, async () => {
return await usersRepository.listUsers();
})
}

View File

@ -0,0 +1,27 @@
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
import { IGetUserByEmailUseCase } from "@/src/application/use-cases/users/get-user-by-email.use-case";
import { InputParseError } from "@/src/entities/errors/common";
import { z } from "zod";
const inputSchema = z.object({
email: z.string().email()
})
export type IGetUserByEmailController = ReturnType<typeof getUserByEmailController>
export const getUserByEmailController =
(
instrumentationService: IInstrumentationService,
getUserByEmailUseCase: IGetUserByEmailUseCase
) =>
async (input: Partial<z.infer<typeof inputSchema>>) => {
return await instrumentationService.startSpan({ name: "getUserByEmail Controller" }, async () => {
const { data, error: inputParseError } = inputSchema.safeParse(input);
if (inputParseError) {
throw new InputParseError("Invalid data", { cause: inputParseError });
}
return await getUserByEmailUseCase(data.email);
})
}

View File

@ -0,0 +1,26 @@
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface"
import { IGetUserByIdUseCase } from "@/src/application/use-cases/users/get-user-by-id.use-case"
import { InputParseError } from "@/src/entities/errors/common"
import { z } from "zod"
const inputSchema = z.object({
id: z.string()
})
export type IGetUserByIdController = ReturnType<typeof getUserByIdController>
export const getUserByIdController = (
instrumentationService: IInstrumentationService,
getUserByIdUseCase: IGetUserByIdUseCase
) =>
async (input: Partial<z.infer<typeof inputSchema>>) => {
return await instrumentationService.startSpan({ name: "getUserById Controller" }, async () => {
const { data, error: inputParseError } = inputSchema.safeParse(input);
if (inputParseError) {
throw new InputParseError("Invalid data", { cause: inputParseError });
}
return await getUserByIdUseCase(data.id);
})
}

View File

@ -0,0 +1,27 @@
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
import { IGetUserByUsernameUseCase } from "@/src/application/use-cases/users/get-user-by-username.use-case";
import { InputParseError } from "@/src/entities/errors/common";
import { z } from "zod";
const inputSchema = z.object({
username: z.string()
})
export type IGetUserByUsernameController = ReturnType<typeof getUserByUsernameController>
export const getUserByUsernameController =
(
instrumentationService: IInstrumentationService,
getUserByUsernameUseCase: IGetUserByUsernameUseCase
) =>
async (input: Partial<z.infer<typeof inputSchema>>) => {
return await instrumentationService.startSpan({ name: "getUserByUsername Controller" }, async () => {
const { data, error: inputParseError } = inputSchema.safeParse(input);
if (inputParseError) {
throw new InputParseError("Invalid data", { cause: inputParseError });
}
return await getUserByUsernameUseCase(data.username);
})
}

View File

@ -0,0 +1,28 @@
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
import { IInviteUserUseCase } from "@/src/application/use-cases/users/invite-user.use-case";
import { InputParseError } from "@/src/entities/errors/common";
import { z } from "zod";
const inputSchema = z.object({
email: z.string().email(),
})
export type IInviteUserController = ReturnType<typeof inviteUserController>
export const inviteUserController =
(
instrumentationService: IInstrumentationService,
inviteUserUseCase: IInviteUserUseCase
) =>
async (input: Partial<z.infer<typeof inputSchema>>) => {
return await instrumentationService.startSpan({ name: "inviteUser Controller" }, async () => {
const { data, error: inputParseError } = inputSchema.safeParse(input);
if (inputParseError) {
throw new InputParseError("Invalid data", { cause: inputParseError });
}
return await inviteUserUseCase(data.email);
})
}

View File

@ -0,0 +1,26 @@
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
import { IUnbanUserUseCase } from "@/src/application/use-cases/users/unban-user.use-case";
import { InputParseError } from "@/src/entities/errors/common";
import { z } from "zod";
const inputSchema = z.object({
id: z.string()
})
export type IUnbanUserController = ReturnType<typeof unbanUserController>
export const unbanUserController = (
instrumentationService: IInstrumentationService,
unbanUserUseCase: IUnbanUserUseCase
) =>
async (input: Partial<z.infer<typeof inputSchema>>) => {
return await instrumentationService.startSpan({ name: "unbanUser Controller" }, async () => {
const { data, error: inputParseError } = inputSchema.safeParse(input);
if (inputParseError) {
throw new InputParseError("Invalid data", { cause: inputParseError });
}
return await unbanUserUseCase(data.id);
})
}

View File

@ -0,0 +1,36 @@
import { IAuthenticationService } from "@/src/application/services/authentication.service.interface";
import { IInstrumentationService } from "@/src/application/services/instrumentation.service.interface";
import { IUpdateUserUseCase } from "@/src/application/use-cases/users/update-user.use-case";
import { UnauthenticatedError } from "@/src/entities/errors/auth";
import { InputParseError } from "@/src/entities/errors/common";
import { UpdateUser, UpdateUserSchema } from "@/src/entities/models/users/users.model";
import { z } from "zod";
const inputSchema = UpdateUserSchema
export type IUpdateUserController = ReturnType<typeof updateUserController>
export const updateUserController =
(
instrumentationService: IInstrumentationService,
updateUserUseCase: IUpdateUserUseCase,
authenticationService: IAuthenticationService
) =>
async (id: string, input: Partial<z.infer<typeof inputSchema>>,) => {
return await instrumentationService.startSpan({ name: "updateUser Controller" }, async () => {
const session = await authenticationService.getSession()
if (!session) {
throw new UnauthenticatedError("Must be logged in to create a todo")
}
const { data, error: inputParseError } = inputSchema.safeParse(input)
if (inputParseError) {
throw new InputParseError("Invalid data", { cause: inputParseError })
}
return await updateUserUseCase(id, data);
})
}

View File

@ -52,6 +52,16 @@ const config = {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
sidebar: {
DEFAULT: "hsl(var(--sidebar-background))",
foreground: "hsl(var(--sidebar-foreground))",
primary: "hsl(var(--sidebar-primary))",
"primary-foreground": "hsl(var(--sidebar-primary-foreground))",
accent: "hsl(var(--sidebar-accent))",
"accent-foreground": "hsl(var(--sidebar-accent-foreground))",
border: "hsl(var(--sidebar-border))",
ring: "hsl(var(--sidebar-ring))",
},
},
borderRadius: {
lg: "var(--radius)",