refactor: user authentication service

This commit is contained in:
monoid 2025-08-31 18:11:50 +09:00
parent 482892ffc1
commit 7ad7a00500
3 changed files with 172 additions and 73 deletions

View 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;
}

View file

@ -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();

View file

@ -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";
}
};