refactor: user authentication service
This commit is contained in:
parent
482892ffc1
commit
7ad7a00500
3 changed files with 172 additions and 73 deletions
92
packages/client/src/state/api.ts
Normal file
92
packages/client/src/state/api.ts
Normal file
|
@ -0,0 +1,92 @@
|
|||
import { makeApiUrl } from "../hook/fetcher.ts";
|
||||
import { LoginRequest } from "dbtype/mod.ts";
|
||||
|
||||
export type LoginResponse = {
|
||||
username: string;
|
||||
permission: string[];
|
||||
accessExpired: number;
|
||||
};
|
||||
|
||||
export type LogoutResponse = {
|
||||
ok: boolean;
|
||||
username: string;
|
||||
permission: string[];
|
||||
};
|
||||
|
||||
export type RefreshResponse = LoginResponse & {
|
||||
refresh: boolean;
|
||||
};
|
||||
|
||||
export type ErrorFormat = {
|
||||
code: number;
|
||||
message: string;
|
||||
detail?: string;
|
||||
};
|
||||
|
||||
export class ApiError extends Error {
|
||||
public readonly code: number;
|
||||
public readonly detail?: string;
|
||||
|
||||
constructor(error: ErrorFormat) {
|
||||
super(error.message);
|
||||
this.name = "ApiError";
|
||||
this.code = error.code;
|
||||
this.detail = error.detail;
|
||||
}
|
||||
}
|
||||
|
||||
export async function refreshService(): Promise<RefreshResponse> {
|
||||
const u = makeApiUrl("/api/user/refresh");
|
||||
const res = await fetch(u, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
});
|
||||
const b = await res.json();
|
||||
if (!res.ok) {
|
||||
throw new ApiError(b as ErrorFormat);
|
||||
}
|
||||
return b as RefreshResponse;
|
||||
}
|
||||
|
||||
export async function logoutService(): Promise<LogoutResponse> {
|
||||
const u = makeApiUrl("/api/user/logout");
|
||||
const req = await fetch(u, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
});
|
||||
const b = await req.json();
|
||||
if (!req.ok) {
|
||||
throw new ApiError(b as ErrorFormat);
|
||||
}
|
||||
return b as LogoutResponse;
|
||||
}
|
||||
|
||||
export async function loginService(userLoginInfo: LoginRequest): Promise<LoginResponse> {
|
||||
const u = makeApiUrl("/api/user/login");
|
||||
const res = await fetch(u, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(userLoginInfo),
|
||||
headers: { "content-type": "application/json" },
|
||||
credentials: "include",
|
||||
});
|
||||
const b = await res.json();
|
||||
if (!res.ok) {
|
||||
throw new ApiError(b as ErrorFormat);
|
||||
}
|
||||
return b as LoginResponse;
|
||||
}
|
||||
|
||||
export async function resetPasswordService(username: string, oldpassword: string, newpassword: string): Promise<{ ok: boolean }> {
|
||||
const u = makeApiUrl("/api/user/reset");
|
||||
const res = await fetch(u, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ username, oldpassword, newpassword }),
|
||||
headers: { "content-type": "application/json" },
|
||||
credentials: "include",
|
||||
});
|
||||
const b = await res.json();
|
||||
if (!res.ok) {
|
||||
throw new ApiError(b as ErrorFormat);
|
||||
}
|
||||
return b;
|
||||
}
|
|
@ -1,18 +1,19 @@
|
|||
import { atom, useAtomValue, setAtomValue } from "../lib/atom.ts";
|
||||
import { makeApiUrl } from "../hook/fetcher.ts";
|
||||
import { LoginRequest } from "dbtype/mod.ts";
|
||||
import {
|
||||
ApiError,
|
||||
loginService,
|
||||
LoginResponse,
|
||||
logoutService,
|
||||
refreshService,
|
||||
resetPasswordService,
|
||||
} from "./api.ts";
|
||||
|
||||
type LoginLocalStorage = {
|
||||
username: string;
|
||||
permission: string[];
|
||||
accessExpired: number;
|
||||
};
|
||||
|
||||
let localObj: LoginLocalStorage | null = null;
|
||||
let localObj: LoginResponse | null = null;
|
||||
function getUserSessions() {
|
||||
if (localObj === null) {
|
||||
const storagestr = localStorage.getItem("UserLoginContext") as string | null;
|
||||
const storage = storagestr !== null ? (JSON.parse(storagestr) as LoginLocalStorage | null) : null;
|
||||
const storage = storagestr !== null ? (JSON.parse(storagestr) as LoginResponse | null) : null;
|
||||
localObj = storage;
|
||||
}
|
||||
if (localObj !== null && localObj.accessExpired > Math.floor(Date.now() / 1000)) {
|
||||
|
@ -25,16 +26,11 @@ function getUserSessions() {
|
|||
}
|
||||
|
||||
export async function refresh() {
|
||||
const u = makeApiUrl("/api/user/refresh");
|
||||
const res = await fetch(u, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
});
|
||||
if (res.status !== 200) throw new Error("Maybe Network Error");
|
||||
const r = (await res.json()) as LoginLocalStorage & { refresh: boolean };
|
||||
try {
|
||||
const r = await refreshService();
|
||||
if (r.refresh) {
|
||||
localObj = {
|
||||
...r
|
||||
...r,
|
||||
};
|
||||
} else {
|
||||
localObj = {
|
||||
|
@ -48,17 +44,20 @@ export async function refresh() {
|
|||
username: r.username,
|
||||
permission: r.permission,
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError) {
|
||||
console.error(`Refresh failed: ${e.detail}`);
|
||||
}
|
||||
localObj = { accessExpired: 0, username: "", permission: [] };
|
||||
localStorage.setItem("UserLoginContext", JSON.stringify(localObj));
|
||||
return { username: "", permission: [] };
|
||||
}
|
||||
}
|
||||
|
||||
export const doLogout = async () => {
|
||||
const u = makeApiUrl("/api/user/logout");
|
||||
const req = await fetch(u, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
});
|
||||
const setVal = setAtomValue(userLoginStateAtom);
|
||||
try {
|
||||
const res = await req.json();
|
||||
const res = await logoutService();
|
||||
localObj = {
|
||||
accessExpired: 0,
|
||||
username: "",
|
||||
|
@ -72,29 +71,31 @@ export const doLogout = async () => {
|
|||
};
|
||||
} catch (error) {
|
||||
console.error(`Server Error ${error}`);
|
||||
// Even if logout fails, clear client-side session
|
||||
localObj = { accessExpired: 0, username: "", permission: [] };
|
||||
window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj));
|
||||
setVal(localObj);
|
||||
return {
|
||||
username: "",
|
||||
permission: [],
|
||||
};
|
||||
}
|
||||
};
|
||||
export const doLogin = async (userLoginInfo: LoginRequest): Promise<string | LoginLocalStorage> => {
|
||||
const u = makeApiUrl("/api/user/login");
|
||||
const res = await fetch(u, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(userLoginInfo),
|
||||
headers: { "content-type": "application/json" },
|
||||
credentials: "include",
|
||||
});
|
||||
const b = await res.json();
|
||||
if (res.status !== 200) {
|
||||
return b.detail as string;
|
||||
}
|
||||
|
||||
export const doLogin = async (userLoginInfo: LoginRequest): Promise<string | LoginResponse> => {
|
||||
try {
|
||||
const b = await loginService(userLoginInfo);
|
||||
const setVal = setAtomValue(userLoginStateAtom);
|
||||
localObj = b;
|
||||
setVal(b);
|
||||
window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj));
|
||||
return b;
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError) {
|
||||
return e.detail ?? e.message;
|
||||
}
|
||||
return "An unknown error occurred.";
|
||||
}
|
||||
};
|
||||
|
||||
Object.assign(window, {
|
||||
|
@ -104,19 +105,15 @@ Object.assign(window, {
|
|||
});
|
||||
|
||||
export const doResetPassword = async (username: string, oldpassword: string, newpassword: string) => {
|
||||
const u = makeApiUrl("/api/user/reset");
|
||||
const res = await fetch(u, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ username, oldpassword, newpassword }),
|
||||
headers: { "content-type": "application/json" },
|
||||
credentials: "include",
|
||||
});
|
||||
const b = await res.json();
|
||||
if (res.status !== 200) {
|
||||
return b.detail as string;
|
||||
try {
|
||||
return await resetPasswordService(username, oldpassword, newpassword);
|
||||
} catch (e) {
|
||||
if (e instanceof ApiError) {
|
||||
return e.detail ?? e.message;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
return "An unknown error occurred.";
|
||||
}
|
||||
};
|
||||
|
||||
export async function getInitialValue() {
|
||||
const user = getUserSessions();
|
||||
|
|
|
@ -6,6 +6,15 @@ import { sendError } from "./route/error_handler.ts";
|
|||
import { get_setting } from "./SettingConfig.ts";
|
||||
import { LoginRequestSchema, LoginResetRequestSchema } from "dbtype";
|
||||
|
||||
type LoginResponse = {
|
||||
accessExpired: number;
|
||||
} & PayloadInfo;
|
||||
|
||||
type RefreshResponse = {
|
||||
accessExpired: number;
|
||||
refresh: boolean;
|
||||
} & PayloadInfo;
|
||||
|
||||
type PayloadInfo = {
|
||||
username: string;
|
||||
permission: string[];
|
||||
|
@ -110,7 +119,7 @@ export const createLoginHandler = (userController: UserAccessor) => async (ctx:
|
|||
username: user.username,
|
||||
permission: userPermission,
|
||||
accessExpired: Math.floor(Date.now() / 1000) + accessExpiredTime,
|
||||
};
|
||||
} satisfies LoginResponse;
|
||||
console.log(`${username} logined`);
|
||||
return;
|
||||
};
|
||||
|
@ -206,17 +215,18 @@ export const createRefreshTokenMiddleware = (cntr: UserAccessor) => async (ctx:
|
|||
const user = ctx.state.user as PayloadInfo;
|
||||
ctx.body = {
|
||||
refresh: false,
|
||||
accessExpired: 0,
|
||||
...user,
|
||||
};
|
||||
} satisfies RefreshResponse;
|
||||
ctx.type = "json";
|
||||
}
|
||||
};
|
||||
async function success() {
|
||||
const user = ctx.state.user as PayloadInfo;
|
||||
ctx.body = {
|
||||
...user,
|
||||
refresh: true,
|
||||
refreshExpired: Math.floor(Date.now() / 1000 + accessExpiredTime),
|
||||
};
|
||||
accessExpired: Math.floor(Date.now() / 1000 + accessExpiredTime),
|
||||
} satisfies RefreshResponse;
|
||||
ctx.type = "json";
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue