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 { atom, useAtomValue, setAtomValue } from "../lib/atom.ts";
 | 
				
			||||||
import { makeApiUrl } from "../hook/fetcher.ts";
 | 
					 | 
				
			||||||
import { LoginRequest } from "dbtype/mod.ts";
 | 
					import { LoginRequest } from "dbtype/mod.ts";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
						ApiError,
 | 
				
			||||||
 | 
						loginService,
 | 
				
			||||||
 | 
						LoginResponse,
 | 
				
			||||||
 | 
						logoutService,
 | 
				
			||||||
 | 
						refreshService,
 | 
				
			||||||
 | 
						resetPasswordService,
 | 
				
			||||||
 | 
					} from "./api.ts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type LoginLocalStorage = {
 | 
					let localObj: LoginResponse | null = null;
 | 
				
			||||||
	username: string;
 | 
					 | 
				
			||||||
	permission: string[];
 | 
					 | 
				
			||||||
	accessExpired: number;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
let localObj: LoginLocalStorage | null = null;
 | 
					 | 
				
			||||||
function getUserSessions() {
 | 
					function getUserSessions() {
 | 
				
			||||||
    if (localObj === null) {
 | 
					    if (localObj === null) {
 | 
				
			||||||
		const storagestr = localStorage.getItem("UserLoginContext") as string | 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;
 | 
							localObj = storage;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
    if (localObj !== null && localObj.accessExpired > Math.floor(Date.now() / 1000)) {
 | 
					    if (localObj !== null && localObj.accessExpired > Math.floor(Date.now() / 1000)) {
 | 
				
			||||||
| 
						 | 
					@ -25,16 +26,11 @@ function getUserSessions() {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function refresh() {
 | 
					export async function refresh() {
 | 
				
			||||||
	const u = makeApiUrl("/api/user/refresh");
 | 
						try {
 | 
				
			||||||
    const res = await fetch(u, {
 | 
							const r = await refreshService();
 | 
				
			||||||
        method: "POST",
 | 
					 | 
				
			||||||
		credentials: "include",
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
    if (res.status !== 200) throw new Error("Maybe Network Error");
 | 
					 | 
				
			||||||
    const r = (await res.json()) as LoginLocalStorage & { refresh: boolean };
 | 
					 | 
				
			||||||
		if (r.refresh) {
 | 
							if (r.refresh) {
 | 
				
			||||||
			localObj = {
 | 
								localObj = {
 | 
				
			||||||
            ...r
 | 
									...r,
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			localObj = {
 | 
								localObj = {
 | 
				
			||||||
| 
						 | 
					@ -48,17 +44,20 @@ export async function refresh() {
 | 
				
			||||||
			username: r.username,
 | 
								username: r.username,
 | 
				
			||||||
			permission: r.permission,
 | 
								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 () => {
 | 
					export const doLogout = async () => {
 | 
				
			||||||
	const u = makeApiUrl("/api/user/logout");
 | 
					 | 
				
			||||||
	const req = await fetch(u, {
 | 
					 | 
				
			||||||
		method: "POST",
 | 
					 | 
				
			||||||
		credentials: "include",
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
	const setVal = setAtomValue(userLoginStateAtom);
 | 
						const setVal = setAtomValue(userLoginStateAtom);
 | 
				
			||||||
	try {
 | 
						try {
 | 
				
			||||||
		const res = await req.json();
 | 
							const res = await logoutService();
 | 
				
			||||||
		localObj = {
 | 
							localObj = {
 | 
				
			||||||
			accessExpired: 0,
 | 
								accessExpired: 0,
 | 
				
			||||||
			username: "",
 | 
								username: "",
 | 
				
			||||||
| 
						 | 
					@ -72,29 +71,31 @@ export const doLogout = async () => {
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
	} catch (error) {
 | 
						} catch (error) {
 | 
				
			||||||
		console.error(`Server Error ${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 {
 | 
							return {
 | 
				
			||||||
			username: "",
 | 
								username: "",
 | 
				
			||||||
			permission: [],
 | 
								permission: [],
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
export const doLogin = async (userLoginInfo: LoginRequest): Promise<string | LoginLocalStorage> => {
 | 
					
 | 
				
			||||||
	const u = makeApiUrl("/api/user/login");
 | 
					export const doLogin = async (userLoginInfo: LoginRequest): Promise<string | LoginResponse> => {
 | 
				
			||||||
	const res = await fetch(u, {
 | 
						try {
 | 
				
			||||||
		method: "POST",
 | 
							const b = await loginService(userLoginInfo);
 | 
				
			||||||
		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;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
		const setVal = setAtomValue(userLoginStateAtom);
 | 
							const setVal = setAtomValue(userLoginStateAtom);
 | 
				
			||||||
		localObj = b;
 | 
							localObj = b;
 | 
				
			||||||
		setVal(b);
 | 
							setVal(b);
 | 
				
			||||||
		window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj));
 | 
							window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj));
 | 
				
			||||||
		return b;
 | 
							return b;
 | 
				
			||||||
 | 
						} catch (e) {
 | 
				
			||||||
 | 
							if (e instanceof ApiError) {
 | 
				
			||||||
 | 
								return e.detail ?? e.message;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return "An unknown error occurred.";
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Object.assign(window, {
 | 
					Object.assign(window, {
 | 
				
			||||||
| 
						 | 
					@ -104,19 +105,15 @@ Object.assign(window, {
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const doResetPassword = async (username: string, oldpassword: string, newpassword: string) => {
 | 
					export const doResetPassword = async (username: string, oldpassword: string, newpassword: string) => {
 | 
				
			||||||
	const u = makeApiUrl("/api/user/reset");
 | 
						try {
 | 
				
			||||||
	const res = await fetch(u, {
 | 
							return await resetPasswordService(username, oldpassword, newpassword);
 | 
				
			||||||
		method: "POST",
 | 
						} catch (e) {
 | 
				
			||||||
		body: JSON.stringify({ username, oldpassword, newpassword }),
 | 
							if (e instanceof ApiError) {
 | 
				
			||||||
		headers: { "content-type": "application/json" },
 | 
								return e.detail ?? e.message;
 | 
				
			||||||
		credentials: "include",
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
	const b = await res.json();
 | 
					 | 
				
			||||||
	if (res.status !== 200) {
 | 
					 | 
				
			||||||
		return b.detail as string;
 | 
					 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	return b;
 | 
							return "An unknown error occurred.";
 | 
				
			||||||
}
 | 
						}
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export async function getInitialValue() {
 | 
					export async function getInitialValue() {
 | 
				
			||||||
    const user = getUserSessions();
 | 
					    const user = getUserSessions();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,6 +6,15 @@ import { sendError } from "./route/error_handler.ts";
 | 
				
			||||||
import { get_setting } from "./SettingConfig.ts";
 | 
					import { get_setting } from "./SettingConfig.ts";
 | 
				
			||||||
import { LoginRequestSchema, LoginResetRequestSchema } from "dbtype";
 | 
					import { LoginRequestSchema, LoginResetRequestSchema } from "dbtype";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type LoginResponse = {
 | 
				
			||||||
 | 
						accessExpired: number;
 | 
				
			||||||
 | 
					} & PayloadInfo;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type RefreshResponse = {
 | 
				
			||||||
 | 
						accessExpired: number;
 | 
				
			||||||
 | 
						refresh: boolean;
 | 
				
			||||||
 | 
					} & PayloadInfo;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
type PayloadInfo = {
 | 
					type PayloadInfo = {
 | 
				
			||||||
	username: string;
 | 
						username: string;
 | 
				
			||||||
	permission: string[];
 | 
						permission: string[];
 | 
				
			||||||
| 
						 | 
					@ -110,7 +119,7 @@ export const createLoginHandler = (userController: UserAccessor) => async (ctx:
 | 
				
			||||||
		username: user.username,
 | 
							username: user.username,
 | 
				
			||||||
		permission: userPermission,
 | 
							permission: userPermission,
 | 
				
			||||||
		accessExpired: Math.floor(Date.now() / 1000) + accessExpiredTime,
 | 
							accessExpired: Math.floor(Date.now() / 1000) + accessExpiredTime,
 | 
				
			||||||
	};
 | 
						} satisfies LoginResponse;
 | 
				
			||||||
	console.log(`${username} logined`);
 | 
						console.log(`${username} logined`);
 | 
				
			||||||
	return;
 | 
						return;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					@ -206,17 +215,18 @@ export const createRefreshTokenMiddleware = (cntr: UserAccessor) => async (ctx:
 | 
				
			||||||
		const user = ctx.state.user as PayloadInfo;
 | 
							const user = ctx.state.user as PayloadInfo;
 | 
				
			||||||
		ctx.body = {
 | 
							ctx.body = {
 | 
				
			||||||
			refresh: false,
 | 
								refresh: false,
 | 
				
			||||||
 | 
								accessExpired: 0,
 | 
				
			||||||
			...user,
 | 
								...user,
 | 
				
			||||||
		};
 | 
							} satisfies RefreshResponse;
 | 
				
			||||||
		ctx.type = "json";
 | 
							ctx.type = "json";
 | 
				
			||||||
	}
 | 
						};
 | 
				
			||||||
	async function success() {
 | 
						async function success() {
 | 
				
			||||||
		const user = ctx.state.user as PayloadInfo;
 | 
							const user = ctx.state.user as PayloadInfo;
 | 
				
			||||||
		ctx.body = {
 | 
							ctx.body = {
 | 
				
			||||||
			...user,
 | 
								...user,
 | 
				
			||||||
			refresh: true,
 | 
								refresh: true,
 | 
				
			||||||
			refreshExpired: Math.floor(Date.now() / 1000 + accessExpiredTime),
 | 
								accessExpired: Math.floor(Date.now() / 1000 + accessExpiredTime),
 | 
				
			||||||
		};
 | 
							} satisfies RefreshResponse;
 | 
				
			||||||
		ctx.type = "json";
 | 
							ctx.type = "json";
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		
		Reference in a new issue