@moneypot/hub 0.0.1
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.
- package/README.md +108 -0
- package/dist/cli/add-casino.d.ts +2 -0
- package/dist/cli/add-casino.js +116 -0
- package/dist/dashboard/assets/index-BtrbrisP.js +360 -0
- package/dist/dashboard/assets/index-tK7EUtyc.css +5 -0
- package/dist/dashboard/index.html +13 -0
- package/dist/src/GraphQLError.d.ts +8 -0
- package/dist/src/GraphQLError.js +79 -0
- package/dist/src/__generated__/fragment-masking.d.ts +19 -0
- package/dist/src/__generated__/fragment-masking.js +16 -0
- package/dist/src/__generated__/gql.d.ts +26 -0
- package/dist/src/__generated__/gql.js +15 -0
- package/dist/src/__generated__/graphql.d.ts +3129 -0
- package/dist/src/__generated__/graphql.js +454 -0
- package/dist/src/__generated__/index.d.ts +2 -0
- package/dist/src/__generated__/index.js +2 -0
- package/dist/src/config.d.ts +14 -0
- package/dist/src/config.js +57 -0
- package/dist/src/db/index.d.ts +89 -0
- package/dist/src/db/index.js +339 -0
- package/dist/src/db/internal.d.ts +7 -0
- package/dist/src/db/internal.js +33 -0
- package/dist/src/db/public.d.ts +7 -0
- package/dist/src/db/public.js +20 -0
- package/dist/src/db/types.d.ts +80 -0
- package/dist/src/db/types.js +1 -0
- package/dist/src/db/util.d.ts +6 -0
- package/dist/src/db/util.js +9 -0
- package/dist/src/express.d.ts +13 -0
- package/dist/src/express.js +1 -0
- package/dist/src/grafast.d.ts +1 -0
- package/dist/src/grafast.js +1 -0
- package/dist/src/graphile.d.ts +1 -0
- package/dist/src/graphile.js +1 -0
- package/dist/src/graphql-client.d.ts +6 -0
- package/dist/src/graphql-client.js +8 -0
- package/dist/src/graphql-queries.d.ts +18 -0
- package/dist/src/graphql-queries.js +123 -0
- package/dist/src/graphql.d.ts +1 -0
- package/dist/src/graphql.js +1 -0
- package/dist/src/index.d.ts +15 -0
- package/dist/src/index.js +65 -0
- package/dist/src/logger.d.ts +9 -0
- package/dist/src/logger.js +21 -0
- package/dist/src/pg-versions/001-schema.sql +456 -0
- package/dist/src/plugins/caas-add-casino.d.ts +1 -0
- package/dist/src/plugins/caas-add-casino.js +150 -0
- package/dist/src/plugins/caas-authenticate.d.ts +1 -0
- package/dist/src/plugins/caas-authenticate.js +175 -0
- package/dist/src/plugins/caas-balance-alert.d.ts +1 -0
- package/dist/src/plugins/caas-balance-alert.js +43 -0
- package/dist/src/plugins/caas-claim-faucet.d.ts +1 -0
- package/dist/src/plugins/caas-claim-faucet.js +85 -0
- package/dist/src/plugins/caas-current-x.d.ts +1 -0
- package/dist/src/plugins/caas-current-x.js +62 -0
- package/dist/src/plugins/caas-schema-prefix.d.ts +1 -0
- package/dist/src/plugins/caas-schema-prefix.js +25 -0
- package/dist/src/plugins/caas-user-balance-by-currency.d.ts +1 -0
- package/dist/src/plugins/caas-user-balance-by-currency.js +55 -0
- package/dist/src/plugins/caas-withdraw.d.ts +1 -0
- package/dist/src/plugins/caas-withdraw.js +133 -0
- package/dist/src/plugins/debug.d.ts +1 -0
- package/dist/src/plugins/debug.js +14 -0
- package/dist/src/plugins/hub-add-casino.d.ts +1 -0
- package/dist/src/plugins/hub-add-casino.js +150 -0
- package/dist/src/plugins/hub-authenticate.d.ts +1 -0
- package/dist/src/plugins/hub-authenticate.js +175 -0
- package/dist/src/plugins/hub-balance-alert.d.ts +1 -0
- package/dist/src/plugins/hub-balance-alert.js +43 -0
- package/dist/src/plugins/hub-claim-faucet.d.ts +1 -0
- package/dist/src/plugins/hub-claim-faucet.js +85 -0
- package/dist/src/plugins/hub-current-x.d.ts +1 -0
- package/dist/src/plugins/hub-current-x.js +62 -0
- package/dist/src/plugins/hub-schema-prefix.d.ts +1 -0
- package/dist/src/plugins/hub-schema-prefix.js +25 -0
- package/dist/src/plugins/hub-user-balance-by-currency.d.ts +1 -0
- package/dist/src/plugins/hub-user-balance-by-currency.js +55 -0
- package/dist/src/plugins/hub-withdraw.d.ts +1 -0
- package/dist/src/plugins/hub-withdraw.js +133 -0
- package/dist/src/plugins/id-to-node-id.d.ts +1 -0
- package/dist/src/plugins/id-to-node-id.js +31 -0
- package/dist/src/plugins/validate-fields.d.ts +1 -0
- package/dist/src/plugins/validate-fields.js +61 -0
- package/dist/src/process-transfers.d.ts +7 -0
- package/dist/src/process-transfers.js +413 -0
- package/dist/src/process-withdrawal-request.d.ts +5 -0
- package/dist/src/process-withdrawal-request.js +129 -0
- package/dist/src/server/graphile.config.d.ts +33 -0
- package/dist/src/server/graphile.config.js +166 -0
- package/dist/src/server/handle-errors.d.ts +10 -0
- package/dist/src/server/handle-errors.js +88 -0
- package/dist/src/server/index.d.ts +2 -0
- package/dist/src/server/index.js +69 -0
- package/dist/src/server/middleware/authentication.d.ts +4 -0
- package/dist/src/server/middleware/authentication.js +55 -0
- package/dist/src/server/middleware/cors.d.ts +3 -0
- package/dist/src/server/middleware/cors.js +14 -0
- package/dist/src/services/jwt-service.d.ts +13 -0
- package/dist/src/services/jwt-service.js +131 -0
- package/dist/src/smart-tags.d.ts +1 -0
- package/dist/src/smart-tags.js +55 -0
- package/dist/src/util.d.ts +12 -0
- package/dist/src/util.js +4 -0
- package/dist/src/validate.d.ts +9 -0
- package/dist/src/validate.js +91 -0
- package/package.json +69 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import "graphile-config";
|
|
2
|
+
import "postgraphile";
|
|
3
|
+
import { makePgService } from "postgraphile/adaptors/pg";
|
|
4
|
+
import { PostGraphileAmberPreset } from "postgraphile/presets/amber";
|
|
5
|
+
import config from "../config.js";
|
|
6
|
+
import { maskError } from "./handle-errors.js";
|
|
7
|
+
import * as db from "../db/index.js";
|
|
8
|
+
import { SmartTagsPlugin } from "../smart-tags.js";
|
|
9
|
+
import { HubAuthenticatePlugin } from "../plugins/hub-authenticate.js";
|
|
10
|
+
import { IdToNodeIdPlugin } from "../plugins/id-to-node-id.js";
|
|
11
|
+
import { HubClaimFaucetPlugin } from "../plugins/hub-claim-faucet.js";
|
|
12
|
+
import { DebugPlugin } from "../plugins/debug.js";
|
|
13
|
+
import { HubWithdrawPlugin } from "../plugins/hub-withdraw.js";
|
|
14
|
+
import { HubPrefixPlugin } from "../plugins/hub-schema-prefix.js";
|
|
15
|
+
import { isUuid } from "../util.js";
|
|
16
|
+
import { HubUserBalanceByCurrencyPlugin } from "../plugins/hub-user-balance-by-currency.js";
|
|
17
|
+
import { logger } from "../logger.js";
|
|
18
|
+
import { ValidateCasinoFieldsPlugin } from "../plugins/validate-fields.js";
|
|
19
|
+
import { HubAddCasinoPlugin } from "../plugins/hub-add-casino.js";
|
|
20
|
+
import { HubBalanceAlertPlugin } from "../plugins/hub-balance-alert.js";
|
|
21
|
+
import { custom as customPgOmitArchivedPlugin } from "@graphile-contrib/pg-omit-archived";
|
|
22
|
+
import { HubCurrentXPlugin } from "../plugins/hub-current-x.js";
|
|
23
|
+
export const requiredPlugins = [
|
|
24
|
+
SmartTagsPlugin,
|
|
25
|
+
IdToNodeIdPlugin,
|
|
26
|
+
HubPrefixPlugin,
|
|
27
|
+
HubAuthenticatePlugin,
|
|
28
|
+
HubCurrentXPlugin,
|
|
29
|
+
HubWithdrawPlugin,
|
|
30
|
+
HubBalanceAlertPlugin,
|
|
31
|
+
HubUserBalanceByCurrencyPlugin,
|
|
32
|
+
HubAddCasinoPlugin,
|
|
33
|
+
ValidateCasinoFieldsPlugin,
|
|
34
|
+
];
|
|
35
|
+
export const defaultPlugins = [
|
|
36
|
+
...(config.NODE_ENV === "development" ? [DebugPlugin] : []),
|
|
37
|
+
...requiredPlugins,
|
|
38
|
+
HubClaimFaucetPlugin,
|
|
39
|
+
customPgOmitArchivedPlugin("deleted"),
|
|
40
|
+
];
|
|
41
|
+
export function createPreset({ plugins, exportSchemaSDLPath, extraPgSchemas, }) {
|
|
42
|
+
if (!exportSchemaSDLPath.startsWith("/")) {
|
|
43
|
+
throw new Error("exportSchemaSDLPath must be an absolute path");
|
|
44
|
+
}
|
|
45
|
+
if (!exportSchemaSDLPath.endsWith(".graphql")) {
|
|
46
|
+
throw new Error("exportSchemaSDLPath must end with .graphql");
|
|
47
|
+
}
|
|
48
|
+
const mutablePlugins = [...plugins];
|
|
49
|
+
for (const requiredPlugin of requiredPlugins) {
|
|
50
|
+
if (!plugins.some((plugin) => plugin === requiredPlugin)) {
|
|
51
|
+
logger.warn(`Adding required plugin "${requiredPlugin.name}"`);
|
|
52
|
+
mutablePlugins.unshift(requiredPlugin);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
logger.info("Will save generated graphql schema to:", exportSchemaSDLPath);
|
|
56
|
+
const preset = {
|
|
57
|
+
extends: [PostGraphileAmberPreset],
|
|
58
|
+
disablePlugins: ["NodePlugin"],
|
|
59
|
+
pgServices: [
|
|
60
|
+
makePgService({
|
|
61
|
+
connectionString: config.DATABASE_URL,
|
|
62
|
+
schemas: [...extraPgSchemas, "hub"],
|
|
63
|
+
}),
|
|
64
|
+
],
|
|
65
|
+
schema: {
|
|
66
|
+
dontSwallowErrors: true,
|
|
67
|
+
exportSchemaSDLPath,
|
|
68
|
+
sortExport: true,
|
|
69
|
+
defaultBehavior: [
|
|
70
|
+
"-resource:update",
|
|
71
|
+
"-resource:delete",
|
|
72
|
+
"-resource:insert",
|
|
73
|
+
"-query:resource:list",
|
|
74
|
+
"-query:resource:connection",
|
|
75
|
+
].join(" "),
|
|
76
|
+
pgDeletedColumnName: "deleted_at",
|
|
77
|
+
},
|
|
78
|
+
grafserv: {
|
|
79
|
+
dangerouslyAllowAllCORSRequests: true,
|
|
80
|
+
websockets: true,
|
|
81
|
+
maskError(error) {
|
|
82
|
+
return maskError(error);
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
grafast: {
|
|
86
|
+
context(ctx, _args) {
|
|
87
|
+
if (ctx.ws) {
|
|
88
|
+
return handleWebsocketContext(ctx) || {};
|
|
89
|
+
}
|
|
90
|
+
const expressReq = ctx.expressv4.req;
|
|
91
|
+
const reqIdentity = expressReq.identity;
|
|
92
|
+
let pluginIdentity = undefined;
|
|
93
|
+
if (reqIdentity?.kind === "operator") {
|
|
94
|
+
pluginIdentity = {
|
|
95
|
+
kind: "operator",
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
else if (reqIdentity?.kind === "user") {
|
|
99
|
+
pluginIdentity = {
|
|
100
|
+
kind: "user",
|
|
101
|
+
session: {
|
|
102
|
+
user_id: reqIdentity.user.id,
|
|
103
|
+
mp_user_id: reqIdentity.user.mp_user_id,
|
|
104
|
+
casino_id: reqIdentity.user.casino_id,
|
|
105
|
+
experience_id: reqIdentity.user.experience_id,
|
|
106
|
+
session_id: reqIdentity.sessionId,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
const pgSettings = {};
|
|
111
|
+
if (reqIdentity?.kind === "user") {
|
|
112
|
+
pgSettings["session.user_id"] = reqIdentity.user.id;
|
|
113
|
+
pgSettings["session.experience_id"] = reqIdentity.user.experience_id;
|
|
114
|
+
pgSettings["session.casino_id"] = reqIdentity.user.casino_id;
|
|
115
|
+
pgSettings["session.session_id"] = reqIdentity.sessionId;
|
|
116
|
+
}
|
|
117
|
+
else if (reqIdentity?.kind === "operator") {
|
|
118
|
+
pgSettings["operator.api_key"] = reqIdentity.apiKey;
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
pgSettings,
|
|
122
|
+
identity: pluginIdentity,
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
plugins: mutablePlugins,
|
|
127
|
+
};
|
|
128
|
+
return preset;
|
|
129
|
+
}
|
|
130
|
+
function getSessionIdFromWebsocketCtx(ctx) {
|
|
131
|
+
const value = Object.entries(ctx.ws.connectionParams).find(([key]) => key.toLowerCase() === "authorization")?.[1];
|
|
132
|
+
if (typeof value !== "string") {
|
|
133
|
+
return "";
|
|
134
|
+
}
|
|
135
|
+
const uuid = value.slice("session:".length);
|
|
136
|
+
return isUuid(uuid) ? uuid : "";
|
|
137
|
+
}
|
|
138
|
+
async function handleWebsocketContext(ctx) {
|
|
139
|
+
const sessionId = getSessionIdFromWebsocketCtx(ctx);
|
|
140
|
+
if (!sessionId) {
|
|
141
|
+
throw new Error("Unauthorized");
|
|
142
|
+
}
|
|
143
|
+
const result = await db.userFromActiveSessionKey(db.superuserPool, sessionId);
|
|
144
|
+
if (!result) {
|
|
145
|
+
throw new Error("Unauthorized");
|
|
146
|
+
}
|
|
147
|
+
const session = {
|
|
148
|
+
user_id: result.user.id,
|
|
149
|
+
mp_user_id: result.user.mp_user_id,
|
|
150
|
+
casino_id: result.user.casino_id,
|
|
151
|
+
experience_id: result.user.experience_id,
|
|
152
|
+
session_id: result.sessionId,
|
|
153
|
+
};
|
|
154
|
+
return {
|
|
155
|
+
pgSettings: {
|
|
156
|
+
"session.user_id": result.user.id,
|
|
157
|
+
"session.experience_id": result.user.experience_id,
|
|
158
|
+
"session.casino_id": result.user.casino_id,
|
|
159
|
+
"session.session_id": result.sessionId,
|
|
160
|
+
},
|
|
161
|
+
identity: {
|
|
162
|
+
kind: "user",
|
|
163
|
+
session,
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { GraphQLError } from "postgraphile/graphql";
|
|
2
|
+
declare const pluck: (err: any) => {
|
|
3
|
+
[key: string]: any;
|
|
4
|
+
};
|
|
5
|
+
export declare const ERROR_MESSAGE_OVERRIDES: {
|
|
6
|
+
[code: string]: typeof pluck;
|
|
7
|
+
};
|
|
8
|
+
export declare function maskError(error: GraphQLError): GraphQLError;
|
|
9
|
+
export default function handleErrors(errors: readonly GraphQLError[]): GraphQLError[];
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { GraphQLError } from "postgraphile/graphql";
|
|
2
|
+
import config from "../config.js";
|
|
3
|
+
const isDev = config.NODE_ENV === "development";
|
|
4
|
+
const isTest = config.NODE_ENV === "test";
|
|
5
|
+
const camelCase = (s) => s.toLowerCase().replace(/(_\\w)/g, (m) => m[1].toUpperCase());
|
|
6
|
+
const ERROR_PROPERTIES_TO_EXPOSE = isDev || isTest
|
|
7
|
+
? [
|
|
8
|
+
"code",
|
|
9
|
+
"severity",
|
|
10
|
+
"detail",
|
|
11
|
+
"hint",
|
|
12
|
+
"position",
|
|
13
|
+
"internalPosition",
|
|
14
|
+
"internalQuery",
|
|
15
|
+
"where",
|
|
16
|
+
"schema",
|
|
17
|
+
"table",
|
|
18
|
+
"column",
|
|
19
|
+
"dataType",
|
|
20
|
+
"constraint",
|
|
21
|
+
]
|
|
22
|
+
: ["code"];
|
|
23
|
+
const pluck = (err) => {
|
|
24
|
+
return ERROR_PROPERTIES_TO_EXPOSE.reduce((memo, key) => {
|
|
25
|
+
const value = key === "code"
|
|
26
|
+
?
|
|
27
|
+
err.code || err.errcode
|
|
28
|
+
: err[key];
|
|
29
|
+
if (value != null) {
|
|
30
|
+
memo[key] = value;
|
|
31
|
+
}
|
|
32
|
+
return memo;
|
|
33
|
+
}, Object.create(null));
|
|
34
|
+
};
|
|
35
|
+
export const ERROR_MESSAGE_OVERRIDES = {
|
|
36
|
+
"42501": (err) => ({
|
|
37
|
+
...pluck(err),
|
|
38
|
+
message: "Permission denied (by RLS)",
|
|
39
|
+
}),
|
|
40
|
+
"23505": (err) => ({
|
|
41
|
+
...pluck(err),
|
|
42
|
+
message: "Conflict occurred",
|
|
43
|
+
fields: conflictFieldsFromError(err),
|
|
44
|
+
code: "NUNIQ",
|
|
45
|
+
}),
|
|
46
|
+
"23503": (err) => ({
|
|
47
|
+
...pluck(err),
|
|
48
|
+
message: "Invalid reference",
|
|
49
|
+
fields: conflictFieldsFromError(err),
|
|
50
|
+
code: "BADFK",
|
|
51
|
+
}),
|
|
52
|
+
};
|
|
53
|
+
function conflictFieldsFromError(err) {
|
|
54
|
+
const { table, constraint } = err;
|
|
55
|
+
if (constraint && table) {
|
|
56
|
+
const PREFIX = `${table}_`;
|
|
57
|
+
const SUFFIX_LIST = [`_key`, `_fkey`];
|
|
58
|
+
if (constraint.startsWith(PREFIX)) {
|
|
59
|
+
const matchingSuffix = SUFFIX_LIST.find((SUFFIX) => constraint.endsWith(SUFFIX));
|
|
60
|
+
if (matchingSuffix) {
|
|
61
|
+
const maybeColumnNames = constraint.substr(PREFIX.length, constraint.length - PREFIX.length - matchingSuffix.length);
|
|
62
|
+
return [camelCase(maybeColumnNames)];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
export function maskError(error) {
|
|
69
|
+
const { message: rawMessage, originalError } = error;
|
|
70
|
+
const code = originalError ? originalError["code"] : null;
|
|
71
|
+
const localPluck = ERROR_MESSAGE_OVERRIDES[code] || pluck;
|
|
72
|
+
const exception = localPluck(originalError || error);
|
|
73
|
+
const options = {
|
|
74
|
+
nodes: error.nodes,
|
|
75
|
+
source: error.source,
|
|
76
|
+
positions: error.positions,
|
|
77
|
+
path: error.path,
|
|
78
|
+
originalError: error.originalError,
|
|
79
|
+
extensions: {
|
|
80
|
+
exception,
|
|
81
|
+
code: code || exception.code || error.extensions.code,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
return new GraphQLError(exception.message || rawMessage, options);
|
|
85
|
+
}
|
|
86
|
+
export default function handleErrors(errors) {
|
|
87
|
+
return errors.map(maskError);
|
|
88
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { grafserv } from "grafserv/express/v4";
|
|
3
|
+
import postgraphile from "postgraphile";
|
|
4
|
+
import { createPreset, defaultPlugins } from "./graphile.config.js";
|
|
5
|
+
import express from "express";
|
|
6
|
+
import config from "../config.js";
|
|
7
|
+
import { logger } from "../logger.js";
|
|
8
|
+
import cors from "./middleware/cors.js";
|
|
9
|
+
import authentication from "./middleware/authentication.js";
|
|
10
|
+
import path, { dirname } from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import fs from "node:fs";
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = dirname(__filename);
|
|
15
|
+
function findDistDirectory(startPath) {
|
|
16
|
+
let currentPath = startPath;
|
|
17
|
+
while (currentPath !== "/") {
|
|
18
|
+
console.log(currentPath);
|
|
19
|
+
const distPath = path.join(currentPath, "dist");
|
|
20
|
+
if (fs.existsSync(distPath)) {
|
|
21
|
+
return distPath;
|
|
22
|
+
}
|
|
23
|
+
currentPath = path.dirname(currentPath);
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const distDir = findDistDirectory(__dirname);
|
|
28
|
+
if (!distDir) {
|
|
29
|
+
throw new Error("Could not find dist directory");
|
|
30
|
+
}
|
|
31
|
+
const dashboardDir = path.join(distDir, "dashboard");
|
|
32
|
+
if (!fs.existsSync(dashboardDir)) {
|
|
33
|
+
throw new Error(`Could not find dashboard directory. Expected it to be at "${dashboardDir}"`);
|
|
34
|
+
}
|
|
35
|
+
function createExpressServer() {
|
|
36
|
+
const app = express();
|
|
37
|
+
app.disable("x-powered-by");
|
|
38
|
+
app.use(cors());
|
|
39
|
+
app.use(authentication());
|
|
40
|
+
app.use("/dashboard", express.static(dashboardDir));
|
|
41
|
+
app.use("/dashboard/*splat", (_, res) => {
|
|
42
|
+
res.sendFile(path.join(dashboardDir, "index.html"));
|
|
43
|
+
});
|
|
44
|
+
return app;
|
|
45
|
+
}
|
|
46
|
+
export async function listen({ configureApp, plugins, exportSchemaSDLPath, extraPgSchemas, }) {
|
|
47
|
+
const expressServer = createExpressServer();
|
|
48
|
+
const preset = createPreset({
|
|
49
|
+
plugins: plugins ?? defaultPlugins,
|
|
50
|
+
exportSchemaSDLPath,
|
|
51
|
+
extraPgSchemas: extraPgSchemas ?? [],
|
|
52
|
+
});
|
|
53
|
+
const pgl = postgraphile.default(preset);
|
|
54
|
+
const serv = pgl.createServ(grafserv);
|
|
55
|
+
if (configureApp) {
|
|
56
|
+
configureApp(expressServer);
|
|
57
|
+
}
|
|
58
|
+
const server = createServer(expressServer);
|
|
59
|
+
server.on("error", (e) => {
|
|
60
|
+
logger.error(e);
|
|
61
|
+
});
|
|
62
|
+
serv.addTo(expressServer, server).catch((e) => {
|
|
63
|
+
logger.error(e);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
});
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
server.listen(config.PORT, resolve);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { isUuid } from "../../util.js";
|
|
2
|
+
import * as db from "../../db/index.js";
|
|
3
|
+
async function checkAndUpdateApiKey(key) {
|
|
4
|
+
const row = await db.superuserPool
|
|
5
|
+
.query(`
|
|
6
|
+
UPDATE hub.api_key
|
|
7
|
+
SET last_used_at = now()
|
|
8
|
+
WHERE key = $1::uuid
|
|
9
|
+
AND revoked_at IS NULL
|
|
10
|
+
RETURNING id`, [key])
|
|
11
|
+
.then(db.maybeOneRow);
|
|
12
|
+
return !!row;
|
|
13
|
+
}
|
|
14
|
+
const authentication = () => {
|
|
15
|
+
return async (req, _, next) => {
|
|
16
|
+
const header = req.get("authorization");
|
|
17
|
+
if (!header) {
|
|
18
|
+
return next();
|
|
19
|
+
}
|
|
20
|
+
const match = header.match(/^(session|apikey):(.+)$/);
|
|
21
|
+
if (!match) {
|
|
22
|
+
return next();
|
|
23
|
+
}
|
|
24
|
+
const tokenType = match[1];
|
|
25
|
+
const token = match[2];
|
|
26
|
+
if (!isUuid(token)) {
|
|
27
|
+
return next();
|
|
28
|
+
}
|
|
29
|
+
if (tokenType === "session") {
|
|
30
|
+
const sessionKey = token;
|
|
31
|
+
const result = await db.userFromActiveSessionKey(db.superuserPool, sessionKey);
|
|
32
|
+
if (result) {
|
|
33
|
+
req.identity = {
|
|
34
|
+
kind: "user",
|
|
35
|
+
user: result.user,
|
|
36
|
+
sessionId: result.sessionId,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return next();
|
|
40
|
+
}
|
|
41
|
+
else if (tokenType === "apikey") {
|
|
42
|
+
const apiKey = token;
|
|
43
|
+
const isValid = await checkAndUpdateApiKey(apiKey);
|
|
44
|
+
if (isValid) {
|
|
45
|
+
req.identity = {
|
|
46
|
+
kind: "operator",
|
|
47
|
+
apiKey,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return next();
|
|
51
|
+
}
|
|
52
|
+
return next();
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
export default authentication;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
const cors = () => {
|
|
2
|
+
return async (req, res, next) => {
|
|
3
|
+
res.header("Access-Control-Allow-Origin", "*");
|
|
4
|
+
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization");
|
|
5
|
+
if (req.method === "OPTIONS") {
|
|
6
|
+
res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, PATCH");
|
|
7
|
+
res.header("Access-Control-Max-Age", "86400");
|
|
8
|
+
res.status(204).end();
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
next();
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
export default cors;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { PoolClient } from "pg";
|
|
2
|
+
import { GraphQLClient } from "graphql-request";
|
|
3
|
+
import { QueryExecutor, Result } from "../util.js";
|
|
4
|
+
export declare function verifyJwtFromDbCacheAndEnsureNotAlreadyUsed(pgClient: PoolClient, { casinoId, jwt, }: {
|
|
5
|
+
casinoId: string;
|
|
6
|
+
jwt: string;
|
|
7
|
+
}): Promise<Result<{
|
|
8
|
+
userToken: string;
|
|
9
|
+
}, string>>;
|
|
10
|
+
export declare function refreshCasinoJwksTask(pgClient: QueryExecutor, { casinoId, graphqlClient, }: {
|
|
11
|
+
casinoId: string;
|
|
12
|
+
graphqlClient: GraphQLClient;
|
|
13
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import * as jose from "jose";
|
|
3
|
+
import { gql } from "../__generated__/gql.js";
|
|
4
|
+
import { maybeOneRow } from "../db/util.js";
|
|
5
|
+
import { logger } from "../logger.js";
|
|
6
|
+
const CACHE_TTL = "5 minutes";
|
|
7
|
+
const GET_CASINO_JWKS = gql(`
|
|
8
|
+
query GetCasinoJWKS {
|
|
9
|
+
jwks
|
|
10
|
+
}
|
|
11
|
+
`);
|
|
12
|
+
const JSONWebKeySetSchema = z.object({
|
|
13
|
+
keys: z
|
|
14
|
+
.array(z.object({
|
|
15
|
+
kty: z.literal("OKP"),
|
|
16
|
+
kid: z.string().uuid(),
|
|
17
|
+
use: z.literal("sig"),
|
|
18
|
+
alg: z.literal("EdDSA"),
|
|
19
|
+
crv: z.literal("Ed25519"),
|
|
20
|
+
x: z.string(),
|
|
21
|
+
}))
|
|
22
|
+
.min(1),
|
|
23
|
+
});
|
|
24
|
+
async function dbInsertCasinoJWKS(pgClient, { casinoId, jwks: unvalidatedJwks, }) {
|
|
25
|
+
logger.debug(`[dbInsertCasinoJWKS] Inserting JWKS for casino ${casinoId}`, unvalidatedJwks);
|
|
26
|
+
const result = JSONWebKeySetSchema.safeParse(unvalidatedJwks);
|
|
27
|
+
if (!result.success) {
|
|
28
|
+
throw new Error(`Will not insert invalid JWKS into database: ${result.error}`);
|
|
29
|
+
}
|
|
30
|
+
await pgClient.query(`
|
|
31
|
+
WITH old_value AS (
|
|
32
|
+
SELECT jwks FROM hub.jwk_set WHERE casino_id = $1
|
|
33
|
+
),
|
|
34
|
+
updated AS (
|
|
35
|
+
INSERT INTO hub.jwk_set(casino_id, jwks)
|
|
36
|
+
VALUES($1, $2)
|
|
37
|
+
ON CONFLICT (casino_id) DO UPDATE
|
|
38
|
+
SET jwks = EXCLUDED.jwks,
|
|
39
|
+
updated_at = NOW()
|
|
40
|
+
RETURNING casino_id, jwks,
|
|
41
|
+
(
|
|
42
|
+
NOT EXISTS (SELECT 1 FROM old_value) -- new insert
|
|
43
|
+
OR
|
|
44
|
+
(SELECT jwks::jsonb FROM old_value) IS DISTINCT FROM $2::jsonb -- content changed
|
|
45
|
+
) as changed
|
|
46
|
+
)
|
|
47
|
+
INSERT INTO hub.jwk_set_snapshot(casino_id, jwks)
|
|
48
|
+
SELECT casino_id, jwks FROM updated
|
|
49
|
+
WHERE changed = true
|
|
50
|
+
`, [casinoId, result.data]);
|
|
51
|
+
}
|
|
52
|
+
async function dbGetCasinoJWKS(pgClient, { casinoId }) {
|
|
53
|
+
const jwks = await pgClient
|
|
54
|
+
.query(`
|
|
55
|
+
SELECT *
|
|
56
|
+
FROM hub.jwk_set
|
|
57
|
+
WHERE casino_id = $1
|
|
58
|
+
`, [casinoId])
|
|
59
|
+
.then(maybeOneRow);
|
|
60
|
+
return jwks ?? null;
|
|
61
|
+
}
|
|
62
|
+
async function mpFetchCasinoJWKS(graphqlClient) {
|
|
63
|
+
console.log("sending request to casino");
|
|
64
|
+
const response = await graphqlClient.request(GET_CASINO_JWKS);
|
|
65
|
+
console.log("response", response);
|
|
66
|
+
const jwks = response.jwks;
|
|
67
|
+
if (jwks.keys.length === 0) {
|
|
68
|
+
throw new Error("No JWKS keys found in response");
|
|
69
|
+
}
|
|
70
|
+
const parseResult = JSONWebKeySetSchema.safeParse(jwks);
|
|
71
|
+
if (!parseResult.success) {
|
|
72
|
+
throw new Error("Invalid JWKS from casino", parseResult.error);
|
|
73
|
+
}
|
|
74
|
+
return parseResult.data;
|
|
75
|
+
}
|
|
76
|
+
export async function verifyJwtFromDbCacheAndEnsureNotAlreadyUsed(pgClient, { casinoId, jwt, }) {
|
|
77
|
+
const jwksRow = await dbGetCasinoJWKS(pgClient, { casinoId });
|
|
78
|
+
if (!jwksRow) {
|
|
79
|
+
throw new Error("No JWKS found in database");
|
|
80
|
+
}
|
|
81
|
+
const joseJwks = await jose.createLocalJWKSet(jwksRow.jwks);
|
|
82
|
+
let result;
|
|
83
|
+
try {
|
|
84
|
+
result = await jose.jwtVerify(jwt, joseJwks);
|
|
85
|
+
}
|
|
86
|
+
catch (e) {
|
|
87
|
+
if (e instanceof jose.errors.JWTExpired) {
|
|
88
|
+
return { ok: false, error: "User token expired" };
|
|
89
|
+
}
|
|
90
|
+
return { ok: false, error: "Invalid user token" };
|
|
91
|
+
}
|
|
92
|
+
const { userToken } = result.payload;
|
|
93
|
+
if (typeof userToken !== "string") {
|
|
94
|
+
return { ok: false, error: "No user token found in JWT" };
|
|
95
|
+
}
|
|
96
|
+
const alreadyUsed = await dbAlreadyUsedUserToken(pgClient, { userToken });
|
|
97
|
+
if (alreadyUsed) {
|
|
98
|
+
return { ok: false, error: "User token already used" };
|
|
99
|
+
}
|
|
100
|
+
return { ok: true, value: { userToken } };
|
|
101
|
+
}
|
|
102
|
+
async function dbAlreadyUsedUserToken(pgClient, { userToken }) {
|
|
103
|
+
const result = await pgClient
|
|
104
|
+
.query(`
|
|
105
|
+
SELECT 1
|
|
106
|
+
FROM hub.session
|
|
107
|
+
WHERE user_token = $1
|
|
108
|
+
|
|
109
|
+
FOR UPDATE SKIP LOCKED
|
|
110
|
+
`, [userToken])
|
|
111
|
+
.then(maybeOneRow);
|
|
112
|
+
return !!result;
|
|
113
|
+
}
|
|
114
|
+
export async function refreshCasinoJwksTask(pgClient, { casinoId, graphqlClient, }) {
|
|
115
|
+
const cacheHit = await pgClient
|
|
116
|
+
.query(`
|
|
117
|
+
SELECT 1
|
|
118
|
+
FROM hub.jwk_set
|
|
119
|
+
WHERE casino_id = $1
|
|
120
|
+
AND updated_at > NOW() - $2::interval
|
|
121
|
+
`, [casinoId, CACHE_TTL])
|
|
122
|
+
.then((res) => res.rows.length > 0);
|
|
123
|
+
if (cacheHit) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const jwks = await mpFetchCasinoJWKS(graphqlClient);
|
|
127
|
+
await dbInsertCasinoJWKS(pgClient, {
|
|
128
|
+
casinoId,
|
|
129
|
+
jwks,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const SmartTagsPlugin: GraphileConfig.Plugin;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { makeJSONPgSmartTagsPlugin } from "postgraphile/utils";
|
|
2
|
+
export const SmartTagsPlugin = makeJSONPgSmartTagsPlugin({
|
|
3
|
+
version: 1,
|
|
4
|
+
config: {
|
|
5
|
+
attribute: {
|
|
6
|
+
"hub.casino.id": {
|
|
7
|
+
tags: {
|
|
8
|
+
behavior: ["-attribute:update"],
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
"hub.session.user_token": {
|
|
12
|
+
tags: {
|
|
13
|
+
behavior: ["-orderBy"],
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
"hub.session.key": {
|
|
17
|
+
tags: {
|
|
18
|
+
behavior: ["-orderBy"],
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
"hub.bankroll.amount": {
|
|
22
|
+
tags: {
|
|
23
|
+
behavior: ["+attribute:update"],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
class: {
|
|
28
|
+
"hub.balance": {
|
|
29
|
+
tags: {
|
|
30
|
+
behavior: ["-resource:select"],
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
"hub.bankroll": {
|
|
34
|
+
tags: {
|
|
35
|
+
behavior: ["+resource:update"],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
"hub.casino": {
|
|
39
|
+
tags: {
|
|
40
|
+
behavior: [
|
|
41
|
+
"+query:resource:connection",
|
|
42
|
+
"+resource:update",
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
"hub.api_key": {
|
|
47
|
+
tags: {
|
|
48
|
+
behavior: [
|
|
49
|
+
"+query:resource:connection",
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { QueryResult, QueryResultRow } from "pg";
|
|
2
|
+
export declare function isUuid(input: any): boolean;
|
|
3
|
+
export interface QueryExecutor {
|
|
4
|
+
query<T extends QueryResultRow = any>(queryText: string, values?: any[]): Promise<QueryResult<T>>;
|
|
5
|
+
}
|
|
6
|
+
export type Result<V, E> = {
|
|
7
|
+
ok: true;
|
|
8
|
+
value: V;
|
|
9
|
+
} | {
|
|
10
|
+
ok: false;
|
|
11
|
+
error: E;
|
|
12
|
+
};
|
package/dist/src/util.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import * as Yup from "yup";
|
|
2
|
+
export declare const isUuid: {
|
|
3
|
+
name: string;
|
|
4
|
+
message: string;
|
|
5
|
+
test(value: string | undefined): boolean;
|
|
6
|
+
};
|
|
7
|
+
export declare const graphqlUrl: () => Yup.StringSchema<string | undefined, Yup.AnyObject, undefined, "">;
|
|
8
|
+
export declare const baseUrl: () => Yup.StringSchema<string | undefined, Yup.AnyObject, undefined, "">;
|
|
9
|
+
export declare const uuid: () => Yup.StringSchema<string | undefined, Yup.AnyObject, undefined, "">;
|