feat(auth): 重构登录流程并增强错误处理
- 改进登录表单验证逻辑,增加非空和类型检查 - 更新认证 Cookie 名称为 Authorization 并调整安全设置 - 登录成功后返回重定向路径和成功消息 - 增强错误处理,区分网络错误、API 错误和未知错误 - 引入 ResponseError 类统一处理 API 响应错误 - 表单提交使用 enhance 函数优化用户体验 - 登录成功时显示 Toast 提示并自动跳转 - 移除独立的用户信息服务,整合至认证服务 - 删除旧的角色检查工具函数 - 新增错误处理工具函数 handleError
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
import { api } from '$lib/api/httpClient'; // 通常不需要 .ts 后缀
|
import { api } from '$lib/api/httpClient'; // 通常不需要 .ts 后缀
|
||||||
import type { AuthResponse, LoginPayload } from '$lib/types/auth';
|
import type { AuthResponse, LoginPayload } from '$lib/types/auth';
|
||||||
import { authStore } from '$lib/stores/authStore';
|
import { authStore } from '$lib/stores/authStore';
|
||||||
import { userService } from '$lib/api/services/userService';
|
|
||||||
import { toast } from '$lib/stores/toastStore';
|
import { toast } from '$lib/stores/toastStore';
|
||||||
|
import { ResponseError } from '$lib/types/error.ts';
|
||||||
|
|
||||||
export const authService = {
|
export const authService = {
|
||||||
/**
|
/**
|
||||||
@@ -13,33 +13,16 @@ export const authService = {
|
|||||||
const response = await api.post<AuthResponse>('/auth/login', payload);
|
const response = await api.post<AuthResponse>('/auth/login', payload);
|
||||||
|
|
||||||
if (response.code !== 200 || !response.data) {
|
if (response.code !== 200 || !response.data) {
|
||||||
throw new Error(response.msg || '登录失败');
|
throw new ResponseError(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { token, tokenHead } = response.data;
|
const { token, tokenHead,userProfile } = response.data;
|
||||||
|
|
||||||
authStore.update(s => ({ ...s, token, tokenHead, isAuthenticated: true }));
|
authStore.update(s => ({ ...s, token, tokenHead, isAuthenticated: true,user: userProfile }));
|
||||||
|
|
||||||
try {
|
|
||||||
// 3. 获取用户信息
|
|
||||||
const userProfile = await userService.getUserProfile({tokenHead,token});
|
|
||||||
|
|
||||||
// 4. 最终确认登录状态(更新完整信息并持久化)
|
return response.data;
|
||||||
// 这里调用 Store 封装好的 login 方法,它会负责写入 localStorage
|
|
||||||
authStore.login({
|
|
||||||
token,
|
|
||||||
tokenHead,
|
|
||||||
user: userProfile
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
|
|
||||||
console.error('获取用户信息失败,回滚登录状态', error);
|
|
||||||
authStore.logout();
|
|
||||||
throw error; // 继续抛出错误给 UI 层处理
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,18 @@
|
|||||||
|
import type { ActionResult } from '@sveltejs/kit';
|
||||||
|
|
||||||
export interface ApiResult<T> {
|
export interface ApiResult<T> {
|
||||||
code: number,
|
code: number,
|
||||||
msg: string,
|
msg: string,
|
||||||
data: T | null;
|
data: T | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type EnhanceResult<T> = ActionResult & {
|
||||||
|
status: number;
|
||||||
|
type: "failure" | "success" | "redirect" | "error";
|
||||||
|
data: T;
|
||||||
|
}
|
||||||
|
export interface LoginFailure {
|
||||||
|
message: string;
|
||||||
|
username: string;
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import type { JsonObject } from '$lib/types/http.ts';
|
import type { JsonObject } from '$lib/types/http.ts';
|
||||||
|
import type { UserProfile } from '$lib/types/user.ts';
|
||||||
|
|
||||||
|
|
||||||
export interface LoginPayload extends JsonObject {
|
export interface LoginPayload extends JsonObject {
|
||||||
username: string;
|
username: string;
|
||||||
@@ -8,4 +10,6 @@ export interface LoginPayload extends JsonObject {
|
|||||||
export interface AuthResponse {
|
export interface AuthResponse {
|
||||||
token: string;
|
token: string;
|
||||||
tokenHead: string;
|
tokenHead: string;
|
||||||
}
|
userProfile: UserProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { ApiResult } from '$lib/types/api.ts';
|
||||||
|
|
||||||
export interface ApiError {
|
export interface ApiError {
|
||||||
status: number;
|
status: number;
|
||||||
details: {
|
details: {
|
||||||
@@ -5,4 +7,16 @@ export interface ApiError {
|
|||||||
msg: string;
|
msg: string;
|
||||||
};
|
};
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ResponseError extends Error {
|
||||||
|
code: number;
|
||||||
|
msg: string;
|
||||||
|
|
||||||
|
constructor(response: ApiResult<unknown>) {
|
||||||
|
super(response.msg);
|
||||||
|
this.code = response.code;
|
||||||
|
this.msg = response.msg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
import {user} from '$lib/stores/userStore';
|
|
||||||
import {get} from 'svelte/store';
|
|
||||||
|
|
||||||
export const hasRole = (role: string[]) => {
|
|
||||||
const userProfile = get(user);
|
|
||||||
|
|
||||||
if (!userProfile){
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return role.some(r => userProfile.roles.includes(r))
|
|
||||||
};
|
|
||||||
14
src/lib/utils/errorUtils.ts
Normal file
14
src/lib/utils/errorUtils.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { HttpError } from '$lib/api/httpClient.ts';
|
||||||
|
import { fail } from '@sveltejs/kit';
|
||||||
|
|
||||||
|
export const handleError = (error: Error) => {
|
||||||
|
if (error instanceof HttpError) {
|
||||||
|
return fail(error.s, {
|
||||||
|
message: error.details?.msg || '认证失败,请检查凭证'
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
if (error instanceof TypeError || (error instanceof Error && error.message.includes('fetch'))) {
|
||||||
|
return { message: '无法连接服务器,请检查网络' };
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,39 +1,82 @@
|
|||||||
import type { Actions } from './$types';
|
import type { Actions } from './$types';
|
||||||
import { fail } from '@sveltejs/kit';
|
import { fail } from '@sveltejs/kit';
|
||||||
import { authService } from '$lib/api/services/authService.ts';
|
import { authService } from '$lib/api/services/authService.ts';
|
||||||
|
import { resolve } from '$app/paths';
|
||||||
import { HttpError } from '$lib/api/httpClient.ts';
|
import { HttpError } from '$lib/api/httpClient.ts';
|
||||||
import type { ApiError } from '$lib/types/error.ts';
|
import { type ApiError, ResponseError } from '$lib/types/error.ts';
|
||||||
|
|
||||||
|
|
||||||
export const actions:Actions = {
|
export const actions:Actions = {
|
||||||
default: async ({ request,cookies }) => {
|
default: async ({ request,cookies ,url}) => {
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const username = formData.get('username') as string;
|
const username = formData.get('username');
|
||||||
const password = formData.get('password') as string;
|
const password = formData.get('password');
|
||||||
|
|
||||||
if (!username || !password) {
|
if (
|
||||||
return fail(400, {missing:true})
|
typeof username !== 'string' ||
|
||||||
|
typeof password !== 'string' ||
|
||||||
|
!username.trim() ||
|
||||||
|
!password.trim()
|
||||||
|
){
|
||||||
|
return fail(400,{
|
||||||
|
missing: true,
|
||||||
|
message: '请填写用户名和密码',
|
||||||
|
username: username?.toString() ?? ''
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try{
|
||||||
|
const response = await authService.login({username,password});
|
||||||
|
|
||||||
try {
|
cookies.set('Authorization',`${response.tokenHead} ${response.token}`,{
|
||||||
const authResponse = await authService.login({username, password});
|
|
||||||
|
|
||||||
|
|
||||||
cookies.set('auth_token', authResponse.token, {
|
|
||||||
path: '/',
|
path: '/',
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
sameSite: 'strict',
|
sameSite: 'strict',
|
||||||
secure: import.meta.env.PROD,
|
secure: process.env.NODE_ENV === 'production',
|
||||||
maxAge: 60 * 60 * 24 * 7 // 7 days
|
maxAge: 60 * 60 * 24 * 7
|
||||||
});
|
});
|
||||||
|
|
||||||
return {success:true};
|
|
||||||
}catch ( error){
|
return {
|
||||||
if (error instanceof HttpError){
|
success: true,
|
||||||
const apiError = error as unknown as ApiError;
|
message: '登录成功',
|
||||||
return fail(400, {message:apiError.details.msg});
|
redirectTo: url.searchParams.get('redirectTo') ?? resolve("/app/dashboard")
|
||||||
|
};
|
||||||
|
|
||||||
|
}catch (error){
|
||||||
|
if (error instanceof HttpError) {
|
||||||
|
|
||||||
|
const msg = (error as unknown as ApiError)?.details?.msg || '登录失败,请检查账号密码';
|
||||||
|
|
||||||
|
return fail(400, {
|
||||||
|
incorrect: true,
|
||||||
|
message: msg,
|
||||||
|
username // 返回用户名供用户修改
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (error instanceof TypeError || (error instanceof Error && error.message.includes('fetch'))) {
|
||||||
|
return fail(503, {
|
||||||
|
message: '网络连接失败,无法连接到服务器',
|
||||||
|
username
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof ResponseError ){
|
||||||
|
return fail(error.code, {
|
||||||
|
message: error.msg,
|
||||||
|
username
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// 4. 兜底的未知错误处理
|
||||||
|
console.error('Login unexpected error:', error);
|
||||||
|
return fail(500, {
|
||||||
|
message: '系统内部错误,请稍后再试',
|
||||||
|
username
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { enhance } from '$app/forms';
|
|
||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
import Icon from '$lib/components/icon/Icon.svelte';
|
import Icon from '$lib/components/icon/Icon.svelte';
|
||||||
export let form;
|
import { toast } from '$lib/stores/toastStore.ts';
|
||||||
|
import { enhance } from '$app/forms';
|
||||||
|
import type { EnhanceResult, LoginFailure } from '$lib/types/api.ts';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -17,16 +20,25 @@
|
|||||||
<span>IT DTMS登录</span>
|
<span>IT DTMS登录</span>
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
{#if form?.message}
|
|
||||||
<div class="alert alert-error text-sm py-2 mb-4">
|
|
||||||
{form.message}
|
<form method="post"
|
||||||
</div>
|
use:enhance={() => {
|
||||||
{/if}
|
return async ({ result , update }) => {
|
||||||
<form method="post" use:enhance class="space-y-4">
|
const res = result as EnhanceResult<LoginFailure>;
|
||||||
|
if (res.status === 200) {
|
||||||
|
toast.success('登录成功');
|
||||||
|
update();
|
||||||
|
} else {
|
||||||
|
toast.error(res.data.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}}
|
||||||
|
class="space-y-4">
|
||||||
|
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label">
|
<label class="label" for="username">
|
||||||
<span class="label-text">用户名</span>
|
<span class="label-text" >用户名</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<input
|
<input
|
||||||
@@ -43,7 +55,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-control">
|
<div class="form-control">
|
||||||
<label class="label">
|
<label class="label" for="password">
|
||||||
<span class="label-text">密码</span>
|
<span class="label-text">密码</span>
|
||||||
</label>
|
</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
|
|||||||
Reference in New Issue
Block a user