...
This commit is contained in:
47
CLAUDE.md
47
CLAUDE.md
@@ -36,7 +36,7 @@ pnpm prisma studio # 打开 Prisma Studio 查看数据库
|
|||||||
- **PostgreSQL** + **Prisma ORM**(自定义输出目录:`src/generated/prisma`)
|
- **PostgreSQL** + **Prisma ORM**(自定义输出目录:`src/generated/prisma`)
|
||||||
- **better-auth** 身份验证(邮箱/密码 + OAuth)
|
- **better-auth** 身份验证(邮箱/密码 + OAuth)
|
||||||
- **next-intl** 国际化(支持:en-US, zh-CN, ja-JP, ko-KR, de-DE, fr-FR, it-IT, ug-CN)
|
- **next-intl** 国际化(支持:en-US, zh-CN, ja-JP, ko-KR, de-DE, fr-FR, it-IT, ug-CN)
|
||||||
- **edge-tts-universal** 文本转语音
|
- **阿里云千问 TTS** (qwen3-tts-flash) 文本转语音
|
||||||
- **pnpm** 包管理器
|
- **pnpm** 包管理器
|
||||||
|
|
||||||
## 架构设计
|
## 架构设计
|
||||||
@@ -51,10 +51,36 @@ src/app/
|
|||||||
│ └── [locale]/ # 国际化路由
|
│ └── [locale]/ # 国际化路由
|
||||||
├── auth/ # 认证页面(sign-in, sign-up)
|
├── auth/ # 认证页面(sign-in, sign-up)
|
||||||
├── folders/ # 用户学习文件夹管理
|
├── folders/ # 用户学习文件夹管理
|
||||||
├── api/ # API 路由
|
├── users/[username]/# 用户资料页面(Server Component)
|
||||||
└── profile/ # 用户资料页面
|
├── profile/ # 重定向到当前用户的资料页面
|
||||||
|
└── api/ # API 路由
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 后端架构模式
|
||||||
|
|
||||||
|
项目使用 **Action-Service-Repository 三层架构**:
|
||||||
|
|
||||||
|
```
|
||||||
|
src/modules/{module}/
|
||||||
|
├── {module}-action.ts # Server Actions 层(表单处理、重定向)
|
||||||
|
├── {module}-action-dto.ts # Action 层 DTO(Zod 验证)
|
||||||
|
├── {module}-service.ts # Service 层(业务逻辑)
|
||||||
|
├── {module}-service-dto.ts # Service 层 DTO
|
||||||
|
├── {module}-repository.ts # Repository 层(数据库操作)
|
||||||
|
└── {module}-repository-dto.ts # Repository 层 DTO
|
||||||
|
```
|
||||||
|
|
||||||
|
各层职责:
|
||||||
|
- **Action 层**:处理表单数据、验证输入、调用 service 层、处理重定向和错误响应
|
||||||
|
- **Service 层**:实现业务逻辑、调用 better-auth API、协调多个 repository 操作
|
||||||
|
- **Repository 层**:直接使用 Prisma 进行数据库查询和操作
|
||||||
|
|
||||||
|
现有模块:
|
||||||
|
- `auth` - 认证和用户管理(支持用户名/邮箱登录)
|
||||||
|
- `folder` - 学习文件夹管理
|
||||||
|
- `dictionary` - 词典查询
|
||||||
|
- `translator` - 翻译服务
|
||||||
|
|
||||||
### 数据库 Schema
|
### 数据库 Schema
|
||||||
|
|
||||||
核心模型(见 [prisma/schema.prisma](prisma/schema.prisma)):
|
核心模型(见 [prisma/schema.prisma](prisma/schema.prisma)):
|
||||||
@@ -81,10 +107,13 @@ src/app/
|
|||||||
需要在 `.env.local` 中配置:
|
需要在 `.env.local` 中配置:
|
||||||
|
|
||||||
```env
|
```env
|
||||||
# LLM 集成
|
# LLM 集成(智谱 AI 用于翻译和 IPA 生成)
|
||||||
ZHIPU_API_KEY=your-api-key
|
ZHIPU_API_KEY=your-api-key
|
||||||
ZHIPU_MODEL_NAME=your-model-name
|
ZHIPU_MODEL_NAME=your-model-name
|
||||||
|
|
||||||
|
# 阿里云千问 TTS(文本转语音)
|
||||||
|
DASHSCORE_API_KEY=your-dashscore-api-key
|
||||||
|
|
||||||
# 认证
|
# 认证
|
||||||
BETTER_AUTH_SECRET=your-secret
|
BETTER_AUTH_SECRET=your-secret
|
||||||
BETTER_AUTH_URL=http://localhost:3000
|
BETTER_AUTH_URL=http://localhost:3000
|
||||||
@@ -93,9 +122,6 @@ GITHUB_CLIENT_SECRET=your-client-secret
|
|||||||
|
|
||||||
# 数据库
|
# 数据库
|
||||||
DATABASE_URL=postgresql://username:password@localhost:5432/database_name
|
DATABASE_URL=postgresql://username:password@localhost:5432/database_name
|
||||||
|
|
||||||
// DashScore
|
|
||||||
DASHSCORE_API_KEY=
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 重要配置细节
|
## 重要配置细节
|
||||||
@@ -108,13 +134,15 @@ DASHSCORE_API_KEY=
|
|||||||
|
|
||||||
## 代码组织
|
## 代码组织
|
||||||
|
|
||||||
- `src/lib/actions/`: 数据库变更的 Server Actions
|
- `src/modules/`: 业务模块(auth, folder, dictionary, translator)
|
||||||
|
- `src/lib/actions/`: 数据库变更的 Server Actions(旧架构,正在迁移到 modules)
|
||||||
- `src/lib/server/`: 服务端工具(AI 集成、认证、翻译器)
|
- `src/lib/server/`: 服务端工具(AI 集成、认证、翻译器)
|
||||||
- `src/lib/browser/`: 客户端工具
|
- `src/lib/browser/`: 客户端工具
|
||||||
- `src/hooks/`: 自定义 React hooks(认证 hooks、会话管理)
|
- `src/hooks/`: 自定义 React hooks(认证 hooks、会话管理)
|
||||||
- `src/i18n/`: 国际化配置
|
- `src/i18n/`: 国际化配置
|
||||||
- `messages/`: 各支持语言的翻译文件
|
- `messages/`: 各支持语言的翻译文件
|
||||||
- `src/components/`: 可复用的 UI 组件(buttons, cards 等)
|
- `src/components/`: 可复用的 UI 组件(buttons, cards 等)
|
||||||
|
- `src/shared/`: 共享常量和类型定义
|
||||||
|
|
||||||
## 开发注意事项
|
## 开发注意事项
|
||||||
|
|
||||||
@@ -122,4 +150,7 @@ DASHSCORE_API_KEY=
|
|||||||
- schema 变更后,先运行 `pnpm prisma generate` 再运行 `pnpm prisma db push`
|
- schema 变更后,先运行 `pnpm prisma generate` 再运行 `pnpm prisma db push`
|
||||||
- 应用使用 TypeScript 严格模式 - 确保类型安全
|
- 应用使用 TypeScript 严格模式 - 确保类型安全
|
||||||
- 所有面向用户的文本都需要国际化
|
- 所有面向用户的文本都需要国际化
|
||||||
|
- **优先使用 Server Components**,只在需要交互时使用 Client Components
|
||||||
|
- **新功能应遵循 action-service-repository 架构**
|
||||||
- Better-auth 处理会话管理 - 使用 authClient 适配器进行认证操作
|
- Better-auth 处理会话管理 - 使用 authClient 适配器进行认证操作
|
||||||
|
- 使用 better-auth username 插件支持用户名登录
|
||||||
|
|||||||
45
README.md
45
README.md
@@ -9,7 +9,9 @@
|
|||||||
- **SRT字幕播放器** - 结合视频字幕学习,支持多种字幕格式
|
- **SRT字幕播放器** - 结合视频字幕学习,支持多种字幕格式
|
||||||
- **字母学习模块** - 针对初学者的字母和发音基础学习
|
- **字母学习模块** - 针对初学者的字母和发音基础学习
|
||||||
- **记忆强化工具** - 通过科学记忆法巩固学习内容
|
- **记忆强化工具** - 通过科学记忆法巩固学习内容
|
||||||
|
- **词典查询** - 查询单词和短语,提供详细释义和例句
|
||||||
- **个人学习空间** - 用户可以创建、管理和组织自己的学习资料
|
- **个人学习空间** - 用户可以创建、管理和组织自己的学习资料
|
||||||
|
- **用户资料系统** - 支持用户名登录、个人资料页面展示
|
||||||
|
|
||||||
## 🛠 技术栈
|
## 🛠 技术栈
|
||||||
|
|
||||||
@@ -26,7 +28,7 @@
|
|||||||
|
|
||||||
### 国际化与辅助功能
|
### 国际化与辅助功能
|
||||||
- **next-intl** - 国际化解决方案
|
- **next-intl** - 国际化解决方案
|
||||||
- **qwen3-tts-flash** - 通义千问语音合成
|
- **阿里云千问 TTS** - qwen3-tts-flash 语音合成
|
||||||
|
|
||||||
### 开发工具
|
### 开发工具
|
||||||
- **ESLint** - 代码质量检查
|
- **ESLint** - 代码质量检查
|
||||||
@@ -38,8 +40,16 @@
|
|||||||
src/
|
src/
|
||||||
├── app/ # Next.js App Router 路由
|
├── app/ # Next.js App Router 路由
|
||||||
│ ├── (features)/ # 功能模块路由
|
│ ├── (features)/ # 功能模块路由
|
||||||
│ ├── api/ # API 路由
|
│ ├── auth/ # 认证相关页面
|
||||||
│ └── auth/ # 认证相关页面
|
│ ├── profile/ # 用户资料重定向
|
||||||
|
│ ├── users/[username]/ # 用户资料页面
|
||||||
|
│ ├── folders/ # 文件夹管理
|
||||||
|
│ └── api/ # API 路由
|
||||||
|
├── modules/ # 业务模块(action-service-repository 架构)
|
||||||
|
│ ├── auth/ # 认证模块
|
||||||
|
│ ├── folder/ # 文件夹模块
|
||||||
|
│ ├── dictionary/ # 词典模块
|
||||||
|
│ └── translator/ # 翻译模块
|
||||||
├── components/ # React 组件
|
├── components/ # React 组件
|
||||||
│ ├── buttons/ # 按钮组件
|
│ ├── buttons/ # 按钮组件
|
||||||
│ ├── cards/ # 卡片组件
|
│ ├── cards/ # 卡片组件
|
||||||
@@ -50,6 +60,7 @@ src/
|
|||||||
│ └── server/ # 服务器端工具
|
│ └── server/ # 服务器端工具
|
||||||
├── hooks/ # 自定义 React Hooks
|
├── hooks/ # 自定义 React Hooks
|
||||||
├── i18n/ # 国际化配置
|
├── i18n/ # 国际化配置
|
||||||
|
├── shared/ # 共享常量和类型
|
||||||
└── config/ # 应用配置
|
└── config/ # 应用配置
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -57,7 +68,7 @@ src/
|
|||||||
|
|
||||||
### 环境要求
|
### 环境要求
|
||||||
|
|
||||||
- Node.js 24
|
- Node.js 23
|
||||||
- PostgreSQL 数据库
|
- PostgreSQL 数据库
|
||||||
- pnpm (推荐) 或 npm
|
- pnpm (推荐) 或 npm
|
||||||
|
|
||||||
@@ -85,17 +96,20 @@ cp .env.example .env.local
|
|||||||
然后编辑 `.env.local` 文件,配置所有必要的环境变量:
|
然后编辑 `.env.local` 文件,配置所有必要的环境变量:
|
||||||
|
|
||||||
```env
|
```env
|
||||||
// LLM
|
# LLM 集成(智谱 AI 用于翻译和 IPA 生成)
|
||||||
ZHIPU_API_KEY=your-zhipu-api-key
|
ZHIPU_API_KEY=your-zhipu-api-key
|
||||||
ZHIPU_MODEL_NAME=your-zhipu-model-name
|
ZHIPU_MODEL_NAME=your-zhipu-model-name
|
||||||
|
|
||||||
// Auth
|
# 阿里云千问 TTS(文本转语音)
|
||||||
|
DASHSCORE_API_KEY=your-dashscore-api-key
|
||||||
|
|
||||||
|
# 认证
|
||||||
BETTER_AUTH_SECRET=your-better-auth-secret
|
BETTER_AUTH_SECRET=your-better-auth-secret
|
||||||
BETTER_AUTH_URL=http://localhost:3000
|
BETTER_AUTH_URL=http://localhost:3000
|
||||||
GITHUB_CLIENT_ID=your-github-client-id
|
GITHUB_CLIENT_ID=your-github-client-id
|
||||||
GITHUB_CLIENT_SECRET=your-github-client-secret
|
GITHUB_CLIENT_SECRET=your-github-client-secret
|
||||||
|
|
||||||
// Database
|
# 数据库
|
||||||
DATABASE_URL=postgresql://username:password@localhost:5432/database_name
|
DATABASE_URL=postgresql://username:password@localhost:5432/database_name
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -118,14 +132,27 @@ pnpm run dev
|
|||||||
|
|
||||||
### 认证系统
|
### 认证系统
|
||||||
|
|
||||||
应用使用 better-auth 提供安全的用户认证系统,支持邮箱/密码登录和第三方登录。
|
应用使用 better-auth 提供安全的用户认证系统,支持:
|
||||||
|
- 邮箱/密码登录和注册
|
||||||
|
- **用户名登录**(可通过用户名或邮箱登录)
|
||||||
|
- GitHub OAuth 第三方登录
|
||||||
|
- 邮箱验证功能
|
||||||
|
|
||||||
|
### 后端架构
|
||||||
|
|
||||||
|
项目采用 **Action-Service-Repository 三层架构**:
|
||||||
|
- **Action 层**:处理 Server Actions、表单验证、重定向
|
||||||
|
- **Service 层**:业务逻辑、better-auth 集成
|
||||||
|
- **Repository 层**:Prisma 数据库操作
|
||||||
|
|
||||||
### 数据模型
|
### 数据模型
|
||||||
|
|
||||||
核心数据模型包括:
|
核心数据模型包括:
|
||||||
- **User** - 用户信息
|
- **User** - 用户信息(支持用户名、邮箱、头像)
|
||||||
- **Folder** - 学习资料文件夹
|
- **Folder** - 学习资料文件夹
|
||||||
- **Pair** - 语言对(翻译对、词汇对等)
|
- **Pair** - 语言对(翻译对、词汇对等)
|
||||||
|
- **Session/Account** - 认证会话追踪
|
||||||
|
- **Verification** - 邮箱验证系统
|
||||||
|
|
||||||
详细模型定义请参考 [prisma/schema.prisma](./prisma/schema.prisma)
|
详细模型定义请参考 [prisma/schema.prisma](./prisma/schema.prisma)
|
||||||
|
|
||||||
|
|||||||
@@ -231,5 +231,17 @@
|
|||||||
"pleaseCreateFolder": "Please create a folder first",
|
"pleaseCreateFolder": "Please create a folder first",
|
||||||
"savedToFolder": "Saved to folder: {folderName}",
|
"savedToFolder": "Saved to folder: {folderName}",
|
||||||
"saveFailed": "Save failed, please try again later"
|
"saveFailed": "Save failed, please try again later"
|
||||||
|
},
|
||||||
|
"user_profile": {
|
||||||
|
"anonymous": "Anonymous",
|
||||||
|
"email": "Email",
|
||||||
|
"verified": "Verified",
|
||||||
|
"unverified": "Unverified",
|
||||||
|
"accountInfo": "Account Information",
|
||||||
|
"userId": "User ID",
|
||||||
|
"username": "Username",
|
||||||
|
"displayName": "Display Name",
|
||||||
|
"notSet": "Not Set",
|
||||||
|
"memberSince": "Member Since"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -231,5 +231,17 @@
|
|||||||
"pleaseCreateFolder": "请先创建文件夹",
|
"pleaseCreateFolder": "请先创建文件夹",
|
||||||
"savedToFolder": "已保存到文件夹:{folderName}",
|
"savedToFolder": "已保存到文件夹:{folderName}",
|
||||||
"saveFailed": "保存失败,请稍后重试"
|
"saveFailed": "保存失败,请稍后重试"
|
||||||
|
},
|
||||||
|
"user_profile": {
|
||||||
|
"anonymous": "匿名",
|
||||||
|
"email": "邮箱",
|
||||||
|
"verified": "已验证",
|
||||||
|
"unverified": "未验证",
|
||||||
|
"accountInfo": "账户信息",
|
||||||
|
"userId": "用户ID",
|
||||||
|
"username": "用户名",
|
||||||
|
"displayName": "显示名称",
|
||||||
|
"notSet": "未设置",
|
||||||
|
"memberSince": "注册时间"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `name` on the `user` table. All the data in the column will be lost.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "user" DROP COLUMN "name";
|
||||||
@@ -10,7 +10,6 @@ datasource db {
|
|||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id
|
id String @id
|
||||||
name String
|
|
||||||
email String
|
email String
|
||||||
emailVerified Boolean @default(false)
|
emailVerified Boolean @default(false)
|
||||||
image String?
|
image String?
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { SubtitleEntry } from "../types/subtitle";
|
import { SubtitleEntry } from "../types/subtitle";
|
||||||
import { logger } from "@/lib/logger";
|
|
||||||
|
|
||||||
export function parseSrt(data: string): SubtitleEntry[] {
|
export function parseSrt(data: string): SubtitleEntry[] {
|
||||||
const lines = data.split(/\r?\n/);
|
const lines = data.split(/\r?\n/);
|
||||||
@@ -94,7 +93,7 @@ export async function loadSubtitle(url: string): Promise<SubtitleEntry[]> {
|
|||||||
const data = await response.text();
|
const data = await response.text();
|
||||||
return parseSrt(data);
|
return parseSrt(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('加载字幕失败', error);
|
console.error('加载字幕失败', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,7 +15,6 @@ import { SaveList } from "./SaveList";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { getLocalStorageOperator } from "@/lib/browser/localStorageOperators";
|
import { getLocalStorageOperator } from "@/lib/browser/localStorageOperators";
|
||||||
import { genIPA, genLanguage } from "@/modules/translator/translator-action";
|
import { genIPA, genLanguage } from "@/modules/translator/translator-action";
|
||||||
import { logger } from "@/lib/logger";
|
|
||||||
import { PageLayout } from "@/components/ui/PageLayout";
|
import { PageLayout } from "@/components/ui/PageLayout";
|
||||||
import { getTTSUrl, TTS_SUPPORTED_LANGUAGES } from "@/lib/bigmodel/tts";
|
import { getTTSUrl, TTS_SUPPORTED_LANGUAGES } from "@/lib/bigmodel/tts";
|
||||||
|
|
||||||
@@ -75,7 +74,7 @@ export default function TextSpeakerPage() {
|
|||||||
setIPA(data.ipa);
|
setIPA(data.ipa);
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
logger.error("生成 IPA 失败", e);
|
console.error("生成 IPA 失败", e);
|
||||||
setIPA("");
|
setIPA("");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -120,7 +119,7 @@ export default function TextSpeakerPage() {
|
|||||||
load(objurlRef.current);
|
load(objurlRef.current);
|
||||||
play();
|
play();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error("播放音频失败", e);
|
console.error("播放音频失败", e);
|
||||||
setPause(true);
|
setPause(true);
|
||||||
setLanguage(null);
|
setLanguage(null);
|
||||||
setProcessing(false);
|
setProcessing(false);
|
||||||
@@ -212,7 +211,7 @@ export default function TextSpeakerPage() {
|
|||||||
}
|
}
|
||||||
setIntoLocalStorage(save);
|
setIntoLocalStorage(save);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error("保存到本地存储失败", e);
|
console.error("保存到本地存储失败", e);
|
||||||
setLanguage(null);
|
setLanguage(null);
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
|
|||||||
@@ -1,49 +1,16 @@
|
|||||||
import Image from "next/image";
|
|
||||||
import { PageLayout } from "@/components/ui/PageLayout";
|
|
||||||
import { PageHeader } from "@/components/ui/PageHeader";
|
|
||||||
import { auth } from "@/auth";
|
import { auth } from "@/auth";
|
||||||
import { getTranslations } from "next-intl/server";
|
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { LogoutButton } from "./LogoutButton";
|
|
||||||
|
|
||||||
export default async function ProfilePage() {
|
export default async function ProfilePage() {
|
||||||
const t = await getTranslations("profile");
|
|
||||||
|
|
||||||
const session = await auth.api.getSession({ headers: await headers() });
|
const session = await auth.api.getSession({ headers: await headers() });
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
redirect("/auth?redirect=/profile");
|
redirect("/auth?redirect=/profile");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
// 已登录,跳转到用户资料页面
|
||||||
<PageLayout>
|
// 优先使用 username,如果没有则使用 email
|
||||||
<PageHeader title={t("myProfile")} />
|
const username = (session.user.username as string) || (session.user.email as string);
|
||||||
|
redirect(`/users/${username}`);
|
||||||
{/* 用户信息区域 */}
|
|
||||||
<div className="flex flex-col items-center gap-4">
|
|
||||||
{/* 用户头像 */}
|
|
||||||
{session.user.image && (
|
|
||||||
<Image
|
|
||||||
width={80}
|
|
||||||
height={80}
|
|
||||||
alt="User Avatar"
|
|
||||||
src={session.user.image as string}
|
|
||||||
className="rounded-full"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 用户名和邮箱 */}
|
|
||||||
<div className="text-center">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-800">
|
|
||||||
{session.user.name}
|
|
||||||
</h2>
|
|
||||||
<p className="text-gray-600">{t("email", { email: session.user.email })}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 登出按钮 */}
|
|
||||||
<LogoutButton />
|
|
||||||
</div>
|
|
||||||
</PageLayout>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
interface UserPageProps {
|
|
||||||
params: Promise<{ username: string}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function UserPage({params}: UserPageProps) {
|
|
||||||
const {username} = await params;
|
|
||||||
|
|
||||||
}
|
|
||||||
126
src/app/users/[username]/page.tsx
Normal file
126
src/app/users/[username]/page.tsx
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
import { Container } from "@/components/ui/Container";
|
||||||
|
import { actionGetUserProfileByUsername } from "@/modules/auth/auth-action";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import { getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
|
interface UserPageProps {
|
||||||
|
params: Promise<{ username: string; }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function UserPage({ params }: UserPageProps) {
|
||||||
|
const { username } = await params;
|
||||||
|
const t = await getTranslations("user_profile");
|
||||||
|
|
||||||
|
// Get user profile
|
||||||
|
const result = await actionGetUserProfileByUsername({ username });
|
||||||
|
|
||||||
|
if (!result.success || !result.data) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = result.data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-[calc(100vh-64px)] bg-gray-50 py-8">
|
||||||
|
<Container className="max-w-3xl mx-auto">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||||
|
<div className="flex items-center space-x-6">
|
||||||
|
{/* Avatar */}
|
||||||
|
{user.image ? (
|
||||||
|
<div className="relative w-24 h-24 rounded-full border-4 border-[#35786f] overflow-hidden">
|
||||||
|
<Image
|
||||||
|
src={user.image}
|
||||||
|
alt={user.displayUsername || user.username || user.email}
|
||||||
|
fill
|
||||||
|
className="object-cover"
|
||||||
|
unoptimized
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="w-24 h-24 rounded-full bg-[#35786f] border-4 border-[#35786f] flex items-center justify-center">
|
||||||
|
<span className="text-3xl font-bold text-white">
|
||||||
|
{(user.displayUsername || user.username || user.email)[0].toUpperCase()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* User Info */}
|
||||||
|
<div className="flex-1">
|
||||||
|
<h1 className="text-3xl font-bold text-gray-800 mb-2">
|
||||||
|
{user.displayUsername || user.username || t("anonymous")}
|
||||||
|
</h1>
|
||||||
|
{user.username && (
|
||||||
|
<p className="text-gray-600 text-sm mb-1">
|
||||||
|
@{user.username}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center space-x-4 text-sm">
|
||||||
|
<span className="text-gray-500">
|
||||||
|
Joined: {new Date(user.createdAt).toLocaleDateString()}
|
||||||
|
</span>
|
||||||
|
{user.emailVerified && (
|
||||||
|
<span className="flex items-center text-green-600">
|
||||||
|
<svg className="w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 00016zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.293 12.293a1 1 0 101.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
Verified
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Email Section */}
|
||||||
|
<div className="bg-white rounded-lg shadow-md p-6 mb-6">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-800 mb-4">{t("email")}</h2>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center space-x-3">
|
||||||
|
<span className="text-gray-700">{user.email}</span>
|
||||||
|
</div>
|
||||||
|
{user.emailVerified ? (
|
||||||
|
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-green-100 text-green-800">
|
||||||
|
✓ {t("verified")}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-yellow-100 text-yellow-800">
|
||||||
|
{t("unverified")}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Account Info */}
|
||||||
|
<div className="bg-white rounded-lg shadow-md p-6">
|
||||||
|
<h2 className="text-xl font-semibold text-gray-800 mb-4">{t("accountInfo")}</h2>
|
||||||
|
<dl className="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-gray-500">{t("userId")}</dt>
|
||||||
|
<dd className="mt-1 text-sm text-gray-900 font-mono break-all">{user.id}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-gray-500">{t("username")}</dt>
|
||||||
|
<dd className="mt-1 text-sm text-gray-900">
|
||||||
|
{user.username || <span className="text-gray-400">{t("notSet")}</span>}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-gray-500">{t("displayName")}</dt>
|
||||||
|
<dd className="mt-1 text-sm text-gray-900">
|
||||||
|
{user.displayUsername || <span className="text-gray-400">{t("notSet")}</span>}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt className="text-sm font-medium text-gray-500">{t("memberSince")}</dt>
|
||||||
|
<dd className="mt-1 text-sm text-gray-900">
|
||||||
|
{new Date(user.createdAt).toLocaleDateString()}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
class Logger {
|
|
||||||
error(message: string, error?: unknown): void {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
console.error(`[ERROR] ${message}:`, error.message, error.stack);
|
|
||||||
} else {
|
|
||||||
console.error(`[ERROR] ${message}:`, error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
warn(message: string, ...args: unknown[]): void {
|
|
||||||
console.warn(`[WARN] ${message}`, ...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
info(message: string, ...args: unknown[]): void {
|
|
||||||
console.info(`[INFO] ${message}`, ...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
debug(message: string, ...args: unknown[]): void {
|
|
||||||
if (process.env.NODE_ENV === "development") {
|
|
||||||
console.debug(`[DEBUG] ${message}`, ...args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const logger = new Logger();
|
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { generateValidator } from "@/utils/validate";
|
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
|
// Schema for sign up
|
||||||
const schemaActionInputSignUp = z.object({
|
const schemaActionInputSignUp = z.object({
|
||||||
email: z.string().regex(/^[^\s@]+@[^\s@]+\.[^\s@]+$/, "Invalid email address"),
|
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"),
|
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(),
|
redirectTo: z.string().nullish(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ export const validateActionInputSignUp = generateValidator(schemaActionInputSign
|
|||||||
// Schema for sign in
|
// Schema for sign in
|
||||||
const schemaActionInputSignIn = z.object({
|
const schemaActionInputSignIn = z.object({
|
||||||
identifier: z.string().min(1), // Can be email or username
|
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(),
|
redirectTo: z.string().nullish(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -25,14 +25,14 @@ export type ActionInputSignIn = z.infer<typeof schemaActionInputSignIn>;
|
|||||||
|
|
||||||
export const validateActionInputSignIn = generateValidator(schemaActionInputSignIn);
|
export const validateActionInputSignIn = generateValidator(schemaActionInputSignIn);
|
||||||
|
|
||||||
// Schema for sign out
|
// Schema for get user profile by username
|
||||||
const schemaActionInputSignOut = z.object({
|
const schemaActionInputGetUserProfileByUsername = z.object({
|
||||||
redirectTo: z.string().nullish(),
|
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
|
// Output types
|
||||||
export type ActionOutputAuth = {
|
export type ActionOutputAuth = {
|
||||||
@@ -45,3 +45,18 @@ export type ActionOutputAuth = {
|
|||||||
identifier?: string[];
|
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 { redirect } from "next/navigation";
|
||||||
import { ValidateError } from "@/lib/errors";
|
import { ValidateError } from "@/lib/errors";
|
||||||
import {
|
import {
|
||||||
|
ActionInputGetUserProfileByUsername,
|
||||||
ActionInputSignIn,
|
ActionInputSignIn,
|
||||||
ActionInputSignUp,
|
ActionInputSignUp,
|
||||||
ActionOutputAuth,
|
ActionOutputAuth,
|
||||||
|
ActionOutputUserProfile,
|
||||||
|
validateActionInputGetUserProfileByUsername,
|
||||||
validateActionInputSignIn,
|
validateActionInputSignIn,
|
||||||
validateActionInputSignUp
|
validateActionInputSignUp
|
||||||
} from "./auth-action-dto";
|
} from "./auth-action-dto";
|
||||||
import {
|
import {
|
||||||
|
serviceGetUserProfileByUsername,
|
||||||
serviceSignIn,
|
serviceSignIn,
|
||||||
serviceSignUp
|
serviceSignUp
|
||||||
} from "./auth-service";
|
} from "./auth-service";
|
||||||
|
|
||||||
// Re-export types for use in components
|
// 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
|
* Sign up action
|
||||||
@@ -144,3 +148,32 @@ export async function signOutAction() {
|
|||||||
redirect("/auth");
|
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
|
// Sign up input/output
|
||||||
export type ServiceInputSignUp = {
|
export type ServiceInputSignUp = {
|
||||||
email: string;
|
email: string;
|
||||||
username: string;
|
username: string;
|
||||||
password: string; // plain text, will be hashed by better-auth
|
password: string;
|
||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ServiceOutputSignUp = {
|
export type ServiceOutputAuth = {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
userId?: string;
|
|
||||||
email?: string;
|
|
||||||
username?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sign in input/output
|
// Sign in input/output
|
||||||
export type ServiceInputSignIn = {
|
export type ServiceInputSignIn = {
|
||||||
identifier: string; // email or username
|
identifier: string;
|
||||||
password: string;
|
password: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ServiceOutputSignIn = {
|
// Get user profile input/output
|
||||||
success: boolean;
|
export type ServiceInputGetUserProfileByUsername = {
|
||||||
userId?: string;
|
username: string;
|
||||||
email?: string;
|
|
||||||
username?: string;
|
|
||||||
sessionToken?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sign out input/output
|
export type ServiceInputGetUserProfileById = {
|
||||||
export type ServiceInputSignOut = {
|
id: string;
|
||||||
sessionId?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ServiceOutputSignOut = {
|
export type ServiceOutputUserProfile = {
|
||||||
success: boolean;
|
id: string;
|
||||||
};
|
email: string;
|
||||||
|
emailVerified: boolean;
|
||||||
// User existence check
|
username: string | null;
|
||||||
export type ServiceInputCheckUserExists = {
|
displayUsername: string | null;
|
||||||
email?: string;
|
image: string | null;
|
||||||
username?: string;
|
createdAt: Date;
|
||||||
};
|
updatedAt: Date;
|
||||||
|
} | null;
|
||||||
export type ServiceOutputCheckUserExists = {
|
|
||||||
emailExists: boolean;
|
|
||||||
usernameExists: boolean;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,76 +1,94 @@
|
|||||||
import { auth } from "@/auth";
|
import { auth } from "@/auth";
|
||||||
import {
|
import {
|
||||||
ServiceInputSignUp,
|
repoFindUserByUsername,
|
||||||
|
repoFindUserById
|
||||||
|
} from "./auth-repository";
|
||||||
|
import {
|
||||||
|
ServiceInputGetUserProfileByUsername,
|
||||||
|
ServiceInputGetUserProfileById,
|
||||||
ServiceInputSignIn,
|
ServiceInputSignIn,
|
||||||
ServiceOutputSignUp,
|
ServiceInputSignUp,
|
||||||
ServiceOutputSignIn
|
ServiceOutputAuth,
|
||||||
|
ServiceOutputUserProfile
|
||||||
} from "./auth-service-dto";
|
} from "./auth-service-dto";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sign up a new user
|
* Sign up service
|
||||||
* Calls better-auth's signUp.email with username support
|
|
||||||
*/
|
*/
|
||||||
export async function serviceSignUp(dto: ServiceInputSignUp): Promise<ServiceOutputSignUp> {
|
export async function serviceSignUp(dto: ServiceInputSignUp): Promise<ServiceOutputAuth> {
|
||||||
try {
|
// Better-auth handles user creation internally
|
||||||
await auth.api.signUpEmail({
|
const result = await auth.api.signUpEmail({
|
||||||
body: {
|
body: {
|
||||||
email: dto.email,
|
email: dto.email,
|
||||||
password: dto.password,
|
password: dto.password,
|
||||||
username: dto.username,
|
|
||||||
name: dto.name,
|
name: dto.name,
|
||||||
|
username: dto.username,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
if (!result.user) {
|
||||||
success: true,
|
|
||||||
email: dto.email,
|
|
||||||
username: dto.username,
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
// better-auth handles duplicates and validation errors
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sign in user
|
* Sign in service
|
||||||
* Uses better-auth's signIn.username for username-based authentication
|
|
||||||
*/
|
*/
|
||||||
export async function serviceSignIn(dto: ServiceInputSignIn): Promise<ServiceOutputSignIn> {
|
export async function serviceSignIn(dto: ServiceInputSignIn): Promise<ServiceOutputAuth> {
|
||||||
try {
|
// Try to sign in with username first
|
||||||
// Determine if identifier is email or username
|
const userResult = await repoFindUserByUsername({ username: dto.identifier });
|
||||||
const isEmail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(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) {
|
if (result.user) {
|
||||||
// Use email sign in
|
return {
|
||||||
session = await auth.api.signInEmail({
|
success: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Try as email
|
||||||
|
const result = await auth.api.signInEmail({
|
||||||
body: {
|
body: {
|
||||||
email: dto.identifier,
|
email: dto.identifier,
|
||||||
password: dto.password,
|
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 {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
sessionToken: session?.token,
|
|
||||||
};
|
};
|
||||||
} catch (error) {
|
}
|
||||||
// better-auth throws on invalid credentials
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: false,
|
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