prostgles-server 4.2.160 → 4.2.161
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/lib/Auth/AuthHandler.ts +436 -0
- package/lib/Auth/AuthTypes.ts +280 -0
- package/lib/Auth/getSafeReturnURL.ts +35 -0
- package/lib/Auth/sendEmail.ts +83 -0
- package/lib/Auth/setAuthProviders.ts +128 -0
- package/lib/Auth/setEmailProvider.ts +85 -0
- package/lib/Auth/setupAuthRoutes.ts +161 -0
- package/lib/DBEventsManager.ts +178 -0
- package/lib/DBSchemaBuilder.ts +225 -0
- package/lib/DboBuilder/DboBuilder.ts +319 -0
- package/lib/DboBuilder/DboBuilderTypes.ts +361 -0
- package/lib/DboBuilder/QueryBuilder/Functions.ts +1153 -0
- package/lib/DboBuilder/QueryBuilder/QueryBuilder.ts +288 -0
- package/lib/DboBuilder/QueryBuilder/getJoinQuery.ts +263 -0
- package/lib/DboBuilder/QueryBuilder/getNewQuery.ts +271 -0
- package/lib/DboBuilder/QueryBuilder/getSelectQuery.ts +136 -0
- package/lib/DboBuilder/QueryBuilder/prepareHaving.ts +22 -0
- package/lib/DboBuilder/QueryStreamer.ts +250 -0
- package/lib/DboBuilder/TableHandler/DataValidator.ts +428 -0
- package/lib/DboBuilder/TableHandler/TableHandler.ts +205 -0
- package/lib/DboBuilder/TableHandler/delete.ts +115 -0
- package/lib/DboBuilder/TableHandler/insert.ts +183 -0
- package/lib/DboBuilder/TableHandler/insertTest.ts +78 -0
- package/lib/DboBuilder/TableHandler/onDeleteFromFileTable.ts +62 -0
- package/lib/DboBuilder/TableHandler/runInsertUpdateQuery.ts +134 -0
- package/lib/DboBuilder/TableHandler/update.ts +126 -0
- package/lib/DboBuilder/TableHandler/updateBatch.ts +49 -0
- package/lib/DboBuilder/TableHandler/updateFile.ts +48 -0
- package/lib/DboBuilder/TableHandler/upsert.ts +34 -0
- package/lib/DboBuilder/ViewHandler/ViewHandler.ts +393 -0
- package/lib/DboBuilder/ViewHandler/count.ts +38 -0
- package/lib/DboBuilder/ViewHandler/find.ts +153 -0
- package/lib/DboBuilder/ViewHandler/getExistsCondition.ts +73 -0
- package/lib/DboBuilder/ViewHandler/getExistsFilters.ts +74 -0
- package/lib/DboBuilder/ViewHandler/getInfo.ts +32 -0
- package/lib/DboBuilder/ViewHandler/getTableJoinQuery.ts +84 -0
- package/lib/DboBuilder/ViewHandler/parseComplexFilter.ts +96 -0
- package/lib/DboBuilder/ViewHandler/parseFieldFilter.ts +105 -0
- package/lib/DboBuilder/ViewHandler/parseJoinPath.ts +208 -0
- package/lib/DboBuilder/ViewHandler/prepareSortItems.ts +163 -0
- package/lib/DboBuilder/ViewHandler/prepareWhere.ts +90 -0
- package/lib/DboBuilder/ViewHandler/size.ts +37 -0
- package/lib/DboBuilder/ViewHandler/subscribe.ts +118 -0
- package/lib/DboBuilder/ViewHandler/validateViewRules.ts +70 -0
- package/lib/DboBuilder/dboBuilderUtils.ts +222 -0
- package/lib/DboBuilder/getColumns.ts +114 -0
- package/lib/DboBuilder/getCondition.ts +201 -0
- package/lib/DboBuilder/getSubscribeRelatedTables.ts +190 -0
- package/lib/DboBuilder/getTablesForSchemaPostgresSQL.ts +426 -0
- package/lib/DboBuilder/insertNestedRecords.ts +355 -0
- package/lib/DboBuilder/parseUpdateRules.ts +187 -0
- package/lib/DboBuilder/prepareShortestJoinPaths.ts +186 -0
- package/lib/DboBuilder/runSQL.ts +182 -0
- package/lib/DboBuilder/runTransaction.ts +50 -0
- package/lib/DboBuilder/sqlErrCodeToMsg.ts +254 -0
- package/lib/DboBuilder/uploadFile.ts +69 -0
- package/lib/Event_Trigger_Tags.ts +118 -0
- package/lib/FileManager/FileManager.ts +358 -0
- package/lib/FileManager/getValidatedFileType.ts +69 -0
- package/lib/FileManager/initFileManager.ts +187 -0
- package/lib/FileManager/upload.ts +62 -0
- package/lib/FileManager/uploadStream.ts +79 -0
- package/lib/Filtering.ts +463 -0
- package/lib/JSONBValidation/validate_jsonb_schema_sql.ts +502 -0
- package/lib/JSONBValidation/validation.ts +143 -0
- package/lib/Logging.ts +127 -0
- package/lib/PostgresNotifListenManager.ts +143 -0
- package/lib/Prostgles.ts +485 -0
- package/lib/ProstglesTypes.ts +196 -0
- package/lib/PubSubManager/PubSubManager.ts +609 -0
- package/lib/PubSubManager/addSub.ts +138 -0
- package/lib/PubSubManager/addSync.ts +141 -0
- package/lib/PubSubManager/getCreatePubSubManagerError.ts +72 -0
- package/lib/PubSubManager/getPubSubManagerInitQuery.ts +662 -0
- package/lib/PubSubManager/initPubSubManager.ts +79 -0
- package/lib/PubSubManager/notifListener.ts +173 -0
- package/lib/PubSubManager/orphanTriggerCheck.ts +70 -0
- package/lib/PubSubManager/pushSubData.ts +55 -0
- package/lib/PublishParser/PublishParser.ts +162 -0
- package/lib/PublishParser/getFileTableRules.ts +124 -0
- package/lib/PublishParser/getSchemaFromPublish.ts +141 -0
- package/lib/PublishParser/getTableRulesWithoutFileTable.ts +177 -0
- package/lib/PublishParser/publishTypesAndUtils.ts +399 -0
- package/lib/RestApi.ts +127 -0
- package/lib/SchemaWatch/SchemaWatch.ts +90 -0
- package/lib/SchemaWatch/createSchemaWatchEventTrigger.ts +3 -0
- package/lib/SchemaWatch/getValidatedWatchSchemaType.ts +45 -0
- package/lib/SchemaWatch/getWatchSchemaTagList.ts +27 -0
- package/lib/SyncReplication.ts +557 -0
- package/lib/TableConfig/TableConfig.ts +468 -0
- package/lib/TableConfig/getColumnDefinitionQuery.ts +111 -0
- package/lib/TableConfig/getConstraintDefinitionQueries.ts +95 -0
- package/lib/TableConfig/getFutureTableSchema.ts +64 -0
- package/lib/TableConfig/getPGIndexes.ts +53 -0
- package/lib/TableConfig/getTableColumnQueries.ts +129 -0
- package/lib/TableConfig/initTableConfig.ts +326 -0
- package/lib/index.ts +13 -0
- package/lib/initProstgles.ts +319 -0
- package/lib/onSocketConnected.ts +102 -0
- package/lib/runClientRequest.ts +129 -0
- package/lib/shortestPath.ts +122 -0
- package/lib/typeTests/DBoGenerated.d.ts +320 -0
- package/lib/typeTests/dboTypeCheck.ts +81 -0
- package/lib/utils.ts +15 -0
- package/package.json +1 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const getSafeReturnURL = (returnURL: string, returnUrlParamName: string, quiet = false) => {
|
|
2
|
+
/** Dissalow redirect to other domains */
|
|
3
|
+
if(returnURL) {
|
|
4
|
+
const allowedOrigin = "https://localhost";
|
|
5
|
+
const { origin, pathname, search, searchParams } = new URL(returnURL, allowedOrigin);
|
|
6
|
+
if(
|
|
7
|
+
origin !== allowedOrigin ||
|
|
8
|
+
returnURL !== `${pathname}${search}` ||
|
|
9
|
+
searchParams.get(returnUrlParamName)
|
|
10
|
+
){
|
|
11
|
+
if(!quiet){
|
|
12
|
+
console.error(`Unsafe returnUrl: ${returnURL}. Redirecting to /`);
|
|
13
|
+
}
|
|
14
|
+
return "/";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return returnURL;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const issue = ([
|
|
22
|
+
["https://localhost", "/"],
|
|
23
|
+
["//localhost.bad.com", "/"],
|
|
24
|
+
["//localhost.com", "/"],
|
|
25
|
+
["/localhost/com", "/localhost/com"],
|
|
26
|
+
["/localhost/com?here=there", "/localhost/com?here=there"],
|
|
27
|
+
["/localhost/com?returnUrl=there", "/"],
|
|
28
|
+
["//http://localhost.com", "/"],
|
|
29
|
+
["//abc.com", "/"],
|
|
30
|
+
["///abc.com", "/"],
|
|
31
|
+
] as const).find(([returnURL, expected]) => getSafeReturnURL(returnURL, "returnUrl", true) !== expected);
|
|
32
|
+
|
|
33
|
+
if(issue){
|
|
34
|
+
throw new Error(`getSafeReturnURL failed for ${issue[0]}. Expected: ${issue[1]}`);
|
|
35
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Email, SMTPConfig } from "./AuthTypes";
|
|
2
|
+
import nodemailer from "nodemailer";
|
|
3
|
+
import aws from "@aws-sdk/client-ses";
|
|
4
|
+
import SESTransport from "nodemailer/lib/ses-transport";
|
|
5
|
+
|
|
6
|
+
type SESTransporter = nodemailer.Transporter<SESTransport.SentMessageInfo, SESTransport.Options>;
|
|
7
|
+
type SMTPTransporter = nodemailer.Transporter<nodemailer.SentMessageInfo, nodemailer.TransportOptions>;
|
|
8
|
+
type Transporter = SESTransporter | SMTPTransporter;
|
|
9
|
+
|
|
10
|
+
const transporterCache: Map<string, Transporter> = new Map();
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Allows sending emails using nodemailer default config or AWS SES
|
|
14
|
+
* https://www.nodemailer.com/transports/ses/
|
|
15
|
+
*/
|
|
16
|
+
export const sendEmail = (smptConfig: SMTPConfig, email: Email) => {
|
|
17
|
+
const configStr = JSON.stringify(smptConfig);
|
|
18
|
+
const transporter = transporterCache.get(configStr) ?? getTransporter(smptConfig);
|
|
19
|
+
if(!transporterCache.has(configStr)){
|
|
20
|
+
transporterCache.set(configStr, transporter);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return send(transporter, email);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const getTransporter = (smptConfig: SMTPConfig) => {
|
|
27
|
+
let transporter: Transporter | undefined;
|
|
28
|
+
if(smptConfig.type === "aws-ses"){
|
|
29
|
+
const {
|
|
30
|
+
region,
|
|
31
|
+
accessKeyId,
|
|
32
|
+
secretAccessKey,
|
|
33
|
+
/**
|
|
34
|
+
* max 1 messages/second
|
|
35
|
+
*/
|
|
36
|
+
sendingRate = 1
|
|
37
|
+
} = smptConfig;
|
|
38
|
+
const ses = new aws.SES({
|
|
39
|
+
apiVersion: "2010-12-01",
|
|
40
|
+
region,
|
|
41
|
+
credentials: {
|
|
42
|
+
accessKeyId,
|
|
43
|
+
secretAccessKey
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
transporter = nodemailer.createTransport({
|
|
48
|
+
SES: { ses, aws },
|
|
49
|
+
maxConnections: 1,
|
|
50
|
+
sendingRate
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
} else {
|
|
54
|
+
const { user, pass, host, port, secure } = smptConfig;
|
|
55
|
+
transporter = nodemailer.createTransport({
|
|
56
|
+
host,
|
|
57
|
+
port,
|
|
58
|
+
secure,
|
|
59
|
+
auth: { user, pass }
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return transporter;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const send = (transporter: Transporter, email: Email) => {
|
|
67
|
+
return new Promise((resolve, reject) => {
|
|
68
|
+
transporter.once('idle', () => {
|
|
69
|
+
if (transporter.isIdle()) {
|
|
70
|
+
transporter.sendMail(
|
|
71
|
+
email,
|
|
72
|
+
(err, info) => {
|
|
73
|
+
if(err){
|
|
74
|
+
reject(err);
|
|
75
|
+
} else {
|
|
76
|
+
resolve(info);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type e from "express";
|
|
2
|
+
import { RequestHandler } from "express";
|
|
3
|
+
import { Strategy as FacebookStrategy } from "passport-facebook";
|
|
4
|
+
import { Strategy as GitHubStrategy } from "passport-github2";
|
|
5
|
+
import { Strategy as GoogleStrategy } from "passport-google-oauth20";
|
|
6
|
+
import { Strategy as MicrosoftStrategy } from "passport-microsoft";
|
|
7
|
+
import { AuthSocketSchema, getObjectEntries, isEmpty } from "prostgles-types";
|
|
8
|
+
import { getErrorAsObject } from "../DboBuilder/dboBuilderUtils";
|
|
9
|
+
import { removeExpressRouteByName } from "../FileManager/FileManager";
|
|
10
|
+
import { AUTH_ROUTES_AND_PARAMS, AuthHandler, getLoginClientInfo } from "./AuthHandler";
|
|
11
|
+
import { Auth } from './AuthTypes';
|
|
12
|
+
import { setEmailProvider } from "./setEmailProvider";
|
|
13
|
+
/** For some reason normal import is undefined */
|
|
14
|
+
const passport = require("passport") as typeof import("passport");
|
|
15
|
+
|
|
16
|
+
export const upsertNamedExpressMiddleware = (app: e.Express, handler: RequestHandler, name: string) => {
|
|
17
|
+
const funcName = name;
|
|
18
|
+
Object.defineProperty(handler, "name", { value: funcName });
|
|
19
|
+
removeExpressRouteByName(app, name);
|
|
20
|
+
app.use(handler);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function setAuthProviders (this: AuthHandler, { registrations, app }: Required<Auth>["expressConfig"]) {
|
|
24
|
+
if(!registrations) return;
|
|
25
|
+
const { onProviderLoginFail, onProviderLoginStart, websiteUrl, OAuthProviders } = registrations;
|
|
26
|
+
|
|
27
|
+
await setEmailProvider.bind(this)(app);
|
|
28
|
+
|
|
29
|
+
if(!OAuthProviders || isEmpty(OAuthProviders)){
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
upsertNamedExpressMiddleware(app, passport.initialize(), "prostglesPassportMiddleware");
|
|
34
|
+
|
|
35
|
+
getObjectEntries(OAuthProviders).forEach(([providerName, providerConfig]) => {
|
|
36
|
+
|
|
37
|
+
if(!providerConfig?.clientID){
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const { authOpts, ...config } = providerConfig;
|
|
42
|
+
|
|
43
|
+
const strategy = providerName === "google" ? GoogleStrategy :
|
|
44
|
+
providerName === "github" ? GitHubStrategy :
|
|
45
|
+
providerName === "facebook" ? FacebookStrategy :
|
|
46
|
+
providerName === "microsoft" ? MicrosoftStrategy :
|
|
47
|
+
undefined
|
|
48
|
+
;
|
|
49
|
+
|
|
50
|
+
const callbackPath = `${AUTH_ROUTES_AND_PARAMS.loginWithProvider}/${providerName}/callback`;
|
|
51
|
+
passport.use(
|
|
52
|
+
new (strategy as typeof GoogleStrategy)(
|
|
53
|
+
{
|
|
54
|
+
...config,
|
|
55
|
+
callbackURL: `${websiteUrl}${callbackPath}`,
|
|
56
|
+
},
|
|
57
|
+
async (accessToken, refreshToken, profile, done) => {
|
|
58
|
+
// This callback is where you would normally store or retrieve user info from the database
|
|
59
|
+
return done(null, profile, { accessToken, refreshToken, profile });
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
app.get(`${AUTH_ROUTES_AND_PARAMS.loginWithProvider}/${providerName}`,
|
|
65
|
+
passport.authenticate(providerName, authOpts ?? {})
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
app.get(
|
|
69
|
+
callbackPath,
|
|
70
|
+
async (req, res) => {
|
|
71
|
+
try {
|
|
72
|
+
const clientInfo = getLoginClientInfo({ httpReq: req });
|
|
73
|
+
const db = this.db;
|
|
74
|
+
const dbo = this.dbo as any;
|
|
75
|
+
const args = { provider: providerName, req, res, clientInfo, db, dbo };
|
|
76
|
+
const startCheck = await onProviderLoginStart?.(args);
|
|
77
|
+
if(startCheck && "error" in startCheck){
|
|
78
|
+
res.status(500).json({ error: startCheck.error });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
passport.authenticate(
|
|
82
|
+
providerName,
|
|
83
|
+
{
|
|
84
|
+
session: false,
|
|
85
|
+
failureRedirect: "/login",
|
|
86
|
+
failWithError: true,
|
|
87
|
+
},
|
|
88
|
+
async (error: any, _profile: any, authInfo: any) => {
|
|
89
|
+
if(error){
|
|
90
|
+
await onProviderLoginFail?.({ ...args, error });
|
|
91
|
+
res.status(500).json({
|
|
92
|
+
error: "Failed to login with provider",
|
|
93
|
+
});
|
|
94
|
+
} else {
|
|
95
|
+
this.loginThrottledAndSetCookie(req, res, { type: "provider", provider: providerName, ...authInfo })
|
|
96
|
+
.catch((e: any) => {
|
|
97
|
+
res.status(500).json(getErrorAsObject(e));
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
)(req, res);
|
|
102
|
+
|
|
103
|
+
} catch (_e) {
|
|
104
|
+
res.status(500).json({ error: "Something went wrong" });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function getProviders(this: AuthHandler): AuthSocketSchema["providers"] | undefined {
|
|
113
|
+
const { registrations } = this.opts?.expressConfig ?? {}
|
|
114
|
+
if(!registrations) return undefined;
|
|
115
|
+
const { OAuthProviders } = registrations;
|
|
116
|
+
if(!OAuthProviders || isEmpty(OAuthProviders)) return undefined;
|
|
117
|
+
|
|
118
|
+
const result: AuthSocketSchema["providers"] = {}
|
|
119
|
+
getObjectEntries(OAuthProviders).forEach(([providerName, config]) => {
|
|
120
|
+
if(config?.clientID){
|
|
121
|
+
result[providerName] = {
|
|
122
|
+
url: `${AUTH_ROUTES_AND_PARAMS.loginWithProvider}/${providerName}`,
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import e from "express";
|
|
2
|
+
import { AUTH_ROUTES_AND_PARAMS, AuthHandler } from "./AuthHandler";
|
|
3
|
+
import { Email, SMTPConfig } from "./AuthTypes";
|
|
4
|
+
import { sendEmail } from "./sendEmail";
|
|
5
|
+
import { promises } from "node:dns";
|
|
6
|
+
|
|
7
|
+
export async function setEmailProvider(this: AuthHandler, app: e.Express) {
|
|
8
|
+
|
|
9
|
+
const { email, websiteUrl } = this.opts?.expressConfig?.registrations ?? {};
|
|
10
|
+
if(!email) return;
|
|
11
|
+
if(websiteUrl){
|
|
12
|
+
await checkDmarc(websiteUrl);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
app.post(AUTH_ROUTES_AND_PARAMS.emailSignup, async (req, res) => {
|
|
16
|
+
const { username, password } = req.body;
|
|
17
|
+
let validationError = "";
|
|
18
|
+
if(typeof username !== "string"){
|
|
19
|
+
validationError = "Invalid username";
|
|
20
|
+
}
|
|
21
|
+
if(email.signupType === "withPassword"){
|
|
22
|
+
const { minPasswordLength = 8 } = email;
|
|
23
|
+
if(typeof password !== "string"){
|
|
24
|
+
validationError = "Invalid password";
|
|
25
|
+
} else if(password.length < minPasswordLength){
|
|
26
|
+
validationError = `Password must be at least ${minPasswordLength} characters long`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
if(validationError){
|
|
30
|
+
res.status(400).json({ error: validationError });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
let emailMessage: undefined | { message: Email; smtp: SMTPConfig };
|
|
35
|
+
if(email.signupType === "withPassword"){
|
|
36
|
+
if(email.emailConfirmation){
|
|
37
|
+
const { onSend, smtp } = email.emailConfirmation;
|
|
38
|
+
const message = await onSend({ email: username, confirmationUrlPath: `${websiteUrl}${AUTH_ROUTES_AND_PARAMS.confirmEmail}` });
|
|
39
|
+
emailMessage = { message: { ...message, to: username }, smtp };
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
const { emailMagicLink } = email;
|
|
43
|
+
const message = await emailMagicLink.onSend({ email: username, magicLinkPath: `${websiteUrl}${AUTH_ROUTES_AND_PARAMS.magicLinksRoute}` });
|
|
44
|
+
emailMessage = { message: { ...message, to: username }, smtp: emailMagicLink.smtp };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if(emailMessage){
|
|
48
|
+
await sendEmail(emailMessage.smtp, emailMessage.message);
|
|
49
|
+
res.json({ msg: "Email sent" });
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
res.status(500).json({ error: "Failed to send email" });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if(email.signupType === "withPassword" && email.emailConfirmation){
|
|
57
|
+
app.get(AUTH_ROUTES_AND_PARAMS.confirmEmailExpressRoute, async (req, res) => {
|
|
58
|
+
const { id } = req.params ?? {};
|
|
59
|
+
try {
|
|
60
|
+
await email.emailConfirmation?.onConfirmed({ confirmationCode: id });
|
|
61
|
+
res.json({ msg: "Email confirmed" });
|
|
62
|
+
} catch (_e) {
|
|
63
|
+
res.status(500).json({ error: "Failed to confirm email" });
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const checkDmarc = async (websiteUrl: string) => {
|
|
70
|
+
const { host, hostname } = new URL(websiteUrl);
|
|
71
|
+
const ignoredHosts = ["localhost", "127.0.0.1"]
|
|
72
|
+
if(!hostname || ignoredHosts.includes(hostname)){
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const dmarc = await promises.resolveTxt(`_dmarc.${host}`);
|
|
76
|
+
const dmarkTxt = dmarc[0]?.[0];
|
|
77
|
+
if(
|
|
78
|
+
!dmarkTxt?.includes("v=DMARC1") ||
|
|
79
|
+
(!dmarkTxt?.includes("p=reject") && !dmarkTxt?.includes("p=quarantine"))
|
|
80
|
+
){
|
|
81
|
+
throw new Error("DMARC not set to reject/quarantine");
|
|
82
|
+
} else {
|
|
83
|
+
console.log("DMARC set to reject")
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { RequestHandler } from "express";
|
|
2
|
+
import { DBOFullyTyped } from "../DBSchemaBuilder";
|
|
3
|
+
import { AUTH_ROUTES_AND_PARAMS, AuthHandler, getLoginClientInfo, HTTPCODES } from "./AuthHandler";
|
|
4
|
+
import { AuthClientRequest, ExpressReq, ExpressRes, LoginParams } from "./AuthTypes";
|
|
5
|
+
import { setAuthProviders, upsertNamedExpressMiddleware } from "./setAuthProviders";
|
|
6
|
+
|
|
7
|
+
export async function setupAuthRoutes(this: AuthHandler) {
|
|
8
|
+
if (!this.opts) return;
|
|
9
|
+
|
|
10
|
+
const { login, getUser, expressConfig } = this.opts;
|
|
11
|
+
|
|
12
|
+
if (!login) {
|
|
13
|
+
throw "Invalid auth: Provide { sidKeyName: string } ";
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (this.sidKeyName === "sid") {
|
|
17
|
+
throw "sidKeyName cannot be 'sid' due to collision with socket.io";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!getUser) throw "getUser missing from auth config";
|
|
21
|
+
|
|
22
|
+
if (!expressConfig) {
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
const { app, publicRoutes = [], onGetRequestOK, magicLinks, use } = expressConfig;
|
|
26
|
+
if (publicRoutes.find(r => typeof r !== "string" || !r)) {
|
|
27
|
+
throw "Invalid or empty string provided within publicRoutes "
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
await setAuthProviders.bind(this)(expressConfig);
|
|
31
|
+
|
|
32
|
+
if(use){
|
|
33
|
+
const prostglesUseMiddleware: RequestHandler = (req, res, next) => {
|
|
34
|
+
use({
|
|
35
|
+
req,
|
|
36
|
+
res,
|
|
37
|
+
next,
|
|
38
|
+
getUser: () => this.getUser({ httpReq: req }) as any,
|
|
39
|
+
dbo: this.dbo as DBOFullyTyped,
|
|
40
|
+
db: this.db,
|
|
41
|
+
})
|
|
42
|
+
};
|
|
43
|
+
upsertNamedExpressMiddleware(app, prostglesUseMiddleware, "prostglesUseMiddleware");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (magicLinks) {
|
|
47
|
+
const { check } = magicLinks;
|
|
48
|
+
if (!check) {
|
|
49
|
+
throw "Check must be defined for magicLinks";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
app.get(AUTH_ROUTES_AND_PARAMS.magicLinksExpressRoute, async (req: ExpressReq, res: ExpressRes) => {
|
|
53
|
+
const { id } = req.params ?? {};
|
|
54
|
+
|
|
55
|
+
if (typeof id !== "string" || !id) {
|
|
56
|
+
res.status(HTTPCODES.BAD_REQUEST).json({ msg: "Invalid magic-link id. Expecting a string" });
|
|
57
|
+
} else {
|
|
58
|
+
try {
|
|
59
|
+
const session = await this.throttledFunc(async () => {
|
|
60
|
+
return check(id, this.dbo as any, this.db, getLoginClientInfo({ httpReq: req }));
|
|
61
|
+
});
|
|
62
|
+
if (!session) {
|
|
63
|
+
res.status(HTTPCODES.AUTH_ERROR).json({ msg: "Invalid magic-link" });
|
|
64
|
+
} else {
|
|
65
|
+
this.setCookieAndGoToReturnURLIFSet(session, { req, res });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
} catch (e) {
|
|
69
|
+
res.status(HTTPCODES.AUTH_ERROR).json({ msg: e });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
app.post(AUTH_ROUTES_AND_PARAMS.login, async (req: ExpressReq, res: ExpressRes) => {
|
|
76
|
+
try {
|
|
77
|
+
const loginParams: LoginParams = {
|
|
78
|
+
type: "username",
|
|
79
|
+
...req.body,
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
await this.loginThrottledAndSetCookie(req, res, loginParams);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
console.log(err)
|
|
85
|
+
res.status(HTTPCODES.AUTH_ERROR).json({ err });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const onLogout = async (req: ExpressReq, res: ExpressRes) => {
|
|
91
|
+
const sid = this.validateSid(req?.cookies?.[this.sidKeyName]);
|
|
92
|
+
if (sid) {
|
|
93
|
+
try {
|
|
94
|
+
await this.throttledFunc(() => {
|
|
95
|
+
return this.opts?.logout?.(req?.cookies?.[this.sidKeyName], this.dbo as any, this.db);
|
|
96
|
+
})
|
|
97
|
+
} catch (err) {
|
|
98
|
+
console.error(err);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
res.redirect("/")
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* Redirect if not logged in and requesting non public content */
|
|
105
|
+
app.get(AUTH_ROUTES_AND_PARAMS.catchAll, async (req: ExpressReq, res: ExpressRes, next) => {
|
|
106
|
+
|
|
107
|
+
const clientReq: AuthClientRequest = { httpReq: req };
|
|
108
|
+
const getUser = this.getUser;
|
|
109
|
+
if(this.prostgles.restApi){
|
|
110
|
+
if(Object.values(this.prostgles.restApi.routes).some(restRoute => this.matchesRoute(restRoute.split("/:")[0], req.path))){
|
|
111
|
+
next();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
const returnURL = this.getReturnUrl(req);
|
|
117
|
+
|
|
118
|
+
if(this.matchesRoute(AUTH_ROUTES_AND_PARAMS.logoutGetPath, req.path)){
|
|
119
|
+
await onLogout(req, res);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if(this.matchesRoute(AUTH_ROUTES_AND_PARAMS.loginWithProvider, req.path)){
|
|
124
|
+
next();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Requesting a User route
|
|
129
|
+
*/
|
|
130
|
+
if (this.isUserRoute(req.path)) {
|
|
131
|
+
|
|
132
|
+
/* Check auth. Redirect to login if unauthorized */
|
|
133
|
+
const u = await getUser(clientReq);
|
|
134
|
+
if (!u) {
|
|
135
|
+
res.redirect(`${AUTH_ROUTES_AND_PARAMS.login}?returnURL=${encodeURIComponent(req.originalUrl)}`);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/* If authorized and going to returnUrl then redirect. Otherwise serve file */
|
|
140
|
+
} else if (returnURL && (await getUser(clientReq))) {
|
|
141
|
+
|
|
142
|
+
res.redirect(returnURL);
|
|
143
|
+
return;
|
|
144
|
+
|
|
145
|
+
/** If Logged in and requesting login then redirect to main page */
|
|
146
|
+
} else if (this.matchesRoute(AUTH_ROUTES_AND_PARAMS.login, req.path) && (await getUser(clientReq))) {
|
|
147
|
+
|
|
148
|
+
res.redirect("/");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
onGetRequestOK?.(req, res, { getUser: () => getUser(clientReq), dbo: this.dbo as DBOFullyTyped, db: this.db })
|
|
153
|
+
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error(error);
|
|
156
|
+
const errorMessage = typeof error === "string" ? error : error instanceof Error ? error.message : "";
|
|
157
|
+
res.status(HTTPCODES.AUTH_ERROR).json({ msg: "Something went wrong when processing your request" + (errorMessage? (": " + errorMessage) : "") });
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
});
|
|
161
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { PostgresNotifListenManager, PrglNotifListener } from "./PostgresNotifListenManager";
|
|
2
|
+
import { DB, PGP } from "./Prostgles";
|
|
3
|
+
import { getKeys, CHANNELS } from "prostgles-types";
|
|
4
|
+
import { PRGLIOSocket } from "./DboBuilder/DboBuilder";
|
|
5
|
+
|
|
6
|
+
export class DBEventsManager {
|
|
7
|
+
|
|
8
|
+
notifies: {
|
|
9
|
+
[key: string]: {
|
|
10
|
+
socketChannel: string;
|
|
11
|
+
sockets: any[];
|
|
12
|
+
localFuncs: ((payload: string) => void)[];
|
|
13
|
+
notifMgr: PostgresNotifListenManager;
|
|
14
|
+
}
|
|
15
|
+
} = {};
|
|
16
|
+
|
|
17
|
+
notice: {
|
|
18
|
+
socketChannel: string;
|
|
19
|
+
socketUnsubChannel: string;
|
|
20
|
+
sockets: any[];
|
|
21
|
+
} = {
|
|
22
|
+
socketChannel: CHANNELS.NOTICE_EV,
|
|
23
|
+
socketUnsubChannel: CHANNELS.NOTICE_EV + "unsubscribe",
|
|
24
|
+
sockets: []
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
notifManager?: PostgresNotifListenManager;
|
|
28
|
+
|
|
29
|
+
db_pg: DB;
|
|
30
|
+
pgp: PGP
|
|
31
|
+
constructor(db_pg: DB, pgp: PGP){
|
|
32
|
+
this.db_pg = db_pg;
|
|
33
|
+
this.pgp = pgp;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
private onNotif: PrglNotifListener = ({ channel, payload }) => {
|
|
37
|
+
|
|
38
|
+
// console.log(36, { channel, payload }, Object.keys(this.notifies));
|
|
39
|
+
|
|
40
|
+
getKeys(this.notifies)
|
|
41
|
+
.filter(ch => ch === channel)
|
|
42
|
+
.map(ch => {
|
|
43
|
+
const sub = this.notifies[ch]!;
|
|
44
|
+
|
|
45
|
+
sub.sockets.map(s => {
|
|
46
|
+
s.emit(sub.socketChannel, payload)
|
|
47
|
+
});
|
|
48
|
+
sub.localFuncs.map(lf => {
|
|
49
|
+
lf(payload);
|
|
50
|
+
})
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
onNotice = (notice: any) => {
|
|
55
|
+
if(this.notice && this.notice.sockets.length){
|
|
56
|
+
this.notice.sockets.map(s => {
|
|
57
|
+
s.emit(this.notice.socketChannel, notice);
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
getNotifChannelName = async (channel: string) => {
|
|
63
|
+
const c = await this.db_pg.one("SELECT quote_ident($1) as c", channel);
|
|
64
|
+
return c.c;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async addNotify(query: string, socket?: PRGLIOSocket, func?: any): Promise<{
|
|
68
|
+
socketChannel: string;
|
|
69
|
+
socketUnsubChannel: string;
|
|
70
|
+
notifChannel: string;
|
|
71
|
+
unsubscribe?: () => void;
|
|
72
|
+
}> {
|
|
73
|
+
if(typeof query !== "string" || (!socket && !func)){
|
|
74
|
+
throw "Expecting (query: string, socket?, localFunc?) But received: " + JSON.stringify({ query, socket, func });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* Remove comments */
|
|
78
|
+
let q = query.trim()
|
|
79
|
+
.replace(/\/\*[\s\S]*?\*\/|\/\/.*/g,'\n')
|
|
80
|
+
.split("\n").map(v => v.trim()).filter(v => v && !v.startsWith("--"))
|
|
81
|
+
.join("\n");
|
|
82
|
+
|
|
83
|
+
/* Find the notify channel name */
|
|
84
|
+
if(!q.toLowerCase().startsWith("listen")){
|
|
85
|
+
throw "Expecting a LISTEN query but got: " + query;
|
|
86
|
+
}
|
|
87
|
+
q = q.slice(7).trim(); // Remove listen
|
|
88
|
+
if(q.endsWith(";")) q = q.slice(0, -1);
|
|
89
|
+
|
|
90
|
+
if(q.startsWith('"') && q.endsWith('"')) {
|
|
91
|
+
q = q.slice(1, -1);
|
|
92
|
+
} else {
|
|
93
|
+
/* Replicate PG by lowercasing identifier if not quoted */
|
|
94
|
+
q = q.toLowerCase();
|
|
95
|
+
}
|
|
96
|
+
q = q.replace(/""/g, `"`);
|
|
97
|
+
|
|
98
|
+
const channel = q;
|
|
99
|
+
let notifChannel = await this.getNotifChannelName(channel)
|
|
100
|
+
|
|
101
|
+
notifChannel = notifChannel.replace(/""/g, `"`);
|
|
102
|
+
if(notifChannel.startsWith('"')) notifChannel = notifChannel.slice(1, -1);
|
|
103
|
+
|
|
104
|
+
const socketChannel = CHANNELS.LISTEN_EV + notifChannel,
|
|
105
|
+
socketUnsubChannel = socketChannel + "unsubscribe";
|
|
106
|
+
|
|
107
|
+
if(!this.notifies[notifChannel]){
|
|
108
|
+
this.notifies[notifChannel] = {
|
|
109
|
+
socketChannel,
|
|
110
|
+
sockets: socket? [socket] : [],
|
|
111
|
+
localFuncs: func? [func] : [],
|
|
112
|
+
notifMgr: await PostgresNotifListenManager.create(this.db_pg, this.onNotif, channel)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
} else {
|
|
116
|
+
if(socket && !this.notifies[notifChannel]!.sockets.find(s => s.id === socket.id)) {
|
|
117
|
+
this.notifies[notifChannel]!.sockets.push(socket);
|
|
118
|
+
|
|
119
|
+
} else if(func) {
|
|
120
|
+
this.notifies[notifChannel]!.localFuncs.push(func);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if(socket){
|
|
125
|
+
socket.removeAllListeners(socketUnsubChannel);
|
|
126
|
+
socket.on(socketUnsubChannel, ()=>{
|
|
127
|
+
this.removeNotify(notifChannel, socket);
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
socketChannel,
|
|
133
|
+
socketUnsubChannel,
|
|
134
|
+
notifChannel,
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
removeNotify(channel?: string, socket?: PRGLIOSocket, func?: any){
|
|
139
|
+
const notifChannel = channel && this.notifies[channel]
|
|
140
|
+
if(notifChannel){
|
|
141
|
+
if(socket){
|
|
142
|
+
notifChannel.sockets = notifChannel.sockets.filter(s => s.id !== socket.id);
|
|
143
|
+
} else if(func){
|
|
144
|
+
notifChannel.localFuncs = notifChannel.localFuncs.filter(f => f !== func);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* UNLISTEN if no listeners ?? */
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if(socket){
|
|
151
|
+
getKeys(this.notifies).forEach(channel => {
|
|
152
|
+
this.notifies[channel]!.sockets = this.notifies[channel]!.sockets.filter(s => s.id !== socket.id);
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
addNotice(socket: PRGLIOSocket){
|
|
158
|
+
if(!socket || !socket.id) throw "Expecting a socket obj with id";
|
|
159
|
+
|
|
160
|
+
if(!this.notice.sockets.find(s => s.id === socket.id)){
|
|
161
|
+
this.notice.sockets.push(socket);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const { socketChannel, socketUnsubChannel } = this.notice;
|
|
165
|
+
|
|
166
|
+
socket.removeAllListeners(socketUnsubChannel);
|
|
167
|
+
socket.on(socketUnsubChannel, () => {
|
|
168
|
+
this.removeNotice(socket);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return { socketChannel, socketUnsubChannel, }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
removeNotice(socket: PRGLIOSocket){
|
|
175
|
+
if(!socket || !socket.id) throw "Expecting a socket obj with id";
|
|
176
|
+
this.notice.sockets = this.notice.sockets.filter(s => s.id !== socket.id)
|
|
177
|
+
}
|
|
178
|
+
}
|