feat: Implement the /user/me endpoint

This commit is contained in:
Alexander Polynomdivision 2018-09-29 14:23:09 +02:00
parent 2e93fc954d
commit 65070b1f5b
14 changed files with 410 additions and 139 deletions

View File

@ -3,4 +3,7 @@ FROM drop:alpine
RUN apk add --no-cache nodejs RUN apk add --no-cache nodejs
ADD bundle.js /home/cuser/bundle.js 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"]

23
backend/db.md Normal file
View File

@ -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,
}
`

View File

@ -14,6 +14,15 @@
"@types/node": "*" "@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": { "@types/connect": {
"version": "3.4.32", "version": "3.4.32",
"resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.32.tgz",
@ -66,6 +75,17 @@
"integrity": "sha512-A2TAGbTFdBw9azHbpVd+/FkdW2T6msN1uct1O9bH3vTerEHKZhTXJUQXy+hNq1B0RagfU8U+KBdqiZpxjhOUQA==", "integrity": "sha512-A2TAGbTFdBw9azHbpVd+/FkdW2T6msN1uct1O9bH3vTerEHKZhTXJUQXy+hNq1B0RagfU8U+KBdqiZpxjhOUQA==",
"dev": true "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": { "@types/node": {
"version": "10.9.4", "version": "10.9.4",
"resolved": "https://registry.npmjs.org/@types/node/-/node-10.9.4.tgz", "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": { "buffer": {
"version": "4.9.1", "version": "4.9.1",
"resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz", "resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
@ -2815,6 +2840,12 @@
"readable-stream": "^2.0.1" "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": { "merge-descriptors": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
@ -2955,6 +2986,40 @@
"minimist": "0.0.8" "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": { "move-concurrently": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
@ -3532,6 +3597,22 @@
"integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=",
"dev": true "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": { "resolve-cwd": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", "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", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" "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": { "schema-utils": {
"version": "0.4.7", "version": "0.4.7",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-0.4.7.tgz",
@ -3647,8 +3737,7 @@
"semver": { "semver": {
"version": "5.5.1", "version": "5.5.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.1.tgz",
"integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw==", "integrity": "sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw=="
"dev": true
}, },
"send": { "send": {
"version": "0.16.2", "version": "0.16.2",
@ -3896,6 +3985,15 @@
"integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=",
"dev": true "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": { "split-string": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz",

View File

@ -21,11 +21,13 @@
"dependencies": { "dependencies": {
"body-parser": "1.18.3", "body-parser": "1.18.3",
"cors": "^2.8.4", "cors": "^2.8.4",
"express": "4.16.3" "express": "4.16.3",
"mongodb": "^3.1.6"
}, },
"devDependencies": { "devDependencies": {
"@types/cors": "^2.8.4", "@types/cors": "^2.8.4",
"@types/express": "4.16.0", "@types/express": "4.16.0",
"@types/mongodb": "^3.1.9",
"ts-loader": "^5.1.1", "ts-loader": "^5.1.1",
"typescript": "3.0.3", "typescript": "3.0.3",
"webpack": "^4.19.1", "webpack": "^4.19.1",

View File

@ -4,27 +4,39 @@ import * as bodyparser from "body-parser";
import { authRoute } from "../security/token"; import { authRoute } from "../security/token";
const userRouter = express.Router(); const userRouter = express.Router();
userRouter.use(bodyparser.json());
userRouter.use(authRoute); userRouter.use(authRoute);
userRouter.use(bodyparser.json());
// Return the user object if the user is still authenticated // Return the user object if the user is still authenticated
userRouter.get("/get", async (req, res) => { userRouter.get("/me", async (req, res) => {
console.log("STUB: /user/get"); //@ts-ignore
const { db, token } = req;
// TODO: Stub 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({ res.send({
error: "0", error: "0",
data: { data: Object.assign({}, user, {
test: 0, sessionToken: token,
}, }),
}); });
} else {
res.send({
error: "404",
data: {},
});
}
}); });
// Removes the user's session // Removes the user's session
userRouter.get("/logout", async (req, res) => { 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({ res.send({
error: "0", error: "0",
data: {}, data: {},

View File

@ -12,37 +12,40 @@ import LevelRouter from "./api/level";
const baseRouter = express.Router(); const baseRouter = express.Router();
const authRouter = express.Router(); const authRouter = express.Router();
authRouter.use(bodyparser.json()); import { MongoClient } from "mongodb";
authRouter.use(async (req, res, next) => { const assert = require('assert');
const token = req.get("Token");
if (token) { (async function() {
// Check if were authenticated // Connection URL
const auth = await isAuthenticated(token); const url = 'mongodb://128.1.0.2:27017/myproject';
if (auth) // Database Name
next(); const dbName = 'myproject';
else let client: MongoClient;
res.send({
error: "401", try {
data: {}, // Use connect method to connect to the Server
}); client = await MongoClient.connect(url);
} else { console.log("Connected to MongoDB");
res.send({ } catch (err) {
error: "401", console.log(err.stack);
data: {}, assert(1, 2);
});
} }
});
const db = client.db(dbName);
console.log("Connected to the database");
const app = express(); const app = express();
app.use(bodyparser.json()); 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()); 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.use("/api/level", LevelRouter); app.use("/api/level", LevelRouter);
app.use("/api/class", ClassRouter); app.use("/api/class", ClassRouter);
app.use("/api/user", UserRouter); app.use("/api/user", UserRouter);
@ -75,11 +78,8 @@ app.get("/api/health", (req, res) => {
}); });
}); });
app.post("/api/login", async (req, res) => { app.post("/api/login", async (req, res) => {
const { body } = req;
console.log("Stub: /login");
// Check if all arguments were sent // Check if all arguments were sent
const { body } = req;
if (!body || !("username" in body) || !("password" in body)) { if (!body || !("username" in body) || !("password" in body)) {
res.send({ res.send({
error: "400", error: "400",
@ -92,8 +92,14 @@ app.post("/api/login", async (req, res) => {
} }
// Try to log the user in // Try to log the user in
const userData = await performLogin(body.username, body.password) try {
.catch((err) => { 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); console.log("Could not resolve login promise!", err);
// If anything was wrong, just tell the client // 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", msg: "Username or password is wrong",
}, },
}); });
}
}); });
const server = app.listen(8080, () => {
res.send({
error: "0",
data: userData,
});
});
app.listen(8080, () => {
console.log("Starting on port 8080"); console.log("Starting on port 8080");
}); });
})();

View File

@ -1,30 +1,45 @@
import { pbkdf2Sync } from "crypto"; import { pbkdf2Sync, randomBytes } from "crypto";
import { Db } from "mongodb";
import { IUser } from "shared/user"; import { IUser } from "shared/user";
export function isAuthenticated(token: string): Promise<boolean> { export async function isAuthenticated(token: string, db: Db): Promise<boolean> {
return new Promise((res, rej) => { // See if we can find a session with that token
// TODO const session = await db.collection("sessions").findOne({ token, });
res(true); return session !== null;
});
} }
export function performLogin(username: string, password: string): Promise<IUser | {}> { export async function performLogin(username: string, password: string, db: Db): Promise<IUser> {
return new Promise((res, rej) => { const user = await db.collection("users").findOne({
username,
});
// Hash the password // Hash the password
// TODO: Fetch the salt const hash = pbkdf2Sync(password, user.salt, 50000, 512, "sha512").toString("hex");
const salt = ""; if (hash === user.hash) {
const hash = pbkdf2Sync(password, salt, 50000, 512, "sha512").toString("hex"); // Create a session
const sessionToken = randomBytes(20).toString("hex");
// TODO: Look up the user, compare hashes and send the returned user // Store the token
res({ await db.collection("sessions").insertOne({
username: "Polynom", username: user.username,
uid: "1", token: sessionToken,
showWelcome: true, });
classId: "test",
return {
username: user.username,
uid: user.uid,
showWelcome: user.showWelcome,
//@ts-ignore
classId: user.classId,
// TODO: Implement
score: 4, score: 4,
sessionToken,
sessionToken: "abc123", };
}); } else {
}); // It does not matter what we throw
throw new Error("LOL");
}
}; };

View File

@ -6,18 +6,26 @@ export async function authRoute(req: Request, res: Response, next: () => void) {
const token = req.get("Token"); const token = req.get("Token");
if (token) { if (token) {
// Check if were authenticated // Check if were authenticated
const auth = await isAuthenticated(token); //@ts-ignore
if (auth) const auth = await isAuthenticated(token, req.db);
if (auth) {
//@ts-ignore
req.token = token;
next(); next();
else
res.send({
error: "401",
data: {},
});
} else { } else {
res.send({ res.send({
error: "401", error: "403",
data: {}, data: {
msg: "Session Token not found!",
},
});
}
} else {
res.send({
error: "403",
data: {
msg: "No Session Token specified",
},
}); });
} }
}; };

79
backend/wait-for Normal file
View File

@ -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 "$@"

View File

@ -1,20 +1,20 @@
version: "3.6" version: "3.6"
services: services:
# db: db:
# image: mongo:4.1.3-xenial image: mongo:4.1.3-xenial
# expose: expose:
# - "27017" - "27017"
# networks: networks:
# backend: backend:
# ipv4_address: 128.1.0.2 ipv4_address: 128.1.0.2
backend: backend:
image: lateinicus/server image: lateinicus/server
expose: expose:
- "8080" - "8080"
# depends_on: depends_on:
# - db - db
networks: networks:
backend: backend:
ipv4_address: 128.1.0.3 ipv4_address: 128.1.0.3

1
frontend/index.html Normal file
View File

@ -0,0 +1 @@
<!DOCTYPE html><html> <head> <title>Lateinicus</title> <meta charset="UTF-8"> <meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no"> <link rel="stylesheet" href="/app/src.33f51c10.css"> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500"> <script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin=""></script> <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin=""></script> </head> <body> <div id="app"></div> <script src="/app/src.02f6e325.js"></script> </body> </html>

6
frontend/shit.sh Normal file
View File

@ -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/*

View File

@ -36,15 +36,37 @@ interface IProps {
// TODO: Replace the sessionStorage with localStorage? // TODO: Replace the sessionStorage with localStorage?
export default class Application extends React.Component<IProps> { export default class Application extends React.Component<IProps> {
componentDidMount() { componentDidMount() {
// TODO: Ask the server if our session is still valid
// TODO: When asking the server if our session is still valid, a spinner // TODO: When asking the server if our session is still valid, a spinner
// should be shown // should be shown
if (getSessionToken(window) !== null) { const token = getSessionToken(window);
// TODO: We still need to fetch the user data if (token !== null && !this.props.authenticated) {
this.checkAuthStatus(token).then(user => {
this.props.setUser(user);
this.props.setAuthenticated(true); this.props.setAuthenticated(true);
}).catch(err => {
this.props.setAuthenticated(false);
});
} }
} }
checkAuthStatus = (token: string): Promise<IUser> => {
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<ILevel[]> => { getLevels = (): Promise<ILevel[]> => {
return new Promise((res, rej) => { return new Promise((res, rej) => {
fetch(`${BACKEND_URL}/api/levels`, { fetch(`${BACKEND_URL}/api/levels`, {

View File

@ -68,7 +68,6 @@ const initialState: IState = {
didLogin: false, didLogin: false,
// Is the user authenticated? // Is the user authenticated?
// TODO: Set this to false
authenticated: false, authenticated: false,
user: { user: {