@prosopo/client-example-server 2.7.170 → 2.7.172
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/.turbo/turbo-build$colon$cjs.log +2 -5
- package/.turbo/turbo-build$colon$tsc.log +21 -24
- package/.turbo/turbo-build.log +3 -9
- package/CHANGELOG.md +40 -0
- package/LICENSE +201 -0
- package/README.md +6 -6
- package/dist/cjs/controllers/auth.cjs +1 -1
- package/dist/controllers/auth.js +1 -1
- package/dist/controllers/auth.js.map +1 -1
- package/package.json +81 -80
- package/env.development +0 -10
- package/env.production +0 -2
- package/env.test +0 -10
- package/src/app.ts +0 -210
- package/src/controllers/auth.ts +0 -396
- package/src/models/user.ts +0 -45
- package/src/routes/routes.ts +0 -57
- package/src/tests/app.unit.test.ts +0 -428
- package/src/tests/appEntrypoint.unit.test.ts +0 -174
- package/src/tests/auth.unit.test.ts +0 -915
- package/src/tests/authHarness.ts +0 -131
- package/src/tests/clientExampleServer.test-d.ts +0 -146
- package/src/tests/routes.unit.test.ts +0 -166
- package/src/tests/utils.unit.test.ts +0 -169
- package/src/utils/connection.ts +0 -41
- package/src/utils/database.ts +0 -22
- package/tsconfig.cjs.json +0 -38
- package/tsconfig.json +0 -44
- package/tsconfig.tsbuildinfo +0 -1
- package/tsconfig.types.json +0 -9
package/env.development
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
PROSOPO_VERIFY_ENDPOINT=https://localhost:9235/development/siteverify
|
|
2
|
-
PROSOPO_VERIFICATION_TYPE=api
|
|
3
|
-
PROSOPO_SITE_KEY=5GCP69mhanZqJqnTvaaGvJCJZWSahz6xE2c7bTGETqSB5KDK
|
|
4
|
-
PROSOPO_WEB2=true
|
|
5
|
-
PROSOPO_SERVER_URL=https://localhost
|
|
6
|
-
PROSOPO_SITE_PRIVATE_KEY=bottom drive obey lake curtain smoke basket hold race lonely fit walk
|
|
7
|
-
PROSOPO_SERVER_PORT=9228
|
|
8
|
-
PROSOPO_DEFAULT_ENVIRONMENT=development
|
|
9
|
-
PROSOPO_MONGO_EVENTS_URI=mongodb+srv://<MONGO_URI_HERE>/frictionless_events
|
|
10
|
-
_DEV_ONLY_WATCH_EVENTS=false
|
package/env.production
DELETED
package/env.test
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
PROSOPO_VERIFY_ENDPOINT=https://localhost:9235/test/siteverify
|
|
2
|
-
PROSOPO_VERIFICATION_TYPE=api
|
|
3
|
-
PROSOPO_SITE_KEY=5GCP69mhanZqJqnTvaaGvJCJZWSahz6xE2c7bTGETqSB5KDK
|
|
4
|
-
PROSOPO_WEB2=true
|
|
5
|
-
PROSOPO_SERVER_URL=https://localhost
|
|
6
|
-
PROSOPO_SITE_PRIVATE_KEY=bottom drive obey lake curtain smoke basket hold race lonely fit walk
|
|
7
|
-
PROSOPO_SERVER_PORT=9228
|
|
8
|
-
PROSOPO_DEFAULT_ENVIRONMENT=test
|
|
9
|
-
PROSOPO_MONGO_EVENTS_URI=mongodb+srv://<MONGO_URI_HERE>/frictionless_events
|
|
10
|
-
_DEV_ONLY_WATCH_EVENTS=false
|
package/src/app.ts
DELETED
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
|
|
15
|
-
import fs from "node:fs";
|
|
16
|
-
import http from "node:http";
|
|
17
|
-
import type { Server } from "node:http";
|
|
18
|
-
import https from "node:https";
|
|
19
|
-
import type { ServerOptions } from "node:https";
|
|
20
|
-
import path from "node:path";
|
|
21
|
-
import { fileURLToPath } from "node:url";
|
|
22
|
-
import { ProsopoEnvError } from "@prosopo/common";
|
|
23
|
-
import { loadEnv } from "@prosopo/dotenv";
|
|
24
|
-
import { getLogger } from "@prosopo/logger";
|
|
25
|
-
import { getServerConfig } from "@prosopo/server";
|
|
26
|
-
import type { ProsopoServerConfigOutput } from "@prosopo/types";
|
|
27
|
-
import { at, isMain } from "@prosopo/util";
|
|
28
|
-
import cors from "cors";
|
|
29
|
-
import express from "express";
|
|
30
|
-
import routesFactory from "./routes/routes.js";
|
|
31
|
-
import connectionFactory from "./utils/connection.js";
|
|
32
|
-
import memoryServerSetup from "./utils/database.js";
|
|
33
|
-
|
|
34
|
-
export enum ProsopoVerificationType {
|
|
35
|
-
api = "api",
|
|
36
|
-
local = "local",
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const logger = getLogger("info", "client-example-server:app");
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* The siteverify endpoint the deployment talks to. Non-production
|
|
43
|
-
* environments get their name as a subdomain prefix. An unset NODE_ENV is
|
|
44
|
-
* treated as production: prefixing it produced `undefined-api.prosopo.io`,
|
|
45
|
-
* a host that does not exist.
|
|
46
|
-
*/
|
|
47
|
-
export const resolveVerifyEndpoint = (): string => {
|
|
48
|
-
const environment = process.env.NODE_ENV;
|
|
49
|
-
const apiPrefix =
|
|
50
|
-
!environment || environment === "production" ? "" : `${environment}-`;
|
|
51
|
-
return (
|
|
52
|
-
process.env.PROSOPO_VERIFY_ENDPOINT ||
|
|
53
|
-
`https://${apiPrefix}api.prosopo.io/siteverify`
|
|
54
|
-
);
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
/** Anything the enum doesn't name falls back to the hosted API. */
|
|
58
|
-
export const resolveVerifyType = (): ProsopoVerificationType =>
|
|
59
|
-
Object.keys(ProsopoVerificationType).includes(
|
|
60
|
-
process.env.PROSOPO_VERIFICATION_TYPE as string,
|
|
61
|
-
)
|
|
62
|
-
? (process.env.PROSOPO_VERIFICATION_TYPE as ProsopoVerificationType)
|
|
63
|
-
: ProsopoVerificationType.api;
|
|
64
|
-
|
|
65
|
-
export const isDevelopmentOrTest = (): boolean =>
|
|
66
|
-
process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test";
|
|
67
|
-
|
|
68
|
-
/** The express app, minus the routes, which need a database connection. */
|
|
69
|
-
export const createApp = (): express.Express => {
|
|
70
|
-
const app = express();
|
|
71
|
-
|
|
72
|
-
// https://express-rate-limit.mintlify.app/guides/troubleshooting-proxy-issues
|
|
73
|
-
app.set("trust proxy", 1);
|
|
74
|
-
|
|
75
|
-
app.use(cors({ origin: true, credentials: true }));
|
|
76
|
-
|
|
77
|
-
app.use(express.urlencoded({ extended: true }));
|
|
78
|
-
|
|
79
|
-
app.use(express.json());
|
|
80
|
-
|
|
81
|
-
app.use((_, res, next) => {
|
|
82
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
83
|
-
res.setHeader(
|
|
84
|
-
"Access-Control-Allow-Methods",
|
|
85
|
-
"GET, POST, PUT, PATCH, DELETE",
|
|
86
|
-
);
|
|
87
|
-
res.setHeader(
|
|
88
|
-
"Access-Control-Allow-Headers",
|
|
89
|
-
"Origin, Content-Type, X-Auth-Token, Authorization",
|
|
90
|
-
);
|
|
91
|
-
next();
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
// No explicit OPTIONS handler: cors() above already answers preflight
|
|
95
|
-
// requests with a 204 and never calls through, so the handler that used to
|
|
96
|
-
// sit here could not run.
|
|
97
|
-
|
|
98
|
-
return app;
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
export const DEFAULT_PORT = 9228;
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* The port is the last segment of the configured server URL. A URL without
|
|
105
|
-
* one used to yield NaN, which node turns into an arbitrary free port — the
|
|
106
|
-
* server came up on an address nothing was configured to talk to.
|
|
107
|
-
*/
|
|
108
|
-
export const resolvePort = (config: ProsopoServerConfigOutput): number => {
|
|
109
|
-
if (!config.serverUrl) return DEFAULT_PORT;
|
|
110
|
-
const segments = config.serverUrl.split(":");
|
|
111
|
-
if (segments.length < 3) return DEFAULT_PORT;
|
|
112
|
-
const port = Number.parseInt(at(segments, 2));
|
|
113
|
-
return Number.isNaN(port) ? DEFAULT_PORT : port;
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* HTTPS in development/test when the repo's certificates exist, plain HTTP
|
|
118
|
-
* otherwise — in production Caddy terminates TLS.
|
|
119
|
-
*/
|
|
120
|
-
export const startServer = (
|
|
121
|
-
app: express.Express,
|
|
122
|
-
port: number,
|
|
123
|
-
dirname: string = path.dirname(fileURLToPath(import.meta.url)),
|
|
124
|
-
): Server => {
|
|
125
|
-
if (isDevelopmentOrTest()) {
|
|
126
|
-
const certsDir = path.resolve(dirname, "../../../certs");
|
|
127
|
-
|
|
128
|
-
const keyPath = path.join(certsDir, "server.key");
|
|
129
|
-
const certPath = path.join(certsDir, "server.crt");
|
|
130
|
-
|
|
131
|
-
if (fs.existsSync(keyPath) && fs.existsSync(certPath)) {
|
|
132
|
-
const httpsOptions: ServerOptions = {
|
|
133
|
-
key: fs.readFileSync(keyPath),
|
|
134
|
-
cert: fs.readFileSync(certPath),
|
|
135
|
-
};
|
|
136
|
-
|
|
137
|
-
return https.createServer(httpsOptions, app).listen(port, () => {
|
|
138
|
-
logger.info(() => ({ msg: `HTTPS server started on port ${port}` }));
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
logger.warn(() => ({
|
|
142
|
-
msg: "Certificates not found, starting HTTP server instead. Run ./setup_certs.sh to enable HTTPS in development.",
|
|
143
|
-
}));
|
|
144
|
-
return http.createServer(app).listen(port, () => {
|
|
145
|
-
logger.info(() => ({ msg: `HTTP server started on port ${port}` }));
|
|
146
|
-
});
|
|
147
|
-
}
|
|
148
|
-
return http.createServer(app).listen(port, () => {
|
|
149
|
-
logger.info(() => ({
|
|
150
|
-
msg: `HTTP server started on port ${port} (TLS handled by reverse proxy)`,
|
|
151
|
-
}));
|
|
152
|
-
});
|
|
153
|
-
};
|
|
154
|
-
|
|
155
|
-
export async function main(): Promise<Server> {
|
|
156
|
-
loadEnv();
|
|
157
|
-
|
|
158
|
-
const verifyEndpoint = resolveVerifyEndpoint();
|
|
159
|
-
|
|
160
|
-
logger.info(() => ({ data: { verifyEndpoint } }));
|
|
161
|
-
|
|
162
|
-
const verifyType = resolveVerifyType();
|
|
163
|
-
|
|
164
|
-
const app = createApp();
|
|
165
|
-
|
|
166
|
-
if (!process.env.MONGO_URI && !isDevelopmentOrTest()) {
|
|
167
|
-
throw new Error(
|
|
168
|
-
"Cannot run mongo memory when NODE_ENV is neither development nor test",
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
logger.info(() => ({ msg: process.env.MONGO_URI }));
|
|
172
|
-
const uri = process.env.MONGO_URI || (await memoryServerSetup());
|
|
173
|
-
logger.info(() => ({ msg: "mongo uri", data: { uri } }));
|
|
174
|
-
const mongoose = connectionFactory(uri);
|
|
175
|
-
if (!process.env.PROSOPO_SITE_PRIVATE_KEY) {
|
|
176
|
-
const mnemonicError = new ProsopoEnvError("GENERAL.MNEMONIC_UNDEFINED", {
|
|
177
|
-
context: { missingParams: ["PROSOPO_SITE_PRIVATE_KEY"] },
|
|
178
|
-
logger,
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
logger.error(() => ({ err: mnemonicError }));
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const config = getServerConfig();
|
|
185
|
-
|
|
186
|
-
logger.info(() => ({ msg: "Config", data: { config } }));
|
|
187
|
-
|
|
188
|
-
app.use(routesFactory(mongoose, config, verifyEndpoint, verifyType));
|
|
189
|
-
|
|
190
|
-
const port = resolvePort(config);
|
|
191
|
-
|
|
192
|
-
logger.info(() => ({ msg: "Listening on port", data: { port } }));
|
|
193
|
-
|
|
194
|
-
return startServer(app, port);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
// Only boot when run as a binary: importing this module (a test, or another
|
|
198
|
-
// entrypoint reusing createApp) must not open a port or spin up mongo.
|
|
199
|
-
if (isMain(import.meta.url)) {
|
|
200
|
-
main()
|
|
201
|
-
.then(() => {
|
|
202
|
-
logger.info(() => ({ msg: "Server started" }));
|
|
203
|
-
})
|
|
204
|
-
.catch((err) => {
|
|
205
|
-
logger.error(() => ({ err }));
|
|
206
|
-
// Non-zero, so a boot failure is visible to whatever supervises the
|
|
207
|
-
// process rather than reading as a clean shutdown.
|
|
208
|
-
process.exit(1);
|
|
209
|
-
});
|
|
210
|
-
}
|
package/src/controllers/auth.ts
DELETED
|
@@ -1,396 +0,0 @@
|
|
|
1
|
-
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
|
|
15
|
-
import { blake2b } from "@noble/hashes/blake2.js";
|
|
16
|
-
import { utf8ToBytes } from "@noble/hashes/utils.js";
|
|
17
|
-
import { u8aToHex } from "@polkadot/util";
|
|
18
|
-
import { randomAsHex } from "@polkadot/util-crypto";
|
|
19
|
-
import { ProsopoEnvError } from "@prosopo/common";
|
|
20
|
-
import { getPair } from "@prosopo/keyring";
|
|
21
|
-
import { getLogger } from "@prosopo/logger";
|
|
22
|
-
import { ProsopoServer } from "@prosopo/server";
|
|
23
|
-
import {
|
|
24
|
-
ApiParams,
|
|
25
|
-
CaptchaType,
|
|
26
|
-
type KeyringPair,
|
|
27
|
-
ProcaptchaResponse,
|
|
28
|
-
type ProcaptchaToken,
|
|
29
|
-
type ProsopoServerConfigOutput,
|
|
30
|
-
} from "@prosopo/types";
|
|
31
|
-
import { at } from "@prosopo/util";
|
|
32
|
-
import type { NextFunction, Request, Response } from "express";
|
|
33
|
-
import jwt, { type JwtPayload } from "jsonwebtoken";
|
|
34
|
-
import type { Connection } from "mongoose";
|
|
35
|
-
import { z } from "zod";
|
|
36
|
-
import type { UserInterface } from "../models/user.js";
|
|
37
|
-
|
|
38
|
-
const logger = getLogger("info", "client-example-server:auth");
|
|
39
|
-
|
|
40
|
-
const SubscribeBodySpec = ProcaptchaResponse.merge(
|
|
41
|
-
z.object({
|
|
42
|
-
email: z.string().email(),
|
|
43
|
-
password: z.string(),
|
|
44
|
-
siteKey: z.string(),
|
|
45
|
-
}),
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
function hashPassword(password: string): string {
|
|
49
|
-
return u8aToHex(blake2b(utf8ToBytes(password)));
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const NO_IP = "NO_IP";
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Derives the correct KeyringPair and secret for a given site key by trying different captcha types.
|
|
56
|
-
* @param baseSecret - The base secret from config
|
|
57
|
-
* @param siteKey - The site key to match against
|
|
58
|
-
* @returns Object containing the matched pair and its corresponding secret
|
|
59
|
-
* @throws Error if no matching pair is found
|
|
60
|
-
*/
|
|
61
|
-
const getPairAndSecretForSiteKey = (
|
|
62
|
-
baseSecret: string,
|
|
63
|
-
siteKey: string,
|
|
64
|
-
): { pair: KeyringPair; secret: string } => {
|
|
65
|
-
let pair: KeyringPair | undefined;
|
|
66
|
-
let secret = baseSecret;
|
|
67
|
-
|
|
68
|
-
pair = getPair(secret);
|
|
69
|
-
|
|
70
|
-
if (pair.address === siteKey) {
|
|
71
|
-
logger.info(() => ({
|
|
72
|
-
msg: "Site key matches the server configuration without captcha type suffix",
|
|
73
|
-
data: {
|
|
74
|
-
siteKeyDerived: pair?.address,
|
|
75
|
-
siteKey,
|
|
76
|
-
},
|
|
77
|
-
}));
|
|
78
|
-
return { pair, secret };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
for (const captchaType of [
|
|
82
|
-
CaptchaType.pow,
|
|
83
|
-
CaptchaType.image,
|
|
84
|
-
CaptchaType.frictionless,
|
|
85
|
-
CaptchaType.puzzle,
|
|
86
|
-
]) {
|
|
87
|
-
const newSecret = `${baseSecret}//${captchaType}`;
|
|
88
|
-
pair = getPair(newSecret);
|
|
89
|
-
logger.info(() => ({
|
|
90
|
-
msg: "Checking pair",
|
|
91
|
-
data: {
|
|
92
|
-
captchaType,
|
|
93
|
-
siteKeyDerived: pair?.address,
|
|
94
|
-
siteKey,
|
|
95
|
-
},
|
|
96
|
-
}));
|
|
97
|
-
if (pair.address === siteKey) {
|
|
98
|
-
secret = newSecret;
|
|
99
|
-
break;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (!pair || pair.address !== siteKey) {
|
|
104
|
-
throw new Error(
|
|
105
|
-
`Site key does not match the server configuration: Provided ${siteKey} vs Server ${pair?.address}`,
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
return { pair, secret };
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
const getResponse = async (
|
|
113
|
-
ip: string,
|
|
114
|
-
token: ProcaptchaToken,
|
|
115
|
-
secret: string,
|
|
116
|
-
verifyEndpoint: string,
|
|
117
|
-
email?: string,
|
|
118
|
-
) => {
|
|
119
|
-
// Only include ip if environment is production
|
|
120
|
-
const body: Record<string, string> = {
|
|
121
|
-
[ApiParams.token]: token,
|
|
122
|
-
[ApiParams.secret]: secret,
|
|
123
|
-
...(email ? { [ApiParams.email]: email } : {}),
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
if (process.env.NODE_ENV !== "development") {
|
|
127
|
-
body[ApiParams.ip] = ip;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
logger.info(() => ({
|
|
131
|
-
data: {
|
|
132
|
-
body: { ...body, [ApiParams.secret]: "[redacted]" },
|
|
133
|
-
},
|
|
134
|
-
}));
|
|
135
|
-
|
|
136
|
-
const response = await fetch(verifyEndpoint, {
|
|
137
|
-
method: "POST",
|
|
138
|
-
body: JSON.stringify(body),
|
|
139
|
-
});
|
|
140
|
-
return response.json();
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
const verify = async (
|
|
144
|
-
prosopoServer: ProsopoServer,
|
|
145
|
-
verifyType: string,
|
|
146
|
-
verifyEndpoint: string,
|
|
147
|
-
token: ProcaptchaToken,
|
|
148
|
-
secret: string,
|
|
149
|
-
ip: string,
|
|
150
|
-
email?: string,
|
|
151
|
-
// The session id the widget was rendered with. Forwarded so the provider
|
|
152
|
-
// can confirm the token was earned in this same session; omitted, no
|
|
153
|
-
// correlation is attempted.
|
|
154
|
-
clientSessionId?: string,
|
|
155
|
-
) => {
|
|
156
|
-
if (verifyType === "api") {
|
|
157
|
-
// verify using the API endpoint
|
|
158
|
-
logger.info(() => ({
|
|
159
|
-
msg: "Verifying using the API endpoint",
|
|
160
|
-
data: {
|
|
161
|
-
verifyEndpoint,
|
|
162
|
-
},
|
|
163
|
-
}));
|
|
164
|
-
|
|
165
|
-
const response = await getResponse(
|
|
166
|
-
ip,
|
|
167
|
-
token,
|
|
168
|
-
secret,
|
|
169
|
-
verifyEndpoint,
|
|
170
|
-
email,
|
|
171
|
-
);
|
|
172
|
-
logger.info(() => ({
|
|
173
|
-
data: {
|
|
174
|
-
response,
|
|
175
|
-
},
|
|
176
|
-
}));
|
|
177
|
-
return response.verified;
|
|
178
|
-
}
|
|
179
|
-
// verify using the TypeScript library
|
|
180
|
-
const verified = await prosopoServer.isVerified(
|
|
181
|
-
token,
|
|
182
|
-
ip,
|
|
183
|
-
email,
|
|
184
|
-
clientSessionId,
|
|
185
|
-
);
|
|
186
|
-
logger.info(() => ({
|
|
187
|
-
data: {
|
|
188
|
-
verified,
|
|
189
|
-
},
|
|
190
|
-
}));
|
|
191
|
-
return verified[ApiParams.verified];
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
const signup = async (
|
|
195
|
-
mongoose: Connection,
|
|
196
|
-
config: ProsopoServerConfigOutput,
|
|
197
|
-
verifyEndpoint: string,
|
|
198
|
-
verifyType: string,
|
|
199
|
-
req: Request,
|
|
200
|
-
res: Response,
|
|
201
|
-
next: NextFunction,
|
|
202
|
-
) => {
|
|
203
|
-
try {
|
|
204
|
-
if (!config.account.secret) {
|
|
205
|
-
throw new ProsopoEnvError("GENERAL.MNEMONIC_UNDEFINED", {
|
|
206
|
-
context: { missingParams: ["PROSOPO_SITE_PRIVATE_KEY"] },
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
const User = mongoose.model<UserInterface>("User");
|
|
210
|
-
// checks if email exists
|
|
211
|
-
const dbUser = await User.findOne({
|
|
212
|
-
email: { $eq: req.body.email },
|
|
213
|
-
});
|
|
214
|
-
const payload = SubscribeBodySpec.parse(req.body);
|
|
215
|
-
// get the procaptcha-response token
|
|
216
|
-
const token = payload[ApiParams.procaptchaResponse];
|
|
217
|
-
const siteKey = payload[ApiParams.siteKey];
|
|
218
|
-
|
|
219
|
-
const { pair, secret } = getPairAndSecretForSiteKey(
|
|
220
|
-
config.account.secret,
|
|
221
|
-
siteKey,
|
|
222
|
-
);
|
|
223
|
-
|
|
224
|
-
const prosopoServer = new ProsopoServer(config, pair);
|
|
225
|
-
if (dbUser) {
|
|
226
|
-
return res.status(409).json({ message: "email already exists" });
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const email = req.body.email;
|
|
230
|
-
const verified = await verify(
|
|
231
|
-
prosopoServer,
|
|
232
|
-
verifyType,
|
|
233
|
-
verifyEndpoint,
|
|
234
|
-
token,
|
|
235
|
-
secret,
|
|
236
|
-
req.headers["x-client-ip"]?.toString() || "127.0.0.1",
|
|
237
|
-
email,
|
|
238
|
-
req.body[ApiParams.clientSessionId],
|
|
239
|
-
);
|
|
240
|
-
|
|
241
|
-
if (verified === true) {
|
|
242
|
-
// salt
|
|
243
|
-
const salt = randomAsHex(32);
|
|
244
|
-
// !!!DUMMY CODE!!! - Do not use in production. Use bcrypt or similar for password hashing.
|
|
245
|
-
const passwordHash = hashPassword(`${req.body.password}${salt}`);
|
|
246
|
-
// No `if (passwordHash)` guard: the hash is always a non-empty hex
|
|
247
|
-
// string, and the guard's absent else-branch left the request hanging
|
|
248
|
-
// with no response at all.
|
|
249
|
-
return User.create({
|
|
250
|
-
email: email,
|
|
251
|
-
name: req.body.name,
|
|
252
|
-
password: passwordHash,
|
|
253
|
-
salt: salt,
|
|
254
|
-
})
|
|
255
|
-
.then(() => {
|
|
256
|
-
res.status(200).json({ message: "user created" });
|
|
257
|
-
})
|
|
258
|
-
.catch((err) => {
|
|
259
|
-
logger.error(() => ({
|
|
260
|
-
err,
|
|
261
|
-
msg: "Error creating user in database",
|
|
262
|
-
}));
|
|
263
|
-
res.status(502).json({ message: "error while creating the user" });
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
res
|
|
267
|
-
.status(401)
|
|
268
|
-
.json({ message: "user has not completed a captcha", verified });
|
|
269
|
-
} catch (err) {
|
|
270
|
-
console.error("error", err);
|
|
271
|
-
res
|
|
272
|
-
.status(500)
|
|
273
|
-
.json({ message: (err as Error).message || "internal server error" });
|
|
274
|
-
}
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
const login = async (
|
|
278
|
-
mongoose: Connection,
|
|
279
|
-
config: ProsopoServerConfigOutput,
|
|
280
|
-
verifyEndpoint: string,
|
|
281
|
-
verifyType: string,
|
|
282
|
-
req: Request,
|
|
283
|
-
res: Response,
|
|
284
|
-
) => {
|
|
285
|
-
const User = mongoose.model<UserInterface>("User");
|
|
286
|
-
|
|
287
|
-
// checks if email exists
|
|
288
|
-
await User.findOne({
|
|
289
|
-
email: { $eq: req.body.email },
|
|
290
|
-
})
|
|
291
|
-
.then(async (dbUser) => {
|
|
292
|
-
if (dbUser) {
|
|
293
|
-
const payload = SubscribeBodySpec.parse(req.body);
|
|
294
|
-
|
|
295
|
-
const token = payload[ApiParams.procaptchaResponse];
|
|
296
|
-
const siteKey = payload[ApiParams.siteKey];
|
|
297
|
-
|
|
298
|
-
if (!config.account.secret) {
|
|
299
|
-
throw new ProsopoEnvError("GENERAL.SECRET_MISSING", {
|
|
300
|
-
context: { missingParams: ["PROSOPO_SITE_PRIVATE_KEY"] },
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const { pair, secret } = getPairAndSecretForSiteKey(
|
|
305
|
-
config.account.secret,
|
|
306
|
-
siteKey,
|
|
307
|
-
);
|
|
308
|
-
const prosopoServer = new ProsopoServer(config, pair);
|
|
309
|
-
|
|
310
|
-
const verified = await verify(
|
|
311
|
-
prosopoServer,
|
|
312
|
-
verifyType,
|
|
313
|
-
verifyEndpoint,
|
|
314
|
-
token,
|
|
315
|
-
secret,
|
|
316
|
-
req.headers["x-client-ip"]?.toString() || NO_IP,
|
|
317
|
-
// login carries no email; the session id still has to reach
|
|
318
|
-
// the provider or a widget rendered with data-sessionid is
|
|
319
|
-
// silently unverified against its session.
|
|
320
|
-
undefined,
|
|
321
|
-
req.body[ApiParams.clientSessionId],
|
|
322
|
-
);
|
|
323
|
-
|
|
324
|
-
logger.info(() => ({
|
|
325
|
-
data: {
|
|
326
|
-
verified,
|
|
327
|
-
},
|
|
328
|
-
}));
|
|
329
|
-
|
|
330
|
-
if (verified) {
|
|
331
|
-
// password hash
|
|
332
|
-
// !!!DUMMY CODE!!! - Do not use in production. Use bcrypt or similar for password hashing.
|
|
333
|
-
const passwordHash = hashPassword(
|
|
334
|
-
`${req.body.password}${dbUser.salt}`,
|
|
335
|
-
);
|
|
336
|
-
if (passwordHash !== dbUser.password) {
|
|
337
|
-
// password doesnt match
|
|
338
|
-
return res.status(401).json({ message: "invalid credentials" });
|
|
339
|
-
}
|
|
340
|
-
// password match
|
|
341
|
-
const token = jwt.sign({ email: req.body.email }, "secret", {
|
|
342
|
-
expiresIn: "1h",
|
|
343
|
-
});
|
|
344
|
-
return res
|
|
345
|
-
.status(200)
|
|
346
|
-
.json({ message: "user logged in", token: token });
|
|
347
|
-
}
|
|
348
|
-
return res
|
|
349
|
-
.status(401)
|
|
350
|
-
.json({ message: "user has not completed a captcha", verified });
|
|
351
|
-
}
|
|
352
|
-
return res.status(404).json({ message: "user not found" });
|
|
353
|
-
})
|
|
354
|
-
.catch((err) => {
|
|
355
|
-
console.error("error", err);
|
|
356
|
-
res.status(500).json({ message: err.message || "internal server error" });
|
|
357
|
-
});
|
|
358
|
-
};
|
|
359
|
-
|
|
360
|
-
const isAuth = (req: Request, res: Response) => {
|
|
361
|
-
const authHeader = req.get("Authorization") || "";
|
|
362
|
-
// Each branch returns: without the returns a missing header still fell
|
|
363
|
-
// through to `at(headerParts, 1)`, which throws on a one-element array, and
|
|
364
|
-
// a failed verify sent a 500 followed by a 401 on the same response.
|
|
365
|
-
if (!authHeader) {
|
|
366
|
-
return res.status(401).json({ message: "not authenticated" });
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const headerParts = authHeader.split(" ");
|
|
370
|
-
if (headerParts.length < 2) {
|
|
371
|
-
return res.status(401).json({ message: "not authenticated" });
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
const token = at(headerParts, 1);
|
|
375
|
-
// "Bearer " with nothing after it is a malformed credential, not a server
|
|
376
|
-
// fault: jwt.verify("") throws and would otherwise surface as a 500.
|
|
377
|
-
if (!token) {
|
|
378
|
-
return res.status(401).json({ message: "not authenticated" });
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
let decodedToken: string | JwtPayload = "";
|
|
382
|
-
try {
|
|
383
|
-
decodedToken = jwt.verify(token, "secret");
|
|
384
|
-
} catch (err) {
|
|
385
|
-
return res.status(500).json({
|
|
386
|
-
message: (err as Error).message || "could not decode the token",
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
if (!decodedToken) {
|
|
391
|
-
return res.status(401).json({ message: "unauthorized" });
|
|
392
|
-
}
|
|
393
|
-
return res.status(200).json({ message: "here is your resource" });
|
|
394
|
-
};
|
|
395
|
-
|
|
396
|
-
export { signup, login, isAuth };
|
package/src/models/user.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
import mongoose, { Schema } from "mongoose";
|
|
15
|
-
|
|
16
|
-
export interface UserInterface {
|
|
17
|
-
id: number;
|
|
18
|
-
email: string;
|
|
19
|
-
name: string;
|
|
20
|
-
password: string;
|
|
21
|
-
salt: string;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const UserSchema = new Schema<UserInterface>({
|
|
25
|
-
email: {
|
|
26
|
-
type: mongoose.SchemaTypes.String,
|
|
27
|
-
required: true,
|
|
28
|
-
},
|
|
29
|
-
name: {
|
|
30
|
-
type: mongoose.SchemaTypes.String,
|
|
31
|
-
},
|
|
32
|
-
password: {
|
|
33
|
-
type: mongoose.SchemaTypes.String,
|
|
34
|
-
required: true,
|
|
35
|
-
},
|
|
36
|
-
id: {
|
|
37
|
-
type: mongoose.SchemaTypes.Number,
|
|
38
|
-
},
|
|
39
|
-
salt: {
|
|
40
|
-
type: mongoose.SchemaTypes.String,
|
|
41
|
-
required: true,
|
|
42
|
-
},
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
export default UserSchema;
|