diff --git a/package.json b/package.json index cd8ba03..d06b2d8 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "webface", + "name": "webface-server", "version": "1.0.0", "description": "", "main": "index.js", diff --git a/squeebot.repo.json b/squeebot.repo.json index d2418b5..a7633a6 100644 --- a/squeebot.repo.json +++ b/squeebot.repo.json @@ -1,5 +1,5 @@ { - "name": "webface", + "name": "webface-server", "plugins": [ { "name": "webface", diff --git a/webface/plugin.ts b/webface/plugin.ts index cb88af7..b6fa602 100644 --- a/webface/plugin.ts +++ b/webface/plugin.ts @@ -45,13 +45,11 @@ class WebSocketServer extends EventEmitter { constructor( public port: number, public host: string, - trustProxy = false, + private authorizedURL: string, + private trustProxy = false, ) { super(); - this.app.disable('x-powered-by'); - if (trustProxy) { - this.app.set('trust proxy', 1); - } + this.setupApp(); } public init(): void { @@ -66,6 +64,21 @@ class WebSocketServer extends EventEmitter { this.wss.close(); this.server.close(); } + + private setupApp(): void { + this.app.disable('x-powered-by'); + if (this.trustProxy) { + this.app.set('trust proxy', 1); + } + this.app.use(express.json() as RequestHandler); + this.app.use((req, res, next) => { + res.header('Access-Control-Allow-Origin', this.authorizedURL); + res.header('Access-Control-Allow-Methods', 'POST, GET, DELETE, OPTIONS'); + res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization'); + res.header('Access-Control-Allow-Credentials', 'true'); + next(); + }); + } } class WebfaceServer extends WebSocketServer { @@ -74,13 +87,13 @@ class WebfaceServer extends WebSocketServer { private issuedTokens = new Map(); constructor( - public port: number, - public host: string, - public authorizedURL: string, - private users: RegisteredUser[], + port: number, + host: string, + authorizedURL: string, trustProxy = false, + private users: RegisteredUser[], ) { - super(port, host, trustProxy); + super(port, host, authorizedURL, trustProxy); this.initializeWebSocket(); this.initializeAPI(); } @@ -196,10 +209,11 @@ class WebfaceServer extends WebSocketServer { * @param ws WebSocket client * @param username Client's username */ - private sendStatus(ws: WebSocket, username: string): void { + private sendStatus(ws: WebSocket, username: string, state?: string): void { ws.send(JSON.stringify({ status: this.controlPlugin ? 'OK' : 'MISSING_CONTROL', commands: this.controlPlugin?.listControlCommands() || [], + state, user: { username, } @@ -230,7 +244,7 @@ class WebfaceServer extends WebSocketServer { status: 'ERROR', message: 'Unknown or missing command', command: '', - state: jCmd.state, + state, })); return; } @@ -241,7 +255,7 @@ class WebfaceServer extends WebSocketServer { } if (command === 'help' || command === 'status') { - this.sendStatus(ws, user); + this.sendStatus(ws, user, state); return; } @@ -312,37 +326,10 @@ class WebfaceServer extends WebSocketServer { * Set up the HTTP API */ private initializeAPI(): void { - this.app.use(express.json() as RequestHandler); - this.app.use((req, res, next) => { - res.header('Access-Control-Allow-Origin', this.authorizedURL); - res.header('Access-Control-Allow-Methods', 'POST, GET, DELETE, OPTIONS'); - res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization'); - res.header('Access-Control-Allow-Credentials', 'true'); - next(); - }); - const router = express.Router(); const authMiddleware = this.tokenOwnerMiddleware(); - router.get('/', (req, res) => { - res.json({ - status: 'OK', - uptime: process.uptime(), - connections: this.connections.size, - }); - }); - - router.get('/squeebot', authMiddleware, (req, res) => { - res.json({ - status: this.controlPlugin ? 'OK' : 'MISSING_CONTROL', - uptime: process.uptime(), - connections: this.connections.size, - user: { - username: res.locals.user, - }, - commands: this.controlPlugin?.listControlCommands() || [], - }); - }); + router.get('/', (req, res) => res.json({ status: 'OK'})); router.post('/authorize', (req, res) => { const { body: { username, password } } = req; @@ -372,8 +359,39 @@ class WebfaceServer extends WebSocketServer { }); }); - router.post('/introspect', authMiddleware, (req, res) => { - res.json({ status: 'OK', message: 'Token valid', user: { username: res.locals.user } }); + router.get('/squeebot', authMiddleware, (req, res) => { + res.json({ + status: this.controlPlugin ? 'OK' : 'MISSING_CONTROL', + uptime: process.uptime(), + connections: this.connections.size, + user: { + username: res.locals.user, + }, + commands: this.controlPlugin?.listControlCommands() || [], + }); + }); + + router.post('/verify', authMiddleware, + (req, res) => res.json({ + status: 'OK', + message: 'Token valid', + user: { + username: res.locals.user + } + })); + + router.post('/password', authMiddleware, (req, res) => { + const { password } = req.body; + if (!password || password.length < 8) { + return res.status(400).json({ + error: 'invalid_password', + error_message: 'Password must be at least 8 characters long.', + }); + } + this.emit('chpwd', { password, user: res.locals.user }); + res.json({ + status: 'OK' + }); }); router.delete('/token', authMiddleware, (req, res) => { @@ -521,10 +539,23 @@ class WebfacePlugin extends Plugin { this.config.get('port') as number, this.config.get('host'), this.config.get('trustedRemote', '*') as string, - users, this.config.get('trustProxy', false), + users, ); + this.srv.on('chpwd', ({ user, password }) => { + const currentUsers = this.config.get('users'); + const currentUser = currentUsers.find(({ username }: RegisteredUser) => username === user); + if (currentUsers) { + bcrypt.hash(password, 10).then((hashed) => { + currentUser.password = hashed; + logger.warn('[webface] User %s password changed.', user); + this.config.set('users', currentUsers); + this.config.save().catch((e) => logger.error('[webface] Failed to save users:', e.message)); + }); + } + }); + this.srv.init(); } }