session login
This commit is contained in:
parent
d67b50edf4
commit
8b47c4b178
@ -1,8 +1,8 @@
|
|||||||
import React, { createContext, useRef, useState } from 'react';
|
import React, { createContext, useEffect, useRef, useState } from 'react';
|
||||||
import ReactDom from 'react-dom';
|
import ReactDom from 'react-dom';
|
||||||
import {BrowserRouter, Redirect, Route, Switch as RouterSwitch} from 'react-router-dom';
|
import {BrowserRouter, Redirect, Route, Switch as RouterSwitch} from 'react-router-dom';
|
||||||
import { Gallery, ContentAbout, LoginPage, NotFoundPage} from './page/mod';
|
import { Gallery, ContentAbout, LoginPage, NotFoundPage} from './page/mod';
|
||||||
import {UserContext} from './state';
|
import {getInitialValue, UserContext} from './state';
|
||||||
|
|
||||||
import './css/style.css';
|
import './css/style.css';
|
||||||
|
|
||||||
@ -10,6 +10,14 @@ const FooProfile = ()=><div>test profile</div>;
|
|||||||
const App = () => {
|
const App = () => {
|
||||||
const [user,setUser] = useState("");
|
const [user,setUser] = useState("");
|
||||||
const [userPermission,setUserPermission] = useState<string[]>([]);
|
const [userPermission,setUserPermission] = useState<string[]>([]);
|
||||||
|
(async ()=>{
|
||||||
|
const {username,permission} = await getInitialValue();
|
||||||
|
if(username !== user){
|
||||||
|
setUser(username);
|
||||||
|
setUserPermission(permission);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
//useEffect(()=>{});
|
||||||
return (
|
return (
|
||||||
<UserContext.Provider value={{
|
<UserContext.Provider value={{
|
||||||
username:user,
|
username:user,
|
||||||
|
@ -47,7 +47,7 @@ export const LoginPage = ()=>{
|
|||||||
<div style={{minHeight:theme.spacing(2)}}></div>
|
<div style={{minHeight:theme.spacing(2)}}></div>
|
||||||
<form style={{display:'flex', flexFlow:'column', alignItems:'stretch'}}>
|
<form style={{display:'flex', flexFlow:'column', alignItems:'stretch'}}>
|
||||||
<TextField label="username" onChange={(e)=>setUserLoginInfo({...userLoginInfo,username:e.target.value ?? "",})}></TextField>
|
<TextField label="username" onChange={(e)=>setUserLoginInfo({...userLoginInfo,username:e.target.value ?? "",})}></TextField>
|
||||||
<TextField label="password" type="password"
|
<TextField label="password" type="password" onKeyDown={(e)=>{if(e.key === 'Enter') doLogin();}}
|
||||||
onChange={(e)=>setUserLoginInfo({...userLoginInfo,password:e.target.value ?? "",})}/>
|
onChange={(e)=>setUserLoginInfo({...userLoginInfo,password:e.target.value ?? "",})}/>
|
||||||
<div style={{minHeight:theme.spacing(2)}}></div>
|
<div style={{minHeight:theme.spacing(2)}}></div>
|
||||||
<div style={{display:'flex'}}>
|
<div style={{display:'flex'}}>
|
||||||
|
@ -1,8 +1,47 @@
|
|||||||
import React, { createContext, useRef, useState } from 'react';
|
import React, { createContext, useRef, useState } from 'react';
|
||||||
|
|
||||||
export const UserContext = createContext({
|
export const UserContext = createContext({
|
||||||
username:"",
|
username: "",
|
||||||
permission:["openContent"],
|
permission: [] as string[],
|
||||||
setUsername:(s:string)=>{},
|
setUsername: (s: string) => { },
|
||||||
setPermission:(permission:string[])=>{}
|
setPermission: (permission: string[]) => { }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
type LoginLocalStorage = {
|
||||||
|
username: string,
|
||||||
|
permission: string[],
|
||||||
|
refreshExpired: number
|
||||||
|
};
|
||||||
|
|
||||||
|
let localObj: LoginLocalStorage|null = null;
|
||||||
|
|
||||||
|
export const getInitialValue = async () => {
|
||||||
|
if(localObj === null){
|
||||||
|
const storagestr = window.localStorage.getItem("UserLoginContext") as string | null;
|
||||||
|
const storage = storagestr !== null ? JSON.parse(storagestr) as LoginLocalStorage | null : null;
|
||||||
|
localObj = storage;
|
||||||
|
}
|
||||||
|
if (localObj !== null && localObj.refreshExpired > Math.floor(Date.now() / 1000)) {
|
||||||
|
return {
|
||||||
|
username: localObj.username,
|
||||||
|
permission: localObj.permission,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const res = await fetch('/user/refresh', {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
if (res.status !== 200) throw new Error("Maybe Network Error")
|
||||||
|
const r = await res.json() as LoginLocalStorage & { refresh: boolean };
|
||||||
|
if (r.refresh) {
|
||||||
|
localObj = {
|
||||||
|
username: r.username,
|
||||||
|
permission: r.permission,
|
||||||
|
refreshExpired: r.refreshExpired
|
||||||
|
}
|
||||||
|
window.localStorage.setItem("UserLoginContext", JSON.stringify(localObj));
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
username: r.username,
|
||||||
|
permission: r.permission
|
||||||
|
}
|
||||||
|
}
|
287
src/login.ts
287
src/login.ts
@ -1,106 +1,253 @@
|
|||||||
import {sign, decode, verify} from 'jsonwebtoken';
|
import { decode, sign, TokenExpiredError, verify } from "jsonwebtoken";
|
||||||
import Koa from 'koa';
|
import Koa from "koa";
|
||||||
import Router from 'koa-router';
|
import Router from "koa-router";
|
||||||
import { sendError } from './route/error_handler';
|
import { sendError } from "./route/error_handler";
|
||||||
import Knex from 'knex'
|
import Knex from "knex";
|
||||||
import { createKnexUserController } from './db/mod';
|
import { createKnexUserController } from "./db/mod";
|
||||||
import { request } from 'http';
|
import { request } from "http";
|
||||||
import { get_setting } from './setting';
|
import { get_setting } from "./setting";
|
||||||
import { IUser } from './model/mod';
|
import { IUser, UserAccessor } from "./model/mod";
|
||||||
|
|
||||||
type PayloadInfo = {
|
type PayloadInfo = {
|
||||||
username:string,
|
username: string;
|
||||||
permission:string[]
|
permission: string[];
|
||||||
}
|
|
||||||
|
|
||||||
export type UserState = {
|
|
||||||
user:PayloadInfo
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const isUserState = (obj:object|string):obj is PayloadInfo =>{
|
export type UserState = {
|
||||||
if(typeof obj ==="string") return false;
|
user: PayloadInfo;
|
||||||
return 'username' in obj && 'permission' in obj && (obj as {permission:unknown}).permission instanceof Array;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
export const loginTokenName = 'access_token'
|
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 getAdminCookieValue = ()=>{
|
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();
|
const setting = get_setting();
|
||||||
const secretKey = setting.jwt_secretkey;
|
if(token_payload === null && !!!ctx.cookies.get(token_name)){
|
||||||
return sign({
|
return;
|
||||||
username: "admin",
|
}
|
||||||
permission: [],
|
ctx.cookies.set(token_name, token_payload, {
|
||||||
},secretKey,{expiresIn:'3d'});
|
httpOnly: true,
|
||||||
}
|
secure: !setting.localmode,
|
||||||
|
sameSite: "strict",
|
||||||
export const createLoginMiddleware = (knex: Knex)=>{
|
expires: new Date(Date.now() + expiredtime),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
export const createLoginMiddleware = (knex: Knex) => {
|
||||||
const userController = createKnexUserController(knex);
|
const userController = createKnexUserController(knex);
|
||||||
return async (ctx: Koa.Context,next: Koa.Next)=>{
|
return async (ctx: Koa.Context, next: Koa.Next) => {
|
||||||
const setting = get_setting();
|
const setting = get_setting();
|
||||||
const secretKey = setting.jwt_secretkey;
|
const secretKey = setting.jwt_secretkey;
|
||||||
const body = ctx.request.body;
|
const body = ctx.request.body;
|
||||||
if(!('username' in body)||!('password' in body)){
|
//check format
|
||||||
return sendError(400,"invalid form : username or password is not found in query.");
|
if (!("username" in body) || !("password" in body)) {
|
||||||
|
return sendError(
|
||||||
|
400,
|
||||||
|
"invalid form : username or password is not found in query.",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const username = body['username'];
|
const username = body["username"];
|
||||||
const password = body['password'];
|
const password = body["password"];
|
||||||
if(typeof username !== "string" || typeof password !== "string"){
|
//check type
|
||||||
return sendError(400,"invalid form : username or password is not string")
|
if (typeof username !== "string" || typeof password !== "string") {
|
||||||
|
return sendError(
|
||||||
|
400,
|
||||||
|
"invalid form : username or password is not string",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if(setting.forbid_remote_admin_login && username === "admin"){
|
//if admin login is forbidden?
|
||||||
return sendError(403,"forbid remote admin login");
|
if (username === "admin" && setting.forbid_remote_admin_login) {
|
||||||
|
return sendError(403, "forbiden remote admin login");
|
||||||
}
|
}
|
||||||
const user = await userController.findUser(username);
|
const user = await userController.findUser(username);
|
||||||
if(user === undefined){
|
//username not exist
|
||||||
return sendError(401,"not authorized");
|
if (user === undefined) return sendError(401, "not authorized");
|
||||||
}
|
//password not matched
|
||||||
if(!user.password.check_password(password)){
|
if (!user.password.check_password(password)) {
|
||||||
return sendError(401,"not authorized");
|
return sendError(401, "not authorized");
|
||||||
}
|
}
|
||||||
|
//create token
|
||||||
const userPermission = await user.get_permissions();
|
const userPermission = await user.get_permissions();
|
||||||
const payload = sign({
|
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,
|
username: user.username,
|
||||||
permission: userPermission
|
permission: userPermission,
|
||||||
},secretKey,{expiresIn:'3h'});
|
refreshExpired: (Math.floor(Date.now() / 1000) + refreshExpiredTime),
|
||||||
ctx.cookies.set(loginTokenName,payload,{httpOnly:true, secure: !setting.localmode,sameSite:'strict'});
|
};
|
||||||
ctx.body = {ok:true, username: user.username, permission: userPermission}
|
|
||||||
console.log(`${username} logined`);
|
console.log(`${username} logined`);
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
export const LogoutMiddleware = (ctx:Koa.Context,next:Koa.Next)=>{
|
export const LogoutMiddleware = (ctx: Koa.Context, next: Koa.Next) => {
|
||||||
ctx.cookies.set(loginTokenName,undefined);
|
ctx.cookies.set(accessTokenName, null);
|
||||||
ctx.body = {ok:true};
|
ctx.cookies.set(refreshTokenName,null);
|
||||||
|
ctx.body = { ok: true };
|
||||||
return;
|
return;
|
||||||
}
|
};
|
||||||
export const UserMiddleWare = async (ctx:Koa.ParameterizedContext<UserState>,next:Koa.Next)=>{
|
export const createUserMiddleWare = (knex: Knex) =>
|
||||||
const secretKey = get_setting().jwt_secretkey;
|
async (ctx: Koa.ParameterizedContext<UserState>, next: Koa.Next) => {
|
||||||
const payload = ctx.cookies.get(loginTokenName);
|
const userController = createKnexUserController(knex);
|
||||||
|
const refreshToken = refreshTokenHandler(userController);
|
||||||
const setting = get_setting();
|
const setting = get_setting();
|
||||||
if(payload == undefined){
|
const setGuest = async () => {
|
||||||
ctx.state['user'] = {username:"",
|
setToken(ctx, accessTokenName, null, 0);
|
||||||
permission:setting.guest};
|
setToken(ctx, refreshTokenName, null, 0);
|
||||||
|
ctx.state["user"] = { username: "", permission: setting.guest };
|
||||||
return await next();
|
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;
|
||||||
}
|
}
|
||||||
const o = verify(payload,secretKey);
|
} catch (e) {
|
||||||
if(isUserState(o)){
|
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;
|
ctx.state.user = o;
|
||||||
return await next();
|
return await next();
|
||||||
}
|
} else {
|
||||||
else{
|
|
||||||
console.error("invalid token detected");
|
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 = (knex:Knex)=> async (ctx:Koa.Context,next:Koa.Next)=>{
|
||||||
export const getAdmin = async(knex : Knex)=>{
|
const cntr= createKnexUserController(knex);
|
||||||
|
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 getAdmin = async (knex: Knex) => {
|
||||||
const cntr = createKnexUserController(knex);
|
const cntr = createKnexUserController(knex);
|
||||||
const admin = await cntr.findUser("admin");
|
const admin = await cntr.findUser("admin");
|
||||||
if(admin === undefined){
|
if (admin === undefined) {
|
||||||
throw new Error("initial process failed!");//???
|
throw new Error("initial process failed!"); //???
|
||||||
}
|
}
|
||||||
return admin;
|
return admin;
|
||||||
}
|
};
|
||||||
|
|
||||||
export const isAdminFirst = (admin: IUser)=>{
|
export const isAdminFirst = (admin: IUser) => {
|
||||||
return admin.password.hash === "unchecked" && admin.password.salt === "unchecked";
|
return admin.password.hash === "unchecked" &&
|
||||||
}
|
admin.password.salt === "unchecked";
|
||||||
|
};
|
||||||
|
@ -11,7 +11,7 @@ import { createKnexContentsAccessor } from './db/contents';
|
|||||||
import bodyparser from 'koa-bodyparser';
|
import bodyparser from 'koa-bodyparser';
|
||||||
import {error_handler} from './route/error_handler';
|
import {error_handler} from './route/error_handler';
|
||||||
|
|
||||||
import {UserMiddleWare,createLoginMiddleware, isAdminFirst, getAdmin, LogoutMiddleware} from './login';
|
import {createUserMiddleWare,createLoginMiddleware, isAdminFirst, getAdmin, LogoutMiddleware, createRefreshTokenMiddleware} from './login';
|
||||||
|
|
||||||
import {createInterface as createReadlineInterface} from 'readline';
|
import {createInterface as createReadlineInterface} from 'readline';
|
||||||
|
|
||||||
@ -35,7 +35,7 @@ export async function create_server(){
|
|||||||
let app = new Koa();
|
let app = new Koa();
|
||||||
app.use(bodyparser());
|
app.use(bodyparser());
|
||||||
app.use(error_handler);
|
app.use(error_handler);
|
||||||
app.use(UserMiddleWare);
|
app.use(createUserMiddleWare(db));
|
||||||
//app.use(ctx=>{ctx.state['setting'] = settings});
|
//app.use(ctx=>{ctx.state['setting'] = settings});
|
||||||
|
|
||||||
const index_html = readFileSync("index.html");
|
const index_html = readFileSync("index.html");
|
||||||
@ -67,12 +67,13 @@ export async function create_server(){
|
|||||||
|
|
||||||
router.post('/user/login',createLoginMiddleware(db));
|
router.post('/user/login',createLoginMiddleware(db));
|
||||||
router.post('/user/logout',LogoutMiddleware);
|
router.post('/user/logout',LogoutMiddleware);
|
||||||
|
router.post('/user/refresh',createRefreshTokenMiddleware(db));
|
||||||
|
|
||||||
if(setting.mode == "development"){
|
if(setting.mode == "development"){
|
||||||
let mm_count = 0;
|
let mm_count = 0;
|
||||||
app.use(async (ctx,next)=>{
|
app.use(async (ctx,next)=>{
|
||||||
console.log(`==========================${mm_count++}`);
|
console.log(`==========================${mm_count++}`);
|
||||||
const fromClient = ctx.state['user'] === undefined ? ctx.ip : ctx.state['user'].username;
|
const fromClient = ctx.state['user'].username === "" ? ctx.ip : ctx.state['user'].username;
|
||||||
console.log(`${fromClient} : ${ctx.method} ${ctx.url}`);
|
console.log(`${fromClient} : ${ctx.method} ${ctx.url}`);
|
||||||
await next();
|
await next();
|
||||||
//console.log(`404`);
|
//console.log(`404`);
|
||||||
|
Loading…
Reference in New Issue
Block a user