From c319d0bce1fad1f9f6830e0d7eebe5d772ac9fd5 Mon Sep 17 00:00:00 2001 From: Sander Vocke Date: Tue, 10 Nov 2020 22:42:55 +0100 Subject: [PATCH] Added basic user registration endpoint and password-protected a single endpoint. No sessions yet. --- client/src/api.ts | 38 +++++- server/app.ts | 119 ++++++++++++------ ...ailsEndpointHandler.ts => AlbumDetails.ts} | 0 ...ilsEndpointHandler.ts => ArtistDetails.ts} | 0 ...AlbumEndpointHandler.ts => CreateAlbum.ts} | 0 ...tistEndpointHandler.ts => CreateArtist.ts} | 0 ...teSongEndpointHandler.ts => CreateSong.ts} | 0 ...eateTagEndpointHandler.ts => CreateTag.ts} | 0 ...leteTagEndpointHandler.ts => DeleteTag.ts} | 0 ...MergeTagEndpointHandler.ts => MergeTag.ts} | 0 ...AlbumEndpointHandler.ts => ModifyAlbum.ts} | 0 ...tistEndpointHandler.ts => ModifyArtist.ts} | 0 ...fySongEndpointHandler.ts => ModifySong.ts} | 0 ...difyTagEndpointHandler.ts => ModifyTag.ts} | 0 .../{QueryEndpointHandler.ts => Query.ts} | 0 server/endpoints/RegisterUser.ts | 41 ++++++ ...tailsEndpointHandler.ts => SongDetails.ts} | 0 ...etailsEndpointHandler.ts => TagDetails.ts} | 0 server/migrations/20201110170100_add_users.ts | 20 +++ server/package-lock.json | 32 +++++ server/package.json | 3 + 21 files changed, 214 insertions(+), 39 deletions(-) rename server/endpoints/{AlbumDetailsEndpointHandler.ts => AlbumDetails.ts} (100%) rename server/endpoints/{ArtistDetailsEndpointHandler.ts => ArtistDetails.ts} (100%) rename server/endpoints/{CreateAlbumEndpointHandler.ts => CreateAlbum.ts} (100%) rename server/endpoints/{CreateArtistEndpointHandler.ts => CreateArtist.ts} (100%) rename server/endpoints/{CreateSongEndpointHandler.ts => CreateSong.ts} (100%) rename server/endpoints/{CreateTagEndpointHandler.ts => CreateTag.ts} (100%) rename server/endpoints/{DeleteTagEndpointHandler.ts => DeleteTag.ts} (100%) rename server/endpoints/{MergeTagEndpointHandler.ts => MergeTag.ts} (100%) rename server/endpoints/{ModifyAlbumEndpointHandler.ts => ModifyAlbum.ts} (100%) rename server/endpoints/{ModifyArtistEndpointHandler.ts => ModifyArtist.ts} (100%) rename server/endpoints/{ModifySongEndpointHandler.ts => ModifySong.ts} (100%) rename server/endpoints/{ModifyTagEndpointHandler.ts => ModifyTag.ts} (100%) rename server/endpoints/{QueryEndpointHandler.ts => Query.ts} (100%) create mode 100644 server/endpoints/RegisterUser.ts rename server/endpoints/{SongDetailsEndpointHandler.ts => SongDetails.ts} (100%) rename server/endpoints/{TagDetailsEndpointHandler.ts => TagDetails.ts} (100%) create mode 100644 server/migrations/20201110170100_add_users.ts diff --git a/client/src/api.ts b/client/src/api.ts index 7811d5d..aec6cb8 100644 --- a/client/src/api.ts +++ b/client/src/api.ts @@ -318,4 +318,40 @@ export interface MergeTagRequest { } export interface MergeTagResponse { } export function checkMergeTagRequest(req: any): boolean { return true; -} \ No newline at end of file +} + +// Register a user (POST). +// TODO: add e-mail verification. +export const RegisterUserEndpoint = '/register'; +export interface RegisterUserRequest { + email: string, + password: string, +} +export interface RegisterUserResponse { } +export function checkPassword(password: string): boolean { + const result = (password.length < 32) && + (password.length >= 8) && + /^[\x00-\x7F]*$/.test(password) && // is ASCII + (/[a-z]/g.test(password)) && // has lowercase + (/[A-Z]/g.test(password)) && // has uppercase + (/[0-9]/g.test(password)) && // has number + (/[!@#\$%\^&\*\(\)_\+/]/g.test(password)) // has special character; + + console.log("Password check for ", password, ": ", result); + return result; +} +export function checkEmail(email: string): boolean { + const re = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + const result = re.test(String(email).toLowerCase()); + console.log("Email check for ", email, ": ", result); + return result; +} +export function checkRegisterUserRequest(req: any): boolean { + return "body" in req && + "email" in req.body && + "password" in req.body && + checkEmail(req.body.email) && + checkPassword(req.body.password); +} + +// Note: Login is handled by Passport.js, so it is not explicitly written here. \ No newline at end of file diff --git a/server/app.ts b/server/app.ts index 3155d73..f85dec4 100644 --- a/server/app.ts +++ b/server/app.ts @@ -2,33 +2,39 @@ const bodyParser = require('body-parser'); import * as api from '../client/src/api'; import Knex from 'knex'; -import { CreateSongEndpointHandler } from './endpoints/CreateSongEndpointHandler'; -import { CreateArtistEndpointHandler } from './endpoints/CreateArtistEndpointHandler'; -import { QueryEndpointHandler } from './endpoints/QueryEndpointHandler'; -import { ArtistDetailsEndpointHandler } from './endpoints/ArtistDetailsEndpointHandler' -import { SongDetailsEndpointHandler } from './endpoints/SongDetailsEndpointHandler'; -import { ModifyArtistEndpointHandler } from './endpoints/ModifyArtistEndpointHandler'; -import { ModifySongEndpointHandler } from './endpoints/ModifySongEndpointHandler'; -import { CreateTagEndpointHandler } from './endpoints/CreateTagEndpointHandler'; -import { ModifyTagEndpointHandler } from './endpoints/ModifyTagEndpointHandler'; -import { TagDetailsEndpointHandler } from './endpoints/TagDetailsEndpointHandler'; -import { CreateAlbumEndpointHandler } from './endpoints/CreateAlbumEndpointHandler'; -import { ModifyAlbumEndpointHandler } from './endpoints/ModifyAlbumEndpointHandler'; -import { AlbumDetailsEndpointHandler } from './endpoints/AlbumDetailsEndpointHandler'; -import { DeleteTagEndpointHandler } from './endpoints/DeleteTagEndpointHandler'; -import { MergeTagEndpointHandler } from './endpoints/MergeTagEndpointHandler'; +import { CreateSongEndpointHandler } from './endpoints/CreateSong'; +import { CreateArtistEndpointHandler } from './endpoints/CreateArtist'; +import { QueryEndpointHandler } from './endpoints/Query'; +import { ArtistDetailsEndpointHandler } from './endpoints/ArtistDetails' +import { SongDetailsEndpointHandler } from './endpoints/SongDetails'; +import { ModifyArtistEndpointHandler } from './endpoints/ModifyArtist'; +import { ModifySongEndpointHandler } from './endpoints/ModifySong'; +import { CreateTagEndpointHandler } from './endpoints/CreateTag'; +import { ModifyTagEndpointHandler } from './endpoints/ModifyTag'; +import { TagDetailsEndpointHandler } from './endpoints/TagDetails'; +import { CreateAlbumEndpointHandler } from './endpoints/CreateAlbum'; +import { ModifyAlbumEndpointHandler } from './endpoints/ModifyAlbum'; +import { AlbumDetailsEndpointHandler } from './endpoints/AlbumDetails'; +import { DeleteTagEndpointHandler } from './endpoints/DeleteTag'; +import { MergeTagEndpointHandler } from './endpoints/MergeTag'; +import { RegisterUserEndpointHandler } from './endpoints/RegisterUser'; import * as endpointTypes from './endpoints/types'; +import { sha512 } from 'js-sha512'; -const invokeHandler = (handler:endpointTypes.EndpointHandler, knex: Knex) => { +// For authentication +var passport = require('passport'); +var Strategy = require('passport-local').Strategy; + +const invokeHandler = (handler: endpointTypes.EndpointHandler, knex: Knex) => { return async (req: any, res: any) => { console.log("Incoming", req.method, " @ ", req.url); await handler(req, res, knex) - .catch(endpointTypes.catchUnhandledErrors) - .catch((_e:endpointTypes.EndpointError) => { - let e:endpointTypes.EndpointError = _e; - console.log("Error handling request: ", e.internalMessage); - res.sendStatus(e.httpStatus); - }) + .catch(endpointTypes.catchUnhandledErrors) + .catch((_e: endpointTypes.EndpointError) => { + let e: endpointTypes.EndpointError = _e; + console.log("Error handling request: ", e.internalMessage); + res.sendStatus(e.httpStatus); + }) console.log("Finished handling", req.method, "@", req.url); }; } @@ -37,26 +43,63 @@ const SetupApp = (app: any, knex: Knex, apiBaseUrl: string) => { app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); - const invokeWithKnex = (handler: endpointTypes.EndpointHandler) => { + // Set up auth. See: https://github.com/passport/express-4.x-local-example.git + passport.use(new Strategy( + function (email: string, password: string, cb: any) { + (async () => { + try { + const user = await knex.select(['email', 'passwordHash', 'id']) + .from('users') + .where({ 'email': email }) + .then((users: any) => users[0]); + if (!user) { cb(null, false); } + if (sha512(password) != user.passwordHash) { + return cb(null, false); + } + return cb(null, user); + } catch (error) { cb(error); } + })(); + })); + // passport.serializeUser(function (user: any, cb: any) { + // cb(null, user.id); + // }); + // passport.deserializeUser(function (id: number, cb: any) { + // (async () => { + // try { + // const user = await knex.select(['email', 'passwordHash', 'id']) + // .from('users') + // .where({ 'id': id }) + // .then((users: any) => users[0]); + // if (!user) { cb(null, false); } + // return cb(null, user); + // } catch (error) { cb(error); } + // })(); + // }); + + app.use(passport.initialize()); + //app.use(passport.session()); + + const _invoke = (handler: endpointTypes.EndpointHandler) => { return invokeHandler(handler, knex); } // Set up REST API endpoints - app.post(apiBaseUrl + api.CreateSongEndpoint, invokeWithKnex(CreateSongEndpointHandler)); - app.post(apiBaseUrl + api.QueryEndpoint, invokeWithKnex(QueryEndpointHandler)); - app.post(apiBaseUrl + api.CreateArtistEndpoint, invokeWithKnex(CreateArtistEndpointHandler)); - app.put(apiBaseUrl + api.ModifyArtistEndpoint, invokeWithKnex(ModifyArtistEndpointHandler)); - app.put(apiBaseUrl + api.ModifySongEndpoint, invokeWithKnex(ModifySongEndpointHandler)); - app.get(apiBaseUrl + api.SongDetailsEndpoint, invokeWithKnex(SongDetailsEndpointHandler)); - app.get(apiBaseUrl + api.ArtistDetailsEndpoint, invokeWithKnex(ArtistDetailsEndpointHandler)); - app.post(apiBaseUrl + api.CreateTagEndpoint, invokeWithKnex(CreateTagEndpointHandler)); - app.put(apiBaseUrl + api.ModifyTagEndpoint, invokeWithKnex(ModifyTagEndpointHandler)); - app.get(apiBaseUrl + api.TagDetailsEndpoint, invokeWithKnex(TagDetailsEndpointHandler)); - app.post(apiBaseUrl + api.CreateAlbumEndpoint, invokeWithKnex(CreateAlbumEndpointHandler)); - app.put(apiBaseUrl + api.ModifyAlbumEndpoint, invokeWithKnex(ModifyAlbumEndpointHandler)); - app.get(apiBaseUrl + api.AlbumDetailsEndpoint, invokeWithKnex(AlbumDetailsEndpointHandler)); - app.delete(apiBaseUrl + api.DeleteTagEndpoint, invokeWithKnex(DeleteTagEndpointHandler)); - app.post(apiBaseUrl + api.MergeTagEndpoint, invokeWithKnex(MergeTagEndpointHandler)); + app.post(apiBaseUrl + api.CreateSongEndpoint, _invoke(CreateSongEndpointHandler)); + app.post(apiBaseUrl + api.QueryEndpoint, _invoke(QueryEndpointHandler)); + app.post(apiBaseUrl + api.CreateArtistEndpoint, _invoke(CreateArtistEndpointHandler)); + app.put(apiBaseUrl + api.ModifyArtistEndpoint, _invoke(ModifyArtistEndpointHandler)); + app.put(apiBaseUrl + api.ModifySongEndpoint, _invoke(ModifySongEndpointHandler)); + app.get(apiBaseUrl + api.SongDetailsEndpoint, passport.authenticate('local', { session: false }), _invoke(SongDetailsEndpointHandler)); + app.get(apiBaseUrl + api.ArtistDetailsEndpoint, _invoke(ArtistDetailsEndpointHandler)); + app.post(apiBaseUrl + api.CreateTagEndpoint, _invoke(CreateTagEndpointHandler)); + app.put(apiBaseUrl + api.ModifyTagEndpoint, _invoke(ModifyTagEndpointHandler)); + app.get(apiBaseUrl + api.TagDetailsEndpoint, _invoke(TagDetailsEndpointHandler)); + app.post(apiBaseUrl + api.CreateAlbumEndpoint, _invoke(CreateAlbumEndpointHandler)); + app.put(apiBaseUrl + api.ModifyAlbumEndpoint, _invoke(ModifyAlbumEndpointHandler)); + app.get(apiBaseUrl + api.AlbumDetailsEndpoint, _invoke(AlbumDetailsEndpointHandler)); + app.delete(apiBaseUrl + api.DeleteTagEndpoint, _invoke(DeleteTagEndpointHandler)); + app.post(apiBaseUrl + api.MergeTagEndpoint, _invoke(MergeTagEndpointHandler)); + app.post(apiBaseUrl + api.RegisterUserEndpoint, _invoke(RegisterUserEndpointHandler)); } export { SetupApp } \ No newline at end of file diff --git a/server/endpoints/AlbumDetailsEndpointHandler.ts b/server/endpoints/AlbumDetails.ts similarity index 100% rename from server/endpoints/AlbumDetailsEndpointHandler.ts rename to server/endpoints/AlbumDetails.ts diff --git a/server/endpoints/ArtistDetailsEndpointHandler.ts b/server/endpoints/ArtistDetails.ts similarity index 100% rename from server/endpoints/ArtistDetailsEndpointHandler.ts rename to server/endpoints/ArtistDetails.ts diff --git a/server/endpoints/CreateAlbumEndpointHandler.ts b/server/endpoints/CreateAlbum.ts similarity index 100% rename from server/endpoints/CreateAlbumEndpointHandler.ts rename to server/endpoints/CreateAlbum.ts diff --git a/server/endpoints/CreateArtistEndpointHandler.ts b/server/endpoints/CreateArtist.ts similarity index 100% rename from server/endpoints/CreateArtistEndpointHandler.ts rename to server/endpoints/CreateArtist.ts diff --git a/server/endpoints/CreateSongEndpointHandler.ts b/server/endpoints/CreateSong.ts similarity index 100% rename from server/endpoints/CreateSongEndpointHandler.ts rename to server/endpoints/CreateSong.ts diff --git a/server/endpoints/CreateTagEndpointHandler.ts b/server/endpoints/CreateTag.ts similarity index 100% rename from server/endpoints/CreateTagEndpointHandler.ts rename to server/endpoints/CreateTag.ts diff --git a/server/endpoints/DeleteTagEndpointHandler.ts b/server/endpoints/DeleteTag.ts similarity index 100% rename from server/endpoints/DeleteTagEndpointHandler.ts rename to server/endpoints/DeleteTag.ts diff --git a/server/endpoints/MergeTagEndpointHandler.ts b/server/endpoints/MergeTag.ts similarity index 100% rename from server/endpoints/MergeTagEndpointHandler.ts rename to server/endpoints/MergeTag.ts diff --git a/server/endpoints/ModifyAlbumEndpointHandler.ts b/server/endpoints/ModifyAlbum.ts similarity index 100% rename from server/endpoints/ModifyAlbumEndpointHandler.ts rename to server/endpoints/ModifyAlbum.ts diff --git a/server/endpoints/ModifyArtistEndpointHandler.ts b/server/endpoints/ModifyArtist.ts similarity index 100% rename from server/endpoints/ModifyArtistEndpointHandler.ts rename to server/endpoints/ModifyArtist.ts diff --git a/server/endpoints/ModifySongEndpointHandler.ts b/server/endpoints/ModifySong.ts similarity index 100% rename from server/endpoints/ModifySongEndpointHandler.ts rename to server/endpoints/ModifySong.ts diff --git a/server/endpoints/ModifyTagEndpointHandler.ts b/server/endpoints/ModifyTag.ts similarity index 100% rename from server/endpoints/ModifyTagEndpointHandler.ts rename to server/endpoints/ModifyTag.ts diff --git a/server/endpoints/QueryEndpointHandler.ts b/server/endpoints/Query.ts similarity index 100% rename from server/endpoints/QueryEndpointHandler.ts rename to server/endpoints/Query.ts diff --git a/server/endpoints/RegisterUser.ts b/server/endpoints/RegisterUser.ts new file mode 100644 index 0000000..ac9699c --- /dev/null +++ b/server/endpoints/RegisterUser.ts @@ -0,0 +1,41 @@ +import * as api from '../../client/src/api'; +import { EndpointError, EndpointHandler, catchUnhandledErrors } from './types'; +import Knex from 'knex'; + +import { sha512 } from 'js-sha512'; + +export const RegisterUserEndpointHandler: EndpointHandler = async (req: any, res: any, knex: Knex) => { + if (!api.checkRegisterUserRequest(req)) { + const e: EndpointError = { + internalMessage: 'Invalid RegisterUser request: ' + JSON.stringify(req.body), + httpStatus: 400 + }; + throw e; + } + const reqObject: api.RegisterUserRequest = req.body; + + console.log("Register User: ", reqObject); + + await knex.transaction(async (trx) => { + try { + // FIXME check if the user already exists + + // Create the new user. + const passwordHash = sha512(reqObject.password); + const userId = (await trx('users') + .insert({ + email: reqObject.email, + passwordHash: passwordHash, + }) + .returning('id') // Needed for Postgres + )[0]; + + // Respond to the request. + res.status(200).send(); + + } catch (e) { + catchUnhandledErrors(e); + trx.rollback(); + } + }) +} \ No newline at end of file diff --git a/server/endpoints/SongDetailsEndpointHandler.ts b/server/endpoints/SongDetails.ts similarity index 100% rename from server/endpoints/SongDetailsEndpointHandler.ts rename to server/endpoints/SongDetails.ts diff --git a/server/endpoints/TagDetailsEndpointHandler.ts b/server/endpoints/TagDetails.ts similarity index 100% rename from server/endpoints/TagDetailsEndpointHandler.ts rename to server/endpoints/TagDetails.ts diff --git a/server/migrations/20201110170100_add_users.ts b/server/migrations/20201110170100_add_users.ts new file mode 100644 index 0000000..ec62753 --- /dev/null +++ b/server/migrations/20201110170100_add_users.ts @@ -0,0 +1,20 @@ +import * as Knex from "knex"; + + +export async function up(knex: Knex): Promise { + // Users table. + await knex.schema.createTable( + 'users', + (table: any) => { + table.increments('id'); + table.string('email'); + table.string('passwordHash') + } + ) +} + + +export async function down(knex: Knex): Promise { + await knex.schema.dropTable('users'); +} + diff --git a/server/package-lock.json b/server/package-lock.json index 6b6e850..a52e71e 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1936,6 +1936,11 @@ "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-3.5.0.tgz", "integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==" }, + "js-sha512": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/js-sha512/-/js-sha512-0.8.0.tgz", + "integrity": "sha512-PWsmefG6Jkodqt+ePTvBZCSMFgN7Clckjd0O7su3I0+BW2QWUTJNzjktHsztGLhncP2h8mcF9V9Y2Ha59pAViQ==" + }, "jsbi": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/jsbi/-/jsbi-3.1.3.tgz", @@ -2756,6 +2761,28 @@ "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=" }, + "passport": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/passport/-/passport-0.4.1.tgz", + "integrity": "sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg==", + "requires": { + "passport-strategy": "1.x.x", + "pause": "0.0.1" + } + }, + "passport-local": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-local/-/passport-local-1.0.0.tgz", + "integrity": "sha1-H+YyaMkudWBmJkN+O5BmYsFbpu4=", + "requires": { + "passport-strategy": "1.x.x" + } + }, + "passport-strategy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz", + "integrity": "sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ=" + }, "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -2789,6 +2816,11 @@ "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.0.tgz", "integrity": "sha1-uULm1L3mUwBe9rcTYd74cn0GReA=" }, + "pause": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/pause/-/pause-0.0.1.tgz", + "integrity": "sha1-HUCLP9t2kjuVQ9lvtMnf1TXZy10=" + }, "performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", diff --git a/server/package.json b/server/package.json index 11035c4..a16940c 100644 --- a/server/package.json +++ b/server/package.json @@ -13,12 +13,15 @@ "chai-http": "^4.3.0", "express": "^4.16.4", "jasmine": "^3.5.0", + "js-sha512": "^0.8.0", "knex": "^0.21.5", "mssql": "^6.2.1", "mysql": "^2.18.1", "mysql2": "^2.1.0", "nodemon": "^2.0.4", "oracledb": "^5.0.0", + "passport": "^0.4.1", + "passport-local": "^1.0.0", "pg": "^8.3.3", "sqlite3": "^5.0.0", "ts-node": "^8.10.2",