287 lines
9.1 KiB
TypeScript
287 lines
9.1 KiB
TypeScript
import { decode, sign, TokenExpiredError, verify } from "jsonwebtoken";
|
|
import Koa from "koa";
|
|
import Router from "koa-router";
|
|
import { sendError } from "./route/error_handler";
|
|
import Knex from "knex";
|
|
import { createKnexUserController } from "./db/mod";
|
|
import { request } from "http";
|
|
import { get_setting } from "./SettingConfig";
|
|
import { IUser, UserAccessor } from "./model/mod";
|
|
|
|
type PayloadInfo = {
|
|
username: string;
|
|
permission: string[];
|
|
};
|
|
|
|
export type UserState = {
|
|
user: PayloadInfo;
|
|
};
|
|
|
|
const isUserState = (obj: object | string): obj is PayloadInfo => {
|
|
if (typeof obj === "string") return false;
|
|
return "username" in obj && "permission" in obj &&
|
|
(obj as { permission: unknown }).permission instanceof Array;
|
|
};
|
|
type RefreshPayloadInfo = { username: string };
|
|
const isRefreshToken = (obj: object | string): obj is RefreshPayloadInfo => {
|
|
if (typeof obj === "string") return false;
|
|
return "username" in obj &&
|
|
typeof (obj as { username: unknown }).username === "string";
|
|
};
|
|
|
|
export const accessTokenName = "access_token";
|
|
export const refreshTokenName = "refresh_token";
|
|
const accessExpiredTime = 60 * 60; //1 hour
|
|
const refreshExpiredTime = 60 * 60 * 24 * 14; // 14 day;
|
|
|
|
export const getAdminAccessTokenValue = () => {
|
|
const { jwt_secretkey } = get_setting();
|
|
return publishAccessToken(jwt_secretkey, "admin", [], accessExpiredTime);
|
|
};
|
|
export const getAdminRefreshTokenValue = () => {
|
|
const { jwt_secretkey } = get_setting();
|
|
return publishRefreshToken(jwt_secretkey, "admin", refreshExpiredTime);
|
|
};
|
|
const publishAccessToken = (
|
|
secretKey: string,
|
|
username: string,
|
|
permission: string[],
|
|
expiredtime: number,
|
|
) => {
|
|
const payload = sign(
|
|
{
|
|
username: username,
|
|
permission: permission,
|
|
},
|
|
secretKey,
|
|
{ expiresIn: expiredtime },
|
|
);
|
|
return payload;
|
|
};
|
|
const publishRefreshToken = (
|
|
secretKey: string,
|
|
username: string,
|
|
expiredtime: number,
|
|
) => {
|
|
const payload = sign(
|
|
{ username: username },
|
|
secretKey,
|
|
{ expiresIn: expiredtime },
|
|
);
|
|
return payload;
|
|
};
|
|
const setToken = (
|
|
ctx: Koa.Context,
|
|
token_name: string,
|
|
token_payload: string | null,
|
|
expiredtime: number,
|
|
) => {
|
|
const setting = get_setting();
|
|
if (token_payload === null && !!!ctx.cookies.get(token_name)) {
|
|
return;
|
|
}
|
|
ctx.cookies.set(token_name, token_payload, {
|
|
httpOnly: true,
|
|
secure: !setting.localmode,
|
|
sameSite: "strict",
|
|
expires: new Date(Date.now() + expiredtime),
|
|
});
|
|
};
|
|
export const createLoginMiddleware = (userController: UserAccessor) =>
|
|
async (ctx: Koa.Context, next: Koa.Next) => {
|
|
const setting = get_setting();
|
|
const secretKey = setting.jwt_secretkey;
|
|
const body = ctx.request.body;
|
|
//check format
|
|
if (typeof body == "string" || !("username" in body) || !("password" in body)) {
|
|
return sendError(
|
|
400,
|
|
"invalid form : username or password is not found in query.",
|
|
);
|
|
}
|
|
const username = body["username"];
|
|
const password = body["password"];
|
|
//check type
|
|
if (typeof username !== "string" || typeof password !== "string") {
|
|
return sendError(
|
|
400,
|
|
"invalid form : username or password is not string",
|
|
);
|
|
}
|
|
//if admin login is forbidden?
|
|
if (username === "admin" && setting.forbid_remote_admin_login) {
|
|
return sendError(403, "forbidden remote admin login");
|
|
}
|
|
const user = await userController.findUser(username);
|
|
//username not exist
|
|
if (user === undefined) return sendError(401, "not authorized");
|
|
//password not matched
|
|
if (!user.password.check_password(password)) {
|
|
return sendError(401, "not authorized");
|
|
}
|
|
//create token
|
|
const userPermission = await user.get_permissions();
|
|
const payload = publishAccessToken(
|
|
secretKey,
|
|
user.username,
|
|
userPermission,
|
|
accessExpiredTime,
|
|
);
|
|
const payload2 = publishRefreshToken(
|
|
secretKey,
|
|
user.username,
|
|
refreshExpiredTime,
|
|
);
|
|
setToken(ctx, accessTokenName, payload, accessExpiredTime);
|
|
setToken(ctx, refreshTokenName, payload2, refreshExpiredTime);
|
|
ctx.body = {
|
|
username: user.username,
|
|
permission: userPermission,
|
|
accessExpired : (Math.floor(Date.now() / 1000) + accessExpiredTime),
|
|
};
|
|
console.log(`${username} logined`);
|
|
return;
|
|
};
|
|
|
|
export const LogoutMiddleware = (ctx: Koa.Context, next: Koa.Next) => {
|
|
const setting = get_setting()
|
|
ctx.cookies.set(accessTokenName, null);
|
|
ctx.cookies.set(refreshTokenName, null);
|
|
ctx.body = { ok: true,
|
|
username: "",
|
|
permission: setting.guest };
|
|
return;
|
|
};
|
|
export const createUserMiddleWare = (userController: UserAccessor) =>
|
|
async (ctx: Koa.ParameterizedContext<UserState>, next: Koa.Next) => {
|
|
const refreshToken = refreshTokenHandler(userController);
|
|
const setting = get_setting();
|
|
const setGuest = async () => {
|
|
setToken(ctx, accessTokenName, null, 0);
|
|
setToken(ctx, refreshTokenName, null, 0);
|
|
ctx.state["user"] = { username: "", permission: setting.guest };
|
|
return await next();
|
|
};
|
|
return await refreshToken(ctx, setGuest, next);
|
|
};
|
|
const refreshTokenHandler = (cntr: UserAccessor) =>
|
|
async (ctx: Koa.Context, fail: Koa.Next, next: Koa.Next) => {
|
|
const payload = ctx.cookies.get(accessTokenName);
|
|
const setting = get_setting();
|
|
const secretKey = setting.jwt_secretkey;
|
|
const checkRefreshAndUpdate = async () => {
|
|
const payload2 = ctx.cookies.get(refreshTokenName);
|
|
if (payload2 === undefined) return await fail(); // refresh token doesn't exist
|
|
else {
|
|
try {
|
|
const o = verify(payload2, secretKey);
|
|
if (isRefreshToken(o)) {
|
|
const user = await cntr.findUser(o.username);
|
|
if (user === undefined) return await fail(); //already non-existence user
|
|
const perm = await user.get_permissions();
|
|
const payload = publishAccessToken(
|
|
secretKey,
|
|
user.username,
|
|
perm,
|
|
accessExpiredTime,
|
|
);
|
|
setToken(ctx, accessTokenName, payload, accessExpiredTime);
|
|
ctx.state.user = { username: o.username, permission: perm };
|
|
} else {
|
|
console.error("invalid token detected");
|
|
throw new Error("token form invalid");
|
|
return;
|
|
}
|
|
} catch (e) {
|
|
if (e instanceof TokenExpiredError) { // refresh token is expired.
|
|
return await fail();
|
|
} else throw e;
|
|
}
|
|
}
|
|
return await next();
|
|
};
|
|
if (payload == undefined) {
|
|
return await checkRefreshAndUpdate();
|
|
}
|
|
try {
|
|
const o = verify(payload, secretKey);
|
|
if (isUserState(o)) {
|
|
ctx.state.user = o;
|
|
return await next();
|
|
} else {
|
|
console.error("invalid token detected");
|
|
throw new Error("token form invalid");
|
|
}
|
|
} catch (e) {
|
|
if (e instanceof TokenExpiredError) {
|
|
return await checkRefreshAndUpdate();
|
|
} else throw e;
|
|
}
|
|
};
|
|
export const createRefreshTokenMiddleware = (cntr: UserAccessor) =>
|
|
async (ctx: Koa.Context, next: Koa.Next) => {
|
|
const handler = refreshTokenHandler(cntr);
|
|
const fail = async () => {
|
|
const user = ctx.state.user as PayloadInfo;
|
|
ctx.body = {
|
|
refresh: false,
|
|
...user,
|
|
};
|
|
ctx.type = "json";
|
|
};
|
|
const success = async () => {
|
|
const user = ctx.state.user as PayloadInfo;
|
|
ctx.body = {
|
|
...user,
|
|
refresh: true,
|
|
refreshExpired: Math.floor(Date.now() / 1000 + accessExpiredTime),
|
|
};
|
|
ctx.type = "json";
|
|
};
|
|
await handler(ctx, fail, success);
|
|
};
|
|
export const resetPasswordMiddleware = (cntr: UserAccessor) =>
|
|
async (ctx:Koa.Context, next: Koa.Next) => {
|
|
const body = ctx.request.body;
|
|
if(typeof body !== "object" || !('username' in body)||!('oldpassword' in body) || !('newpassword' in body)){
|
|
return sendError(400,"request body is invalid format");
|
|
}
|
|
const username = body['username'];
|
|
const oldpw = body['oldpassword'];
|
|
const newpw = body['newpassword'];
|
|
if(typeof username !== "string" || typeof oldpw !== "string" || typeof newpw !== "string"){
|
|
return sendError(400,"request body is invalid format");
|
|
}
|
|
const user = await cntr.findUser(username);
|
|
if(user === undefined){
|
|
return sendError(403,"not authorized");
|
|
}
|
|
if(!user.password.check_password(oldpw)){
|
|
return sendError(403,"not authorized");
|
|
}
|
|
user.reset_password(newpw);
|
|
ctx.body = {ok:true}
|
|
ctx.type = 'json';
|
|
}
|
|
|
|
export function createLoginRouter(userController: UserAccessor){
|
|
let router = new Router();
|
|
router.post('/login',createLoginMiddleware(userController));
|
|
router.post('/logout',LogoutMiddleware);
|
|
router.post('/refresh',createRefreshTokenMiddleware(userController));
|
|
router.post('/reset', resetPasswordMiddleware(userController));
|
|
return router;
|
|
}
|
|
|
|
export const getAdmin = async (cntr: UserAccessor) => {
|
|
const admin = await cntr.findUser("admin");
|
|
if (admin === undefined) {
|
|
throw new Error("initial process failed!"); //???
|
|
}
|
|
return admin;
|
|
};
|
|
|
|
export const isAdminFirst = (admin: IUser) => {
|
|
return admin.password.hash === "unchecked" &&
|
|
admin.password.salt === "unchecked";
|
|
};
|