add password reset

This commit is contained in:
monoid 2021-01-16 21:22:30 +09:00
parent 41c0c39620
commit 14401b2784
7 changed files with 302 additions and 136 deletions

26
app.ts
View File

@ -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 = `<!DOCTYPE html>

View File

@ -1 +1,7 @@
import {} from 'electron';
import {ipcRenderer, contextBridge} from 'electron';
contextBridge.exposeInMainWorld('electron',{
passwordReset:async (username:string,toPw:string)=>{
return await ipcRenderer.invoke('reset_password',username,toPw);
}
});

View File

@ -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 (<Headline menu={menu}>
{(ctx.username == "admin") ? (diffList.map(x=><Paper key={x.type}>
<Typography variant='h3'>{x.type}</Typography>
{x.value.map(y=>(<Typography key={y.path} variant='h5' onClick={()=>Commit(y)}>{y.path}</Typography>))}
{(ctx.username == "admin") ? (diffList.map(x=><Paper key={x.type} className={classes.paper}>
<Typography variant='h3' className={classes.contentTitle}>{x.type}</Typography>
<Box className={classes.commitable}>
{x.value.map(y=>(
<>
<Button key={`button_${y.path}`} onClick={()=>Commit(y)}>Commit</Button>
<Typography key={`typography_${y.path}`} variant='h5'>{y.path}</Typography>
</>))}
</Box>
</Paper>)):(<Typography variant='h2'>Not Allowed : please login as an admin</Typography>)
}

View File

@ -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 = ()=>{
</DialogActions>
</Dialog>
</Headline>
}
}

View File

@ -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=>(
<Chip key={p} label={p}></Chip>
));
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 (<Headline menu={menu}>
<Paper style={{alignSelf : 'center'}}>
<Grid container>
<Paper className={classes.paper}>
<Grid container direction="column" alignItems="center">
<Grid item>
<Typography>{userctx.username}</Typography>
<Typography variant='h4'>{userctx.username}</Typography>
</Grid>
<Divider></Divider>
<Grid item>
Permission
</Grid>
<Grid item>
{permission_list.length == 0 ? "-" : permission_list}
</Grid>
<Grid item>
<Button onClick={handle_open}>Password Reset</Button>
</Grid>
</Grid>
</Paper>
<Dialog open={pw_open} onClose={handle_close}>
<DialogTitle>Password Reset</DialogTitle>
<DialogContent>
<Typography>type the old and new password</Typography>
<div className={classes.formfield}>
{(!isElectronContent) && (<TextField autoFocus margin='dense' type="password" label="old password"
value={oldpw} onChange={(e)=>setOldpw(e.target.value)}></TextField>)}
<TextField margin='dense' type="password" label="new password"
value={newpw} onChange={e=>setNewpw(e.target.value)}></TextField>
<TextField margin='dense' type="password" label="new password check"
value={newpwch} onChange={e=>setNewpwch(e.target.value)}></TextField>
</div>
</DialogContent>
<DialogActions>
<Button onClick={handle_close} color="primary">Cancel</Button>
<Button onClick={handle_ok} color="primary">Ok</Button>
</DialogActions>
</Dialog>
<Dialog open={msg_dialog.opened} onClose={()=>set_msg_dialog({opened:false,msg:""})}>
<DialogTitle>Alert!</DialogTitle>
<DialogContent>
<DialogContentText>{msg_dialog.msg}</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={()=>set_msg_dialog({opened:false,msg:""})} color="primary">Close</Button>
</DialogActions>
</Dialog>
</Headline>)
}

View File

@ -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<UserState>, next: Koa.Next) => {
const userController = createKnexUserController(knex);
export const createUserMiddleWare = (userController: UserAccessor) =>
async (ctx: Koa.ParameterizedContext<UserState>, next: Koa.Next) => {
const refreshToken = refreshTokenHandler(userController);
const setting = get_setting();
const setGuest = async () => {
@ -161,9 +159,10 @@ async (ctx: Koa.ParameterizedContext<UserState>, 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!"); //???

View File

@ -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};
export default {create_server};