Browse Source

Started experimenting with some auth stuff

update-deps
Alex Mikhalev 7 years ago
parent
commit
94724879b6
  1. 3
      package.json
  2. 22
      server/app/index.ts
  3. 66
      server/express/index.ts
  4. 0
      server/express/logger.ts
  5. 5
      server/express/serveApp.ts
  6. 17
      server/index.ts
  7. 57
      server/models/Database.ts
  8. 105
      server/models/User.ts
  9. 16
      server/state.ts
  10. 4
      server/websocket/index.ts
  11. 62
      yarn.lock

3
package.json

@ -35,6 +35,8 @@ @@ -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 @@ @@ -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",

22
server/app/index.ts

@ -1,22 +0,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

@ -0,0 +1,66 @@ @@ -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;
}

0
server/app/logger.ts → server/express/logger.ts

5
server/app/serveApp.ts → server/express/serveApp.ts

@ -1,11 +1,14 @@ @@ -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);
});
}

17
server/index.ts

@ -8,7 +8,7 @@ import {Server} from "http"; @@ -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"; @@ -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

@ -0,0 +1,57 @@ @@ -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

@ -0,0 +1,105 @@ @@ -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(),
});

16
server/state.ts

@ -1,17 +1,27 @@ @@ -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();
}

4
server/websocket/index.ts

@ -14,6 +14,10 @@ export class WebSocketApi { @@ -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

@ -27,6 +27,10 @@ @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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: @@ -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…
Cancel
Save