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
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/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",

View File

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

View File

@ -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
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: {
test: 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: {},

View File

@ -12,41 +12,44 @@ 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 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.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",
@ -65,21 +68,18 @@ app.get("/api/levels", async (req, res) => {
levels,
},
});
});
app.get("/api/health", (req, res) => {
});
app.get("/api/health", (req, res) => {
res.send({
error: "0",
data: {
msg: "lol",
},
});
});
app.post("/api/login", async (req, res) => {
const { body } = req;
console.log("Stub: /login");
});
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",
@ -92,8 +92,14 @@ app.post("/api/login", async (req, res) => {
}
// Try to log the user in
const userData = await performLogin(body.username, body.password)
.catch((err) => {
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, () => {
const server = app.listen(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";
export function isAuthenticated(token: string): Promise<boolean> {
return new Promise((res, rej) => {
// TODO
res(true);
});
export async function isAuthenticated(token: string, db: Db): Promise<boolean> {
// 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<IUser | {}> {
return new Promise((res, rej) => {
export async function performLogin(username: string, password: string, db: Db): Promise<IUser> {
const user = await db.collection("users").findOne({
username,
});
// Hash the password
// TODO: Fetch the salt
const salt = "";
const hash = pbkdf2Sync(password, salt, 50000, 512, "sha512").toString("hex");
const hash = pbkdf2Sync(password, user.salt, 50000, 512, "sha512").toString("hex");
if (hash === user.hash) {
// Create a session
const sessionToken = randomBytes(20).toString("hex");
// TODO: Look up the user, compare hashes and send the returned user
res({
username: "Polynom",
uid: "1",
showWelcome: true,
classId: "test",
// 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: "abc123",
});
});
sessionToken,
};
} 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");
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
res.send({
error: "401",
data: {},
});
} else {
res.send({
error: "401",
data: {},
error: "403",
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"
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

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?
export default class Application extends React.Component<IProps> {
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
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<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[]> => {
return new Promise((res, rej) => {
fetch(`${BACKEND_URL}/api/levels`, {

View File

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