This commit is contained in:
2026-02-03 20:29:55 +08:00
parent d5dde77ee9
commit 9d42a45bb1
18 changed files with 484 additions and 186 deletions

View File

@@ -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;
};
};

View File

@@ -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",
};
}
}

View 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;
};

View 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;
}

View File

@@ -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;

View File

@@ -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);
}