feat: add auth API clients and view scaffolding
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
import type { ChangePasswordPayload, CurrentUser, LoginPayload, RegisterPayload, UpdateProfilePayload } from '../types/auth'
|
||||
|
||||
export class AuthApiUnavailableError extends Error {
|
||||
constructor(public readonly status: number, message: string) { super(message) }
|
||||
}
|
||||
|
||||
async function authRequest<T>(path: string, init: RequestInit = {}): Promise<T> {
|
||||
const response = await fetch(path, {
|
||||
credentials: 'include',
|
||||
headers: { Accept: 'application/json', 'Content-Type': 'application/json', ...init.headers },
|
||||
...init,
|
||||
})
|
||||
if (!response.ok) {
|
||||
const body = await response.json().catch(() => null)
|
||||
throw new AuthApiUnavailableError(response.status, body?.detail || `认证请求失败(${response.status})`)
|
||||
}
|
||||
return response.status === 204 ? undefined as T : response.json() as Promise<T>
|
||||
}
|
||||
|
||||
export const register = (payload: RegisterPayload) => authRequest<CurrentUser>('/api/auth/register', { method: 'POST', body: JSON.stringify(payload) })
|
||||
export const login = (payload: LoginPayload) => authRequest<CurrentUser>('/api/auth/login', { method: 'POST', body: JSON.stringify(payload) })
|
||||
export const logout = () => authRequest<void>('/api/auth/logout', { method: 'POST' })
|
||||
export const getCurrentUser = () => authRequest<CurrentUser>('/api/auth/me')
|
||||
export const updateMyProfile = (payload: UpdateProfilePayload) => authRequest<CurrentUser>('/api/users/me', { method: 'PATCH', body: JSON.stringify(payload) })
|
||||
export const changePassword = (payload: ChangePasswordPayload) => authRequest<void>('/api/auth/change-password', { method: 'POST', body: JSON.stringify(payload) })
|
||||
export const getUsers = () => authRequest<CurrentUser[]>('/api/admin/users')
|
||||
@@ -0,0 +1,16 @@
|
||||
export type UserRole = 'user' | 'admin'
|
||||
|
||||
export type CurrentUser = {
|
||||
id: number
|
||||
username: string
|
||||
nickname: string | null
|
||||
email: string | null
|
||||
role: UserRole
|
||||
created_at?: string
|
||||
conversation_count?: number
|
||||
}
|
||||
|
||||
export type RegisterPayload = { username: string; password: string; email?: string; nickname?: string }
|
||||
export type LoginPayload = { username: string; password: string }
|
||||
export type UpdateProfilePayload = { nickname?: string; email?: string }
|
||||
export type ChangePasswordPayload = { current_password: string; new_password: string }
|
||||
@@ -0,0 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { changePassword, updateMyProfile } from '../services/auth'
|
||||
const nickname=ref(''), email=ref(''), currentPassword=ref(''), newPassword=ref(''), notice=ref(''), error=ref('')
|
||||
async function saveProfile(){try{await updateMyProfile({nickname:nickname.value||undefined,email:email.value||undefined});notice.value='资料已更新。'}catch(e){error.value=e instanceof Error?e.message:'更新失败'}}
|
||||
async function savePassword(){try{if(newPassword.value.length<8)throw new Error('新密码至少 8 位。');await changePassword({current_password:currentPassword.value,new_password:newPassword.value});notice.value='密码已更新。'}catch(e){error.value=e instanceof Error?e.message:'修改失败'}}
|
||||
</script>
|
||||
<template><main><h1>账户设置</h1><p v-if="notice">{{notice}}</p><p v-if="error">{{error}}</p><form @submit.prevent="saveProfile"><input v-model.trim="nickname" placeholder="昵称"/><input v-model.trim="email" type="email" placeholder="邮箱"/><button>保存资料</button></form><form @submit.prevent="savePassword"><input v-model="currentPassword" type="password" placeholder="当前密码"/><input v-model="newPassword" type="password" placeholder="新密码"/><button>修改密码</button></form></main></template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { login } from '../services/auth'
|
||||
const username=ref(''), password=ref(''), error=ref(''), loading=ref(false)
|
||||
async function submit(){error.value='';if(!username.value||!password.value){error.value='请输入用户名和密码。';return}loading.value=true;try{await login({username:username.value,password:password.value})}catch(e){error.value=e instanceof Error?e.message:'登录失败'}finally{loading.value=false}}
|
||||
</script>
|
||||
<template><main><h1>登录</h1><form @submit.prevent="submit"><input v-model.trim="username" autocomplete="username" placeholder="用户名"/><input v-model="password" type="password" autocomplete="current-password" placeholder="密码"/><p v-if="error">{{error}}</p><button :disabled="loading">{{loading?'登录中…':'登录'}}</button></form></main></template>
|
||||
@@ -0,0 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { getCurrentUser, logout } from '../services/auth'
|
||||
import type { CurrentUser } from '../types/auth'
|
||||
const user=ref<CurrentUser|null>(null), error=ref('')
|
||||
onMounted(async()=>{try{user.value=await getCurrentUser()}catch(e){error.value=e instanceof Error?e.message:'无法读取个人资料'}})
|
||||
</script>
|
||||
<template><main><h1>个人中心</h1><p v-if="error">{{error}}</p><dl v-else-if="user"><dt>用户名</dt><dd>{{user.username}}</dd><dt>昵称</dt><dd>{{user.nickname||'未设置'}}</dd><dt>邮箱</dt><dd>{{user.email||'未设置'}}</dd><dt>角色</dt><dd>{{user.role}}</dd></dl><button @click="logout">退出登录</button></main></template>
|
||||
@@ -0,0 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { register } from '../services/auth'
|
||||
const username=ref(''), password=ref(''), email=ref(''), error=ref('')
|
||||
async function submit(){error.value='';if(username.value.length<3||password.value.length<8){error.value='用户名至少 3 位,密码至少 8 位。';return}try{await register({username:username.value,password:password.value,email:email.value||undefined})}catch(e){error.value=e instanceof Error?e.message:'注册失败'}}
|
||||
</script>
|
||||
<template><main><h1>注册</h1><form @submit.prevent="submit"><input v-model.trim="username" placeholder="用户名"/><input v-model.trim="email" type="email" placeholder="邮箱(可选)"/><input v-model="password" type="password" placeholder="密码"/><p v-if="error">{{error}}</p><button>创建账户</button></form></main></template>
|
||||
@@ -0,0 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { getUsers } from '../services/auth'
|
||||
import type { CurrentUser } from '../types/auth'
|
||||
const users=ref<CurrentUser[]>([]), error=ref('')
|
||||
onMounted(async()=>{try{users.value=await getUsers()}catch(e){error.value=e instanceof Error?e.message:'无权读取用户列表'}})
|
||||
</script>
|
||||
<template><main><h1>用户管理</h1><p v-if="error">{{error}}</p><table v-else><tr><th>用户名</th><th>昵称</th><th>邮箱</th><th>角色</th></tr><tr v-for="user in users" :key="user.id"><td>{{user.username}}</td><td>{{user.nickname}}</td><td>{{user.email}}</td><td>{{user.role}}</td></tr></table></main></template>
|
||||
Reference in New Issue
Block a user