@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.
Files changed (77) hide show
  1. package/.claude/CLAUDE.local.md +204 -0
  2. package/.cursor/rules/00-root.mdc +338 -0
  3. package/.github/copilot-instructions.md +333 -0
  4. package/AGENTS.md +333 -0
  5. package/README.md +76 -7
  6. package/biome.jsonc +1 -1
  7. package/dist/api.d.ts +68 -1
  8. package/dist/api.js +140 -5
  9. package/dist/api.query.test.js +1 -1
  10. package/dist/api.test.js +222 -484
  11. package/dist/auth.js +3 -1
  12. package/dist/errors.js +15 -12
  13. package/dist/example.js +7 -7
  14. package/dist/expressServer.d.ts +8 -2
  15. package/dist/expressServer.js +8 -1
  16. package/dist/githubAuth.d.ts +64 -0
  17. package/dist/githubAuth.js +293 -0
  18. package/dist/githubAuth.test.d.ts +1 -0
  19. package/dist/githubAuth.test.js +351 -0
  20. package/dist/index.d.ts +3 -0
  21. package/dist/index.js +3 -0
  22. package/dist/logger.js +1 -1
  23. package/dist/middleware.js +1 -1
  24. package/dist/notifiers/googleChatNotifier.js +1 -1
  25. package/dist/notifiers/googleChatNotifier.test.js +1 -1
  26. package/dist/notifiers/slackNotifier.js +1 -1
  27. package/dist/notifiers/slackNotifier.test.js +1 -1
  28. package/dist/notifiers/zoomNotifier.js +1 -1
  29. package/dist/notifiers/zoomNotifier.test.js +1 -1
  30. package/dist/openApi.test.js +8 -5
  31. package/dist/openApiBuilder.d.ts +69 -1
  32. package/dist/openApiBuilder.js +109 -5
  33. package/dist/openApiValidator.d.ts +296 -0
  34. package/dist/openApiValidator.js +698 -0
  35. package/dist/openApiValidator.test.d.ts +1 -0
  36. package/dist/openApiValidator.test.js +346 -0
  37. package/dist/permissions.js +1 -1
  38. package/dist/plugins.test.js +3 -3
  39. package/dist/terrenoPlugin.d.ts +4 -0
  40. package/dist/terrenoPlugin.js +2 -0
  41. package/dist/tests/bunSetup.js +2 -2
  42. package/dist/tests.js +34 -24
  43. package/package.json +7 -2
  44. package/src/__snapshots__/openApi.test.ts.snap +399 -0
  45. package/src/__snapshots__/openApiBuilder.test.ts.snap +108 -0
  46. package/src/api.query.test.ts +1 -1
  47. package/src/api.test.ts +161 -374
  48. package/src/api.ts +210 -4
  49. package/src/auth.ts +3 -1
  50. package/src/errors.ts +15 -12
  51. package/src/example.ts +7 -7
  52. package/src/expressServer.ts +18 -2
  53. package/src/githubAuth.test.ts +223 -0
  54. package/src/githubAuth.ts +335 -0
  55. package/src/index.ts +3 -0
  56. package/src/logger.ts +1 -1
  57. package/src/middleware.ts +1 -1
  58. package/src/notifiers/googleChatNotifier.test.ts +1 -1
  59. package/src/notifiers/googleChatNotifier.ts +1 -1
  60. package/src/notifiers/slackNotifier.test.ts +1 -1
  61. package/src/notifiers/slackNotifier.ts +1 -1
  62. package/src/notifiers/zoomNotifier.test.ts +1 -1
  63. package/src/notifiers/zoomNotifier.ts +1 -1
  64. package/src/openApi.test.ts +8 -5
  65. package/src/openApiBuilder.ts +188 -15
  66. package/src/openApiValidator.test.ts +241 -0
  67. package/src/openApiValidator.ts +860 -0
  68. package/src/permissions.ts +1 -1
  69. package/src/plugins.test.ts +3 -3
  70. package/src/terrenoPlugin.ts +5 -0
  71. package/src/tests/bunSetup.ts +2 -2
  72. package/src/tests.ts +34 -24
  73. package/CLAUDE.md +0 -107
  74. package/dist/response.d.ts +0 -0
  75. package/dist/response.js +0 -1
  76. package/index.ts +0 -1
  77. 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
- logger_1.logger.info("User logged in: ".concat(user._id, ", type: ").concat(user.type || "N/A"));
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/node"));
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: String,
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() {
@@ -1,8 +1,9 @@
1
- import * as Sentry from "@sentry/node";
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.NodeOptions;
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;
@@ -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/node"));
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 {};