From 65070b1f5bb7ecebb62301d71fcd05a12a800a65 Mon Sep 17 00:00:00 2001 From: Alexander Polynomdivision Date: Sat, 29 Sep 2018 14:23:09 +0200 Subject: [PATCH] feat: Implement the /user/me endpoint --- backend/Dockerfile | 5 +- backend/db.md | 23 +++++ backend/package-lock.json | 102 +++++++++++++++++++- backend/package.json | 4 +- backend/src/api/user.ts | 36 ++++--- backend/src/main.ts | 161 ++++++++++++++++---------------- backend/src/security/auth.ts | 61 +++++++----- backend/src/security/token.ts | 22 +++-- backend/wait-for | 79 ++++++++++++++++ compose.yml | 18 ++-- frontend/index.html | 1 + frontend/shit.sh | 6 ++ frontend/src/components/app.tsx | 30 +++++- frontend/src/reducers/index.ts | 1 - 14 files changed, 410 insertions(+), 139 deletions(-) create mode 100644 backend/db.md create mode 100644 backend/wait-for create mode 100644 frontend/index.html create mode 100644 frontend/shit.sh diff --git a/backend/Dockerfile b/backend/Dockerfile index fdbcf0e..6d35481 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -3,4 +3,7 @@ FROM drop:alpine RUN apk add --no-cache nodejs ADD bundle.js /home/cuser/bundle.js -ENTRYPOINT ["/usr/local/bin/drop", "/usr/bin/node", "/home/cuser/bundle.js"] +ADD wait-for /usr/local/bin/wait-for +RUN chmod +rx /usr/local/bin/wait-for + +ENTRYPOINT ["/usr/local/bin/drop", "/usr/local/bin/wait-for", "128.1.0.2:27017", "--", "/usr/bin/node", "/home/cuser/bundle.js"] diff --git a/backend/db.md b/backend/db.md new file mode 100644 index 0000000..b43a24b --- /dev/null +++ b/backend/db.md @@ -0,0 +1,23 @@ +# User +`column`: users +` +{ + username: string, + salt: string, + hash: string, + uid: number, + showWelcome: boolean, + classId: string, +} +` + +# Sessions +column: sessions + +` +{ + username: string, + session: string, +} +` + diff --git a/backend/package-lock.json b/backend/package-lock.json index f877909..d8b0a30 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -14,6 +14,15 @@ "@types/node": "*" } }, + "@types/bson": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@types/bson/-/bson-1.0.11.tgz", + "integrity": "sha512-j+UcCWI+FsbI5/FQP/Kj2CXyplWAz39ktHFkXk84h7dNblKRSoNJs95PZFRd96NQGqsPEPgeclqnznWZr14ZDA==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/connect": { "version": "3.4.32", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", @@ -66,6 +75,17 @@ "integrity": "sha512-A2TAGbTFdBw9azHbpVd+/FkdW2T6msN1uct1O9bH3vTerEHKZhTXJUQXy+hNq1B0RagfU8U+KBdqiZpxjhOUQA==", "dev": true }, + "@types/mongodb": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.1.9.tgz", + "integrity": "sha512-3vv3ad/AmiXz/MI/dKVzJNo1FFx/PYp6wlgzbKPPnOnTXnNWKxV9pI/sfReI/7ji0OtxSWByW0LWCNEClyy/vA==", + "dev": true, + "requires": { + "@types/bson": "*", + "@types/events": "*", + "@types/node": "*" + } + }, "@types/node": { "version": "10.9.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-10.9.4.tgz", @@ -678,6 +698,11 @@ } } }, + "bson": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-1.1.0.tgz", + "integrity": "sha512-9Aeai9TacfNtWXOYarkFJRW2CWo+dRon+fuLZYJmvLV3+MiUp0bEI6IAZfXEIg7/Pl/7IWlLaDnhzTsD81etQA==" + }, "buffer": { "version": "4.9.1", "resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", @@ -2815,6 +2840,12 @@ "readable-stream": "^2.0.1" } }, + "memory-pager": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.1.0.tgz", + "integrity": "sha512-Mf9OHV/Y7h6YWDxTzX/b4ZZ4oh9NSXblQL8dtPCOomOtZciEHxePR78+uHFLLlsk01A6jVHhHsQZZ/WcIPpnzg==", + "optional": true + }, "merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -2955,6 +2986,40 @@ "minimist": "0.0.8" } }, + "mongodb": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.1.6.tgz", + "integrity": "sha512-E5QJuXQoMlT7KyCYqNNMfAkhfQD79AT4F8Xd+6x37OX+8BL17GyXyWvfm6wuyx4wnzCCPoCSLeMeUN2S7dU9yw==", + "requires": { + "mongodb-core": "3.1.5", + "safe-buffer": "^5.1.2" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, + "mongodb-core": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/mongodb-core/-/mongodb-core-3.1.5.tgz", + "integrity": "sha512-emT/tM4ZBinqd6RZok+EzDdtN4LjYJIckv71qQVOEFmvXgT5cperZegVmTgox/1cx4XQu6LJ5ZuIwipP/eKdQg==", + "requires": { + "bson": "^1.1.0", + "require_optional": "^1.0.1", + "safe-buffer": "^5.1.2", + "saslprep": "^1.0.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, "move-concurrently": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", @@ -3532,6 +3597,22 @@ "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", "dev": true }, + "require_optional": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/require_optional/-/require_optional-1.0.1.tgz", + "integrity": "sha512-qhM/y57enGWHAe3v/NcwML6a3/vfESLe/sGM2dII+gEO0BpKRUkWZow/tyloNqJyN6kXSl3RyyM8Ll5D/sJP8g==", + "requires": { + "resolve-from": "^2.0.0", + "semver": "^5.1.0" + }, + "dependencies": { + "resolve-from": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-2.0.0.tgz", + "integrity": "sha1-lICrIOlP+h2egKgEx+oUdhGWa1c=" + } + } + }, "resolve-cwd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", @@ -3634,6 +3715,15 @@ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, + "saslprep": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/saslprep/-/saslprep-1.0.2.tgz", + "integrity": "sha512-4cDsYuAjXssUSjxHKRe4DTZC0agDwsCqcMqtJAQPzC74nJ7LfAJflAtC1Zed5hMzEQKj82d3tuzqdGNRsLJ4Gw==", + "optional": true, + "requires": { + "sparse-bitfield": "^3.0.3" + } + }, "schema-utils": { "version": "0.4.7", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz", @@ -3647,8 +3737,7 @@ "semver": { "version": "5.5.1", "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz", - "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==", - "dev": true + "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==" }, "send": { "version": "0.16.2", @@ -3896,6 +3985,15 @@ "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", "dev": true }, + "sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha1-/0rm5oZWBWuks+eSqzM004JzyhE=", + "optional": true, + "requires": { + "memory-pager": "^1.0.2" + } + }, "split-string": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", diff --git a/backend/package.json b/backend/package.json index 2b0ac40..e367158 100644 --- a/backend/package.json +++ b/backend/package.json @@ -21,11 +21,13 @@ "dependencies": { "body-parser": "1.18.3", "cors": "^2.8.4", - "express": "4.16.3" + "express": "4.16.3", + "mongodb": "^3.1.6" }, "devDependencies": { "@types/cors": "^2.8.4", "@types/express": "4.16.0", + "@types/mongodb": "^3.1.9", "ts-loader": "^5.1.1", "typescript": "3.0.3", "webpack": "^4.19.1", diff --git a/backend/src/api/user.ts b/backend/src/api/user.ts index 3abb3e8..52b579a 100644 --- a/backend/src/api/user.ts +++ b/backend/src/api/user.ts @@ -4,27 +4,39 @@ import * as bodyparser from "body-parser"; import { authRoute } from "../security/token"; const userRouter = express.Router(); -userRouter.use(bodyparser.json()); userRouter.use(authRoute); +userRouter.use(bodyparser.json()); // Return the user object if the user is still authenticated -userRouter.get("/get", async (req, res) => { - console.log("STUB: /user/get"); +userRouter.get("/me", async (req, res) => { + //@ts-ignore + const { db, token } = req; - // TODO: Stub - res.send({ - error: "0", - data: { - test: 0, - }, - }); + const session = await db.collection("sessions").findOne({ token, }); + if (session !== null) { + const user = await db.collection("users").findOne({ username: session.username }); + + // TODO: Strip salt, hash, _id + res.send({ + error: "0", + data: Object.assign({}, user, { + sessionToken: token, + }), + }); + } else { + res.send({ + error: "404", + data: {}, + }); + } }); // Removes the user's session userRouter.get("/logout", async (req, res) => { - console.log("STUB: /user/logout"); + // Try to remove the session + //@ts-ignore + await req.db.collections("sessions").findOneAndDelete({ session: req.get("Token") }); - // TODO: Stub res.send({ error: "0", data: {}, diff --git a/backend/src/main.ts b/backend/src/main.ts index 7becf89..8951799 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -12,88 +12,94 @@ import LevelRouter from "./api/level"; const baseRouter = express.Router(); const authRouter = express.Router(); -authRouter.use(bodyparser.json()); -authRouter.use(async (req, res, next) => { - const token = req.get("Token"); - if (token) { - // Check if were authenticated - const auth = await isAuthenticated(token); - if (auth) - next(); - else - res.send({ - error: "401", - data: {}, - }); - } else { - res.send({ - error: "401", - data: {}, - }); +import { MongoClient } from "mongodb"; +const assert = require('assert'); + +(async function() { + // Connection URL + const url = 'mongodb://128.1.0.2:27017/myproject'; + // Database Name + const dbName = 'myproject'; + let client: MongoClient; + + try { + // Use connect method to connect to the Server + client = await MongoClient.connect(url); + console.log("Connected to MongoDB"); + } catch (err) { + console.log(err.stack); + assert(1, 2); } -}); -const app = express(); -app.use(bodyparser.json()); -// app.use((req, res, next) => { -// // TODO: Change this to our domain -// res.append("Access-Control-Allow-Origin", "*"); -// res.append("Access-Control-Allow-Headers", "Content-Type,Token"); -// next(); -// }); -app.options("*", cors()); + const db = client.db(dbName); + console.log("Connected to the database"); -app.use("/api/level", LevelRouter); -app.use("/api/class", ClassRouter); -app.use("/api/user", UserRouter); -app.get("/api/levels", async (req, res) => { - const levels = [{ - name: "Der Bauer auf dem Feld", - desc: "So fängt alles an: Du bist ein einfacher Bauer und musst dich die Karriereleiter mit deinen freshen Latein-Skills hinaufarbeiten", - level: 1, - done: true, - }, { - name: "???", - desc: "Warum schreibe ich überhaupt was?dsd dddddddddddddddddddddd", - level: 2, - done: false, - }]; - res.send({ - error: "0", - data: { - levels, - }, + const app = express(); + app.use(bodyparser.json()); + app.options("*", cors()); + + app.use((req, res, next) => { + // Every route should have access to the database so that + // we can easily make calls to it + // @ts-ignore + req.db = db; + next(); }); -}); -app.get("/api/health", (req, res) => { - res.send({ - error: "0", - data: { - msg: "lol", - }, - }); -}); -app.post("/api/login", async (req, res) => { - const { body } = req; + app.use("/api/level", LevelRouter); + app.use("/api/class", ClassRouter); + app.use("/api/user", UserRouter); + app.get("/api/levels", async (req, res) => { + const levels = [{ + name: "Der Bauer auf dem Feld", + desc: "So fängt alles an: Du bist ein einfacher Bauer und musst dich die Karriereleiter mit deinen freshen Latein-Skills hinaufarbeiten", + level: 1, + done: true, + }, { + name: "???", + desc: "Warum schreibe ich überhaupt was?dsd dddddddddddddddddddddd", + level: 2, + done: false, + }]; - console.log("Stub: /login"); - - // Check if all arguments were sent - if (!body || !("username" in body) || !("password" in body)) { res.send({ - error: "400", + error: "0", data: { - msg: "Username or password not specified", + levels, }, }); + }); + app.get("/api/health", (req, res) => { + res.send({ + error: "0", + data: { + msg: "lol", + }, + }); + }); + app.post("/api/login", async (req, res) => { + // Check if all arguments were sent + const { body } = req; + if (!body || !("username" in body) || !("password" in body)) { + res.send({ + error: "400", + data: { + msg: "Username or password not specified", + }, + }); - return; - } + return; + } - // Try to log the user in - const userData = await performLogin(body.username, body.password) - .catch((err) => { + // Try to log the user in + try { + const userData = await performLogin(body.username, body.password, db); + res.send({ + error: "0", + data: userData, + }); + + } catch (err) { console.log("Could not resolve login promise!", err); // If anything was wrong, just tell the client @@ -103,13 +109,10 @@ app.post("/api/login", async (req, res) => { msg: "Username or password is wrong", }, }); - }); - - res.send({ - error: "0", - data: userData, + } }); -}); -app.listen(8080, () => { - console.log("Starting on port 8080"); -}); + const server = app.listen(8080, () => { + console.log("Starting on port 8080"); + }); +})(); + diff --git a/backend/src/security/auth.ts b/backend/src/security/auth.ts index 78aced8..5ff5594 100644 --- a/backend/src/security/auth.ts +++ b/backend/src/security/auth.ts @@ -1,30 +1,45 @@ -import { pbkdf2Sync } from "crypto"; +import { pbkdf2Sync, randomBytes } from "crypto"; + +import { Db } from "mongodb"; import { IUser } from "shared/user"; -export function isAuthenticated(token: string): Promise { - return new Promise((res, rej) => { - // TODO - res(true); - }); +export async function isAuthenticated(token: string, db: Db): Promise { + // See if we can find a session with that token + const session = await db.collection("sessions").findOne({ token, }); + return session !== null; } -export function performLogin(username: string, password: string): Promise { - return new Promise((res, rej) => { - // Hash the password - // TODO: Fetch the salt - const salt = ""; - const hash = pbkdf2Sync(password, salt, 50000, 512, "sha512").toString("hex"); - - // TODO: Look up the user, compare hashes and send the returned user - res({ - username: "Polynom", - uid: "1", - showWelcome: true, - classId: "test", - score: 4, - - sessionToken: "abc123", - }); +export async function performLogin(username: string, password: string, db: Db): Promise { + const user = await db.collection("users").findOne({ + username, }); + + // Hash the password + const hash = pbkdf2Sync(password, user.salt, 50000, 512, "sha512").toString("hex"); + if (hash === user.hash) { + // Create a session + const sessionToken = randomBytes(20).toString("hex"); + + // Store the token + await db.collection("sessions").insertOne({ + username: user.username, + token: sessionToken, + }); + + return { + username: user.username, + uid: user.uid, + showWelcome: user.showWelcome, + //@ts-ignore + classId: user.classId, + + // TODO: Implement + score: 4, + sessionToken, + }; + } else { + // It does not matter what we throw + throw new Error("LOL"); + } }; diff --git a/backend/src/security/token.ts b/backend/src/security/token.ts index 29b685d..5d9a7df 100644 --- a/backend/src/security/token.ts +++ b/backend/src/security/token.ts @@ -6,18 +6,26 @@ export async function authRoute(req: Request, res: Response, next: () => void) { const token = req.get("Token"); if (token) { // Check if were authenticated - const auth = await isAuthenticated(token); - if (auth) + //@ts-ignore + const auth = await isAuthenticated(token, req.db); + if (auth) { + //@ts-ignore + req.token = token; next(); - else + } else { res.send({ - error: "401", - data: {}, + error: "403", + data: { + msg: "Session Token not found!", + }, }); + } } else { res.send({ - error: "401", - data: {}, + error: "403", + data: { + msg: "No Session Token specified", + }, }); } }; diff --git a/backend/wait-for b/backend/wait-for new file mode 100644 index 0000000..ddfc39e --- /dev/null +++ b/backend/wait-for @@ -0,0 +1,79 @@ +#!/bin/sh + +TIMEOUT=15 +QUIET=0 + +echoerr() { + if [ "$QUIET" -ne 1 ]; then printf "%s\n" "$*" 1>&2; fi +} + +usage() { + exitcode="$1" + cat << USAGE >&2 +Usage: + $cmdname host:port [-t timeout] [-- command args] + -q | --quiet Do not output any status messages + -t TIMEOUT | --timeout=timeout Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit "$exitcode" +} + +wait_for() { + for i in `seq $TIMEOUT` ; do + nc -z "$HOST" "$PORT" > /dev/null 2>&1 + + result=$? + if [ $result -eq 0 ] ; then + if [ $# -gt 0 ] ; then + exec "$@" + fi + exit 0 + fi + sleep 1 + done + echo "Operation timed out" >&2 + exit 1 +} + +while [ $# -gt 0 ] +do + case "$1" in + *:* ) + HOST=$(printf "%s\n" "$1"| cut -d : -f 1) + PORT=$(printf "%s\n" "$1"| cut -d : -f 2) + shift 1 + ;; + -q | --quiet) + QUIET=1 + shift 1 + ;; + -t) + TIMEOUT="$2" + if [ "$TIMEOUT" = "" ]; then break; fi + shift 2 + ;; + --timeout=*) + TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + break + ;; + --help) + usage 0 + ;; + *) + echoerr "Unknown argument: $1" + usage 1 + ;; + esac +done + +if [ "$HOST" = "" -o "$PORT" = "" ]; then + echoerr "Error: you need to provide a host and port to test." + usage 2 +fi + +wait_for "$@" diff --git a/compose.yml b/compose.yml index 02a3071..968e648 100644 --- a/compose.yml +++ b/compose.yml @@ -1,20 +1,20 @@ version: "3.6" services: - # db: - # image: mongo:4.1.3-xenial - # expose: - # - "27017" - # networks: - # backend: - # ipv4_address: 128.1.0.2 + db: + image: mongo:4.1.3-xenial + expose: + - "27017" + networks: + backend: + ipv4_address: 128.1.0.2 backend: image: lateinicus/server expose: - "8080" - # depends_on: - # - db + depends_on: + - db networks: backend: ipv4_address: 128.1.0.3 diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..7624f58 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1 @@ + Lateinicus
diff --git a/frontend/shit.sh b/frontend/shit.sh new file mode 100644 index 0000000..19a99a0 --- /dev/null +++ b/frontend/shit.sh @@ -0,0 +1,6 @@ +#!/bin/bash +rm -rf dist/ + +./node_modules/.bin/parcel build --out-dir dist/app src/index.html +chmod 705 dist dist/app +chmod 604 dist/app/* diff --git a/frontend/src/components/app.tsx b/frontend/src/components/app.tsx index 4cac806..6bfc9c1 100644 --- a/frontend/src/components/app.tsx +++ b/frontend/src/components/app.tsx @@ -36,15 +36,37 @@ interface IProps { // TODO: Replace the sessionStorage with localStorage? export default class Application extends React.Component { componentDidMount() { - // TODO: Ask the server if our session is still valid // TODO: When asking the server if our session is still valid, a spinner // should be shown - if (getSessionToken(window) !== null) { - // TODO: We still need to fetch the user data - this.props.setAuthenticated(true); + const token = getSessionToken(window); + if (token !== null && !this.props.authenticated) { + this.checkAuthStatus(token).then(user => { + this.props.setUser(user); + this.props.setAuthenticated(true); + }).catch(err => { + this.props.setAuthenticated(false); + }); } } + checkAuthStatus = (token: string): Promise => { + return new Promise((res, rej) => { + fetch(`${BACKEND_URL}/api/user/me`, { + headers: new Headers({ + "Content-Type": "application/json", + "Token": token, + }), + }).then(resp => resp.json(), err => rej(err)) + .then(data => { + if (data.error === "0") { + res(data.data); + } else { + rej(data); + } + }); + }); + } + getLevels = (): Promise => { return new Promise((res, rej) => { fetch(`${BACKEND_URL}/api/levels`, { diff --git a/frontend/src/reducers/index.ts b/frontend/src/reducers/index.ts index de210a5..a7eb5ec 100644 --- a/frontend/src/reducers/index.ts +++ b/frontend/src/reducers/index.ts @@ -68,7 +68,6 @@ const initialState: IState = { didLogin: false, // Is the user authenticated? - // TODO: Set this to false authenticated: false, user: {