@canmingir/link-express 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/.env.test +3 -0
  2. package/.eslintrc.js +20 -0
  3. package/.gitattributes +1 -0
  4. package/.github/workflows/publish.yml +9 -0
  5. package/README.md +1 -0
  6. package/index.js +1 -0
  7. package/package.json +51 -0
  8. package/prepare.js +15 -0
  9. package/src/authorization.js +102 -0
  10. package/src/config.js +20 -0
  11. package/src/dynamodb.js +18 -0
  12. package/src/error.js +46 -0
  13. package/src/express.js +58 -0
  14. package/src/lib/settings.js +36 -0
  15. package/src/lib/test.js +58 -0
  16. package/src/models/Organization.js +17 -0
  17. package/src/models/Permission.js +33 -0
  18. package/src/models/Project.js +37 -0
  19. package/src/models/Settings.js +21 -0
  20. package/src/models/index.js +51 -0
  21. package/src/openapi.js +40 -0
  22. package/src/platform.js +46 -0
  23. package/src/postgres.js +211 -0
  24. package/src/routes/index.js +15 -0
  25. package/src/routes/metrics.js +12 -0
  26. package/src/routes/oauth.js +100 -0
  27. package/src/routes/organizations.js +42 -0
  28. package/src/routes/permissions.js +50 -0
  29. package/src/routes/projects.js +125 -0
  30. package/src/routes/settings.js +25 -0
  31. package/src/routes/test/oauth.spec.js +113 -0
  32. package/src/routes/test/permissions.spec.js +154 -0
  33. package/src/routes/test/projects.spec.js +186 -0
  34. package/src/routes/test/settings.spec.js +40 -0
  35. package/src/schemas/Organization.js +13 -0
  36. package/src/schemas/Permission.js +20 -0
  37. package/src/schemas/Project.js +14 -0
  38. package/src/schemas/index.js +5 -0
  39. package/src/seeds/Organization.json +17 -0
  40. package/src/seeds/Permission.json +165 -0
  41. package/src/seeds/Project.json +42 -0
  42. package/src/seeds/Settings.json +25 -0
  43. package/src/test.js +105 -0
package/.env.test ADDED
@@ -0,0 +1,3 @@
1
+ PROFILE=TEST
2
+ OAUTH_CLIENT_SECRET=53b08fe45a3c616a9ce3e05174ea82e502df6baf
3
+ JWT_SECRET=q8fvthcTaz8qKQDAS7hJRK
package/.eslintrc.js ADDED
@@ -0,0 +1,20 @@
1
+ module.exports = {
2
+ env: {
3
+ es6: true,
4
+ node: true,
5
+ mocha: true,
6
+ },
7
+ extends: ["eslint:recommended", "plugin:prettier/recommended"],
8
+ parserOptions: {
9
+ ecmaVersion: 2022,
10
+ sourceType: "module",
11
+ },
12
+ ignorePatterns: "/node_modules",
13
+ rules: {
14
+ eqeqeq: ["error", "always"],
15
+ "no-console": "off",
16
+ "no-eval": "error",
17
+ "no-var": "error",
18
+ "prefer-arrow-callback": "error",
19
+ },
20
+ };
package/.gitattributes ADDED
@@ -0,0 +1 @@
1
+ * text=auto eol=lf
@@ -0,0 +1,9 @@
1
+ name: publish
2
+ on:
3
+ push:
4
+ tags:
5
+ - v[0-9]+.[0-9]+.[0-9]+
6
+ jobs:
7
+ deploy:
8
+ uses: NucleoidAI/actions/.github/workflows/publish.yml@main
9
+ secrets: inherit
package/README.md ADDED
@@ -0,0 +1 @@
1
+ # platform-express
package/index.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require("./src/platform");
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@canmingir/link-express",
3
+ "version": "1.6.0",
4
+ "description": "",
5
+ "main": "index.js",
6
+ "author": "NucTeam",
7
+ "exports": {
8
+ ".": "./index.js",
9
+ "./models": "./src/models/index.js",
10
+ "./test": "./src/test.js",
11
+ "./authorization": "./src/authorization.js",
12
+ "./error": "./src/error.js"
13
+ },
14
+ "scripts": {
15
+ "prepare": "node prepare.js",
16
+ "dev": "node server --dev",
17
+ "start": "node server",
18
+ "test": "mocha 'src/**/*.spec.js' --forbid-only",
19
+ "lint": "eslint . --ext .js"
20
+ },
21
+ "dependencies": {
22
+ "@aws-sdk/client-dynamodb": "^3.614.0",
23
+ "@aws-sdk/lib-dynamodb": "^3.614.0",
24
+ "axios": "^1.7.2",
25
+ "cors": "^2.8.5",
26
+ "dotenv": "^16.4.5",
27
+ "express": "^4.19.2",
28
+ "express-async-errors": "^3.1.1",
29
+ "helmet": "^7.1.0",
30
+ "joi": "^17.13.3",
31
+ "joi-to-swagger": "^6.2.0",
32
+ "jsonwebtoken": "^9.0.2",
33
+ "lodash": "^4.17.21",
34
+ "morgan": "^1.10.0",
35
+ "pg": "^8.12.0",
36
+ "sequelize": "^6.37.3",
37
+ "swagger-jsdoc": "^6.2.8",
38
+ "swagger-ui-express": "^5.0.1",
39
+ "uuid": "^10.0.0"
40
+ },
41
+ "devDependencies": {
42
+ "axios-mock-adapter": "^1.21.1",
43
+ "eslint": "^8.16.0",
44
+ "eslint-config-prettier": "^8.5.0",
45
+ "eslint-plugin-prettier": "^4.0.0",
46
+ "mocha": "10.8.2",
47
+ "prettier": "^2.6.2",
48
+ "sqlite3": "^5.1.6",
49
+ "supertest": "^7.0.0"
50
+ }
51
+ }
package/prepare.js ADDED
@@ -0,0 +1,15 @@
1
+ const fs = require("fs");
2
+
3
+ const prePush = `#!/usr/bin/env node
4
+ const { execSync } = require("child_process");
5
+
6
+ try {
7
+ execSync("npm run lint");
8
+ execSync("npm test");
9
+ } catch (err) {
10
+ console.log(err.stdout.toString());
11
+ process.exit(1);
12
+ }
13
+ `;
14
+
15
+ fs.writeFileSync(`${__dirname}/.git/hooks/pre-push`, prePush);
@@ -0,0 +1,102 @@
1
+ const jwt = require("jsonwebtoken");
2
+ const { AuthorizationError } = require("./error");
3
+ function verify(req, res, next) {
4
+ if (process.env.PROFILE === "TEST") {
5
+ switch (process.env.PROJECT_ID) {
6
+ case "0c756054-2d28-4f87-9b12-8023a79136a5":
7
+ req.session = {
8
+ projectId: "0c756054-2d28-4f87-9b12-8023a79136a5",
9
+ userId: "1001",
10
+ roles: ["ADMIN"],
11
+ appId: "977f5f57-8936-4388-8eb0-00a512cf01cc",
12
+ organizationId: "1c063446-7e78-432a-a273-34f481d0f0c3",
13
+ };
14
+ break;
15
+ case "21d2530b-4657-4ac0-b8cd-1a9f82786e32":
16
+ req.session = {
17
+ projectId: "21d2530b-4657-4ac0-b8cd-1a9f82786e32",
18
+ userId: "1001",
19
+ roles: ["ADMIN"],
20
+ appId: "977f5f57-8936-4388-8eb0-00a512cf01cc",
21
+ organizationId: "5459ab03-204a-4627-bdde-667b7802cb35",
22
+ };
23
+ break;
24
+ case "add6dfa4-45ba-4da2-bc5c-5a529610b52f":
25
+ req.session = {
26
+ appId: "10b7bc8c-a49c-4002-b0ec-63599e4b5210",
27
+ organizationId: "dfb990bb-81dd-4584-82ce-050eb8f6a12f",
28
+ projectId: "add6dfa4-45ba-4da2-bc5c-5a529610b52f",
29
+ userId: "1001",
30
+ roles: ["ADMIN"],
31
+ };
32
+ break;
33
+ case "e6d4744d-a11b-4c75-acad-e24a02903729":
34
+ req.session = {
35
+ appId: "10b7bc8c-a49c-4002-b0ec-63599e4b5210",
36
+ organizationId: "1c063446-7e78-432a-a273-34f481d0f0c3",
37
+ projectId: "e6d4744d-a11b-4c75-acad-e24a02903729",
38
+ userId: "1001",
39
+ roles: ["ADMIN"],
40
+ };
41
+ break;
42
+ default:
43
+ req.session = {
44
+ projectId: "cb16e069-6214-47f1-9922-1f7fe7629525",
45
+ userId: "1001",
46
+ roles: ["ADMIN"],
47
+ appId: "977f5f57-8936-4388-8eb0-00a512cf01cc",
48
+ organizationId: "dfb990bb-81dd-4584-82ce-050eb8f6a12f",
49
+ };
50
+ break;
51
+ }
52
+ return next();
53
+ }
54
+
55
+ const authorization = req.headers["authorization"];
56
+
57
+ if (!authorization) {
58
+ throw new AuthorizationError();
59
+ }
60
+
61
+ const parts = authorization.split(" ");
62
+
63
+ if (parts.length !== 2 || parts[0] !== "Bearer") {
64
+ throw new AuthorizationError();
65
+ }
66
+
67
+ const token = parts[1];
68
+
69
+ try {
70
+ const { sub, aud, rls, aid, oid } = jwt.verify(
71
+ token,
72
+ process.env.JWT_SECRET
73
+ );
74
+ req.session = {
75
+ projectId: aud,
76
+ userId: sub,
77
+ roles: rls,
78
+ appId: aid,
79
+ organizationId: oid,
80
+ };
81
+ } catch (error) {
82
+ throw new AuthorizationError();
83
+ }
84
+
85
+ next();
86
+ }
87
+
88
+ // eslint-disable-next-line no-unused-vars
89
+ function authorize(role) {
90
+ return (req, res, next) => {
91
+ const { roles } = req.session;
92
+
93
+ // TODO Add expression check for role
94
+ if (!roles || roles.length) {
95
+ next();
96
+ } else {
97
+ throw new AuthorizationError();
98
+ }
99
+ };
100
+ }
101
+
102
+ module.exports = { verify, authorize };
package/src/config.js ADDED
@@ -0,0 +1,20 @@
1
+ const _ = require("lodash");
2
+ let _config = {};
3
+
4
+ function init(config = {}) {
5
+ _config = _.merge(
6
+ {
7
+ link: {},
8
+ project: null,
9
+ openapi: {},
10
+ postgres: null,
11
+ dynamodb: null,
12
+ },
13
+ config
14
+ );
15
+
16
+ return _config;
17
+ }
18
+
19
+ module.exports = () => _config;
20
+ module.exports.init = init;
@@ -0,0 +1,18 @@
1
+ const { DynamoDBClient } = require("@aws-sdk/client-dynamodb");
2
+ const { DynamoDBDocumentClient } = require("@aws-sdk/lib-dynamodb");
3
+
4
+ const config = require("./config");
5
+ require("dotenv").config();
6
+
7
+ const { dynamodb } = config();
8
+
9
+ const client = new DynamoDBClient({
10
+ region: dynamodb.region,
11
+ credentials: {
12
+ accessKeyId: process.env.AWS_ACCESS_KEY_ID,
13
+ secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
14
+ },
15
+ });
16
+ const docClient = DynamoDBDocumentClient.from(client);
17
+
18
+ module.exports = { docClient };
package/src/error.js ADDED
@@ -0,0 +1,46 @@
1
+ const { ValidationError } = require("joi");
2
+
3
+ // eslint-disable-next-line no-unused-vars
4
+ const handle = (err, req, res, next) => {
5
+ if (typeof err === "string") {
6
+ return res.status(400).json({ error: err });
7
+ }
8
+
9
+ if (err instanceof ValidationError) {
10
+ return res.status(400).json({ message: err.message });
11
+ }
12
+
13
+ if (err instanceof AuthorizationError) {
14
+ return res.status(401).end();
15
+ }
16
+
17
+ if (err instanceof AuthenticationError) {
18
+ return res.status(403).end();
19
+ }
20
+
21
+ if (err instanceof NotFoundError) {
22
+ return res.status(404).end();
23
+ }
24
+
25
+ if (err.isAxiosError) {
26
+ return res.status(err.response?.status || 503).end();
27
+ }
28
+
29
+ if (err.error) {
30
+ return res.status(400).json(err);
31
+ } else {
32
+ console.error(err);
33
+ return res.status(500).end();
34
+ }
35
+ };
36
+
37
+ class AuthorizationError extends Error {}
38
+ class AuthenticationError extends Error {}
39
+ class NotFoundError extends Error {}
40
+
41
+ module.exports = {
42
+ handle,
43
+ AuthorizationError,
44
+ NotFoundError,
45
+ AuthenticationError,
46
+ };
package/src/express.js ADDED
@@ -0,0 +1,58 @@
1
+ const express = require("express");
2
+ require("express-async-errors");
3
+
4
+ const cors = require("cors");
5
+ const morgan = require("morgan");
6
+ const helmet = require("helmet");
7
+ const app = express();
8
+
9
+ const metrics = require("./routes/metrics");
10
+
11
+ const swaggerUi = require("swagger-ui-express");
12
+ const openapi = require("./openapi");
13
+ const error = require("./error");
14
+ const authorization = require("./authorization");
15
+ const settings = require("./routes/settings");
16
+
17
+ const config = require("./config")();
18
+
19
+ app.use(helmet());
20
+ app.use(cors());
21
+ app.use(morgan("tiny"));
22
+
23
+ app.use(express.json(), (err, req, res, next) =>
24
+ err ? res.status(422).end() : next()
25
+ );
26
+
27
+ if (config.project) {
28
+ const oauth = require("./routes/oauth");
29
+ app.use(
30
+ "/oauth",
31
+ express.urlencoded(),
32
+ (err, req, res, next) => (err ? res.status(422).end() : next()),
33
+ oauth
34
+ );
35
+ }
36
+
37
+ app.use("/openapi", swaggerUi.serve, swaggerUi.setup(openapi));
38
+ app.use("/metrics", metrics);
39
+
40
+ setImmediate(() => {
41
+ process.env.PROFILE === "TEST" && app.use(authorization.verify);
42
+
43
+ if (config.project) {
44
+ const permissions = require("./routes/permissions");
45
+ const organizations = require("./routes/organizations");
46
+ const projects = require("./routes/projects");
47
+
48
+ app.use("/projects", projects);
49
+ app.use("/organizations", organizations);
50
+ app.use("/permissions", permissions);
51
+ app.use("/projects/:projectId/settings", settings);
52
+ }
53
+
54
+ app.use((req, res) => res.status(404).end());
55
+ app.use(error.handle);
56
+ });
57
+
58
+ module.exports = app;
@@ -0,0 +1,36 @@
1
+ const Settings = require("../models/Settings");
2
+
3
+ async function get({ projectId }) {
4
+ const settingsInstance = await Settings.findOne({
5
+ where: { projectId },
6
+ });
7
+
8
+ if (settingsInstance) {
9
+ return settingsInstance.settings;
10
+ } else {
11
+ return {};
12
+ }
13
+ }
14
+
15
+ async function upsert({ projectId }, settings) {
16
+ const settingsInstance = await Settings.findOne({
17
+ where: { projectId },
18
+ });
19
+
20
+ if (settingsInstance) {
21
+ await settingsInstance.update({
22
+ projectId,
23
+ settings: {
24
+ ...settingsInstance.toJSON().settings,
25
+ ...settings,
26
+ },
27
+ });
28
+ } else {
29
+ await Settings.create({
30
+ projectId,
31
+ settings: { ...settings },
32
+ });
33
+ }
34
+ }
35
+
36
+ module.exports = { get, upsert };
@@ -0,0 +1,58 @@
1
+ require("dotenv").config({ path: ".env.test" });
2
+ const { init } = require("../platform");
3
+
4
+ init({
5
+ project: {
6
+ oauth: {
7
+ jwt: {
8
+ identifier: "email",
9
+ },
10
+ tokenUrl: "https://github.com/login/oauth/access_token",
11
+ userUrl: "https://api.github.com/user",
12
+ clientId: "0c2844d3d19dc9293fc5",
13
+ redirectUri: "http://localhost:5173/callback",
14
+ },
15
+ },
16
+ postgres: {
17
+ uri: "sqlite::memory:",
18
+ debug: true,
19
+ sync: false,
20
+ },
21
+ });
22
+
23
+ const models = require("../models");
24
+
25
+ async function reset() {
26
+ const { sequelize } = require("../postgres");
27
+
28
+ if (await models.init()) {
29
+ await sequelize.sync({ force: true });
30
+ }
31
+
32
+ const Organization = require("../models/Organization");
33
+
34
+ const Project = require("../models/Project");
35
+ const Permission = require("../models/Permission");
36
+ const Settings = require("../models/Settings");
37
+
38
+ await Organization.destroy({ truncate: true });
39
+ await Project.destroy({ truncate: true });
40
+ await Permission.destroy({ truncate: true });
41
+ await Settings.destroy({ truncate: true });
42
+
43
+ async function seed() {
44
+ const { seed: organizations } = require("../seeds/Organization.json");
45
+ const { seed: permissions } = require("../seeds/Permission.json");
46
+ const { seed: projects } = require("../seeds/Project.json");
47
+ const { seed: settings } = require("../seeds/Settings.json");
48
+
49
+ await Organization.bulkCreate(organizations);
50
+ await Project.bulkCreate(projects);
51
+ await Permission.bulkCreate(permissions);
52
+ await Settings.bulkCreate(settings);
53
+ }
54
+
55
+ await seed();
56
+ }
57
+
58
+ module.exports = { reset };
@@ -0,0 +1,17 @@
1
+ const { sequelize } = require("../postgres");
2
+ const { DataTypes, UUIDV4 } = require("sequelize");
3
+
4
+ const Organization = sequelize.define("Organization", {
5
+ id: {
6
+ type: DataTypes.UUID,
7
+ defaultValue: UUIDV4,
8
+ primaryKey: true,
9
+ allowNull: false,
10
+ },
11
+ name: {
12
+ type: DataTypes.STRING,
13
+ allowNull: false,
14
+ },
15
+ });
16
+
17
+ module.exports = Organization;
@@ -0,0 +1,33 @@
1
+ const { sequelize } = require("../postgres");
2
+ const { DataTypes, UUIDV4 } = require("sequelize");
3
+
4
+ const Permission = sequelize.define("Permission", {
5
+ id: {
6
+ type: DataTypes.UUID,
7
+ defaultValue: UUIDV4,
8
+ primaryKey: true,
9
+ allowNull: false,
10
+ },
11
+ appId: {
12
+ type: DataTypes.UUID,
13
+ allowNull: false,
14
+ },
15
+ organizationId: {
16
+ type: DataTypes.UUID,
17
+ allowNull: false,
18
+ },
19
+ projectId: {
20
+ type: DataTypes.UUID,
21
+ allowNull: false,
22
+ },
23
+ userId: {
24
+ type: DataTypes.STRING,
25
+ allowNull: false,
26
+ },
27
+ role: {
28
+ type: DataTypes.STRING,
29
+ allowNull: false,
30
+ },
31
+ });
32
+
33
+ module.exports = Permission;
@@ -0,0 +1,37 @@
1
+ const { sequelize } = require("../postgres");
2
+ const { DataTypes, UUIDV4 } = require("sequelize");
3
+
4
+ const Project = sequelize.define("Project", {
5
+ id: {
6
+ type: DataTypes.UUID,
7
+ defaultValue: UUIDV4,
8
+ primaryKey: true,
9
+ allowNull: false,
10
+ },
11
+ name: {
12
+ type: DataTypes.STRING,
13
+ allowNull: false,
14
+ },
15
+ icon: {
16
+ type: DataTypes.STRING,
17
+ allowNull: false,
18
+ },
19
+ description: {
20
+ type: DataTypes.STRING,
21
+ allowNull: true,
22
+ },
23
+ type: {
24
+ type: DataTypes.STRING,
25
+ allowNull: true,
26
+ },
27
+ organizationId: {
28
+ type: DataTypes.UUID,
29
+ allowNull: true,
30
+ },
31
+ coach: {
32
+ type: DataTypes.STRING,
33
+ allowNull: true,
34
+ },
35
+ });
36
+
37
+ module.exports = Project;
@@ -0,0 +1,21 @@
1
+ const { sequelize } = require("../postgres");
2
+ const { DataTypes, UUIDV4 } = require("sequelize");
3
+
4
+ const Settings = sequelize.define("Settings", {
5
+ id: {
6
+ type: DataTypes.UUID,
7
+ defaultValue: UUIDV4,
8
+ primaryKey: true,
9
+ allowNull: false,
10
+ },
11
+ projectId: {
12
+ type: DataTypes.UUID,
13
+ allowNull: false,
14
+ },
15
+ settings: {
16
+ type: DataTypes.JSONB,
17
+ allowNull: false,
18
+ },
19
+ });
20
+
21
+ module.exports = Settings;
@@ -0,0 +1,51 @@
1
+ const Project = require("./Project");
2
+ const Organization = require("./Organization");
3
+ const Permission = require("./Permission");
4
+ const Setting = require("./Settings");
5
+
6
+ let _init = false;
7
+
8
+ async function init() {
9
+ if (!_init) {
10
+ _init = true;
11
+ } else {
12
+ return false;
13
+ }
14
+
15
+ Organization.hasMany(Permission, {
16
+ foreignKey: "organizationId",
17
+ as: "permissions",
18
+ });
19
+
20
+ Permission.belongsTo(Organization, {
21
+ foreignKey: "organizationId",
22
+ as: "organization",
23
+ });
24
+
25
+ Project.belongsTo(Organization, {
26
+ foreignKey: "organizationId",
27
+ as: "organization",
28
+ });
29
+
30
+ Organization.hasMany(Project, {
31
+ foreignKey: "organizationId",
32
+ as: "projects",
33
+ });
34
+
35
+ Project.hasMany(Permission, {
36
+ foreignKey: "projectId",
37
+ as: "permissions",
38
+ });
39
+
40
+ Organization.addHook("beforeDestroy", async (organization) => {
41
+ await Project.destroy({
42
+ where: {
43
+ organizationId: organization.id,
44
+ },
45
+ });
46
+ });
47
+
48
+ return true;
49
+ }
50
+
51
+ module.exports = { Project, Organization, Permission, init, Setting };
package/src/openapi.js ADDED
@@ -0,0 +1,40 @@
1
+ const { title, version } = require("./config")();
2
+ const swaggerJsdoc = require("swagger-jsdoc");
3
+ const j2s = require("joi-to-swagger");
4
+
5
+ const swaggerSpec = swaggerJsdoc({
6
+ definition: {
7
+ openapi: "3.0.0",
8
+ info: { title, version },
9
+ servers: [{ url: "/api" }],
10
+ },
11
+
12
+ apis: [`${process.cwd()}/src/routes/*.js`],
13
+ });
14
+
15
+ swaggerSpec.components = {
16
+ schemas: {},
17
+ };
18
+
19
+ let schemas;
20
+ try {
21
+ schemas = require(`${process.cwd()}/src/schemas`);
22
+ } catch (error) {
23
+ console.warn("[NUC]: Could not load schemas");
24
+ schemas = {};
25
+ }
26
+
27
+ if (schemas && typeof schemas === "object") {
28
+ if (schemas.default) {
29
+ for (const schema in schemas) {
30
+ try {
31
+ const { swagger } = j2s(schemas[schema]);
32
+ swaggerSpec.components.schemas[schema] = swagger;
33
+ } catch (error) {
34
+ console.warn(`Warning: Failed to process schema "${schema}":`);
35
+ }
36
+ }
37
+ }
38
+ }
39
+
40
+ module.exports = swaggerSpec;