Started experimenting with some auth stuff
This commit is contained in:
parent
321738a517
commit
94724879b6
@ -35,6 +35,8 @@
|
||||
},
|
||||
"homepage": "https://github.com/amikhalev/sprinklers3#readme",
|
||||
"dependencies": {
|
||||
"bcrypt": "^2.0.1",
|
||||
"body-parser": "^1.18.3",
|
||||
"chalk": "^2.4.1",
|
||||
"express": "^4.16.3",
|
||||
"express-pino-logger": "^3.0.2",
|
||||
@ -51,6 +53,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/async": "^2.0.49",
|
||||
"@types/bcrypt": "^2.0.0",
|
||||
"@types/classnames": "^2.2.4",
|
||||
"@types/core-js": "^2.5.0",
|
||||
"@types/express": "^4.16.0",
|
||||
|
@ -1,22 +0,0 @@
|
||||
import * as express from "express";
|
||||
|
||||
import * as schema from "@common/sprinklers/schema";
|
||||
import {serialize} from "serializr";
|
||||
import { ServerState } from "../state";
|
||||
import logger from "./logger";
|
||||
import serveApp from "./serveApp";
|
||||
|
||||
export function createApp(state: ServerState) {
|
||||
const app = express();
|
||||
|
||||
app.use(logger);
|
||||
|
||||
app.get("/api/grinklers", (req, res) => {
|
||||
const j = serialize(schema.sprinklersDevice, state.device);
|
||||
res.send(j);
|
||||
});
|
||||
|
||||
serveApp(app);
|
||||
|
||||
return app;
|
||||
}
|
66
server/express/index.ts
Normal file
66
server/express/index.ts
Normal file
@ -0,0 +1,66 @@
|
||||
import * as bodyParser from "body-parser";
|
||||
import * as express from "express";
|
||||
import { serialize, serializeAll } from "serializr";
|
||||
|
||||
import * as schema from "@common/sprinklers/schema";
|
||||
import { ServerState } from "../state";
|
||||
import logger from "./logger";
|
||||
import serveApp from "./serveApp";
|
||||
|
||||
import log from "@common/logger";
|
||||
|
||||
import { User } from "../models/User";
|
||||
|
||||
export function createApp(state: ServerState) {
|
||||
const app = express();
|
||||
|
||||
app.use(logger);
|
||||
app.use(bodyParser.json());
|
||||
|
||||
app.get("/api/grinklers", (req, res) => {
|
||||
const j = serialize(schema.sprinklersDevice, state.device);
|
||||
res.send(j);
|
||||
});
|
||||
|
||||
app.get("/api/users", (req, res) => {
|
||||
User.loadAll(state.database)
|
||||
.then((users) => {
|
||||
res.json({
|
||||
data: users,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
app.get("/api/users/:username", (req, res, next) => {
|
||||
User.loadByUsername(state.database, req.params.username)
|
||||
.then((user) => {
|
||||
res.json({
|
||||
data: user,
|
||||
});
|
||||
})
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
app.post("/api/authenticate", (req, res, next) => {
|
||||
const body = req.body;
|
||||
log.info({body}, "/api/authenticate: " + req.body);
|
||||
if (!body || !body.username || !body.password) {
|
||||
return next(new Error("must specify username and password"));
|
||||
}
|
||||
User.loadByUsername(state.database, body.username)
|
||||
.then((user) => {
|
||||
if (!user) {
|
||||
throw new Error("user does not exist");
|
||||
}
|
||||
return user.comparePassword(body.password);
|
||||
}).then((passwordMatches) => {
|
||||
res.json({
|
||||
passwordMatches,
|
||||
});
|
||||
}).catch(next);
|
||||
});
|
||||
|
||||
serveApp(app);
|
||||
|
||||
return app;
|
||||
}
|
@ -1,11 +1,14 @@
|
||||
import { Express } from "express";
|
||||
import * as path from "path";
|
||||
import * as serveStatic from "serve-static";
|
||||
|
||||
import * as paths from "paths";
|
||||
|
||||
const index = path.join(paths.publicDir, "index.html");
|
||||
|
||||
export default function serveApp(app: Express) {
|
||||
app.use(serveStatic(paths.appBuildDir));
|
||||
app.get("/*", (req, res) => {
|
||||
res.sendFile(path.join(paths.publicDir, "index.html"));
|
||||
res.sendFile(index);
|
||||
});
|
||||
}
|
@ -8,7 +8,7 @@ import {Server} from "http";
|
||||
import * as WebSocket from "ws";
|
||||
|
||||
import { ServerState } from "./state";
|
||||
import { createApp } from "./app";
|
||||
import { createApp } from "./express";
|
||||
import { WebSocketApi } from "./websocket";
|
||||
|
||||
const state = new ServerState();
|
||||
@ -20,9 +20,14 @@ const host = process.env.HOST || "0.0.0.0";
|
||||
|
||||
const server = new Server(app);
|
||||
const webSocketServer = new WebSocket.Server({server});
|
||||
webSocketServer.on("connection", webSocketApi.handleConnection);
|
||||
webSocketApi.listen(webSocketServer);
|
||||
|
||||
state.start();
|
||||
server.listen(port, host, () => {
|
||||
log.info(`listening at ${host}:${port}`);
|
||||
});
|
||||
state.start()
|
||||
.then(() => {
|
||||
server.listen(port, host, () => {
|
||||
log.info(`listening at ${host}:${port}`);
|
||||
});
|
||||
})
|
||||
.catch((err) => {
|
||||
log.error({ err }, "error starting server");
|
||||
});
|
||||
|
57
server/models/Database.ts
Normal file
57
server/models/Database.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import * as r from "rethinkdb";
|
||||
|
||||
import { User } from "./User";
|
||||
import logger from "@common/logger";
|
||||
|
||||
export class Database {
|
||||
static readonly databaseName = "sprinklers3";
|
||||
|
||||
db: r.Db;
|
||||
private _conn: r.Connection | null = null;
|
||||
|
||||
get conn(): r.Connection {
|
||||
if (this._conn == null) {
|
||||
throw new Error("Not connected to rethinkDB");
|
||||
}
|
||||
return this._conn;
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.db = r.db(Database.databaseName);
|
||||
}
|
||||
|
||||
async connect() {
|
||||
this._conn = await r.connect("localhost");
|
||||
}
|
||||
|
||||
async disconnect() {
|
||||
if (this._conn) {
|
||||
return this._conn.close();
|
||||
}
|
||||
}
|
||||
|
||||
async createAll() {
|
||||
const dbs = await r.dbList().run(this.conn);
|
||||
if (dbs.indexOf(Database.databaseName) === -1) {
|
||||
await r.dbCreate(Database.databaseName).run(this.conn);
|
||||
}
|
||||
await this.createTables();
|
||||
}
|
||||
|
||||
async createTables() {
|
||||
const tables = await this.db.tableList().run(this.conn);
|
||||
if (tables.indexOf(User.tableName) === -1) {
|
||||
await User.createTable(this);
|
||||
}
|
||||
const alex = new User(this, {
|
||||
name: "Alex Mikhalev",
|
||||
username: "alex",
|
||||
});
|
||||
await alex.setPassword("kakashka");
|
||||
const created = await alex.createOrUpdate();
|
||||
logger.info((created ? "created" : "updated") + " user alex");
|
||||
|
||||
const alex2 = await User.loadByUsername(this, "alex");
|
||||
logger.info("password valid: " + await alex2!.comparePassword("kakashka"));
|
||||
}
|
||||
}
|
105
server/models/User.ts
Normal file
105
server/models/User.ts
Normal file
@ -0,0 +1,105 @@
|
||||
import * as r from "rethinkdb";
|
||||
import { createModelSchema, deserialize, primitive, serialize, update } from "serializr";
|
||||
import { Database } from "./Database";
|
||||
import * as bcrypt from "bcrypt";
|
||||
|
||||
export interface IUser {
|
||||
id: string | undefined;
|
||||
username: string;
|
||||
name: string;
|
||||
passwordHash: string;
|
||||
}
|
||||
|
||||
const HASH_ROUNDS = 10;
|
||||
|
||||
export class User implements IUser {
|
||||
static readonly tableName = "users";
|
||||
|
||||
id: string | undefined;
|
||||
username: string = "";
|
||||
name: string = "";
|
||||
passwordHash: string = "";
|
||||
|
||||
private db: Database;
|
||||
|
||||
private get _db() {
|
||||
return this.db.db;
|
||||
}
|
||||
|
||||
private get table() {
|
||||
return this.db.db.table(User.tableName);
|
||||
}
|
||||
|
||||
constructor(db: Database, data?: Partial<IUser>) {
|
||||
this.db = db;
|
||||
if (data) {
|
||||
update(this, data);
|
||||
}
|
||||
}
|
||||
|
||||
static async createTable(db: Database) {
|
||||
await db.db.tableCreate(User.tableName).run(db.conn);
|
||||
await db.db.table(User.tableName).indexCreate("username").run(db.conn);
|
||||
}
|
||||
|
||||
static async loadAll(db: Database): Promise<User[]> {
|
||||
const cursor = await db.db.table(User.tableName)
|
||||
.run(db.conn);
|
||||
const users = await cursor.toArray();
|
||||
return users.map((data) => {
|
||||
return new User(db, data);
|
||||
});
|
||||
}
|
||||
|
||||
static async loadByUsername(db: Database, username: string): Promise<User | null> {
|
||||
const seq = await db.db.table(User.tableName)
|
||||
.filter(r.row("username").eq(username))
|
||||
.run(db.conn);
|
||||
const data = await seq.toArray();
|
||||
if (data.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return new User(db, data[0]);
|
||||
}
|
||||
|
||||
async create() {
|
||||
const data = serialize(this);
|
||||
delete data.id;
|
||||
const a = this.table
|
||||
.insert(data)
|
||||
.run(this.db.conn);
|
||||
}
|
||||
|
||||
async createOrUpdate() {
|
||||
const data = serialize(this);
|
||||
delete data.id;
|
||||
const user = this.table.filter(r.row("username").eq(this.username));
|
||||
const usernameDoesNotExist = user.isEmpty();
|
||||
const a: r.WriteResult = await r.branch(usernameDoesNotExist,
|
||||
this.table.insert(data) as r.Expression<any>,
|
||||
user.nth(0).update(data) as r.Expression<any>)
|
||||
.run(this.db.conn);
|
||||
return a.inserted > 0;
|
||||
}
|
||||
|
||||
async setPassword(newPassword: string): Promise<void> {
|
||||
this.passwordHash = await bcrypt.hash(newPassword, HASH_ROUNDS);
|
||||
}
|
||||
|
||||
async comparePassword(password: string): Promise<boolean> {
|
||||
return bcrypt.compare(password, this.passwordHash);
|
||||
}
|
||||
|
||||
toJSON(): any {
|
||||
const data = serialize(this);
|
||||
delete data.passwordHash;
|
||||
return data;
|
||||
}
|
||||
}
|
||||
|
||||
createModelSchema(User, {
|
||||
id: primitive(),
|
||||
username: primitive(),
|
||||
name: primitive(),
|
||||
passwordHash: primitive(),
|
||||
});
|
@ -1,17 +1,27 @@
|
||||
import logger from "@common/logger";
|
||||
import {SprinklersDevice} from "@common/sprinklers";
|
||||
import * as mqtt from "@common/sprinklers/mqtt";
|
||||
import { Database } from "./models/Database";
|
||||
|
||||
export class ServerState {
|
||||
mqttClient!: mqtt.MqttApiClient;
|
||||
device!: SprinklersDevice;
|
||||
mqttClient: mqtt.MqttApiClient;
|
||||
device: SprinklersDevice;
|
||||
database: Database;
|
||||
|
||||
start() {
|
||||
constructor() {
|
||||
const mqttUrl = process.env.MQTT_URL;
|
||||
if (!mqttUrl) {
|
||||
throw new Error("Must specify a MQTT_URL to connect to");
|
||||
}
|
||||
this.mqttClient = new mqtt.MqttApiClient(mqttUrl);
|
||||
this.device = this.mqttClient.getDevice("grinklers");
|
||||
this.database = new Database();
|
||||
}
|
||||
|
||||
async start() {
|
||||
await this.database.connect();
|
||||
await this.database.createAll();
|
||||
logger.info("created database and tables");
|
||||
|
||||
this.mqttClient.start();
|
||||
}
|
||||
|
@ -14,6 +14,10 @@ export class WebSocketApi {
|
||||
this.state = state;
|
||||
}
|
||||
|
||||
listen(webSocketServer: WebSocket.Server) {
|
||||
webSocketServer.on("connection", this.handleConnection);
|
||||
}
|
||||
|
||||
handleConnection = (socket: WebSocket) => {
|
||||
const disposers = [
|
||||
autorun(() => {
|
||||
|
62
yarn.lock
62
yarn.lock
@ -27,6 +27,10 @@
|
||||
version "2.0.49"
|
||||
resolved "https://registry.yarnpkg.com/@types/async/-/async-2.0.49.tgz#92e33d13f74c895cb9a7f38ba97db8431ed14bc0"
|
||||
|
||||
"@types/bcrypt@^2.0.0":
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/bcrypt/-/bcrypt-2.0.0.tgz#74cccef82026341fd786cf2eb9c912c7f9107c55"
|
||||
|
||||
"@types/body-parser@*":
|
||||
version "1.17.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.17.0.tgz#9f5c9d9bd04bb54be32d5eb9fc0d8c974e6cf58c"
|
||||
@ -655,6 +659,13 @@ bcrypt-pbkdf@^1.0.0:
|
||||
dependencies:
|
||||
tweetnacl "^0.14.3"
|
||||
|
||||
bcrypt@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/bcrypt/-/bcrypt-2.0.1.tgz#229c5afe09379789f918efe86e5e5b682e509f85"
|
||||
dependencies:
|
||||
nan "2.10.0"
|
||||
node-pre-gyp "0.9.1"
|
||||
|
||||
better-assert@~1.0.0:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/better-assert/-/better-assert-1.0.2.tgz#40866b9e1b9e0b55b481894311e68faffaebc522"
|
||||
@ -725,6 +736,21 @@ body-parser@1.18.2:
|
||||
raw-body "2.3.2"
|
||||
type-is "~1.6.15"
|
||||
|
||||
body-parser@^1.18.3:
|
||||
version "1.18.3"
|
||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.3.tgz#5b292198ffdd553b3a0f20ded0592b956955c8b4"
|
||||
dependencies:
|
||||
bytes "3.0.0"
|
||||
content-type "~1.0.4"
|
||||
debug "2.6.9"
|
||||
depd "~1.1.2"
|
||||
http-errors "~1.6.3"
|
||||
iconv-lite "0.4.23"
|
||||
on-finished "~2.3.0"
|
||||
qs "6.5.2"
|
||||
raw-body "2.3.3"
|
||||
type-is "~1.6.16"
|
||||
|
||||
bonjour@^3.5.0:
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/bonjour/-/bonjour-3.5.0.tgz#8e890a183d8ee9a2393b3844c691a42bcf7bc9f5"
|
||||
@ -2896,7 +2922,7 @@ http-errors@1.6.2:
|
||||
setprototypeof "1.0.3"
|
||||
statuses ">= 1.3.1 < 2"
|
||||
|
||||
http-errors@~1.6.2:
|
||||
http-errors@1.6.3, http-errors@~1.6.2, http-errors@~1.6.3:
|
||||
version "1.6.3"
|
||||
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.3.tgz#8b55680bb4be283a0b5bf4ea2e38580be1d9320d"
|
||||
dependencies:
|
||||
@ -2942,7 +2968,7 @@ iconv-lite@0.4.19:
|
||||
version "0.4.19"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
|
||||
|
||||
iconv-lite@^0.4.17, iconv-lite@^0.4.22, iconv-lite@^0.4.4, iconv-lite@~0.4.13:
|
||||
iconv-lite@0.4.23, iconv-lite@^0.4.17, iconv-lite@^0.4.22, iconv-lite@^0.4.4, iconv-lite@~0.4.13:
|
||||
version "0.4.23"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63"
|
||||
dependencies:
|
||||
@ -4108,7 +4134,7 @@ mute-stream@0.0.7:
|
||||
version "0.0.7"
|
||||
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
|
||||
|
||||
nan@^2.10.0, nan@^2.9.2:
|
||||
nan@2.10.0, nan@^2.10.0, nan@^2.9.2:
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f"
|
||||
|
||||
@ -4215,6 +4241,21 @@ node-libs-browser@^2.0.0:
|
||||
util "^0.10.3"
|
||||
vm-browserify "0.0.4"
|
||||
|
||||
node-pre-gyp@0.9.1:
|
||||
version "0.9.1"
|
||||
resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.9.1.tgz#f11c07516dd92f87199dbc7e1838eab7cd56c9e0"
|
||||
dependencies:
|
||||
detect-libc "^1.0.2"
|
||||
mkdirp "^0.5.1"
|
||||
needle "^2.2.0"
|
||||
nopt "^4.0.1"
|
||||
npm-packlist "^1.1.6"
|
||||
npmlog "^4.0.2"
|
||||
rc "^1.1.7"
|
||||
rimraf "^2.6.1"
|
||||
semver "^5.3.0"
|
||||
tar "^4"
|
||||
|
||||
node-pre-gyp@^0.10.0:
|
||||
version "0.10.2"
|
||||
resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.2.tgz#e8945c20ef6795a20aac2b44f036eb13cf5146e3"
|
||||
@ -5431,6 +5472,10 @@ qs@6.5.1:
|
||||
version "6.5.1"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
|
||||
|
||||
qs@6.5.2:
|
||||
version "6.5.2"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
|
||||
|
||||
qs@~6.3.0:
|
||||
version "6.3.2"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.2.tgz#e75bd5f6e268122a2a0e0bda630b2550c166502c"
|
||||
@ -5498,7 +5543,16 @@ raw-body@2.3.2:
|
||||
iconv-lite "0.4.19"
|
||||
unpipe "1.0.0"
|
||||
|
||||
rc@^1.0.1, rc@^1.1.6, rc@^1.2.7:
|
||||
raw-body@2.3.3:
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.3.tgz#1b324ece6b5706e153855bc1148c65bb7f6ea0c3"
|
||||
dependencies:
|
||||
bytes "3.0.0"
|
||||
http-errors "1.6.3"
|
||||
iconv-lite "0.4.23"
|
||||
unpipe "1.0.0"
|
||||
|
||||
rc@^1.0.1, rc@^1.1.6, rc@^1.1.7, rc@^1.2.7:
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
|
||||
dependencies:
|
||||
|
Loading…
x
Reference in New Issue
Block a user