@terreno/api 0.0.17 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/CLAUDE.local.md +204 -0
- package/.cursor/rules/00-root.mdc +338 -0
- package/.github/copilot-instructions.md +333 -0
- package/AGENTS.md +333 -0
- package/README.md +76 -7
- package/biome.jsonc +1 -1
- package/dist/api.d.ts +68 -1
- package/dist/api.js +140 -5
- package/dist/api.query.test.js +1 -1
- package/dist/api.test.js +222 -484
- package/dist/auth.js +3 -1
- package/dist/errors.js +15 -12
- package/dist/example.js +7 -7
- package/dist/expressServer.d.ts +8 -2
- package/dist/expressServer.js +8 -1
- package/dist/githubAuth.d.ts +64 -0
- package/dist/githubAuth.js +293 -0
- package/dist/githubAuth.test.d.ts +1 -0
- package/dist/githubAuth.test.js +351 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/logger.js +1 -1
- package/dist/middleware.js +1 -1
- package/dist/notifiers/googleChatNotifier.js +1 -1
- package/dist/notifiers/googleChatNotifier.test.js +1 -1
- package/dist/notifiers/slackNotifier.js +1 -1
- package/dist/notifiers/slackNotifier.test.js +1 -1
- package/dist/notifiers/zoomNotifier.js +1 -1
- package/dist/notifiers/zoomNotifier.test.js +1 -1
- package/dist/openApi.test.js +8 -5
- package/dist/openApiBuilder.d.ts +69 -1
- package/dist/openApiBuilder.js +109 -5
- package/dist/openApiValidator.d.ts +296 -0
- package/dist/openApiValidator.js +698 -0
- package/dist/openApiValidator.test.d.ts +1 -0
- package/dist/openApiValidator.test.js +346 -0
- package/dist/permissions.js +1 -1
- package/dist/plugins.test.js +3 -3
- package/dist/terrenoPlugin.d.ts +4 -0
- package/dist/terrenoPlugin.js +2 -0
- package/dist/tests/bunSetup.js +2 -2
- package/dist/tests.js +34 -24
- package/package.json +7 -2
- package/src/__snapshots__/openApi.test.ts.snap +399 -0
- package/src/__snapshots__/openApiBuilder.test.ts.snap +108 -0
- package/src/api.query.test.ts +1 -1
- package/src/api.test.ts +161 -374
- package/src/api.ts +210 -4
- package/src/auth.ts +3 -1
- package/src/errors.ts +15 -12
- package/src/example.ts +7 -7
- package/src/expressServer.ts +18 -2
- package/src/githubAuth.test.ts +223 -0
- package/src/githubAuth.ts +335 -0
- package/src/index.ts +3 -0
- package/src/logger.ts +1 -1
- package/src/middleware.ts +1 -1
- package/src/notifiers/googleChatNotifier.test.ts +1 -1
- package/src/notifiers/googleChatNotifier.ts +1 -1
- package/src/notifiers/slackNotifier.test.ts +1 -1
- package/src/notifiers/slackNotifier.ts +1 -1
- package/src/notifiers/zoomNotifier.test.ts +1 -1
- package/src/notifiers/zoomNotifier.ts +1 -1
- package/src/openApi.test.ts +8 -5
- package/src/openApiBuilder.ts +188 -15
- package/src/openApiValidator.test.ts +241 -0
- package/src/openApiValidator.ts +860 -0
- package/src/permissions.ts +1 -1
- package/src/plugins.test.ts +3 -3
- package/src/terrenoPlugin.ts +5 -0
- package/src/tests/bunSetup.ts +2 -2
- package/src/tests.ts +34 -24
- package/CLAUDE.md +0 -107
- package/dist/response.d.ts +0 -0
- package/dist/response.js +0 -1
- package/index.ts +0 -1
- package/src/response.ts +0 -0
package/dist/auth.js
CHANGED
|
@@ -377,7 +377,9 @@ function addAuthRoutes(app, userModel, authOptions) {
|
|
|
377
377
|
logger_1.logger.warn("Invalid login: ".concat(info));
|
|
378
378
|
return [2 /*return*/, res.status(401).json({ message: info === null || info === void 0 ? void 0 : info.message })];
|
|
379
379
|
}
|
|
380
|
-
|
|
380
|
+
if (process.env.NODE_ENV !== "test") {
|
|
381
|
+
logger_1.logger.info("User logged in: ".concat(user._id, ", type: ").concat(user.type || "N/A"));
|
|
382
|
+
}
|
|
381
383
|
return [4 /*yield*/, (0, exports.generateTokens)(user, authOptions)];
|
|
382
384
|
case 1:
|
|
383
385
|
tokens = _a.sent();
|
package/dist/errors.js
CHANGED
|
@@ -67,7 +67,7 @@ exports.getAPIErrorBody = getAPIErrorBody;
|
|
|
67
67
|
exports.apiUnauthorizedMiddleware = apiUnauthorizedMiddleware;
|
|
68
68
|
exports.apiErrorMiddleware = apiErrorMiddleware;
|
|
69
69
|
// https://jsonapi.org/format/#errors
|
|
70
|
-
var Sentry = __importStar(require("@sentry/
|
|
70
|
+
var Sentry = __importStar(require("@sentry/bun"));
|
|
71
71
|
var mongoose_1 = require("mongoose");
|
|
72
72
|
var logger_1 = require("./logger");
|
|
73
73
|
/**
|
|
@@ -125,21 +125,24 @@ exports.APIError = APIError;
|
|
|
125
125
|
// model.
|
|
126
126
|
function errorsPlugin(schema) {
|
|
127
127
|
var errorSchema = new mongoose_1.Schema({
|
|
128
|
-
code: String,
|
|
129
|
-
detail: String,
|
|
130
|
-
id: String,
|
|
128
|
+
code: { description: "Application-specific error code", type: String },
|
|
129
|
+
detail: { description: "Human-readable explanation of the error", type: String },
|
|
130
|
+
id: { description: "Unique identifier for this error occurrence", type: String },
|
|
131
131
|
links: {
|
|
132
|
-
about: String,
|
|
133
|
-
type: String,
|
|
132
|
+
about: { description: "Link to documentation about this error", type: String },
|
|
133
|
+
type: { description: "Link describing the error type", type: String },
|
|
134
134
|
},
|
|
135
|
-
meta: mongoose_1.Schema.Types.Mixed,
|
|
135
|
+
meta: { description: "Non-standard meta information about the error", type: mongoose_1.Schema.Types.Mixed },
|
|
136
136
|
source: {
|
|
137
|
-
header: String,
|
|
138
|
-
parameter: String,
|
|
139
|
-
pointer:
|
|
137
|
+
header: { description: "HTTP header that caused the error", type: String },
|
|
138
|
+
parameter: { description: "Query parameter that caused the error", type: String },
|
|
139
|
+
pointer: {
|
|
140
|
+
description: "JSON pointer to the request field that caused the error",
|
|
141
|
+
type: String,
|
|
142
|
+
},
|
|
140
143
|
},
|
|
141
|
-
status: Number,
|
|
142
|
-
title: { required: true, type: String },
|
|
144
|
+
status: { description: "HTTP status code for this error", type: Number },
|
|
145
|
+
title: { description: "Short summary of the error", required: true, type: String },
|
|
143
146
|
});
|
|
144
147
|
schema.add({ apiErrors: errorSchema });
|
|
145
148
|
}
|
package/dist/example.js
CHANGED
|
@@ -65,19 +65,19 @@ mongoose_1.default
|
|
|
65
65
|
logger_1.logger.error("Error connecting to mongo ".concat(err));
|
|
66
66
|
});
|
|
67
67
|
var userSchema = new mongoose_1.Schema({
|
|
68
|
-
admin: { default: false, type: Boolean },
|
|
69
|
-
username: String,
|
|
68
|
+
admin: { default: false, description: "Whether the user has admin privileges", type: Boolean },
|
|
69
|
+
username: { description: "The user's username", type: String },
|
|
70
70
|
});
|
|
71
71
|
userSchema.plugin(passport_local_mongoose_1.default, { usernameField: "email" });
|
|
72
72
|
userSchema.plugin(plugins_1.createdUpdatedPlugin);
|
|
73
73
|
userSchema.plugin(plugins_1.baseUserPlugin);
|
|
74
74
|
var UserModel = (0, mongoose_1.model)("User", userSchema);
|
|
75
75
|
var schema = new mongoose_1.Schema({
|
|
76
|
-
calories: Number,
|
|
77
|
-
created: Date,
|
|
78
|
-
hidden: { default: false, type: Boolean },
|
|
79
|
-
name: String,
|
|
80
|
-
ownerId: { ref: "User", type: "ObjectId" },
|
|
76
|
+
calories: { description: "Number of calories in the food", type: Number },
|
|
77
|
+
created: { description: "When this food was created", type: Date },
|
|
78
|
+
hidden: { default: false, description: "Whether this food is hidden from listings", type: Boolean },
|
|
79
|
+
name: { description: "The name of the food", type: String },
|
|
80
|
+
ownerId: { description: "The user who owns this food entry", ref: "User", type: "ObjectId" },
|
|
81
81
|
});
|
|
82
82
|
var FoodModel = (0, mongoose_1.model)("Food", schema);
|
|
83
83
|
function getBaseServer() {
|
package/dist/expressServer.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import * as Sentry from "@sentry/
|
|
1
|
+
import * as Sentry from "@sentry/bun";
|
|
2
2
|
import express, { type Router } from "express";
|
|
3
3
|
import type jwt from "jsonwebtoken";
|
|
4
4
|
import type { ModelRouterOptions } from "./api";
|
|
5
5
|
import { type UserModel as UserMongooseModel } from "./auth";
|
|
6
|
+
import { type GitHubAuthOptions } from "./githubAuth";
|
|
6
7
|
import { type LoggingOptions } from "./logger";
|
|
7
8
|
export declare function setupEnvironment(): void;
|
|
8
9
|
export type AddRoutes = (router: Router, options?: Partial<ModelRouterOptions<any>>) => void;
|
|
@@ -19,11 +20,16 @@ export interface SetupServerOptions {
|
|
|
19
20
|
addRoutes: AddRoutes;
|
|
20
21
|
loggingOptions?: LoggingOptions;
|
|
21
22
|
authOptions?: AuthOptions;
|
|
23
|
+
/**
|
|
24
|
+
* GitHub OAuth configuration. When provided, enables GitHub authentication.
|
|
25
|
+
* Requires the user schema to have GitHub fields (use githubUserPlugin).
|
|
26
|
+
*/
|
|
27
|
+
githubAuth?: GitHubAuthOptions;
|
|
22
28
|
skipListen?: boolean;
|
|
23
29
|
corsOrigin?: string | boolean | RegExp | Array<boolean | string | RegExp> | ((requestOrigin: string | undefined, callback: (err: Error | null, origin?: boolean | string | RegExp | Array<boolean | string | RegExp>) => void) => void);
|
|
24
30
|
addMiddleware?: AddRoutes;
|
|
25
31
|
ignoreTraces?: string[];
|
|
26
|
-
sentryOptions?: Sentry.
|
|
32
|
+
sentryOptions?: Sentry.BunOptions;
|
|
27
33
|
}
|
|
28
34
|
export declare function setupServer(options: SetupServerOptions): express.Application;
|
|
29
35
|
export declare function cronjob(name: string, schedule: "hourly" | "minutely" | string, callback: () => void): void;
|
package/dist/expressServer.js
CHANGED
|
@@ -104,7 +104,7 @@ exports.createRouterWithAuth = createRouterWithAuth;
|
|
|
104
104
|
exports.setupServer = setupServer;
|
|
105
105
|
exports.cronjob = cronjob;
|
|
106
106
|
exports.wrapScript = wrapScript;
|
|
107
|
-
var Sentry = __importStar(require("@sentry/
|
|
107
|
+
var Sentry = __importStar(require("@sentry/bun"));
|
|
108
108
|
var openapi_1 = __importDefault(require("@wesleytodd/openapi"));
|
|
109
109
|
var cors_1 = __importDefault(require("cors"));
|
|
110
110
|
var cron_1 = __importDefault(require("cron"));
|
|
@@ -115,6 +115,7 @@ var passport_1 = __importDefault(require("passport"));
|
|
|
115
115
|
var qs_1 = __importDefault(require("qs"));
|
|
116
116
|
var auth_1 = require("./auth");
|
|
117
117
|
var errors_1 = require("./errors");
|
|
118
|
+
var githubAuth_1 = require("./githubAuth");
|
|
118
119
|
var logger_1 = require("./logger");
|
|
119
120
|
var notifiers_1 = require("./notifiers");
|
|
120
121
|
var openApiEtag_1 = require("./openApiEtag");
|
|
@@ -284,6 +285,11 @@ function initializeRoutes(UserModel, addRoutes, options) {
|
|
|
284
285
|
app.use("/swagger", oapi.swaggerui());
|
|
285
286
|
}
|
|
286
287
|
(0, auth_1.addMeRoutes)(app, UserModel, options === null || options === void 0 ? void 0 : options.authOptions);
|
|
288
|
+
// Set up GitHub OAuth if configured
|
|
289
|
+
if (options.githubAuth) {
|
|
290
|
+
(0, githubAuth_1.setupGitHubAuth)(app, UserModel, options.githubAuth);
|
|
291
|
+
(0, githubAuth_1.addGitHubAuthRoutes)(app, UserModel, options.githubAuth, options.authOptions);
|
|
292
|
+
}
|
|
287
293
|
addRoutes(app, { openApi: oapi });
|
|
288
294
|
Sentry.setupExpressErrorHandler(app);
|
|
289
295
|
// Catch any thrown APIErrors and return them in an OpenAPI compatible format
|
|
@@ -308,6 +314,7 @@ function setupServer(options) {
|
|
|
308
314
|
addMiddleware: options.addMiddleware,
|
|
309
315
|
authOptions: options.authOptions,
|
|
310
316
|
corsOrigin: options.corsOrigin,
|
|
317
|
+
githubAuth: options.githubAuth,
|
|
311
318
|
});
|
|
312
319
|
}
|
|
313
320
|
catch (error) {
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type express from "express";
|
|
2
|
+
import { type Profile } from "passport-github2";
|
|
3
|
+
import { type UserModel } from "./auth";
|
|
4
|
+
import type { AuthOptions } from "./expressServer";
|
|
5
|
+
/** Options for configuring GitHub OAuth authentication */
|
|
6
|
+
export interface GitHubAuthOptions {
|
|
7
|
+
/** GitHub OAuth Client ID */
|
|
8
|
+
clientId: string;
|
|
9
|
+
/** GitHub OAuth Client Secret */
|
|
10
|
+
clientSecret: string;
|
|
11
|
+
/** Callback URL for GitHub OAuth (e.g., https://yourapp.com/auth/github/callback) */
|
|
12
|
+
callbackURL: string;
|
|
13
|
+
/** OAuth scopes to request from GitHub. Defaults to ["user:email"] */
|
|
14
|
+
scope?: string[];
|
|
15
|
+
/**
|
|
16
|
+
* Whether to allow linking GitHub to existing accounts.
|
|
17
|
+
* If true, authenticated users can link their GitHub account.
|
|
18
|
+
* Defaults to true.
|
|
19
|
+
*/
|
|
20
|
+
allowAccountLinking?: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Custom function to handle user creation or lookup from GitHub profile.
|
|
23
|
+
* If not provided, a default implementation will be used.
|
|
24
|
+
*/
|
|
25
|
+
findOrCreateUser?: (profile: Profile, accessToken: string, refreshToken: string, existingUser?: any) => Promise<any>;
|
|
26
|
+
}
|
|
27
|
+
/** Fields added to user documents for GitHub authentication */
|
|
28
|
+
export interface GitHubUserFields {
|
|
29
|
+
/** GitHub user ID */
|
|
30
|
+
githubId?: string;
|
|
31
|
+
/** GitHub username */
|
|
32
|
+
githubUsername?: string;
|
|
33
|
+
/** GitHub profile URL */
|
|
34
|
+
githubProfileUrl?: string;
|
|
35
|
+
/** GitHub avatar URL */
|
|
36
|
+
githubAvatarUrl?: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Plugin to add GitHub authentication fields to a user schema.
|
|
40
|
+
* Apply this plugin to your User schema if you want to enable GitHub auth.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```typescript
|
|
44
|
+
* import {githubUserPlugin} from "@terreno/api";
|
|
45
|
+
*
|
|
46
|
+
* userSchema.plugin(githubUserPlugin);
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
export declare function githubUserPlugin(schema: any): void;
|
|
50
|
+
/**
|
|
51
|
+
* Sets up GitHub OAuth authentication strategy.
|
|
52
|
+
* Call this after setupAuth() in your server initialization.
|
|
53
|
+
*/
|
|
54
|
+
export declare function setupGitHubAuth(_app: express.Application, userModel: UserModel, githubOptions: GitHubAuthOptions): void;
|
|
55
|
+
/**
|
|
56
|
+
* Adds GitHub OAuth routes to the Express application.
|
|
57
|
+
*
|
|
58
|
+
* Routes added:
|
|
59
|
+
* - GET /auth/github - Initiates GitHub OAuth flow
|
|
60
|
+
* - GET /auth/github/callback - Handles GitHub OAuth callback
|
|
61
|
+
* - POST /auth/github/link - Links GitHub account to authenticated user (requires JWT auth)
|
|
62
|
+
* - DELETE /auth/github/unlink - Unlinks GitHub account from authenticated user (requires JWT auth)
|
|
63
|
+
*/
|
|
64
|
+
export declare function addGitHubAuthRoutes(app: express.Application, userModel: UserModel, githubOptions: GitHubAuthOptions, authOptions?: AuthOptions): void;
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
12
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
|
13
|
+
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
14
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
15
|
+
function step(op) {
|
|
16
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
17
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
18
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
19
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
20
|
+
switch (op[0]) {
|
|
21
|
+
case 0: case 1: t = op; break;
|
|
22
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
23
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
24
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
25
|
+
default:
|
|
26
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
27
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
28
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
29
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
30
|
+
if (t[2]) _.ops.pop();
|
|
31
|
+
_.trys.pop(); continue;
|
|
32
|
+
}
|
|
33
|
+
op = body.call(thisArg, _);
|
|
34
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
35
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
39
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
40
|
+
};
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.githubUserPlugin = githubUserPlugin;
|
|
43
|
+
exports.setupGitHubAuth = setupGitHubAuth;
|
|
44
|
+
exports.addGitHubAuthRoutes = addGitHubAuthRoutes;
|
|
45
|
+
var passport_1 = __importDefault(require("passport"));
|
|
46
|
+
var passport_github2_1 = require("passport-github2");
|
|
47
|
+
var auth_1 = require("./auth");
|
|
48
|
+
var errors_1 = require("./errors");
|
|
49
|
+
var logger_1 = require("./logger");
|
|
50
|
+
/**
|
|
51
|
+
* Plugin to add GitHub authentication fields to a user schema.
|
|
52
|
+
* Apply this plugin to your User schema if you want to enable GitHub auth.
|
|
53
|
+
*
|
|
54
|
+
* @example
|
|
55
|
+
* ```typescript
|
|
56
|
+
* import {githubUserPlugin} from "@terreno/api";
|
|
57
|
+
*
|
|
58
|
+
* userSchema.plugin(githubUserPlugin);
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
function githubUserPlugin(schema) {
|
|
62
|
+
schema.add({
|
|
63
|
+
githubAvatarUrl: { type: String },
|
|
64
|
+
githubId: { index: true, sparse: true, type: String, unique: true },
|
|
65
|
+
githubProfileUrl: { type: String },
|
|
66
|
+
githubUsername: { type: String },
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Sets up GitHub OAuth authentication strategy.
|
|
71
|
+
* Call this after setupAuth() in your server initialization.
|
|
72
|
+
*/
|
|
73
|
+
function setupGitHubAuth(_app, userModel, githubOptions) {
|
|
74
|
+
var _this = this;
|
|
75
|
+
var _a;
|
|
76
|
+
var scope = (_a = githubOptions.scope) !== null && _a !== void 0 ? _a : ["user:email"];
|
|
77
|
+
passport_1.default.use("github", new passport_github2_1.Strategy({
|
|
78
|
+
callbackURL: githubOptions.callbackURL,
|
|
79
|
+
clientID: githubOptions.clientId,
|
|
80
|
+
clientSecret: githubOptions.clientSecret,
|
|
81
|
+
passReqToCallback: true,
|
|
82
|
+
scope: scope,
|
|
83
|
+
}, function (req, accessToken, refreshToken, profile, done) { return __awaiter(_this, void 0, void 0, function () {
|
|
84
|
+
var existingUser, user, githubId, existingGitHubUser, user, email, existingEmailUser, newUser, error_1;
|
|
85
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
86
|
+
return __generator(this, function (_j) {
|
|
87
|
+
switch (_j.label) {
|
|
88
|
+
case 0:
|
|
89
|
+
_j.trys.push([0, 13, , 14]);
|
|
90
|
+
existingUser = req.user;
|
|
91
|
+
if (!githubOptions.findOrCreateUser) return [3 /*break*/, 2];
|
|
92
|
+
return [4 /*yield*/, githubOptions.findOrCreateUser(profile, accessToken, refreshToken, existingUser)];
|
|
93
|
+
case 1:
|
|
94
|
+
user = _j.sent();
|
|
95
|
+
return [2 /*return*/, done(null, user)];
|
|
96
|
+
case 2:
|
|
97
|
+
githubId = profile.id;
|
|
98
|
+
return [4 /*yield*/, userModel.findOne({ githubId: githubId })];
|
|
99
|
+
case 3:
|
|
100
|
+
existingGitHubUser = _j.sent();
|
|
101
|
+
if (!existingUser) return [3 /*break*/, 7];
|
|
102
|
+
if (!githubOptions.allowAccountLinking) {
|
|
103
|
+
return [2 /*return*/, done(new errors_1.APIError({ status: 400, title: "Account linking is disabled" }))];
|
|
104
|
+
}
|
|
105
|
+
if (existingGitHubUser &&
|
|
106
|
+
existingGitHubUser._id.toString() !== existingUser._id.toString()) {
|
|
107
|
+
return [2 /*return*/, done(new errors_1.APIError({
|
|
108
|
+
status: 400,
|
|
109
|
+
title: "This GitHub account is already linked to another user",
|
|
110
|
+
}))];
|
|
111
|
+
}
|
|
112
|
+
return [4 /*yield*/, userModel.findById(existingUser._id)];
|
|
113
|
+
case 4:
|
|
114
|
+
user = _j.sent();
|
|
115
|
+
if (!user) return [3 /*break*/, 6];
|
|
116
|
+
user.githubId = githubId;
|
|
117
|
+
user.githubUsername = profile.username;
|
|
118
|
+
user.githubProfileUrl = profile.profileUrl;
|
|
119
|
+
user.githubAvatarUrl = (_b = (_a = profile.photos) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.value;
|
|
120
|
+
return [4 /*yield*/, user.save()];
|
|
121
|
+
case 5:
|
|
122
|
+
_j.sent();
|
|
123
|
+
return [2 /*return*/, done(null, user)];
|
|
124
|
+
case 6: return [2 /*return*/, done(new errors_1.APIError({ status: 404, title: "User not found" }))];
|
|
125
|
+
case 7:
|
|
126
|
+
// Case 2: User with this GitHub ID exists - log them in
|
|
127
|
+
if (existingGitHubUser) {
|
|
128
|
+
return [2 /*return*/, done(null, existingGitHubUser)];
|
|
129
|
+
}
|
|
130
|
+
email = (_d = (_c = profile.emails) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d.value;
|
|
131
|
+
if (!email) return [3 /*break*/, 11];
|
|
132
|
+
return [4 /*yield*/, userModel.findOne({ email: email })];
|
|
133
|
+
case 8:
|
|
134
|
+
existingEmailUser = _j.sent();
|
|
135
|
+
if (!existingEmailUser) return [3 /*break*/, 11];
|
|
136
|
+
if (!(githubOptions.allowAccountLinking !== false)) return [3 /*break*/, 10];
|
|
137
|
+
existingEmailUser.githubId = githubId;
|
|
138
|
+
existingEmailUser.githubUsername = profile.username;
|
|
139
|
+
existingEmailUser.githubProfileUrl = profile.profileUrl;
|
|
140
|
+
existingEmailUser.githubAvatarUrl = (_f = (_e = profile.photos) === null || _e === void 0 ? void 0 : _e[0]) === null || _f === void 0 ? void 0 : _f.value;
|
|
141
|
+
return [4 /*yield*/, existingEmailUser.save()];
|
|
142
|
+
case 9:
|
|
143
|
+
_j.sent();
|
|
144
|
+
return [2 /*return*/, done(null, existingEmailUser)];
|
|
145
|
+
case 10: return [2 /*return*/, done(new errors_1.APIError({
|
|
146
|
+
status: 400,
|
|
147
|
+
title: "An account with this email already exists. Please log in and link your GitHub account.",
|
|
148
|
+
}))];
|
|
149
|
+
case 11:
|
|
150
|
+
newUser = new userModel({
|
|
151
|
+
admin: false,
|
|
152
|
+
email: email,
|
|
153
|
+
githubAvatarUrl: (_h = (_g = profile.photos) === null || _g === void 0 ? void 0 : _g[0]) === null || _h === void 0 ? void 0 : _h.value,
|
|
154
|
+
githubId: githubId,
|
|
155
|
+
githubProfileUrl: profile.profileUrl,
|
|
156
|
+
githubUsername: profile.username,
|
|
157
|
+
});
|
|
158
|
+
return [4 /*yield*/, newUser.save()];
|
|
159
|
+
case 12:
|
|
160
|
+
_j.sent();
|
|
161
|
+
return [2 /*return*/, done(null, newUser)];
|
|
162
|
+
case 13:
|
|
163
|
+
error_1 = _j.sent();
|
|
164
|
+
logger_1.logger.error("GitHub auth error: ".concat(error_1));
|
|
165
|
+
return [2 /*return*/, done(error_1)];
|
|
166
|
+
case 14: return [2 /*return*/];
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}); }));
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Adds GitHub OAuth routes to the Express application.
|
|
173
|
+
*
|
|
174
|
+
* Routes added:
|
|
175
|
+
* - GET /auth/github - Initiates GitHub OAuth flow
|
|
176
|
+
* - GET /auth/github/callback - Handles GitHub OAuth callback
|
|
177
|
+
* - POST /auth/github/link - Links GitHub account to authenticated user (requires JWT auth)
|
|
178
|
+
* - DELETE /auth/github/unlink - Unlinks GitHub account from authenticated user (requires JWT auth)
|
|
179
|
+
*/
|
|
180
|
+
function addGitHubAuthRoutes(app, userModel, githubOptions, authOptions) {
|
|
181
|
+
var _this = this;
|
|
182
|
+
var router = require("express").Router();
|
|
183
|
+
// Initiate GitHub OAuth flow
|
|
184
|
+
router.get("/github", function (req, _res, next) {
|
|
185
|
+
// Store the return URL in session or query for redirect after auth
|
|
186
|
+
var returnTo = req.query.returnTo;
|
|
187
|
+
if (returnTo) {
|
|
188
|
+
req.session = req.session || {};
|
|
189
|
+
req.session.returnTo = returnTo;
|
|
190
|
+
}
|
|
191
|
+
next();
|
|
192
|
+
}, passport_1.default.authenticate("github", { session: false }));
|
|
193
|
+
// GitHub OAuth callback
|
|
194
|
+
router.get("/github/callback", passport_1.default.authenticate("github", {
|
|
195
|
+
failureRedirect: "/auth/github/failure",
|
|
196
|
+
session: false,
|
|
197
|
+
}), function (req, res) { return __awaiter(_this, void 0, void 0, function () {
|
|
198
|
+
var tokens, returnTo, url, error_2;
|
|
199
|
+
var _a, _b, _c, _d;
|
|
200
|
+
return __generator(this, function (_e) {
|
|
201
|
+
switch (_e.label) {
|
|
202
|
+
case 0:
|
|
203
|
+
_e.trys.push([0, 2, , 3]);
|
|
204
|
+
return [4 /*yield*/, (0, auth_1.generateTokens)(req.user, authOptions)];
|
|
205
|
+
case 1:
|
|
206
|
+
tokens = _e.sent();
|
|
207
|
+
returnTo = (_a = req.session) === null || _a === void 0 ? void 0 : _a.returnTo;
|
|
208
|
+
// If there's a return URL, redirect with tokens as query params
|
|
209
|
+
if (returnTo) {
|
|
210
|
+
url = new URL(returnTo);
|
|
211
|
+
url.searchParams.set("token", tokens.token || "");
|
|
212
|
+
if (tokens.refreshToken) {
|
|
213
|
+
url.searchParams.set("refreshToken", tokens.refreshToken);
|
|
214
|
+
}
|
|
215
|
+
url.searchParams.set("userId", ((_c = (_b = req.user) === null || _b === void 0 ? void 0 : _b._id) === null || _c === void 0 ? void 0 : _c.toString()) || "");
|
|
216
|
+
return [2 /*return*/, res.redirect(url.toString())];
|
|
217
|
+
}
|
|
218
|
+
// Otherwise return JSON response
|
|
219
|
+
return [2 /*return*/, res.json({
|
|
220
|
+
data: {
|
|
221
|
+
refreshToken: tokens.refreshToken,
|
|
222
|
+
token: tokens.token,
|
|
223
|
+
userId: (_d = req.user) === null || _d === void 0 ? void 0 : _d._id,
|
|
224
|
+
},
|
|
225
|
+
})];
|
|
226
|
+
case 2:
|
|
227
|
+
error_2 = _e.sent();
|
|
228
|
+
logger_1.logger.error("GitHub callback error: ".concat(error_2));
|
|
229
|
+
return [2 /*return*/, res.status(500).json({ message: "Authentication failed" })];
|
|
230
|
+
case 3: return [2 /*return*/];
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
}); });
|
|
234
|
+
// GitHub auth failure handler
|
|
235
|
+
router.get("/github/failure", function (_req, res) {
|
|
236
|
+
return res.status(401).json({ message: "GitHub authentication failed" });
|
|
237
|
+
});
|
|
238
|
+
// Link GitHub to existing authenticated user
|
|
239
|
+
if (githubOptions.allowAccountLinking !== false) {
|
|
240
|
+
router.get("/github/link", function (req, res, next) {
|
|
241
|
+
// Require JWT authentication for linking
|
|
242
|
+
passport_1.default.authenticate("jwt", { session: false }, function (err, user) {
|
|
243
|
+
if (err || !user) {
|
|
244
|
+
res.status(401).json({ message: "Authentication required to link GitHub account" });
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
req.user = user;
|
|
248
|
+
next();
|
|
249
|
+
})(req, res, next);
|
|
250
|
+
}, passport_1.default.authenticate("github", { session: false }));
|
|
251
|
+
// Unlink GitHub from user account
|
|
252
|
+
router.delete("/github/unlink", passport_1.default.authenticate("jwt", { session: false }), function (req, res) { return __awaiter(_this, void 0, void 0, function () {
|
|
253
|
+
var user, hasPassword, error_3;
|
|
254
|
+
return __generator(this, function (_a) {
|
|
255
|
+
switch (_a.label) {
|
|
256
|
+
case 0:
|
|
257
|
+
if (!req.user) {
|
|
258
|
+
return [2 /*return*/, res.status(401).json({ message: "Authentication required" })];
|
|
259
|
+
}
|
|
260
|
+
_a.label = 1;
|
|
261
|
+
case 1:
|
|
262
|
+
_a.trys.push([1, 4, , 5]);
|
|
263
|
+
return [4 /*yield*/, userModel.findById(req.user._id).select("+hash +salt")];
|
|
264
|
+
case 2:
|
|
265
|
+
user = _a.sent();
|
|
266
|
+
if (!user) {
|
|
267
|
+
return [2 /*return*/, res.status(404).json({ message: "User not found" })];
|
|
268
|
+
}
|
|
269
|
+
hasPassword = !!user.hash || !!user.salt;
|
|
270
|
+
if (!hasPassword) {
|
|
271
|
+
return [2 /*return*/, res.status(400).json({
|
|
272
|
+
message: "Cannot unlink GitHub account without another authentication method. Set a password first.",
|
|
273
|
+
})];
|
|
274
|
+
}
|
|
275
|
+
user.githubId = undefined;
|
|
276
|
+
user.githubUsername = undefined;
|
|
277
|
+
user.githubProfileUrl = undefined;
|
|
278
|
+
user.githubAvatarUrl = undefined;
|
|
279
|
+
return [4 /*yield*/, user.save()];
|
|
280
|
+
case 3:
|
|
281
|
+
_a.sent();
|
|
282
|
+
return [2 /*return*/, res.json({ data: { message: "GitHub account unlinked successfully" } })];
|
|
283
|
+
case 4:
|
|
284
|
+
error_3 = _a.sent();
|
|
285
|
+
logger_1.logger.error("GitHub unlink error: ".concat(error_3));
|
|
286
|
+
return [2 /*return*/, res.status(500).json({ message: "Failed to unlink GitHub account" })];
|
|
287
|
+
case 5: return [2 /*return*/];
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
}); });
|
|
291
|
+
}
|
|
292
|
+
app.use("/auth", router);
|
|
293
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|