add proxy routes, webauthn bug fix

This commit is contained in:
2025-02-21 11:37:23 +05:30
parent 5d5cee2d7e
commit a41127b2fd
9 changed files with 198 additions and 2 deletions

View File

@@ -28,6 +28,7 @@
"fastify-zod": "^1.4.0",
"lru-cache": "^11.0.2",
"mongoose": "^8.9.0",
"qs": "^6.14.0",
"zod": "^3.24.1"
},
"devDependencies": {

70
pnpm-lock.yaml generated
View File

@@ -53,6 +53,9 @@ importers:
mongoose:
specifier: ^8.9.0
version: 8.9.0
qs:
specifier: ^6.14.0
version: 6.14.0
zod:
specifier: ^3.24.1
version: 3.24.1
@@ -628,6 +631,10 @@ packages:
resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
engines: {node: '>= 0.4'}
call-bound@1.0.3:
resolution: {integrity: sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==}
engines: {node: '>= 0.4'}
camel-case@4.1.2:
resolution: {integrity: sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==}
@@ -927,6 +934,10 @@ packages:
no-case@3.0.4:
resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==}
object-inspect@1.13.4:
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
engines: {node: '>= 0.4'}
obliterator@2.0.4:
resolution: {integrity: sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==}
@@ -984,6 +995,10 @@ packages:
resolution: {integrity: sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ==}
engines: {node: '>=6.0.0'}
qs@6.14.0:
resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==}
engines: {node: '>=0.6'}
quick-format-unescaped@4.0.4:
resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==}
@@ -1033,6 +1048,22 @@ packages:
setprototypeof@1.2.0:
resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==}
side-channel-list@1.0.0:
resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
engines: {node: '>= 0.4'}
side-channel-map@1.0.1:
resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
engines: {node: '>= 0.4'}
side-channel-weakmap@1.0.2:
resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
engines: {node: '>= 0.4'}
side-channel@1.1.0:
resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
engines: {node: '>= 0.4'}
sift@17.1.3:
resolution: {integrity: sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==}
@@ -2225,6 +2256,11 @@ snapshots:
es-errors: 1.3.0
function-bind: 1.1.2
call-bound@1.0.3:
dependencies:
call-bind-apply-helpers: 1.0.2
get-intrinsic: 1.2.7
camel-case@4.1.2:
dependencies:
pascal-case: 3.1.2
@@ -2567,6 +2603,8 @@ snapshots:
lower-case: 2.0.2
tslib: 2.8.1
object-inspect@1.13.4: {}
obliterator@2.0.4: {}
on-exit-leak-free@2.1.2: {}
@@ -2633,6 +2671,10 @@ snapshots:
pvutils@1.1.3: {}
qs@6.14.0:
dependencies:
side-channel: 1.1.0
quick-format-unescaped@4.0.4: {}
real-require@0.2.0: {}
@@ -2667,6 +2709,34 @@ snapshots:
setprototypeof@1.2.0: {}
side-channel-list@1.0.0:
dependencies:
es-errors: 1.3.0
object-inspect: 1.13.4
side-channel-map@1.0.1:
dependencies:
call-bound: 1.0.3
es-errors: 1.3.0
get-intrinsic: 1.2.7
object-inspect: 1.13.4
side-channel-weakmap@1.0.2:
dependencies:
call-bound: 1.0.3
es-errors: 1.3.0
get-intrinsic: 1.2.7
object-inspect: 1.13.4
side-channel-map: 1.0.1
side-channel@1.1.0:
dependencies:
es-errors: 1.3.0
object-inspect: 1.13.4
side-channel-list: 1.0.0
side-channel-map: 1.0.1
side-channel-weakmap: 1.0.2
sift@17.1.3: {}
simple-oauth2@5.1.0:

View File

@@ -0,0 +1,39 @@
import { FastifyInstance } from "fastify";
import { $mail, ProxyRequest } from "./mailProxy.schema";
import { getOutlookTokens } from "./mailProxy.service";
import axios from "axios";
export async function mailProxyRoutes(fastify: FastifyInstance) {
fastify.post(
"/",
{
schema: {
body: $mail("proxyRequest"),
},
config: { requiredClaims: ["mail:all"] },
preHandler: [fastify.authorize],
},
async (req, res) => {
const input = req.body as ProxyRequest;
try {
const tokens = await getOutlookTokens(input.email);
if (!tokens) return res.code(404).send({ error: "resource not found" });
const result = await axios({
url: input.url,
method: input.method,
headers: {
Authorization: "Bearer " + tokens.access_token,
},
data: input.body,
validateStatus: () => true,
});
return res.code(result.status).send(result.data);
} catch (err) {
return err;
}
}
);
}

View File

@@ -0,0 +1,34 @@
import { buildJsonSchemas } from "fastify-zod";
import mongoose from "mongoose";
import { z } from "zod";
export const mailModel = mongoose.model(
"oauth",
new mongoose.Schema({
email: {
type: String,
unique: true,
required: true,
},
access_token: String,
expiry: Date,
refresh_token: String,
}),
"oauth"
);
const proxyRequest = z.object({
email: z.string().email(),
url: z.string(),
method: z.enum(["GET", "POST", "PATCH", "DELETE"]),
body: z.any(),
});
export type ProxyRequest = z.infer<typeof proxyRequest>;
export const { schemas: mailSchemas, $ref: $mail } = buildJsonSchemas(
{
proxyRequest,
},
{ $id: "mail" }
);

View File

@@ -0,0 +1,48 @@
import qs from "qs";
import axios from "axios";
import { mailModel } from "./mailProxy.schema";
export async function getOutlookTokens(email: string) {
let tokens = await mailModel.findOne({ email: email });
if (!tokens) {
return null;
} else {
const date = new Date();
const expiry = new Date(tokens.expiry);
if (expiry > date) {
return tokens.access_token;
} else {
try {
let res = await axios({
url: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
data: qs.stringify({
client_id: process.env.MAIL_CLIENT_ID,
scope: process.env.MAIL_SCOPE,
refresh_token: tokens.refresh_token,
redirect_uri: process.env.MAIL_REDIRECT_URI,
grant_type: "refresh_token",
client_secret: process.env.MAIL_CLIENT_SECRET,
}),
});
let expiresAt = new Date(Date.now() + res.data.expires_in * 1000);
await mailModel.findByIdAndUpdate(tokens._id, {
expiry: expiresAt,
refresh_token: res.data.refresh_token,
access_token: res.data.access_token,
});
return res.data.access_token;
} catch (err) {
throw new Error("error fetching tokens");
}
}
}
}

View File

@@ -18,6 +18,7 @@ import { notificationSchemas } from "./notification/notification.schema";
import { noteSchemas } from "./note/note.schema";
import { webAuthnRoutes } from "./webauthn/webauthn.route";
import { configSchemas } from "./config/config.schema";
import { mailSchemas } from "./mailProxy/mailProxy.schema";
const app = fastify({ logger: true, trustProxy: true });
@@ -49,6 +50,7 @@ for (const schema of [
...notificationSchemas,
...noteSchemas,
...configSchemas,
...mailSchemas,
]) {
app.addSchema(schema);
}

View File

@@ -23,4 +23,5 @@ export type Claim =
| "notification:read"
| "notification:write"
| "config:read"
| "config:write";
| "config:write"
| "mail:all";

View File

@@ -28,6 +28,7 @@ export const rules: Record<
"notification:write",
"config:read",
"config:write",
"mail:all",
],
hiddenFields: {
orgs: ["__v"],

View File

@@ -154,7 +154,7 @@ export async function webAuthnRoutes(fastify: FastifyInstance) {
{
schema: {
body: {
type: "string",
type: "object",
properties: {
email: { type: "string" },
},