Skip to content

Commit

Permalink
V0 (#33)
Browse files Browse the repository at this point in the history
* feat: check user roles

* refactor: nit

* test: ensure and admin can assign and unassign a task

* fix: authorization plugin has no dependency

* fix: update migrations dir path

* fix: eslint

* refactor: nit

* Update .env.example

Signed-off-by: Jean <[email protected]>

* refactor: use knex

* refactor: migrations

* fix: remove useless c8 ignore comments

* docs: update path

* refactor: change JWT auth for cookie session auth

* chore: ci - env must have required property 'COOKIE_NAME'

* fix: uncomment unauthenticated test

* refactor: leverage fastify sensible decorators

* chore: use tsx

* feat: add pagination to tasks

* refactor: use COUNT(*) OVER() AS rowNum for tasks pagination

* refactor: decorate request for authorization

* fix: use transaction for login controller

* refactor: register cookie plugin in session plugin

* test: mock app.compare implementation instead of reassignation

* test: spy logger to ensure 500 error is due to Transaction failure

* feat: allow to upload task image

* refactor: improve scripts typing

* docs: static and multipart plugin

* chore: dangerous DB  operations should be explicitly authorized

* refactor: use node test runner utitities

* refactor: check file size before mime-type

* fix: identifier typo

* feat: do not use rm -rf

* fix: storage path disclosure

* fix: nit

---------

Signed-off-by: Jean <[email protected]>
  • Loading branch information
jean-michelet authored Oct 21, 2024
1 parent 8ae2437 commit dfb66bb
Show file tree
Hide file tree
Showing 47 changed files with 1,209 additions and 519 deletions.
9 changes: 7 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
# @see {@link https://www.youtube.com/watch?v=HMM7GJC5E2o}
NODE_ENV=production

CAN_CREATE_DATABASE=0
CAN_DROP_DATABASE=0
CAN_SEED_DATABASE=0

# Database
MYSQL_HOST=localhost
MYSQL_PORT=3306
Expand All @@ -14,5 +18,6 @@ FASTIFY_CLOSE_GRACE_DELAY=1000
LOG_LEVEL=info

# Security
JWT_SECRET=
RATE_LIMIT_MAX=
COOKIE_SECRET=
COOKIE_NAME=
RATE_LIMIT_MAX=4 # 4 for tests
12 changes: 7 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ on:
paths-ignore:
- "docs/**"
- "*.md"
- "*.example"
pull_request:
paths-ignore:
- "docs/**"
Expand Down Expand Up @@ -50,11 +51,10 @@ jobs:
- name: Lint Code
run: npm run lint

- name: Generate JWT Secret
id: gen-jwt
- name: Generate COOKIE Secret
run: |
JWT_SECRET=$(openssl rand -hex 32)
echo "JWT_SECRET=$JWT_SECRET" >> $GITHUB_ENV
COOKIE_SECRET=$(openssl rand -hex 32)
echo "COOKIE_SECRET=$COOKIE_SECRET" >> $GITHUB_ENV
- name: Generate dummy .env for scripts using -env-file=.env flag
run: touch .env
Expand All @@ -66,6 +66,8 @@ jobs:
MYSQL_DATABASE: test_db
MYSQL_USER: test_user
MYSQL_PASSWORD: test_password
# JWT_SECRET is dynamically generated and loaded from the environment
# COOKIE_SECRET is dynamically generated and loaded from the environment
COOKIE_NAME: 'sessid'
RATE_LIMIT_MAX: 4
CAN_SEED_DATABASE: 1
run: npm run db:migrate && npm run test
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,6 @@ bun.lockb
package-lock.json
pnpm-lock.yaml
yarn.lock

# uploaded files
uploads/tasks/*
7 changes: 0 additions & 7 deletions @types/fastify/fastify.d.ts

This file was deleted.

2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@

![CI](https://github.com/fastify/demo/workflows/CI/badge.svg)

> :warning: **Please note:** This repository is still under active development.
The aim of this repository is to provide a concrete example of a Fastify application using what are considered best practices by the Fastify community.

**Prerequisites:** You need to have Node.js version 22 or higher installed.
Expand Down
14 changes: 10 additions & 4 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,19 @@ services:
db:
image: mysql:8.4
environment:
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
MYSQL_ROOT_PASSWORD: root_password
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
ports:
- 3306:3306
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-u${MYSQL_USER}", "-p${MYSQL_PASSWORD}"]
interval: 10s
timeout: 5s
retries: 3
volumes:
- db_data:/var/lib/mysql

volumes:
db_data:
1 change: 1 addition & 0 deletions migrations/002.do.tasks.sql
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ CREATE TABLE tasks (
name VARCHAR(255) NOT NULL,
author_id INT NOT NULL,
assigned_user_id INT,
filename VARCHAR(255),
status VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
Expand Down
4 changes: 4 additions & 0 deletions migrations/004.do.roles.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
CREATE TABLE roles (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255) NOT NULL
);
1 change: 1 addition & 0 deletions migrations/004.undo.roles.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE IF EXISTS roles;
7 changes: 7 additions & 0 deletions migrations/005.do.user_roles.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
CREATE TABLE user_roles (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
role_id INT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE
);
1 change: 1 addition & 0 deletions migrations/005.undo.user_roles.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE IF EXISTS user_roles;
21 changes: 14 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,26 +12,30 @@
"build": "tsc",
"watch": "tsc -w",
"dev": "npm run build && concurrently -k -p \"[{name}]\" -n \"TypeScript,App\" -c \"yellow.bold,cyan.bold\" \"npm:watch\" \"npm:dev:start\"",
"dev:start": "fastify start --ignore-watch=.ts$ -w -l info -P dist/app.js",
"dev:start": "npm run build && fastify start --ignore-watch=.ts$ -w -l info -P dist/app.js",
"test": "npm run db:seed && tap --jobs=1 test/**/*",
"standalone": "node --env-file=.env dist/server.js",
"standalone": "npm run build && node --env-file=.env dist/server.js",
"lint": "eslint --ignore-pattern=dist",
"lint:fix": "npm run lint -- --fix",
"db:migrate": "node --env-file=.env scripts/migrate.js",
"db:seed": "node --env-file=.env scripts/seed-database.js"
"db:create": "tsx --env-file=.env ./scripts/create-database.ts",
"db:drop": "tsx --env-file=.env ./scripts/drop-database.ts",
"db:migrate": "tsx --env-file=.env ./scripts/migrate.ts",
"db:seed": "tsx --env-file=.env ./scripts/seed-database.ts"
},
"keywords": [],
"author": "Michelet Jean <[email protected]>",
"license": "MIT",
"dependencies": {
"@fastify/autoload": "^6.0.0",
"@fastify/cookie": "^11.0.1",
"@fastify/cors": "^10.0.0",
"@fastify/env": "^5.0.1",
"@fastify/helmet": "^12.0.0",
"@fastify/jwt": "^9.0.0",
"@fastify/mysql": "^5.0.1",
"@fastify/multipart": "^9.0.1",
"@fastify/rate-limit": "^10.0.1",
"@fastify/sensible": "^6.0.1",
"@fastify/session": "^11.0.1",
"@fastify/static": "^8.0.2",
"@fastify/swagger": "^9.0.0",
"@fastify/swagger-ui": "^5.0.1",
"@fastify/type-provider-typebox": "^5.0.0",
Expand All @@ -41,15 +45,18 @@
"fastify": "^5.0.0",
"fastify-cli": "^7.0.0",
"fastify-plugin": "^5.0.1",
"form-data": "^4.0.1",
"knex": "^3.1.0",
"mysql2": "^3.11.3",
"postgrator": "^7.3.0"
},
"devDependencies": {
"@types/node": "^22.5.5",
"eslint": "^9.11.0",
"fastify-tsconfig": "^2.0.0",
"mysql2": "^3.11.3",
"neostandard": "^0.11.5",
"tap": "^21.0.1",
"tsx": "^4.19.1",
"typescript": "~5.6.2"
}
}
30 changes: 30 additions & 0 deletions scripts/create-database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { createConnection, Connection } from 'mysql2/promise'

if (Number(process.env.CAN_CREATE_DATABASE) !== 1) {
throw new Error("You can't create the database. Set `CAN_CREATE_DATABASE=1` environment variable to allow this operation.")
}

async function createDatabase () {
const connection = await createConnection({
host: process.env.MYSQL_HOST,
port: Number(process.env.MYSQL_PORT),
user: process.env.MYSQL_USER,
password: process.env.MYSQL_PASSWORD
})

try {
await createDB(connection)
console.log(`Database ${process.env.MYSQL_DATABASE} has been created successfully.`)
} catch (error) {
console.error('Error creating database:', error)
} finally {
await connection.end()
}
}

async function createDB (connection: Connection) {
await connection.query(`CREATE DATABASE IF NOT EXISTS \`${process.env.MYSQL_DATABASE}\``)
console.log(`Database ${process.env.MYSQL_DATABASE} created or already exists.`)
}

createDatabase()
30 changes: 30 additions & 0 deletions scripts/drop-database.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { createConnection, Connection } from 'mysql2/promise'

if (Number(process.env.CAN_DROP_DATABASE) !== 1) {
throw new Error("You can't drop the database. Set `CAN_DROP_DATABASE=1` environment variable to allow this operation.")
}

async function dropDatabase () {
const connection = await createConnection({
host: process.env.MYSQL_HOST,
port: Number(process.env.MYSQL_PORT),
user: process.env.MYSQL_USER,
password: process.env.MYSQL_PASSWORD
})

try {
await dropDB(connection)
console.log(`Database ${process.env.MYSQL_DATABASE} has been dropped successfully.`)
} catch (error) {
console.error('Error dropping database:', error)
} finally {
await connection.end()
}
}

async function dropDB (connection: Connection) {
await connection.query(`DROP DATABASE IF EXISTS \`${process.env.MYSQL_DATABASE}\``)
console.log(`Database ${process.env.MYSQL_DATABASE} dropped.`)
}

dropDatabase()
39 changes: 0 additions & 39 deletions scripts/migrate.js

This file was deleted.

51 changes: 51 additions & 0 deletions scripts/migrate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import mysql, { FieldPacket } from 'mysql2/promise'
import path from 'node:path'
import fs from 'node:fs'
import Postgrator from 'postgrator'

interface PostgratorResult {
rows: any;
fields: FieldPacket[];
}

async function doMigration (): Promise<void> {
const connection = await mysql.createConnection({
multipleStatements: true,
host: process.env.MYSQL_HOST,
port: Number(process.env.MYSQL_PORT),
database: process.env.MYSQL_DATABASE,
user: process.env.MYSQL_USER,
password: process.env.MYSQL_PASSWORD
})

try {
const migrationDir = path.join(import.meta.dirname, '../migrations')

if (!fs.existsSync(migrationDir)) {
throw new Error(
`Migration directory "${migrationDir}" does not exist. Skipping migrations.`
)
}

const postgrator = new Postgrator({
migrationPattern: path.join(migrationDir, '*'),
driver: 'mysql',
database: process.env.MYSQL_DATABASE,
execQuery: async (query: string): Promise<PostgratorResult> => {
const [rows, fields] = await connection.query(query)
return { rows, fields }
},
schemaTable: 'schemaversion'
})

await postgrator.migrate()

console.log('Migration completed!')
} catch (err) {
console.error(err)
} finally {
await connection.end().catch(err => console.error(err))
}
}

doMigration()
60 changes: 0 additions & 60 deletions scripts/seed-database.js

This file was deleted.

Loading

0 comments on commit dfb66bb

Please sign in to comment.