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,76 +26,76 @@ 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",
|
if (r.refresh) {
|
||||||
credentials: "include",
|
localObj = {
|
||||||
});
|
...r,
|
||||||
if (res.status !== 200) throw new Error("Maybe Network Error");
|
};
|
||||||
const r = (await res.json()) as LoginLocalStorage & { refresh: boolean };
|
} else {
|
||||||
if (r.refresh) {
|
localObj = {
|
||||||
localObj = {
|
accessExpired: 0,
|
||||||
...r
|
username: "",
|
||||||
};
|
permission: r.permission,
|
||||||
} else {
|
};
|
||||||
localObj = {
|
}
|
||||||
accessExpired: 0,
|
localStorage.setItem("UserLoginContext", JSON.stringify(localObj));
|
||||||
username: "",
|
return {
|
||||||
permission: r.permission,
|
username: r.username,
|
||||||
};
|
permission: r.permission,
|
||||||
}
|
};
|
||||||
localStorage.setItem("UserLoginContext", JSON.stringify(localObj));
|
} catch (e) {
|
||||||
return {
|
if (e instanceof ApiError) {
|
||||||
username: r.username,
|
console.error(`Refresh failed: ${e.detail}`);
|
||||||
permission: r.permission,
|
}
|
||||||
};
|
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 setVal = setAtomValue(userLoginStateAtom);
|
||||||
const req = await fetch(u, {
|
|
||||||
method: "POST",
|
|
||||||
credentials: "include",
|
|
||||||
});
|
|
||||||
const setVal = setAtomValue(userLoginStateAtom);
|
|
||||||
try {
|
try {
|
||||||
const res = await req.json();
|
const res = await logoutService();
|
||||||
localObj = {
|
localObj = {
|
||||||
accessExpired: 0,
|
accessExpired: 0,
|
||||||
username: "",
|
username: "",
|
||||||
permission: res.permission,
|
permission: res.permission,
|
||||||
};
|
};
|
||||||
window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj));
|
window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj));
|
||||||
setVal(localObj);
|
setVal(localObj);
|
||||||
return {
|
return {
|
||||||
username: localObj.username,
|
username: localObj.username,
|
||||||
permission: localObj.permission,
|
permission: localObj.permission,
|
||||||
};
|
};
|
||||||
} 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),
|
const setVal = setAtomValue(userLoginStateAtom);
|
||||||
headers: { "content-type": "application/json" },
|
localObj = b;
|
||||||
credentials: "include",
|
setVal(b);
|
||||||
});
|
window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj));
|
||||||
const b = await res.json();
|
return b;
|
||||||
if (res.status !== 200) {
|
} catch (e) {
|
||||||
return b.detail as string;
|
if (e instanceof ApiError) {
|
||||||
|
return e.detail ?? e.message;
|
||||||
|
}
|
||||||
|
return "An unknown error occurred.";
|
||||||
}
|
}
|
||||||
const setVal = setAtomValue(userLoginStateAtom);
|
|
||||||
localObj = b;
|
|
||||||
setVal(b);
|
|
||||||
window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj));
|
|
||||||
return b;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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",
|
}
|
||||||
});
|
return "An unknown error occurred.";
|
||||||
const b = await res.json();
|
|
||||||
if (res.status !== 200) {
|
|
||||||
return b.detail as string;
|
|
||||||
}
|
}
|
||||||
return b;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
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