Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,8 @@
"mhutchie.git-graph",
"firsttris.vscode-jest-runner",
"Orta.vscode-jest",
"Gruntfuggly.todo-tree"
"Gruntfuggly.todo-tree",
"streetsidesoftware.code-spell-checker"
]
}
}
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ env:
H5P_PATH_PREFIX: h5p-content/
H5P_STORAGE_ROOT_PATH: /tmp/h5p
H5P_FILE_STORAGE_HOST: http://localhost:1081
ADMIN_SESSION_SECRET_KEY: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
JWT_SECRET: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
PASSWORD_RESET_JWT_SECRET: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
EMAIL_CHANGE_JWT_SECRET: 0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
Expand All @@ -51,6 +52,8 @@ env:
MEILISEARCH_MASTER_KEY: fake
GEOLOCATION_API_KEY: geolocation-key
GEOLOCATION_API_HOST: http://localhost:12345
GITHUB_CLIENT_ID: 6237481259
GITHUB_CLIENT_SECRET: 72843529435293450182439587289345714895819043580

jobs:
build-node:
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ CORS_ORIGIN_REGEX=^http?:\/\/(localhost)?:[0-9]{4}$
# Session cookie key (to generate one: https://github.com/fastify/fastify-secure-session#using-a-pregenerated-key and https://github.com/fastify/fastify-secure-session#using-keys-as-strings)
# TLDR: npx @fastify/secure-session > secret-key && node -e "let fs=require('fs'),file=path.join(__dirname, 'secret-key');console.log(fs.readFileSync(file).toString('hex'));fs.unlinkSync(file)"
SECURE_SESSION_SECRET_KEY=<secret-key>

# session key for the admin dashboard, (can use the same command as for SECURE_SESSION_SECRET_KEY)
ADMIN_SESSION_SECRET_KEY=<secret-key>

### Auth

Expand Down Expand Up @@ -218,6 +219,11 @@ OPENAI_API_KEY=<openai-api-key>

# GEOLOCATION API - this can be empty if you don't use geolocation
GEOLOCATION_API_KEY=

# Github Oauth provider secrets
# refer to the documentation in /src/plugins/admin on how to configure the OAuth app in GitHub
GITHUB_CLIENT_ID=<your-github-oauth-app-client-id>
GITHUB_CLIENT_SECRET=<your-github-oauth-app-secret>
```

### Umami
Expand Down
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"@bull-board/fastify": "6.10.1",
"@bull-board/ui": "6.10.1",
"@fastify/busboy": "3.1.1",
"@fastify/cookie": "11.0.2",
"@fastify/cors": "11.0.1",
"@fastify/error": "4.1.0",
"@fastify/forwarded": "3.0.0",
Expand Down Expand Up @@ -80,7 +81,7 @@
"drizzle-orm": "0.41.0",
"extract-zip": "2.0.1",
"fast-json-stringify": "6.0.1",
"fastify": "5.3.3",
"fastify": "5.4.0",
"fastify-nodemailer": "5.0.0",
"fastify-plugin": "5.0.1",
"form-data": "4.0.2",
Expand All @@ -102,6 +103,7 @@
"openai": "4.91.1",
"papaparse": "^5.4.1",
"passport-custom": "1.1.1",
"passport-github2": "0.1.12",
"passport-jwt": "4.0.1",
"passport-local": "1.0.0",
"pdf-text-reader": "3.0.2",
Expand Down Expand Up @@ -144,10 +146,12 @@
"@types/node-fetch": "2",
"@types/nodemailer": "6.4.15",
"@types/papaparse": "5.3.15",
"@types/passport-github2": "1.2.9",
"@types/passport-jwt": "4.0.1",
"@types/passport-local": "^1",
"@types/pg": "8.15.4",
"@types/sanitize-html": "2.16.0",
"@types/supertest": "6.0.3",
"@types/uuid": "10.0.0",
"@types/ws": "8.18.1",
"@types/yazl": "3.3.0",
Expand All @@ -167,6 +171,7 @@
"nodemon": "3.1.9",
"pino-pretty": "13.0.0",
"prettier": "3.6.2",
"supertest": "7.1.3",
"ts-jest": "29.4.0",
"ts-node": "10.9.2",
"typescript": "5.8.3",
Expand Down
45 changes: 26 additions & 19 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
// should not be reimported in any other files !
import 'reflect-metadata';

import { FastifyPluginAsyncTypebox } from '@fastify/type-provider-typebox';
import type { FastifyInstance } from 'fastify';
import fp from 'fastify-plugin';

import { REDIS_CONNECTION } from './config/redis';
import { registerDependencies } from './di/container';
import adminPlugin from './plugins/admin/admin.plugin';
import databasePlugin from './plugins/database';

Check warning on line 12 in src/app.ts

View workflow job for this annotation

GitHub Actions / build-node

Using exported name 'databasePlugin' as identifier for default import
import metaPlugin from './plugins/meta';

Check warning on line 13 in src/app.ts

View workflow job for this annotation

GitHub Actions / build-node

Using exported name 'metaPlugin' as identifier for default import
import swaggerPlugin from './plugins/swagger';
import { schemaRegisterPlugin } from './plugins/typebox';
import authPlugin from './services/auth';
Expand All @@ -32,27 +34,32 @@

await instance.register(fp(metaPlugin));

await instance.register(fp(passportPlugin));
// need to be defined before member and item for auth check
await instance.register(adminPlugin);

await instance.register(maintenancePlugin);

// scope the next registration to the core functionalities
await instance.register(coreApp);
}

export const coreApp: FastifyPluginAsyncTypebox = async (instance) => {
// need to be defined before member and item for auth check
await instance.register(fp(passportPlugin));

await instance.register(fp(authPlugin));

await instance.register(async (instance) => {
// core API modules
await instance
// the websockets plugin must be registered before but in the same scope as the apis
// otherwise tests somehow bypass mocking the authentication through jest.spyOn(app, 'verifyAuthentication')
.register(fp(websocketsPlugin), {
prefix: '/ws',
redis: {
channelName: 'graasp-realtime-updates',
connection: REDIS_CONNECTION,
},
})
.register(fp(MemberServiceApi))
.register(fp(ItemServiceApi))
.register(tagPlugin);
});
}
// core API modules
await instance
// the websockets plugin must be registered before but in the same scope as the apis
// otherwise tests somehow bypass mocking the authentication through jest.spyOn(app, 'verifyAuthentication')
.register(fp(websocketsPlugin), {
prefix: '/ws',
redis: {
channelName: 'graasp-realtime-updates',
connection: REDIS_CONNECTION,
},
})
.register(fp(MemberServiceApi))
.register(fp(ItemServiceApi))
.register(tagPlugin);
};
12 changes: 12 additions & 0 deletions src/config/admin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { getEnv } from './env';
import { requiredEnvVar } from './helpers';

// ensure env is setup
getEnv();

// GitHub OAuth app secrets
// to generate these values go to: Settings -> Developer Settings -> OAuth Apps -> New OAuth app
// the callback url should be <your-origin>/admin/auth/github/callback
// so for local developement: http://localhost:3000/admin/auth/github/callback
export const GITHUB_CLIENT_ID = requiredEnvVar('GITHUB_CLIENT_ID');
export const GITHUB_CLIENT_SECRET = requiredEnvVar('GITHUB_CLIENT_SECRET');
12 changes: 12 additions & 0 deletions src/config/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { getEnv } from './env';

getEnv();

/////////////////////////////////////
// Database Environement Variables //
/////////////////////////////////////
// Can be undefined, so tests can run without setting it.
export const DB_CONNECTION_POOL_SIZE: number = +process.env.DB_CONNECTION_POOL_SIZE! || 10;
export const DB_READ_REPLICA_CONNECTIONS: string[] = process.env.DB_READ_REPLICA_CONNECTIONS
? process.env.DB_READ_REPLICA_CONNECTIONS?.split(',')
: [];
1 change: 0 additions & 1 deletion src/config/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,5 @@ export const getEnv = once(() => {
});

export const PROD = NODE_ENV === Environment.production;
export const STAGING = NODE_ENV === Environment.staging;
export const DEV = NODE_ENV === Environment.development;
export const TEST = NODE_ENV === Environment.test;
41 changes: 41 additions & 0 deletions src/config/location.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { getEnv } from './env';

getEnv();

export const PROTOCOL = process.env.PROTOCOL || 'http';
export const HOSTNAME = process.env.HOSTNAME || 'localhost';
/**
* Host address the server listen on, default to 0.0.0.0 to bind to all addresses.
*/
export const HOST_LISTEN_ADDRESS = process.env.HOST_LISTEN_ADDRESS || '0.0.0.0';

export const PORT = process.env.PORT ? +process.env.PORT : 3000;
export const HOST = `${PROTOCOL}://${HOSTNAME}:${PORT}`; /**
* Public url is the url where the server is hosted. Mostly used to set the cookie on the right domain
* Warning for PUBLIC_URL:
* make sure that process.env.PUBLIC_URL / HOST have the format ${PROTOCOL}://${HOSTNAME}:${PORT}
* See the following example where the format is only ${HOSTNAME}:${PORT} in which case
* it interprets the hostname as protocol and the port as the pathname. Using the complete URL
* scheme fixes that
*
* $ node
* Welcome to Node.js v16.20.1.
* Type ".help" for more information.
* > new URL('localhost:3000')
* URL {
* href: 'localhost:3000',
* origin: 'null',
* protocol: 'localhost:',
* username: '',
* password: '',
* host: '',
* hostname: '',
* port: '',
* pathname: '3000',
* search: '',
* searchParams: URLSearchParams {},
* hash: ''
* }
* >
*/
export const PUBLIC_URL = new URL(process.env.PUBLIC_URL ?? HOST);
6 changes: 6 additions & 0 deletions src/config/secrets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ export const SECURE_SESSION_SECRET_KEY = requiredEnvVar('SECURE_SESSION_SECRET_K
export const SECURE_SESSION_EXPIRATION_IN_SECONDS = 604800; // 7days
export const MAX_SECURE_SESSION_EXPIRATION_IN_SECONDS = 15552000; // 6 * 30days -> 6 months

/**
* Admin session key
*/
export const ADMIN_SESSION_SECRET_KEY = requiredEnvVar('ADMIN_SESSION_SECRET_KEY');
export const ADMIN_SESSION_EXPIRATION_IN_SECONDS = 86_400; // 1 day

/**
* JWT
*/
Expand Down
8 changes: 8 additions & 0 deletions src/drizzle/0008_add_admin.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CREATE TABLE "admin" (
"github_id" varchar(15) PRIMARY KEY NOT NULL,
"github_name" varchar(39) NOT NULL,
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
"last_authenticated_at" timestamp with time zone,
CONSTRAINT "admin_github_id_unique" UNIQUE("github_id"),
CONSTRAINT "admin_github_name_unique" UNIQUE("github_name")
);
2 changes: 1 addition & 1 deletion src/drizzle/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { drizzle } from 'drizzle-orm/node-postgres';
import { withReplicas } from 'drizzle-orm/pg-core';
import { Pool } from 'pg';

import { DB_CONNECTION_POOL_SIZE, DB_READ_REPLICA_CONNECTIONS } from '../utils/config';
import { DB_CONNECTION_POOL_SIZE, DB_READ_REPLICA_CONNECTIONS } from '../config/db';
import * as relations from './relations';
import * as schema from './schema';

Expand Down
3 changes: 2 additions & 1 deletion src/drizzle/meta/0007_snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -3252,6 +3252,7 @@
"player",
"library",
"account",
"analytics",
"home",
"auth",
"unknown"
Expand Down Expand Up @@ -3597,4 +3598,4 @@
"schemas": {},
"tables": {}
}
}
}
Loading
Loading