11
This commit is contained in:
38
src/api/auth.js
Normal file
38
src/api/auth.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { apiRequest } from "./client.js";
|
||||
|
||||
/**
|
||||
* @param {{ username: string, password: string, confirmPassword: string }} payload
|
||||
*/
|
||||
export function registerApi(payload) {
|
||||
return apiRequest("/auth/register", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
username: payload.username,
|
||||
password: payload.password,
|
||||
confirmPassword: payload.confirmPassword,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{ username: string, password: string }} payload
|
||||
*/
|
||||
export function loginApi(payload) {
|
||||
return apiRequest("/auth/login", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
username: payload.username,
|
||||
password: payload.password,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {string} token */
|
||||
export function logoutApi(token) {
|
||||
return apiRequest("/auth/logout", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
62
src/api/client.js
Normal file
62
src/api/client.js
Normal file
@@ -0,0 +1,62 @@
|
||||
/** API 基址:开发走 Vite 代理 `/api`;生产可设 VITE_API_BASE_URL */
|
||||
const API_BASE = import.meta.env.VITE_API_BASE_URL || "/api/v1";
|
||||
|
||||
function formatValidationDetail(detail) {
|
||||
if (!Array.isArray(detail)) {
|
||||
return "请求参数无效";
|
||||
}
|
||||
const first = detail[0];
|
||||
if (first?.msg) {
|
||||
return String(first.msg);
|
||||
}
|
||||
return "请求参数无效";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path - 相对路径,如 `/auth/register`
|
||||
* @param {RequestInit} options
|
||||
* @returns {Promise<{ ok: boolean, message: string, data?: unknown }>}
|
||||
*/
|
||||
export async function apiRequest(path, options = {}) {
|
||||
const url = `${API_BASE}${path}`;
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
...options.headers,
|
||||
};
|
||||
|
||||
let res;
|
||||
try {
|
||||
res = await fetch(url, { ...options, headers });
|
||||
} catch {
|
||||
return { ok: false, message: "无法连接服务器,请确认后端已启动" };
|
||||
}
|
||||
|
||||
let body = null;
|
||||
try {
|
||||
body = await res.json();
|
||||
} catch {
|
||||
body = null;
|
||||
}
|
||||
|
||||
if (res.status === 422) {
|
||||
return {
|
||||
ok: false,
|
||||
message: formatValidationDetail(body?.detail),
|
||||
};
|
||||
}
|
||||
|
||||
if (body && typeof body.ok === "boolean") {
|
||||
return body;
|
||||
}
|
||||
|
||||
if (res.status === 401) {
|
||||
return { ok: false, message: "未登录或登录已过期" };
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
message: body?.message || `请求失败(${res.status})`,
|
||||
};
|
||||
}
|
||||
|
||||
export { API_BASE };
|
||||
@@ -1,24 +1,25 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { loginApi, logoutApi, registerApi } from "../api/auth.js";
|
||||
|
||||
const USERS_KEY = "aiclient_users";
|
||||
const TOKEN_KEY = "aiclient_token";
|
||||
const USER_KEY = "aiclient_current_user";
|
||||
|
||||
function loadUsers() {
|
||||
try {
|
||||
const raw = localStorage.getItem(USERS_KEY);
|
||||
return raw ? JSON.parse(raw) : [];
|
||||
} catch {
|
||||
return [];
|
||||
function applyTokenResponse(store, res, fallbackMessage) {
|
||||
if (!res.ok) {
|
||||
return { ok: false, message: res.message || fallbackMessage };
|
||||
}
|
||||
}
|
||||
|
||||
function saveUsers(users) {
|
||||
localStorage.setItem(USERS_KEY, JSON.stringify(users));
|
||||
}
|
||||
const data = res.data;
|
||||
if (!data?.access_token || !data?.user) {
|
||||
return { ok: false, message: "服务器返回数据异常" };
|
||||
}
|
||||
|
||||
function createToken(username) {
|
||||
return btoa(`${username}:${Date.now()}`);
|
||||
store.setSession(data.access_token, {
|
||||
id: data.user.id,
|
||||
username: data.user.username,
|
||||
});
|
||||
|
||||
return { ok: true, message: res.message || fallbackMessage };
|
||||
}
|
||||
|
||||
export const useAuthStore = defineStore("auth", {
|
||||
@@ -39,7 +40,21 @@ export const useAuthStore = defineStore("auth", {
|
||||
},
|
||||
|
||||
actions: {
|
||||
register({ username, password, confirmPassword }) {
|
||||
setSession(accessToken, user) {
|
||||
this.token = accessToken;
|
||||
this.currentUser = user;
|
||||
localStorage.setItem(TOKEN_KEY, accessToken);
|
||||
localStorage.setItem(USER_KEY, JSON.stringify(user));
|
||||
},
|
||||
|
||||
clearSession() {
|
||||
this.token = "";
|
||||
this.currentUser = null;
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
localStorage.removeItem(USER_KEY);
|
||||
},
|
||||
|
||||
async register({ username, password, confirmPassword }) {
|
||||
const name = username?.trim();
|
||||
if (!name || !password) {
|
||||
return { ok: false, message: "用户名和密码不能为空" };
|
||||
@@ -51,51 +66,36 @@ export const useAuthStore = defineStore("auth", {
|
||||
return { ok: false, message: "密码长度至少为 6 位" };
|
||||
}
|
||||
|
||||
const users = loadUsers();
|
||||
if (users.some((u) => u.username === name)) {
|
||||
return { ok: false, message: "用户名已存在" };
|
||||
}
|
||||
const res = await registerApi({
|
||||
username: name,
|
||||
password,
|
||||
confirmPassword,
|
||||
});
|
||||
|
||||
users.push({ username: name, password });
|
||||
saveUsers(users);
|
||||
|
||||
return this.login({ username: name, password });
|
||||
return applyTokenResponse(this, res, "注册成功");
|
||||
},
|
||||
|
||||
login({ username, password }) {
|
||||
async login({ username, password }) {
|
||||
const name = username?.trim();
|
||||
if (!name || !password) {
|
||||
return { ok: false, message: "用户名和密码不能为空" };
|
||||
}
|
||||
|
||||
const users = loadUsers();
|
||||
const user = users.find((u) => u.username === name);
|
||||
if (!user || user.password !== password) {
|
||||
return { ok: false, message: "用户名或密码错误" };
|
||||
const res = await loginApi({
|
||||
username: name,
|
||||
password,
|
||||
});
|
||||
|
||||
return applyTokenResponse(this, res, "登录成功");
|
||||
},
|
||||
|
||||
async logout() {
|
||||
const token = this.token;
|
||||
if (token) {
|
||||
await logoutApi(token);
|
||||
}
|
||||
|
||||
const token = createToken(name);
|
||||
const currentUser = { username: name };
|
||||
|
||||
this.token = token;
|
||||
this.currentUser = currentUser;
|
||||
localStorage.setItem(TOKEN_KEY, token);
|
||||
localStorage.setItem(USER_KEY, JSON.stringify(currentUser));
|
||||
|
||||
return { ok: true, message: "登录成功" };
|
||||
},
|
||||
|
||||
logout() {
|
||||
this.token = "";
|
||||
this.currentUser = null;
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
localStorage.removeItem(USER_KEY);
|
||||
this.clearSession();
|
||||
return { ok: true, message: "已退出登录" };
|
||||
},
|
||||
|
||||
// 预留:后续对接 Tauri invoke / HTTP API
|
||||
async loginWithApi(_credentials) {
|
||||
return { ok: false, message: "API 登录尚未实现" };
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -5,8 +5,8 @@ import { useAuthStore } from "../stores/auth";
|
||||
const router = useRouter();
|
||||
const auth = useAuthStore();
|
||||
|
||||
function onLogout() {
|
||||
auth.logout();
|
||||
async function onLogout() {
|
||||
await auth.logout();
|
||||
router.push({ name: "login" });
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -16,7 +16,7 @@ async function onSubmit() {
|
||||
feedback.value = { severity: "", message: "" };
|
||||
loading.value = true;
|
||||
|
||||
const result = auth.login({
|
||||
const result = await auth.login({
|
||||
username: username.value,
|
||||
password: password.value,
|
||||
});
|
||||
|
||||
@@ -17,7 +17,7 @@ async function onSubmit() {
|
||||
feedback.value = { severity: "", message: "" };
|
||||
loading.value = true;
|
||||
|
||||
const result = auth.register({
|
||||
const result = await auth.register({
|
||||
username: username.value,
|
||||
password: password.value,
|
||||
confirmPassword: confirmPassword.value,
|
||||
|
||||
Reference in New Issue
Block a user