From 14401b27849a1f1b8212125478ebe4aeb4ebd74f Mon Sep 17 00:00:00 2001 From: monoid Date: Sat, 16 Jan 2021 21:22:30 +0900 Subject: [PATCH] add password reset --- app.ts | 26 ++++- preload.ts | 8 +- src/client/page/difference.tsx | 27 ++++- src/client/page/login.tsx | 4 +- src/client/page/profile.tsx | 90 +++++++++++++++- src/login.ts | 94 +++++++++------- src/server.ts | 189 +++++++++++++++++++-------------- 7 files changed, 302 insertions(+), 136 deletions(-) diff --git a/app.ts b/app.ts index a6a1a2e..ec7e786 100644 --- a/app.ts +++ b/app.ts @@ -1,8 +1,20 @@ import { app, BrowserWindow, session, dialog } from "electron"; import { get_setting } from "./src/SettingConfig"; -import { create_server, start_server } from "./src/server"; +import { create_server } from "./src/server"; import { getAdminAccessTokenValue,getAdminRefreshTokenValue, accessTokenName, refreshTokenName } from "./src/login"; - +import { join } from "path"; +import { ipcMain } from 'electron'; +import { UserAccessor } from "./src/model/mod"; +function registerChannel(cntr: UserAccessor){ + ipcMain.handle('reset_password', async(event,username:string,password:string)=>{ + const user = await cntr.findUser(username); + if(user === undefined){ + return false; + } + user.reset_password(password); + return true; + }); +} const setting = get_setting(); if (!setting.cli) { let wnd: BrowserWindow | null = null; @@ -13,9 +25,14 @@ if (!setting.cli) { height: 600, center: true, useContentSize: true, + webPreferences:{ + preload:join(__dirname,'preload.js'), + contextIsolation:true, + } }); await wnd.loadURL(`data:text/html;base64,`+Buffer.from(loading_html).toString('base64')); //await wnd.loadURL('../loading.html'); + //set admin cookies. await session.defaultSession.cookies.set({ url:`http://localhost:${setting.port}`, name:accessTokenName, @@ -34,7 +51,8 @@ if (!setting.cli) { }); try{ const server = await create_server(); - start_server(server); + server.start_server(); + registerChannel(server.userController); await wnd.loadURL(`http://localhost:${setting.port}`); } catch(e){ @@ -85,7 +103,7 @@ if (!setting.cli) { } else { (async () => { const server = await create_server(); - start_server(server); + server.start_server(); })(); } const loading_html = ` diff --git a/preload.ts b/preload.ts index fd3d7bf..553982d 100644 --- a/preload.ts +++ b/preload.ts @@ -1 +1,7 @@ -import {} from 'electron'; \ No newline at end of file +import {ipcRenderer, contextBridge} from 'electron'; + +contextBridge.exposeInMainWorld('electron',{ + passwordReset:async (username:string,toPw:string)=>{ + return await ipcRenderer.invoke('reset_password',username,toPw); +} +}); \ No newline at end of file diff --git a/src/client/page/difference.tsx b/src/client/page/difference.tsx index b27a8af..4fdeb73 100644 --- a/src/client/page/difference.tsx +++ b/src/client/page/difference.tsx @@ -1,11 +1,24 @@ import React, { useContext, useEffect, useState } from 'react'; import { CommonMenuList, Headline } from "../component/mod"; import { UserContext } from "../state"; -import { Grid, Paper, Typography } from "@material-ui/core"; +import { Box, Grid, Paper, Typography,Button, makeStyles, Theme } from "@material-ui/core"; +const useStyles = makeStyles((theme:Theme)=>({ + paper:{ + padding: theme.spacing(2), + }, + commitable:{ + display:'grid', + gridTemplateColumns: `100px auto`, + }, + contentTitle:{ + marginLeft: theme.spacing(2) + } +})); export function DifferencePage(){ const ctx = useContext(UserContext); + const classes = useStyles(); const [diffList,setDiffList] = useState< {type:string,value:{path:string,type:string}[]}[] >([]); @@ -44,9 +57,15 @@ export function DifferencePage(){ const menu = CommonMenuList(); (ctx.username == "admin") return ( - {(ctx.username == "admin") ? (diffList.map(x=> - {x.type} - {x.value.map(y=>(Commit(y)}>{y.path}))} + {(ctx.username == "admin") ? (diffList.map(x=> + {x.type} + + {x.value.map(y=>( + <> + + {y.path} + ))} + )):(Not Allowed : please login as an admin) } diff --git a/src/client/page/login.tsx b/src/client/page/login.tsx index 4202393..08458b4 100644 --- a/src/client/page/login.tsx +++ b/src/client/page/login.tsx @@ -9,7 +9,7 @@ export const LoginPage = ()=>{ const theme = useTheme(); const [userLoginInfo,setUserLoginInfo]= useState({username:"",password:""}); const [openDialog,setOpenDialog] = useState({open:false,message:""}); - const {username,setUsername,permission,setPermission} = useContext(UserContext); + const {setUsername,setPermission} = useContext(UserContext); const history = useHistory(); const handleDialogClose = ()=>{ setOpenDialog({...openDialog,open:false}); @@ -66,4 +66,4 @@ export const LoginPage = ()=>{ -} +} \ No newline at end of file diff --git a/src/client/page/profile.tsx b/src/client/page/profile.tsx index d041527..3ef63b5 100644 --- a/src/client/page/profile.tsx +++ b/src/client/page/profile.tsx @@ -1,18 +1,98 @@ import { CommonMenuList, Headline } from "../component/mod"; -import React, { useContext } from 'react'; +import React, { useContext, useState } from 'react'; import { UserContext } from "../state"; -import { Grid, Paper, Typography } from "@material-ui/core"; +import { Chip, Grid, makeStyles, Paper, Theme, Typography, Divider, Button, + Dialog, DialogTitle, DialogContentText, DialogContent, TextField, DialogActions } from "@material-ui/core"; + +const useStyles = makeStyles((theme:Theme)=>({ + paper:{ + alignSelf:"center", + padding:theme.spacing(2), + }, + formfield:{ + display:'flex', + flexFlow:'column', + } +})); export function ProfilePage(){ const userctx = useContext(UserContext); + const classes = useStyles(); const menu = CommonMenuList(); + const [pw_open,set_pw_open] = useState(false); + const [oldpw,setOldpw] = useState(""); + const [newpw,setNewpw] = useState(""); + const [newpwch,setNewpwch] = useState(""); + const [msg_dialog,set_msg_dialog] = useState({opened:false,msg:""}); + const permission_list =userctx.permission.map(p=>( + + )); + const isElectronContent = window['electron'] !== undefined; + const handle_open = ()=>set_pw_open(true); + const handle_close = ()=>{ + set_pw_open(false); + setNewpw(""); + setNewpwch(""); + }; + const handle_ok= ()=>{ + if(isElectronContent){ + const elec = window['electron']; + if(newpw == newpwch){ + const success = elec.passwordReset(userctx.username,newpw); + if(!success){ + set_msg_dialog({opened:true,msg:"user not exist."}); + } + } + else{ + set_msg_dialog({opened:true,msg:"password and password check is not equal."}); + } + handle_close(); + } + } return ( - - + + - {userctx.username} + {userctx.username} + + + + Permission + + + {permission_list.length == 0 ? "-" : permission_list} + + + + + Password Reset + + type the old and new password +
+ {(!isElectronContent) && (setOldpw(e.target.value)}>)} + setNewpw(e.target.value)}> + setNewpwch(e.target.value)}> +
+
+ + + + +
+ set_msg_dialog({opened:false,msg:""})}> + Alert! + + {msg_dialog.msg} + + + + +
) } \ No newline at end of file diff --git a/src/login.ts b/src/login.ts index d7af809..40ceb87 100644 --- a/src/login.ts +++ b/src/login.ts @@ -35,12 +35,12 @@ 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); + 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 { jwt_secretkey } = get_setting(); + return publishRefreshToken(jwt_secretkey, "admin", refreshExpiredTime); }; const publishAccessToken = ( secretKey: string, @@ -73,12 +73,12 @@ const publishRefreshToken = ( const setToken = ( ctx: Koa.Context, token_name: string, - token_payload: string|null, + token_payload: string | null, expiredtime: number, ) => { const setting = get_setting(); - if(token_payload === null && !!!ctx.cookies.get(token_name)){ - return; + if (token_payload === null && !!!ctx.cookies.get(token_name)) { + return; } ctx.cookies.set(token_name, token_payload, { httpOnly: true, @@ -87,9 +87,8 @@ const setToken = ( expires: new Date(Date.now() + expiredtime), }); }; -export const createLoginMiddleware = (knex: Knex) => { - const userController = createKnexUserController(knex); - return async (ctx: Koa.Context, next: Koa.Next) => { +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; @@ -143,16 +142,15 @@ export const createLoginMiddleware = (knex: Knex) => { console.log(`${username} logined`); return; }; -}; + export const LogoutMiddleware = (ctx: Koa.Context, next: Koa.Next) => { ctx.cookies.set(accessTokenName, null); - ctx.cookies.set(refreshTokenName,null); + ctx.cookies.set(refreshTokenName, null); ctx.body = { ok: true }; return; }; -export const createUserMiddleWare = (knex: Knex) => -async (ctx: Koa.ParameterizedContext, next: Koa.Next) => { - const userController = createKnexUserController(knex); +export const createUserMiddleWare = (userController: UserAccessor) => + async (ctx: Koa.ParameterizedContext, next: Koa.Next) => { const refreshToken = refreshTokenHandler(userController); const setting = get_setting(); const setGuest = async () => { @@ -161,9 +159,10 @@ async (ctx: Koa.ParameterizedContext, next: Koa.Next) => { ctx.state["user"] = { username: "", permission: setting.guest }; return await next(); }; - return await refreshToken(ctx,setGuest,next); + return await refreshToken(ctx, setGuest, next); }; -const refreshTokenHandler = (cntr:UserAccessor) => async (ctx:Koa.Context,fail: Koa.Next,next: Koa.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; @@ -215,31 +214,50 @@ const refreshTokenHandler = (cntr:UserAccessor) => async (ctx:Koa.Context,fail: return await checkRefreshAndUpdate(); } else throw e; } -} -export const createRefreshTokenMiddleware = (knex:Knex)=> async (ctx:Koa.Context,next:Koa.Next)=>{ - const cntr= createKnexUserController(knex); + }; +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 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'; + 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"); } - await handler(ctx,fail,success); + const username = body['username']; + const oldpw = body['oldpassword']; + const newpw = body['newpassword']; + 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 const getAdmin = async (knex: Knex) => { - const cntr = createKnexUserController(knex); +export const getAdmin = async (cntr: UserAccessor) => { const admin = await cntr.findUser("admin"); if (admin === undefined) { throw new Error("initial process failed!"); //??? diff --git a/src/server.ts b/src/server.ts index 7c81d38..037110d 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,101 +1,126 @@ import Koa from 'koa'; import Router from 'koa-router'; -import {get_setting} from './SettingConfig'; +import {get_setting, SettingConfig} from './SettingConfig'; import {connectDB} from './database'; import {DiffManager, createDiffRouter} from './diff/mod'; import { createReadStream, readFileSync } from 'fs'; import getContentRouter from './route/contents'; -import { createKnexDocumentAccessor } from './db/mod'; +import { createKnexDocumentAccessor, createKnexUserController } from './db/mod'; import bodyparser from 'koa-bodyparser'; import {error_handler} from './route/error_handler'; import {createUserMiddleWare,createLoginMiddleware, isAdminFirst, getAdmin, LogoutMiddleware, createRefreshTokenMiddleware} from './login'; import {createInterface as createReadlineInterface} from 'readline'; +import { DocumentAccessor, UserAccessor } from './model/mod'; +class ServerApplication{ + readonly userController: UserAccessor; + readonly documentController: DocumentAccessor; + readonly diffManger; + readonly app: Koa; + private index_html:Buffer; + constructor(userController: UserAccessor,documentController:DocumentAccessor){ + this.userController = userController; + this.documentController = documentController; + this.diffManger = new DiffManager(documentController); + this.app = new Koa(); + this.index_html = readFileSync("index.html"); + } + private async setup(){ + const setting = get_setting(); + const app = this.app; + if(setting.cli){ + const userAdmin = await getAdmin(this.userController); + if(await isAdminFirst(userAdmin)){ + const rl = createReadlineInterface({input:process.stdin,output:process.stdout}); + rl.setPrompt("put admin password : "); + rl.prompt(); + const pw = await new Promise((res:(data:string)=>void,err)=>{ + rl.on('line',(data)=>res(data)); + }); + userAdmin.reset_password(pw); + } + } + app.use(bodyparser()); + app.use(error_handler); + app.use(createUserMiddleWare(this.userController)); + + let diff_router = createDiffRouter(this.diffManger); + this.diffManger.register("manga","testdata"); + const index_html = readFileSync("index.html"); + let router = new Router(); + + router.use('/api/diff',diff_router.routes()); + router.use('/api/diff',diff_router.allowedMethods()); + + this.serve_index(router); + this.serve_static_file(router); + + const content_router = getContentRouter(this.documentController); + router.use('/api/doc',content_router.routes()); + router.use('/api/doc',content_router.allowedMethods()); + + router.post('/user/login',createLoginMiddleware(this.userController)); + router.post('/user/logout',LogoutMiddleware); + router.post('/user/refresh',createRefreshTokenMiddleware(this.userController)); + + if(setting.mode == "development"){ + let mm_count = 0; + app.use(async (ctx,next)=>{ + console.log(`==========================${mm_count++}`); + const fromClient = ctx.state['user'].username === "" ? ctx.ip : ctx.state['user'].username; + console.log(`${fromClient} : ${ctx.method} ${ctx.url}`); + await next(); + //console.log(`404`); + });} + app.use(router.routes()); + app.use(router.allowedMethods()); + } + private serve_index(router:Router){ + const serveindex = (url:string)=>{ + router.get(url, (ctx)=>{ctx.type = 'html'; ctx.body = this.index_html;}) + } + serveindex('/'); + serveindex('/doc/:rest(.*)'); + serveindex('/search'); + serveindex('/login'); + serveindex('/profile'); + serveindex('/difference'); + } + private serve_static_file(router: Router){ + const setting = get_setting(); + const static_file_server = (path:string,type:string) => { + router.get('/'+path,async (ctx,next)=>{ + ctx.type = type; ctx.body = createReadStream(path); + })} + static_file_server('dist/css/style.css','css'); + static_file_server('dist/js/bundle.js','js'); + if(setting.mode === "development") + static_file_server('dist/js/bundle.js.map','text'); + } + start_server(){ + let setting = get_setting(); + console.log("start server"); + //todo : support https + return this.app.listen(setting.port,setting.localmode ? "127.0.0.1" : "0.0.0.0"); + } + static async createServer(){ + const setting = get_setting(); + let db = await connectDB(); + + const app = new ServerApplication(createKnexUserController(db) + ,createKnexDocumentAccessor(db)); + await app.setup(); + return app; + } +} //let Koa = require("koa"); export async function create_server(){ - const setting = get_setting(); - let db = await connectDB(); - - let diffmgr = new DiffManager(createKnexDocumentAccessor(db)); - let diff_router = createDiffRouter(diffmgr); - diffmgr.register("manga","testdata"); - - if(setting.cli){ - const userAdmin = await getAdmin(db); - if(await isAdminFirst(userAdmin)){ - const rl = createReadlineInterface({input:process.stdin,output:process.stdout}); - rl.setPrompt("put admin password : "); - rl.prompt(); - const pw = await new Promise((res:(data:string)=>void,err)=>{ - rl.on('line',(data)=>res(data)); - }); - userAdmin.reset_password(pw); - } - } - let app = new Koa(); - app.use(bodyparser()); - app.use(error_handler); - app.use(createUserMiddleWare(db)); - //app.use(ctx=>{ctx.state['setting'] = settings}); - - - const index_html = readFileSync("index.html"); - let router = new Router(); - - router.use('/api/diff',diff_router.routes()); - router.use('/api/diff',diff_router.allowedMethods()); - - //let watcher = new Watcher(setting.path[0]); - //await watcher.setup([]); - const serveindex = (url:string)=>{ - router.get(url, (ctx)=>{ctx.type = 'html'; ctx.body = index_html;}) - } - serveindex('/'); - serveindex('/doc/:rest(.*)'); - serveindex('/search'); - serveindex('/login'); - serveindex('/profile'); - serveindex('/difference'); - - const static_file_server = (path:string,type:string) => { - router.get('/'+path,async (ctx,next)=>{ - ctx.type = type; ctx.body = createReadStream(path); - })} - static_file_server('dist/css/style.css','css'); - static_file_server('dist/js/bundle.js','js'); - if(setting.mode === "development") - static_file_server('dist/js/bundle.js.map','text'); - - const content_router = getContentRouter(createKnexDocumentAccessor(db)); - router.use('/api/doc',content_router.routes()); - router.use('/api/doc',content_router.allowedMethods()); - - router.post('/user/login',createLoginMiddleware(db)); - router.post('/user/logout',LogoutMiddleware); - router.post('/user/refresh',createRefreshTokenMiddleware(db)); - - if(setting.mode == "development"){ - let mm_count = 0; - app.use(async (ctx,next)=>{ - console.log(`==========================${mm_count++}`); - const fromClient = ctx.state['user'].username === "" ? ctx.ip : ctx.state['user'].username; - console.log(`${fromClient} : ${ctx.method} ${ctx.url}`); - await next(); - //console.log(`404`); - });} - app.use(router.routes()); - app.use(router.allowedMethods()); - return app; + return await ServerApplication.createServer(); } -export const start_server = (server: Koa)=>{ - let setting = get_setting(); - console.log("start server"); - //todo : support https - return server.listen(setting.port,setting.localmode ? "127.0.0.1" : "0.0.0.0"); -} -export default {create_server, start_server}; \ No newline at end of file + +export default {create_server}; \ No newline at end of file