Skip to content

Commit

Permalink
Merge c086c4e into 626b82a
Browse files Browse the repository at this point in the history
  • Loading branch information
vademo committed Jul 18, 2018
2 parents 626b82a + c086c4e commit 683d293
Show file tree
Hide file tree
Showing 33 changed files with 474 additions and 179 deletions.
6 changes: 2 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,13 @@ before_script:
- psql -U postgres silverback_test -c 'create extension if not exists "uuid-ossp";'

node_js:
- "node"
- "6"
- "8"
- "10"

sudo: false

cache:
directories:
- "node_modules"

script:
- npm run lint
- npm run build
Expand Down
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ NodeJS boilerplate project

These instructions will get you a copy of the project up and running on your local machine for development and testing purposes. See deployment for notes on how to deploy the project on a live system.

You can clone this repository using the [Ollie CLI tool](https://github.com/icapps/ollie).

### Prerequisites

Make sure you have [Node.js](http://nodejs.org/) and [Docker](https://docs.docker.com/install/) (preferably) installed.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ exports.up = async (knex) => {
table.text('last_name').notNullable();
table.text('email').notNullable();
table.text('role').notNullable();
table.boolean('registration_completed').defaultTo(false);

// Status of the account
table.uuid('status').notNullable().references('codes.id');

// Nullable
table.text('password').nullable();
Expand Down
14 changes: 14 additions & 0 deletions db/migrations/20180515110949_user_statuses_view.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

exports.up = async (knex) => {
await knex.raw(`
CREATE VIEW user_statuses_view
AS
SELECT codes.id, codes.code, codes.name, codes.description
FROM (codes JOIN code_types ON ((codes.code_type_id = code_types.id)))
WHERE (code_types.code = 'USER_STATUSES');
`)
};

exports.down = (knex) => {
return knex.raw(`DROP VIEW user_statuses_view`);
};
21 changes: 21 additions & 0 deletions db/seeds/00_code-types.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@

exports.seed = async (knex) => {
// Deletes ALL existing entries
await knex('codes').del();
await knex('code_types').del()

const languageCode = await knex('code_types').insert([{ code: 'LANGUAGES', name: 'Languages', description: 'Languages' }], ['id'])
await knex('codes').insert([
{ code: 'EN', name: 'English', code_type_id: languageCode[0].id },
{ code: 'NL', name: 'Nederlands', code_type_id: languageCode[0].id },
{ code: 'FR', name: 'French', code_type_id: languageCode[0].id },
]);

const userStatusCode = await knex('code_types').insert([{ code: 'USER_STATUSES', name: 'User statuses', description: 'User Account Statuses' }], ['id'])
await knex('codes').insert([
{ code: 'COMPLETE_REGISTRATON', name: 'Must complete registration', code_type_id: userStatusCode[0].id },
{ code: 'REGISTERED', name: 'Registered account', code_type_id: userStatusCode[0].id },
{ code: 'BLOCKED', name: 'Blocked account', code_type_id: userStatusCode[0].id },
]);
};

File renamed without changes.
18 changes: 0 additions & 18 deletions db/seeds/code-types.js

This file was deleted.

32 changes: 22 additions & 10 deletions docs/v1.yml
Original file line number Diff line number Diff line change
Expand Up @@ -663,8 +663,8 @@ definitions:
type: string
email:
type: string
hasAccess:
type: boolean
status:
$ref: "#/definitions/Status"
role:
$ref: "#/definitions/UserRole"
createdAt:
Expand Down Expand Up @@ -707,9 +707,9 @@ definitions:
- "email"
- "firstName"
- "lastName"
- "hasAccess"
- "role"
- "password"
- "status"
properties:
email:
type: string
Expand All @@ -719,8 +719,9 @@ definitions:
type: string
password:
type: string
hasAccess:
type: boolean
status:
type: string
enum: ["REGISTERED", "COMPLETE_REGISTRATON", "BLOCKED"]
role:
type: string
enum: ["USER", "ADMIN"]
Expand All @@ -731,16 +732,17 @@ definitions:
- "firstName"
- "lastName"
- "role"
- "hasAccess"
- "status"
properties:
email:
type: string
firstName:
type: string
lastName:
type: string
hasAccess:
type: boolean
status:
type: string
enum: ["REGISTERED", "COMPLETE_REGISTRATON", "BLOCKED"]
role:
type: string
enum: ["USER", "ADMIN"]
Expand All @@ -753,8 +755,9 @@ definitions:
type: string
lastName:
type: string
hasAccess:
type: boolean
status:
type: string
enum: ["REGISTERED", "COMPLETE_REGISTRATON", "BLOCKED"]
role:
type: string
enum: ["USER", "ADMIN"]
Expand Down Expand Up @@ -821,6 +824,15 @@ definitions:
type: array
items:
$ref: "#/definitions/UserRole"
Status:
type: object
properties:
code:
type: string
name:
type: string
description:
type: string
Code:
type: object
properties:
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "OLLIE_PROJECT_NAME",
"name": "nodejs-silverback",
"version": "1.0.0",
"description": "NodeJS project",
"description": "NodeJS Silverback boilerplate project",
"main": "./build/server.js",
"scripts": {
"build": "npm run clean && tsc",
Expand Down
24 changes: 13 additions & 11 deletions src/config/errors.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,19 @@ import { errors as defaults, ErrorType } from 'tree-house-errors';

// tslint:disable:max-line-length
export const errors = <Error>Object.assign({}, defaults, {
CODE_DUPLICATE: { code: 'CODE_DUPLICATE', message: 'A code with this code already exists' },
INVALID_TOKEN: { code: 'INVALID_TOKEN', message: 'The supplied token is invalid.' },
LINK_EXPIRED: { code: 'LINK_EXPIRED', message: 'Sorry, but this link has expired. You can request another one below.' },
MISSING_HEADERS: { code: 'MISSING_HEADERS', message: 'Not all required headers are provided' },
NO_PERMISSION: { code: 'NO_PERMISSION', message:'You do not have the proper permissions to execute this operation' },
TOO_MANY_REQUESTS: { code: 'TOO_MANY_REQUESTS', message: 'You\'ve made too many failed attempts in a short period of time, please try again later' },
USER_DELETE_OWN: { code: 'USER_DELETE_OWN', message: 'You can\'t delete your own user' },
USER_DUPLICATE: { code: 'USER_DUPLICATE', message: 'A user with this email already exists' },
USER_INACTIVE: { code: 'USER_INACTIVE', message: 'Your account is not active. Please contact your administrator.' },
USER_INVALID_CREDENTIALS: { code: 'USER_INVALID_CREDENTIALS', message: 'Incorrect username or password. Please try again.' },
USER_NOT_FOUND: { code: 'USER_NOT_FOUND', message: 'User not found' },
CODE_DUPLICATE: { code: 'CODE_DUPLICATE', message: 'A code with this code already exists' },
INVALID_TOKEN: { code: 'INVALID_TOKEN', message: 'The supplied token is invalid.' },
LINK_EXPIRED: { code: 'LINK_EXPIRED', message: 'Sorry, but this link has expired. You can request another one below.' },
MISSING_HEADERS: { code: 'MISSING_HEADERS', message: 'Not all required headers are provided' },
NO_PERMISSION: { code: 'NO_PERMISSION', message:'You do not have the proper permissions to execute this operation' },
TOO_MANY_REQUESTS: { code: 'TOO_MANY_REQUESTS', message: 'You\'ve made too many failed attempts in a short period of time, please try again later' },
USER_DELETE_OWN: { code: 'USER_DELETE_OWN', message: 'You can\'t delete your own user' },
USER_DUPLICATE: { code: 'USER_DUPLICATE', message: 'A user with this email already exists' },
USER_BLOCKED: { code: 'USER_BLOCKED', message: 'Your account is blocked. Please contact your administrator.' },
USER_UNCONFIRMED: { code: 'USER_UNCONFIRMED', message: 'Your account is not confirmed. Please check your inbox for the confirmation link.' },
USER_INVALID_CREDENTIALS: { code: 'USER_INVALID_CREDENTIALS', message: 'Incorrect username or password. Please try again.' },
USER_NOT_FOUND: { code: 'USER_NOT_FOUND', message: 'User not found' },
STATUS_NOT_FOUND: { code: 'STATUS_NOT_FOUND', message: 'Status not found' },
});
// tslint:enable:max-line-length

Expand Down
4 changes: 4 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export const tableNames = {
CODETYPES: 'code_types',
};

export const viewNames = {
USER_STATUSES: 'user_statuses_view',
};

export const defaultFilters: Filters = {
limit: 50,
offset: 0,
Expand Down
8 changes: 8 additions & 0 deletions src/lib/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,11 @@ export function parseTotalCount(data: any[]): number {
if (data.length === 0) return 0;
return parseInt(data[0].total, 10);
}

/**
* Execute an update/create query and return the updated/created values
*/
export async function execAndFind(query: knex.QueryBuilder, identifier: string, findByIdFn: Function, ...args): Promise<any> {
const result = (await query)[0];
return result ? findByIdFn(result[identifier], ...args) : undefined;
}
11 changes: 10 additions & 1 deletion src/lib/filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ export function applyPagination(query: knex.QueryBuilder, filters: Filters): voi

/**
* Apply basic sorting to a query (field must be available for sorting)
* query can be 'any' due to outdated knex typings file (no support for clearOrder)
*/
export function applySorting(query: knex.QueryBuilder, filters: Filters, availableFields: string[] = []): void {
export function applySorting(query: knex.QueryBuilder | any, filters: Filters, availableFields: string[] = [], defaultSort?: DefaultSorting): void {
if (defaultSort) query.orderBy(defaultSort.field, defaultSort.order === 'desc' ? 'desc' : 'asc');
if (filters.sortOrder && filters.sortField) {
if (availableFields.includes(filters.sortField)) {
query.clearOrder();
query.orderBy(filters.sortField, filters.sortOrder === 'desc' ? 'desc' : 'asc');
}
}
Expand All @@ -32,3 +35,9 @@ export function applySearch(query: knex.QueryBuilder, filters: Filters, availabl
});
}
}

// Interfaces
export interface DefaultSorting {
field: string;
order: string;
}
8 changes: 8 additions & 0 deletions src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,11 @@ export function extractJwt(req: Request) {
if (headers.split(' ')[0] !== 'Bearer') throw new UnauthorizedError(errors.MISSING_HEADERS);
return headers.split(' ')[1];
}

/**
* check if user has the right status for the operation
*/
export function checkStatus(user: User): void {
if (user.status.code === 'COMPLETE_REGISTRATON') throw new UnauthorizedError(errors.USER_UNCONFIRMED);
if (user.status.code === 'BLOCKED') throw new UnauthorizedError(errors.USER_BLOCKED);
}
5 changes: 4 additions & 1 deletion src/middleware/permission.middleware.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Request, Response, NextFunction } from 'express';
import { authenticateJwt } from 'tree-house-authentication';
import { UnauthorizedError, NotFoundError } from 'tree-house-errors';
import { hasRole, extractJwt } from '../lib/utils';
import { hasRole, extractJwt, checkStatus } from '../lib/utils';
import { logger } from '../lib/logger';
import { jwtConfig } from '../config/auth.config';
import { Role } from '../config/roles.config';
Expand All @@ -22,6 +22,9 @@ export async function hasPermission(req: Request, _res: Response, next: NextFunc
const user = await userRepository.findById(decodedToken.userId);
if (!user) throw new NotFoundError(errors.USER_NOT_FOUND);

// Check if user status still ok
checkStatus(user);

// Check if user has proper permission
if (role && !hasRole(user, role)) {
throw new UnauthorizedError(errors.NO_PERMISSION);
Expand Down
16 changes: 10 additions & 6 deletions src/models/user.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ export interface User {
firstName: string;
lastName: string;
password: string;
hasAccess: boolean;
registrationCompleted: boolean;
status: UserStatus;
role: UserRole;
refreshToken?: string;
resetPwToken?: string;
Expand All @@ -18,7 +17,7 @@ export interface UserCreate {
firstName: string;
lastName: string;
password?: string;
hasAccess: boolean;
status?: string;
role: string; // Code of role
resetPwToken?: string;
}
Expand All @@ -27,16 +26,15 @@ export interface UserUpdate {
email: string;
firstName: string;
lastName: string;
hasAccess: boolean;
role: string; // Code of role
status?: string; // code of status
}

export interface PartialUserUpdate {
email?: string;
firstName?: string;
lastName?: string;
hasAccess?: boolean;
registrationCompleted?: boolean;
status?: string;
role?: string; // Code of role
password?: string;
resetPwToken?: string;
Expand All @@ -49,3 +47,9 @@ export interface UserRole {
description?: string;
level: number;
}

export interface UserStatus {
name: string;
code: string;
description?: string;
}

0 comments on commit 683d293

Please sign in to comment.