...
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
import z from "zod";
|
||||
import { generateValidator } from "@/utils/validate";
|
||||
import { LENGTH_MAX_PASSWORD, LENGTH_MAX_USERNAME, LENGTH_MIN_PASSWORD, LENGTH_MIN_USERNAME } from "@/shared/constant";
|
||||
import { LENGTH_MAX_USERNAME, LENGTH_MIN_USERNAME } from "@/shared/constant";
|
||||
|
||||
// Schema for sign up
|
||||
const schemaActionInputSignUp = z.object({
|
||||
email: z.string().regex(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, "Invalid email address"),
|
||||
username: z.string().min(LENGTH_MIN_USERNAME).max(LENGTH_MAX_USERNAME).regex(/^[a-zA-Z0-9_]+$/, "Username can only contain letters, numbers, and underscores"),
|
||||
password: z.string().min(LENGTH_MIN_PASSWORD).max(LENGTH_MAX_PASSWORD),
|
||||
password: z.string().min(8).max(100),
|
||||
redirectTo: z.string().nullish(),
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@ export const validateActionInputSignUp = generateValidator(schemaActionInputSign
|
||||
// Schema for sign in
|
||||
const schemaActionInputSignIn = z.object({
|
||||
identifier: z.string().min(1), // Can be email or username
|
||||
password: z.string().min(LENGTH_MIN_PASSWORD).max(LENGTH_MAX_PASSWORD),
|
||||
password: z.string().min(8).max(100),
|
||||
redirectTo: z.string().nullish(),
|
||||
});
|
||||
|
||||
@@ -25,14 +25,14 @@ export type ActionInputSignIn = z.infer<typeof schemaActionInputSignIn>;
|
||||
|
||||
export const validateActionInputSignIn = generateValidator(schemaActionInputSignIn);
|
||||
|
||||
// Schema for sign out
|
||||
const schemaActionInputSignOut = z.object({
|
||||
redirectTo: z.string().nullish(),
|
||||
// Schema for get user profile by username
|
||||
const schemaActionInputGetUserProfileByUsername = z.object({
|
||||
username: z.string().min(LENGTH_MIN_USERNAME).max(LENGTH_MAX_USERNAME),
|
||||
});
|
||||
|
||||
export type ActionInputSignOut = z.infer<typeof schemaActionInputSignOut>;
|
||||
export type ActionInputGetUserProfileByUsername = z.infer<typeof schemaActionInputGetUserProfileByUsername>;
|
||||
|
||||
export const validateActionInputSignOut = generateValidator(schemaActionInputSignOut);
|
||||
export const validateActionInputGetUserProfileByUsername = generateValidator(schemaActionInputGetUserProfileByUsername);
|
||||
|
||||
// Output types
|
||||
export type ActionOutputAuth = {
|
||||
@@ -45,3 +45,18 @@ export type ActionOutputAuth = {
|
||||
identifier?: string[];
|
||||
};
|
||||
};
|
||||
|
||||
export type ActionOutputUserProfile = {
|
||||
success: boolean;
|
||||
message: string;
|
||||
data?: {
|
||||
id: string;
|
||||
email: string;
|
||||
emailVerified: boolean;
|
||||
username: string | null;
|
||||
displayUsername: string | null;
|
||||
image: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -5,19 +5,23 @@ import { headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
import { ValidateError } from "@/lib/errors";
|
||||
import {
|
||||
ActionInputGetUserProfileByUsername,
|
||||
ActionInputSignIn,
|
||||
ActionInputSignUp,
|
||||
ActionOutputAuth,
|
||||
ActionOutputUserProfile,
|
||||
validateActionInputGetUserProfileByUsername,
|
||||
validateActionInputSignIn,
|
||||
validateActionInputSignUp
|
||||
} from "./auth-action-dto";
|
||||
import {
|
||||
serviceGetUserProfileByUsername,
|
||||
serviceSignIn,
|
||||
serviceSignUp
|
||||
} from "./auth-service";
|
||||
|
||||
// Re-export types for use in components
|
||||
export type { ActionOutputAuth } from "./auth-action-dto";
|
||||
export type { ActionOutputAuth, ActionOutputUserProfile } from "./auth-action-dto";
|
||||
|
||||
/**
|
||||
* Sign up action
|
||||
@@ -144,3 +148,32 @@ export async function signOutAction() {
|
||||
redirect("/auth");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user profile by username
|
||||
* Returns user profile data for display
|
||||
*/
|
||||
export async function actionGetUserProfileByUsername(dto: ActionInputGetUserProfileByUsername): Promise<ActionOutputUserProfile> {
|
||||
try {
|
||||
const userProfile = await serviceGetUserProfileByUsername(dto);
|
||||
|
||||
if (!userProfile) {
|
||||
return {
|
||||
success: false,
|
||||
message: "User not found",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "User profile retrieved successfully",
|
||||
data: userProfile,
|
||||
};
|
||||
} catch (e) {
|
||||
console.error("Get user profile error:", e);
|
||||
return {
|
||||
success: false,
|
||||
message: "Failed to retrieve user profile",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
26
src/modules/auth/auth-repository-dto.ts
Normal file
26
src/modules/auth/auth-repository-dto.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
// Repository layer DTOs for auth module - User profile operations
|
||||
|
||||
// User profile data types
|
||||
export type RepoOutputUserProfile = {
|
||||
id: string;
|
||||
email: string;
|
||||
emailVerified: boolean;
|
||||
username: string | null;
|
||||
displayUsername: string | null;
|
||||
image: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
} | null;
|
||||
|
||||
// Input types
|
||||
export type RepoInputFindUserByUsername = {
|
||||
username: string;
|
||||
};
|
||||
|
||||
export type RepoInputFindUserById = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type RepoInputFindUserByEmail = {
|
||||
email: string;
|
||||
};
|
||||
70
src/modules/auth/auth-repository.ts
Normal file
70
src/modules/auth/auth-repository.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { prisma } from "@/lib/db";
|
||||
import {
|
||||
RepoInputFindUserByEmail,
|
||||
RepoInputFindUserById,
|
||||
RepoInputFindUserByUsername,
|
||||
RepoOutputUserProfile
|
||||
} from "./auth-repository-dto";
|
||||
|
||||
/**
|
||||
* Find user by username
|
||||
*/
|
||||
export async function repoFindUserByUsername(dto: RepoInputFindUserByUsername): Promise<RepoOutputUserProfile> {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { username: dto.username },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
emailVerified: true,
|
||||
username: true,
|
||||
displayUsername: true,
|
||||
image: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
}
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find user by ID
|
||||
*/
|
||||
export async function repoFindUserById(dto: RepoInputFindUserById): Promise<RepoOutputUserProfile> {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { id: dto.id },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
emailVerified: true,
|
||||
username: true,
|
||||
displayUsername: true,
|
||||
image: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
}
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find user by email
|
||||
*/
|
||||
export async function repoFindUserByEmail(dto: RepoInputFindUserByEmail): Promise<RepoOutputUserProfile> {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { email: dto.email },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
emailVerified: true,
|
||||
username: true,
|
||||
displayUsername: true,
|
||||
image: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
}
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
@@ -1,50 +1,39 @@
|
||||
// Service layer DTOs for auth module
|
||||
// Service layer DTOs for auth module - User profile operations
|
||||
|
||||
// Sign up input/output
|
||||
export type ServiceInputSignUp = {
|
||||
email: string;
|
||||
username: string;
|
||||
password: string; // plain text, will be hashed by better-auth
|
||||
password: string;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type ServiceOutputSignUp = {
|
||||
export type ServiceOutputAuth = {
|
||||
success: boolean;
|
||||
userId?: string;
|
||||
email?: string;
|
||||
username?: string;
|
||||
};
|
||||
|
||||
// Sign in input/output
|
||||
export type ServiceInputSignIn = {
|
||||
identifier: string; // email or username
|
||||
identifier: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export type ServiceOutputSignIn = {
|
||||
success: boolean;
|
||||
userId?: string;
|
||||
email?: string;
|
||||
username?: string;
|
||||
sessionToken?: string;
|
||||
// Get user profile input/output
|
||||
export type ServiceInputGetUserProfileByUsername = {
|
||||
username: string;
|
||||
};
|
||||
|
||||
// Sign out input/output
|
||||
export type ServiceInputSignOut = {
|
||||
sessionId?: string;
|
||||
export type ServiceInputGetUserProfileById = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type ServiceOutputSignOut = {
|
||||
success: boolean;
|
||||
};
|
||||
|
||||
// User existence check
|
||||
export type ServiceInputCheckUserExists = {
|
||||
email?: string;
|
||||
username?: string;
|
||||
};
|
||||
|
||||
export type ServiceOutputCheckUserExists = {
|
||||
emailExists: boolean;
|
||||
usernameExists: boolean;
|
||||
};
|
||||
export type ServiceOutputUserProfile = {
|
||||
id: string;
|
||||
email: string;
|
||||
emailVerified: boolean;
|
||||
username: string | null;
|
||||
displayUsername: string | null;
|
||||
image: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
} | null;
|
||||
|
||||
@@ -1,76 +1,94 @@
|
||||
import { auth } from "@/auth";
|
||||
import {
|
||||
ServiceInputSignUp,
|
||||
repoFindUserByUsername,
|
||||
repoFindUserById
|
||||
} from "./auth-repository";
|
||||
import {
|
||||
ServiceInputGetUserProfileByUsername,
|
||||
ServiceInputGetUserProfileById,
|
||||
ServiceInputSignIn,
|
||||
ServiceOutputSignUp,
|
||||
ServiceOutputSignIn
|
||||
ServiceInputSignUp,
|
||||
ServiceOutputAuth,
|
||||
ServiceOutputUserProfile
|
||||
} from "./auth-service-dto";
|
||||
|
||||
/**
|
||||
* Sign up a new user
|
||||
* Calls better-auth's signUp.email with username support
|
||||
* Sign up service
|
||||
*/
|
||||
export async function serviceSignUp(dto: ServiceInputSignUp): Promise<ServiceOutputSignUp> {
|
||||
try {
|
||||
await auth.api.signUpEmail({
|
||||
body: {
|
||||
email: dto.email,
|
||||
password: dto.password,
|
||||
username: dto.username,
|
||||
name: dto.name,
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
export async function serviceSignUp(dto: ServiceInputSignUp): Promise<ServiceOutputAuth> {
|
||||
// Better-auth handles user creation internally
|
||||
const result = await auth.api.signUpEmail({
|
||||
body: {
|
||||
email: dto.email,
|
||||
password: dto.password,
|
||||
name: dto.name,
|
||||
username: dto.username,
|
||||
};
|
||||
} catch (error) {
|
||||
// better-auth handles duplicates and validation errors
|
||||
}
|
||||
});
|
||||
|
||||
if (!result.user) {
|
||||
return {
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign in user
|
||||
* Uses better-auth's signIn.username for username-based authentication
|
||||
* Sign in service
|
||||
*/
|
||||
export async function serviceSignIn(dto: ServiceInputSignIn): Promise<ServiceOutputSignIn> {
|
||||
try {
|
||||
// Determine if identifier is email or username
|
||||
const isEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(dto.identifier);
|
||||
export async function serviceSignIn(dto: ServiceInputSignIn): Promise<ServiceOutputAuth> {
|
||||
// Try to sign in with username first
|
||||
const userResult = await repoFindUserByUsername({ username: dto.identifier });
|
||||
|
||||
let session;
|
||||
if (userResult) {
|
||||
// User found by username, use email signIn with the user's email
|
||||
const result = await auth.api.signInEmail({
|
||||
body: {
|
||||
email: userResult.email,
|
||||
password: dto.password,
|
||||
}
|
||||
});
|
||||
|
||||
if (isEmail) {
|
||||
// Use email sign in
|
||||
session = await auth.api.signInEmail({
|
||||
body: {
|
||||
email: dto.identifier,
|
||||
password: dto.password,
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// Use username sign in (requires username plugin)
|
||||
session = await auth.api.signInUsername({
|
||||
body: {
|
||||
username: dto.identifier,
|
||||
password: dto.password,
|
||||
}
|
||||
});
|
||||
if (result.user) {
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// Try as email
|
||||
const result = await auth.api.signInEmail({
|
||||
body: {
|
||||
email: dto.identifier,
|
||||
password: dto.password,
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
sessionToken: session?.token,
|
||||
};
|
||||
} catch (error) {
|
||||
// better-auth throws on invalid credentials
|
||||
return {
|
||||
success: false,
|
||||
};
|
||||
if (result.user) {
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: false,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user profile by username
|
||||
*/
|
||||
export async function serviceGetUserProfileByUsername(dto: ServiceInputGetUserProfileByUsername): Promise<ServiceOutputUserProfile> {
|
||||
return await repoFindUserByUsername(dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user profile by ID
|
||||
*/
|
||||
export async function serviceGetUserProfileById(dto: ServiceInputGetUserProfileById): Promise<ServiceOutputUserProfile> {
|
||||
return await repoFindUserById(dto);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user