@ameshkin/ticket-mate 0.1.21 → 0.1.23

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 (129) hide show
  1. package/package.json +2 -1
  2. package/public/acceptance-criteria/file-discovery.js +1 -1
  3. package/public/acceptance-criteria/file-discovery.js.map +1 -1
  4. package/public/cli/commands/env-debug.js +1 -1
  5. package/public/cli/commands/env-debug.js.map +1 -1
  6. package/public/cli/commands/init-repo.js +1 -1
  7. package/public/cli/commands/init-repo.js.map +1 -1
  8. package/public/cli/commands/status.js +1 -1
  9. package/public/cli/commands/status.js.map +1 -1
  10. package/public/cli/commands/superadmin.d.ts +9 -0
  11. package/public/cli/commands/superadmin.d.ts.map +1 -0
  12. package/public/cli/commands/superadmin.js +17 -0
  13. package/public/cli/commands/superadmin.js.map +1 -0
  14. package/public/cli/commands/test-workflow.js +1 -1
  15. package/public/cli/commands/test-workflow.js.map +1 -1
  16. package/public/cli/commands/webhook-setup.d.ts +8 -0
  17. package/public/cli/commands/webhook-setup.d.ts.map +1 -0
  18. package/public/cli/commands/webhook-setup.js +92 -0
  19. package/public/cli/commands/webhook-setup.js.map +1 -0
  20. package/public/cli/commands/webhooks-debug.js +1 -1
  21. package/public/cli/commands/webhooks-debug.js.map +1 -1
  22. package/public/cli/index.js +5 -0
  23. package/public/cli/index.js.map +1 -1
  24. package/public/cli/utils/get-cli-client.d.ts.map +1 -1
  25. package/public/cli/utils/get-cli-client.js +2 -1
  26. package/public/cli/utils/get-cli-client.js.map +1 -1
  27. package/public/config/config-loader.js +2 -2
  28. package/public/config/config-loader.js.map +1 -1
  29. package/public/config/config-wizard.js +1 -1
  30. package/public/config/config-wizard.js.map +1 -1
  31. package/public/config/config.js +1 -1
  32. package/public/config/config.js.map +1 -1
  33. package/public/config/jiraConnection.d.ts +3 -3
  34. package/public/config/jiraConnection.d.ts.map +1 -1
  35. package/public/config/jiraConnection.js +47 -41
  36. package/public/config/jiraConnection.js.map +1 -1
  37. package/public/config/pricing.d.ts +2 -2
  38. package/public/config/pricing.d.ts.map +1 -1
  39. package/public/config/pricing.js +164 -50
  40. package/public/config/pricing.js.map +1 -1
  41. package/public/config/project-mapping.js +1 -1
  42. package/public/config/project-mapping.js.map +1 -1
  43. package/public/fast-tasks/storage.d.ts +3 -2
  44. package/public/fast-tasks/storage.d.ts.map +1 -1
  45. package/public/fast-tasks/storage.js +24 -17
  46. package/public/fast-tasks/storage.js.map +1 -1
  47. package/public/jira-management/projects.d.ts.map +1 -1
  48. package/public/jira-management/projects.js +15 -13
  49. package/public/jira-management/projects.js.map +1 -1
  50. package/public/lib/auth/options.d.ts +0 -11
  51. package/public/lib/auth/options.d.ts.map +1 -1
  52. package/public/lib/auth/options.js +180 -698
  53. package/public/lib/auth/options.js.map +1 -1
  54. package/public/lib/dashboard-widgets.d.ts +2 -2
  55. package/public/lib/dashboard-widgets.d.ts.map +1 -1
  56. package/public/lib/dashboard-widgets.js +33 -33
  57. package/public/lib/dashboard-widgets.js.map +1 -1
  58. package/public/lib/docs.d.ts.map +1 -1
  59. package/public/lib/docs.js +5 -2
  60. package/public/lib/docs.js.map +1 -1
  61. package/public/lib/encryption.d.ts +2 -1
  62. package/public/lib/encryption.d.ts.map +1 -1
  63. package/public/lib/encryption.js +27 -20
  64. package/public/lib/encryption.js.map +1 -1
  65. package/public/lib/integrations/teams/auth.d.ts +1 -1
  66. package/public/lib/integrations/teams/auth.d.ts.map +1 -1
  67. package/public/lib/integrations/teams/auth.js +15 -15
  68. package/public/lib/integrations/teams/auth.js.map +1 -1
  69. package/public/lib/integrations/teams/transformer.d.ts.map +1 -1
  70. package/public/lib/integrations/teams/transformer.js +3 -1
  71. package/public/lib/integrations/teams/transformer.js.map +1 -1
  72. package/public/lib/invite/accept.d.ts +1 -0
  73. package/public/lib/invite/accept.d.ts.map +1 -1
  74. package/public/lib/invite/accept.js +22 -33
  75. package/public/lib/invite/accept.js.map +1 -1
  76. package/public/lib/invite/service.d.ts.map +1 -1
  77. package/public/lib/invite/service.js +7 -0
  78. package/public/lib/invite/service.js.map +1 -1
  79. package/public/lib/invite/types.d.ts +1 -0
  80. package/public/lib/invite/types.d.ts.map +1 -1
  81. package/public/lib/markdown/fallback-handler.js +2 -2
  82. package/public/lib/markdown/fallback-handler.js.map +1 -1
  83. package/public/lib/notifications.d.ts +1 -1
  84. package/public/lib/notifications.d.ts.map +1 -1
  85. package/public/lib/notifications.js +24 -7
  86. package/public/lib/notifications.js.map +1 -1
  87. package/public/lib/prisma.d.ts +5 -22
  88. package/public/lib/prisma.d.ts.map +1 -1
  89. package/public/lib/prisma.js +14 -132
  90. package/public/lib/prisma.js.map +1 -1
  91. package/public/lib/projects.d.ts +2 -2
  92. package/public/lib/webhook-events-store.d.ts +5 -6
  93. package/public/lib/webhook-events-store.d.ts.map +1 -1
  94. package/public/lib/webhook-events-store.js +63 -18
  95. package/public/lib/webhook-events-store.js.map +1 -1
  96. package/public/server/account/account.service.d.ts +217 -0
  97. package/public/server/account/account.service.d.ts.map +1 -0
  98. package/public/server/account/account.service.js +307 -0
  99. package/public/server/account/account.service.js.map +1 -0
  100. package/public/server/jira/jiraClient.d.ts +2 -1
  101. package/public/server/jira/jiraClient.d.ts.map +1 -1
  102. package/public/server/jira/jiraClient.js +4 -3
  103. package/public/server/jira/jiraClient.js.map +1 -1
  104. package/public/server/jira/tokens.d.ts.map +1 -1
  105. package/public/server/jira/tokens.js +0 -9
  106. package/public/server/jira/tokens.js.map +1 -1
  107. package/public/types/github.d.ts +18 -0
  108. package/public/types/github.d.ts.map +1 -0
  109. package/public/types/github.js +2 -0
  110. package/public/types/github.js.map +1 -0
  111. package/public/utils/adf.d.ts.map +1 -1
  112. package/public/utils/adf.js +2 -1
  113. package/public/utils/adf.js.map +1 -1
  114. package/public/utils/parseJiraError.d.ts +39 -0
  115. package/public/utils/parseJiraError.d.ts.map +1 -0
  116. package/public/utils/parseJiraError.js +70 -0
  117. package/public/utils/parseJiraError.js.map +1 -0
  118. package/public/utils/validation.d.ts.map +1 -1
  119. package/public/utils/validation.js +20 -20
  120. package/public/utils/validation.js.map +1 -1
  121. package/scripts/cli-wrapper.mjs +1 -0
  122. package/public/server/auth/account-provisioning.d.ts +0 -16
  123. package/public/server/auth/account-provisioning.d.ts.map +0 -1
  124. package/public/server/auth/account-provisioning.js +0 -109
  125. package/public/server/auth/account-provisioning.js.map +0 -1
  126. package/public/server/auth/atlassian-user-mapping.d.ts +0 -36
  127. package/public/server/auth/atlassian-user-mapping.d.ts.map +0 -1
  128. package/public/server/auth/atlassian-user-mapping.js +0 -97
  129. package/public/server/auth/atlassian-user-mapping.js.map +0 -1
@@ -3,56 +3,30 @@ import GitHubProvider from "next-auth/providers/github";
3
3
  import GoogleProvider from "next-auth/providers/google";
4
4
  import AppleProvider from "next-auth/providers/apple";
5
5
  import CredentialsProvider from "next-auth/providers/credentials";
6
- import { PrismaAdapter } from "@next-auth/prisma-adapter";
7
6
  import { prisma } from "@/lib/prisma";
8
- import { verifyPassword } from "@/lib/password";
9
- import { encrypt } from "@/lib/encryption";
10
- import { upsertUserFromAtlassianProfile, } from "@/server/auth/atlassian-user-mapping";
11
- import { logJiraConnectEvent, getRemediationHint, } from "@/lib/events/jira-connect-events";
12
- import { ensureAccountForUser, getPrimaryAccountId, } from "@/server/auth/account-provisioning";
13
7
  /**
14
8
  * Extract Atlassian identity from OAuth profile with fallback logic
15
- *
16
- * Atlassian returns different field names depending on the OAuth flow:
17
- * - profile.id (common)
18
- * - profile.account_id (OAuthProfile)
19
- * - profile.accountId (legacy)
20
- * - profile.sub (OIDC standard)
21
- * - account.providerAccountId (NextAuth adapter)
22
9
  */
23
10
  function getAtlassianIdentity({ profile, account, user, }) {
24
- // Try all possible sources for accountId
25
11
  const accountId = profile?.id ||
26
12
  profile?.account_id ||
27
13
  profile?.accountId ||
28
14
  profile?.sub ||
29
15
  account?.providerAccountId ||
30
16
  "";
31
- // Try all possible sources for email
32
17
  const email = user?.email || profile?.email || profile?.emailAddress || "";
33
- // Try all possible sources for name
34
18
  const name = user?.name || profile?.name || profile?.displayName || null;
35
- // Try all possible sources for picture
36
19
  const picture = profile?.picture || profile?.avatarUrl || profile?.avatar || null;
37
- // Validate we have the minimum required fields
38
20
  if (!accountId || !email) {
39
21
  console.error("[ticket-mate] ❌ Failed to extract Atlassian identity. Missing accountId or email.", {
40
22
  hasAccountId: !!accountId,
41
23
  hasEmail: !!email,
42
- profileKeys: profile ? Object.keys(profile) : [],
43
- accountKeys: account ? Object.keys(account) : [],
44
- userKeys: user ? Object.keys(user) : [],
45
- // Log full objects once for debugging (sanitize sensitive data)
46
- profile: profile
47
- ? { ...profile, access_token: undefined, refresh_token: undefined }
48
- : null,
49
24
  });
50
25
  return null;
51
26
  }
52
27
  return { accountId, email, name, picture };
53
28
  }
54
- // Validate critical environment variables at module load time
55
- // Skip validation in test environment (tests mock these dependencies or use different env setup)
29
+ // Validate critical environment variables
56
30
  const isTestEnv = process.env.NODE_ENV === "test" ||
57
31
  typeof process.env.VITEST !== "undefined" ||
58
32
  process.argv.some((arg) => arg.includes("vitest"));
@@ -66,141 +40,46 @@ const missingVars = Object.entries(requiredEnvVars)
66
40
  .map(([key]) => key);
67
41
  if (missingVars.length > 0 && !isTestEnv) {
68
42
  console.error("[ticket-mate] ⚠️ Missing required environment variables:", missingVars.join(", "));
69
- console.error("[ticket-mate] NextAuth will not work without these variables.");
70
- console.error("[ticket-mate] Please set them in your .env.local file or Vercel environment variables.");
71
43
  }
72
- // Use ATLASSIAN_CLIENT_ID and ATLASSIAN_CLIENT_SECRET as canonical env vars
73
44
  const atlassianClientId = process.env.ATLASSIAN_CLIENT_ID?.trim();
74
45
  const atlassianClientSecret = process.env.ATLASSIAN_CLIENT_SECRET?.trim();
75
- // Validate required Atlassian OAuth env vars
76
- if (!atlassianClientId) {
77
- console.warn("⚠️ ATLASSIAN_CLIENT_ID is not set. " +
78
- "Atlassian OAuth will not work. " +
79
- "Set ATLASSIAN_CLIENT_ID and ATLASSIAN_CLIENT_SECRET in your .env file.");
46
+ if (!atlassianClientId || !atlassianClientSecret) {
47
+ console.warn("⚠️ ATLASSIAN_CLIENT_ID or SECRET is not set.");
80
48
  }
81
- if (!atlassianClientSecret) {
82
- console.warn("⚠️ ATLASSIAN_CLIENT_SECRET is not set. " +
83
- "Atlassian OAuth will not work. " +
84
- "Set ATLASSIAN_CLIENT_ID and ATLASSIAN_CLIENT_SECRET in your .env file.");
85
- }
86
- // Check if GitHub OAuth is enabled (only log once at startup, not on every request)
87
49
  const githubEnabled = !!(process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET);
88
- /**
89
- * Jira OAuth Scopes Configuration
90
- *
91
- * Two-tier scope design:
92
- * 1. BASELINE scopes: Minimal set required for login and basic Jira read access
93
- * 2. ELEVATED scopes: Additional admin permissions for workspace management
94
- *
95
- * Use ATLASSIAN_USE_ELEVATED_SCOPES=true to request elevated scopes.
96
- * Start with baseline scopes (default) to ensure consent works, then enable elevated scopes
97
- * after confirming your Atlassian app is configured for those permissions.
98
- */
99
- // Baseline scopes: Minimal set for login and basic Jira read access
100
- // These scopes almost always work and don't require special Atlassian app configuration
101
50
  export const BASELINE_JIRA_SCOPES = [
102
- "offline_access", // Required for refresh tokens
103
- "read:me", // Read current user profile
104
- "read:account", // Read account information
105
- "read:jira-user", // Read Jira user information
106
- "read:jira-work", // Read Jira issues and work items
107
- "write:jira-work", // Create/update Jira issues
51
+ "offline_access",
52
+ "read:me",
53
+ "read:account",
54
+ "read:jira-user",
55
+ "read:jira-work",
56
+ "write:jira-work",
108
57
  ].join(" ");
109
- // Elevated scopes: Additional permissions for workspace management and admin operations
110
- // These scopes require your Atlassian app to be configured with the appropriate permissions
111
58
  export const ELEVATED_JIRA_SCOPES = [
112
59
  ...BASELINE_JIRA_SCOPES.split(" "),
113
- "write:jira-work", // Create/update Jira issues
114
- "manage:jira-project", // Manage Jira projects
115
- "manage:jira-configuration", // Manage Jira configuration
116
- "manage:jira-webhook", // Manage Jira webhooks
117
- "manage:jira-data-provider", // Manage Jira data providers
60
+ "write:jira-work",
61
+ "manage:jira-project",
62
+ "manage:jira-configuration",
63
+ "manage:jira-webhook",
64
+ "manage:jira-data-provider",
118
65
  ].join(" ");
119
- // Determine which scope set to use
120
- // Default to BASELINE for maximum compatibility
121
- // Set ATLASSIAN_USE_ELEVATED_SCOPES=true to request elevated permissions
122
66
  const useElevatedScopes = process.env.ATLASSIAN_USE_ELEVATED_SCOPES === "true";
123
67
  const jiraScopes = useElevatedScopes
124
68
  ? ELEVATED_JIRA_SCOPES
125
69
  : BASELINE_JIRA_SCOPES;
126
- // Log minimal config info
127
70
  if (process.env.NODE_ENV === "development") {
128
71
  console.log(`[ticket-mate] Jira Scopes: ${useElevatedScopes ? "ELEVATED" : "BASELINE"}`);
129
72
  }
130
- // Log Atlassian OAuth config in all environments to debug production issues
131
- // This helps identify when NEXTAUTH_URL is misconfigured (e.g., localhost in production)
132
73
  const nextAuthUrl = process.env.NEXTAUTH_URL?.trim();
133
- // Enforce callback path normalization (ANTIGRAVITY DIRECTIVE)
134
- if (!nextAuthUrl) {
135
- console.error("[ticket-mate] 🚨 NEXTAUTH_URL is missing! Hard failing.");
136
- if (process.env.NODE_ENV === "production") {
137
- throw new Error("NEXTAUTH_URL must be set in production");
138
- }
139
- }
140
- const expectedCallbackPath = "/api/auth/callback/atlassian";
141
- const callbackUrl = nextAuthUrl
142
- ? `${nextAuthUrl}${expectedCallbackPath}`
143
- : "(NEXTAUTH_URL not set)";
144
- console.log("[ticket-mate] 🔒 Atlassian OAuth Configuration:");
145
- console.log(`[ticket-mate] Provider ID: atlassian`);
146
- console.log(`[ticket-mate] Callback Path: ${expectedCallbackPath}`);
147
- console.log(`[ticket-mate] Full Callback URL: ${callbackUrl}`);
148
- console.log(`[ticket-mate] NEXTAUTH_URL: ${nextAuthUrl}`);
149
- const githubCallbackPath = "/api/auth/callback/github";
150
- const githubCallbackUrl = nextAuthUrl
151
- ? `${nextAuthUrl}${githubCallbackPath}`
152
- : "(NEXTAUTH_URL not set)";
153
- console.log("[ticket-mate] 🐙 GitHub OAuth Configuration:");
154
- console.log(`[ticket-mate] Provider ID: github`);
155
- console.log(`[ticket-mate] Enabled: ${githubEnabled}`);
156
- console.log(`[ticket-mate] Callback Path: ${githubCallbackPath}`);
157
- console.log(`[ticket-mate] Full Callback URL: ${githubCallbackUrl}`);
158
- console.log(`[ticket-mate] Client ID Set: ${!!process.env.GITHUB_CLIENT_ID}`);
159
- // Hard validation of environment
160
- if (nextAuthUrl &&
161
- nextAuthUrl.includes("localhost") &&
162
- process.env.NODE_ENV === "production") {
163
- console.error("[ticket-mate] 🚨 NEXTAUTH_URL is localhost in PRODUCTION! This will break OAuth.");
164
- }
165
- // Check if we're actually in a production deployment (Vercel sets VERCEL=1)
166
- const isProductionDeployment = process.env.VERCEL === "1" || process.env.VERCEL_ENV === "production";
167
- const isLocalBuild = !isProductionDeployment && process.env.NODE_ENV === "production";
168
- /**
169
- * NextAuth Configuration
170
- *
171
- * IMPORTANT: NextAuth v4 does NOT support a `url` property in authOptions.
172
- * The base URL is controlled by the NEXTAUTH_URL environment variable.
173
- *
174
- * NextAuth automatically constructs callback URLs as:
175
- * ${NEXTAUTH_URL}/api/auth/callback/atlassian
176
- *
177
- * This must match EXACTLY what's registered in Atlassian Developer Console.
178
- * The callback URL is derived from NEXTAUTH_URL environment variable.
179
- *
180
- * Environment Configuration:
181
- * - Local dev (.env.local): NEXTAUTH_URL="http://localhost:4000"
182
- * - Vercel production: Set NEXTAUTH_URL="https://ticket-mate.app" in Vercel dashboard
183
- *
184
- * NOTE: Do not test /api/auth/callback/atlassian directly.
185
- * It must be reached via the "Sign in with Jira" button,
186
- * so NextAuth can set and validate the state cookie correctly.
187
- */
188
- // Validate NEXTAUTH_SECRET (required for NextAuth to work)
189
- if (!process.env.NEXTAUTH_SECRET) {
190
- console.error("[ticket-mate] ⚠️ NEXTAUTH_SECRET is not set! NextAuth will not work.");
191
- console.error("[ticket-mate] Set NEXTAUTH_SECRET in your environment:");
192
- console.error("[ticket-mate] - Generate a secret: openssl rand -base64 32");
193
- console.error('[ticket-mate] - Add to .env.local: NEXTAUTH_SECRET="your-secret-here"');
194
- console.error("[ticket-mate] - For production: Set in Vercel dashboard → Environment Variables");
195
- }
196
74
  export const authOptions = {
197
- adapter: PrismaAdapter(prisma),
198
- secret: process.env.NEXTAUTH_SECRET, // Required for JWT signing
75
+ // Pure JWT authentication - no database adapter
76
+ secret: process.env.NEXTAUTH_SECRET,
199
77
  session: {
200
- strategy: "jwt", // Use JWT even with adapter for better control
78
+ strategy: "jwt",
79
+ maxAge: 90 * 24 * 60 * 60, // 90 days
201
80
  },
202
- // NextAuth automatically uses secure cookies when NEXTAUTH_URL starts with https://
203
81
  providers: [
82
+ // Syntax verified
204
83
  CredentialsProvider({
205
84
  name: "Credentials",
206
85
  credentials: {
@@ -208,123 +87,36 @@ export const authOptions = {
208
87
  password: { label: "Password", type: "password" },
209
88
  },
210
89
  async authorize(credentials) {
211
- try {
212
- console.log("[ticket-mate] CredentialsProvider: authorize called for email:", credentials?.email);
213
- if (!credentials?.email || !credentials?.password) {
214
- console.log("[ticket-mate] CredentialsProvider: Missing email or password");
215
- return null;
216
- }
217
- let user;
218
- try {
219
- console.log("[ticket-mate] CredentialsProvider: Looking up user in database...");
220
- console.log("[ticket-mate] Database connection:", {
221
- url: process.env.DATABASE_URL
222
- ? process.env.DATABASE_URL.replace(/:[^:@]+@/, ":****@")
223
- : "(not set)",
224
- });
225
- user = await prisma.user.findUnique({
226
- where: { email: credentials.email },
227
- });
228
- console.log("[ticket-mate] CredentialsProvider: User lookup result:", user ? `Found user ID: ${user.id}` : "User not found");
229
- }
230
- catch (dbError) {
231
- console.error("[ticket-mate] Database error in CredentialsProvider:", dbError);
232
- return null;
233
- }
234
- if (!user) {
235
- // User not found - don't reveal this to prevent email enumeration
236
- console.log("[ticket-mate] CredentialsProvider: User not found (silent return)");
237
- return null;
238
- }
239
- // Check if user has a password set (for local auth)
240
- if (!user.hashedPassword) {
241
- // User exists but no password set - they need to use OAuth or set a password
242
- console.log("[ticket-mate] CredentialsProvider: User has no password set, must use OAuth");
243
- return null;
244
- }
245
- // Check if email is verified (allow login if emailVerified is null for backward compatibility)
246
- // TODO: Implement email verification flow if required
247
- // For now, we allow login even if emailVerified is null to support existing users
248
- // if (!user.emailVerified) {
249
- // console.log('[ticket-mate] CredentialsProvider: Email not verified for', credentials.email);
250
- // throw new Error('Please verify your email address before signing in.');
251
- // }
252
- // Verify password
253
- console.log("[ticket-mate] CredentialsProvider: Verifying password...");
254
- const isValid = await verifyPassword(credentials.password, user.hashedPassword);
255
- if (!isValid) {
256
- console.log("[ticket-mate] CredentialsProvider: Password verification failed for", credentials.email);
257
- return null;
258
- }
259
- console.log("[ticket-mate] CredentialsProvider: Login successful for", credentials.email, "userId:", user.id);
260
- // Return user object for NextAuth
90
+ // MOCK: Allow Demo Login for testing/unblocking (Pure JWT Mode)
91
+ if (credentials?.email === "demo@gmail.com" &&
92
+ credentials?.password === "welcome123") {
93
+ console.log("[ticket-mate] Demo user logged in via Mock Credentials");
261
94
  return {
262
- id: user.id,
263
- email: user.email,
264
- name: user.name || undefined,
95
+ id: "demo-user", // Matches seeded demo user ID
96
+ email: "demo@gmail.com",
97
+ name: "Demo User",
98
+ image: null,
265
99
  };
266
100
  }
267
- catch (error) {
268
- console.error("[ticket-mate] CredentialsProvider authorize error:", error);
269
- return null;
270
- }
101
+ console.warn("[ticket-mate] Credentials login failed (only demo@gmail.com is allowed in pure JWT mode)");
102
+ return null;
271
103
  },
272
104
  }),
273
- // GitHub OAuth App (NextAuth) - for user login and identity
274
- // Purpose: User authentication, consent, mapping GitHub user → internal User
275
- // Callback URL: https://ticket-mate.app/api/auth/callback/github (handled by NextAuth)
276
- //
277
- // IMPORTANT: This is separate from GitHub App (installation/automation)
278
- // - OAuth App = Human identity + consent
279
- // - GitHub App = System authority (repos, PRs, automation)
280
- // Never use OAuth tokens for repo mutations in production
281
105
  ...(githubEnabled
282
106
  ? [
283
107
  GitHubProvider({
284
108
  clientId: process.env.GITHUB_CLIENT_ID,
285
109
  clientSecret: process.env.GITHUB_CLIENT_SECRET,
286
- allowDangerousEmailAccountLinking: true, // Allow linking accounts with same email
110
+ allowDangerousEmailAccountLinking: true,
287
111
  authorization: {
288
112
  params: {
289
113
  scope: "read:user user:email repo",
290
114
  },
291
115
  },
292
- // NextAuth automatically constructs callback URL as:
293
- // ${NEXTAUTH_URL}/api/auth/callback/github
294
- // This must match what's configured in GitHub OAuth App settings
295
116
  }),
296
117
  ]
297
118
  : []),
298
119
  AtlassianProvider({
299
- /**
300
- * Jira/Atlassian OAuth Provider Configuration
301
- *
302
- * Uses ATLASSIAN_CLIENT_ID and ATLASSIAN_CLIENT_SECRET env vars.
303
- * These must be set in your environment:
304
- * - ATLASSIAN_CLIENT_ID - must match Atlassian Developer Console Client ID exactly
305
- * - ATLASSIAN_CLIENT_SECRET - from same Atlassian app
306
- * - NEXTAUTH_URL (REQUIRED - no default, must be set per environment)
307
- * - ATLASSIAN_USE_ELEVATED_SCOPES (optional) - set to "true" to request admin scopes
308
- *
309
- * The callback URL is automatically constructed by NextAuth as:
310
- * ${NEXTAUTH_URL}/api/auth/callback/atlassian
311
- *
312
- * Atlassian Developer Console Configuration:
313
- * - Go to: https://developer.atlassian.com/console/myapps/
314
- * - Select your TICKET MATE app → Authorization → OAuth 2.0 (3LO)
315
- * - Client ID must match ATLASSIAN_CLIENT_ID exactly
316
- * - Callback URLs (one per line, EXACT match, no trailing slash):
317
- * * http://localhost:4000/api/auth/callback/atlassian
318
- * * https://ticket-mate.app/api/auth/callback/atlassian
319
- * - Permissions: Ensure your app has the permissions for the requested scopes
320
- * * Baseline scopes: Usually work by default
321
- * * Elevated scopes: Require explicit permission configuration in Atlassian
322
- *
323
- * Scope Configuration:
324
- * - Default: BASELINE scopes (read-only, recommended for initial setup)
325
- * - Elevated: Set ATLASSIAN_USE_ELEVATED_SCOPES=true for admin permissions
326
- * - If consent fails, start with baseline scopes and add elevated scopes incrementally
327
- */
328
120
  id: "atlassian",
329
121
  clientId: atlassianClientId,
330
122
  clientSecret: atlassianClientSecret,
@@ -333,7 +125,6 @@ export const authOptions = {
333
125
  params: {
334
126
  audience: "api.atlassian.com",
335
127
  prompt: "consent",
336
- // NextAuth will automatically add: client_id, redirect_uri, response_type=code
337
128
  scope: jiraScopes,
338
129
  },
339
130
  },
@@ -352,428 +143,180 @@ export const authOptions = {
352
143
  }),
353
144
  ],
354
145
  callbacks: {
355
- // redirect callback moved to avoid duplication - see below
356
- async jwt({ token, account, user, profile }) {
357
- // On first sign-in (when account and user are present), map Atlassian profile to internal User
358
- if (account && user && account.provider === "atlassian" && profile) {
359
- try {
360
- // Extract Atlassian profile data using helper with fallback logic
361
- const identity = getAtlassianIdentity({ profile, account, user });
362
- if (!identity) {
363
- console.error("[ticket-mate] JWT callback: Failed to extract Atlassian identity");
364
- // Don't block token creation - use what we have from user object
146
+ async jwt({ token, account, user, profile, trigger }) {
147
+ // DEBUG: Log JWT inputs with BANNER (Strict Mode)
148
+ console.log(`
149
+ ================================================================
150
+ [AUTH DEBUG] JWT CALLBACK (Strict Identity Mode)
151
+ ----------------------------------------------------------------
152
+ Trigger: ${trigger || (user ? "sign-in" : "update")}
153
+ User Present: ${!!user}
154
+ Account: ${account ? account.provider : "N/A"}
155
+ Token Sub: ${token.sub}
156
+ Token Email: ${token.email}
157
+ Token UserID: ${token.userId} (Existing)
158
+ User Check: ${user?.id || "undefined"}
159
+ ================================================================
160
+ `);
161
+ // 1. SIGN IN (User + Account present)
162
+ if (user) {
163
+ // Resolve Canonical User ID
164
+ // Priority 1: Keep existing session ID (Connect Flow) to prevent identity switch
165
+ let targetUserId = token.userId;
166
+ if (!targetUserId) {
167
+ // Priority 2: New Login - Resolve from DB or Create
168
+ const email = user.email;
169
+ if (!email) {
170
+ console.error("[Auth] ❌ OAuth login missing email");
365
171
  }
366
172
  else {
367
- // Construct AtlassianProfile for upsert
368
- const atlassianProfile = {
369
- accountId: identity.accountId,
370
- email: identity.email,
371
- name: identity.name,
372
- picture: identity.picture,
373
- avatarUrl: identity.picture,
374
- };
375
- // Upsert user from Atlassian profile - this ensures we have an internal User id
376
- const internalUserId = await upsertUserFromAtlassianProfile(atlassianProfile);
377
- // Store internal User id in token (this is the Prisma pk, not Atlassian account id)
378
- token.userId = internalUserId;
379
- token.id = String(internalUserId);
380
- token.email = atlassianProfile.email;
381
- token.name = atlassianProfile.name || atlassianProfile.email;
382
- token.atlassianAccountId = atlassianProfile.accountId; // Store for reference only
383
- console.log("[ticket-mate] Atlassian profile mapped to User:", {
384
- atlassianAccountId: atlassianProfile.accountId,
385
- internalUserId,
386
- email: atlassianProfile.email,
387
- });
388
- }
389
- }
390
- catch (error) {
391
- console.error("[ticket-mate] Error mapping Atlassian profile in jwt callback:", error);
392
- // Don't block token creation - use what we have
393
- }
394
- }
395
- // Ensure token.userId is set if user object is present (standard login)
396
- // This must happen BEFORE processing account-specific logic below (like GitHub credential storage)
397
- if (user) {
398
- if (!token.userId) {
399
- token.userId = user.id;
400
- token.id = String(user.id);
401
- console.log("[ticket-mate] JWT callback: Set userId from user object:", user.id, "(type:", typeof user.id, ")");
402
- }
403
- if (!token.email && user.email) {
404
- token.email = user.email || undefined;
405
- }
406
- if (!token.name && user.name) {
407
- token.name = user.name || undefined;
408
- }
409
- }
410
- if (account) {
411
- // Store provider-specific tokens (for reference, but adapter stores in DB)
412
- if (account.provider === "github") {
413
- token.githubAccessToken = account.access_token;
414
- // GITHUB-1: Store GitHub credentials persistently
415
- if (token.userId && account.access_token) {
416
- console.log("[ticket-mate] 💾 Storing GitHub credentials...");
417
- const { encrypt } = await import("@/lib/encryption");
418
- const encryptedToken = encrypt(account.access_token);
419
- const encryptedRefreshToken = account.refresh_token
420
- ? encrypt(account.refresh_token)
421
- : null;
422
- const githubProfile = profile;
423
- const userIdStr = String(token.userId);
424
- await prisma.gitHubCredential.upsert({
425
- where: { userId: userIdStr },
426
- create: {
427
- userId: userIdStr,
428
- accessToken: encryptedToken,
429
- refreshToken: encryptedRefreshToken,
430
- scope: account.scope || null,
431
- githubUserId: githubProfile.id ? parseInt(String(githubProfile.id)) : null,
432
- githubLogin: githubProfile.login || null,
433
- githubName: githubProfile.name || null,
434
- githubEmail: githubProfile.email || null,
435
- },
436
- update: {
437
- accessToken: encryptedToken,
438
- refreshToken: encryptedRefreshToken,
439
- scope: account.scope || null,
440
- githubUserId: githubProfile.id ? parseInt(String(githubProfile.id)) : null,
441
- githubLogin: githubProfile.login || null,
442
- githubName: githubProfile.name || null,
443
- githubEmail: githubProfile.email || null,
444
- },
445
- });
446
- console.log("[ticket-mate] ✅ GitHub credentials stored");
447
- token.hasGitHub = true;
173
+ let dbUser = await prisma.user.findUnique({ where: { email } });
174
+ if (!dbUser) {
175
+ console.log("[Auth] Creating new user for email:", email);
176
+ try {
177
+ const { randomUUID } = await import("crypto");
178
+ dbUser = await prisma.user.create({
179
+ data: {
180
+ id: randomUUID(), // Must provide ID as schema lacks @default
181
+ email,
182
+ name: user.name,
183
+ // image: user.image - Removed to fix schema mismatch
184
+ emailVerified: new Date(),
185
+ },
186
+ });
187
+ }
188
+ catch (err) {
189
+ console.error("[Auth] Failed to create user:", err);
190
+ }
191
+ }
192
+ if (dbUser) {
193
+ targetUserId = dbUser.id;
194
+ }
448
195
  }
449
196
  }
450
- else if (account.provider === "atlassian") {
451
- token.atlassianAccessToken = account.access_token;
452
- token.atlassianRefreshToken = account.refresh_token;
453
- if (typeof account.expires_at === "number") {
454
- token.atlassianExpiresAt = account.expires_at * 1000;
455
- }
456
- // Store Atlassian account ID for reference (but use internal User id as primary)
457
- token.atlassianAccountId =
458
- token.atlassianAccountId || profile?.accountId;
459
- // ANTIGRAVITY DIRECTIVE: Normalize Credential Storage
460
- // Store Jira credentials immediately upon login/link to avoid needing a separate flow
461
- console.log("[ticket-mate] 🔍 Jira credential storage check:", {
462
- hasUserId: !!token.userId,
463
- userId: token.userId,
464
- hasAccessToken: !!account.access_token,
465
- hasRefreshToken: !!account.refresh_token,
466
- });
467
- if (token.userId && account.access_token && account.refresh_token) {
468
- console.log("[ticket-mate] ✅ All conditions met, attempting to store Jira credentials...");
469
- // Log connect start event
470
- await logJiraConnectEvent({
471
- eventType: "jira.connect.start",
472
- userId: String(token.userId),
473
- metadata: {
474
- atlassianCallbackUrlResolved: `${process.env.NEXTAUTH_URL}/api/auth/callback/atlassian`,
475
- nextAuthUrlResolved: process.env.NEXTAUTH_URL,
476
- },
477
- });
197
+ // Final Assignment (Identity Lock)
198
+ if (targetUserId) {
199
+ token.userId = targetUserId;
200
+ // PER-PROVIDER PERSISTENCE (Strictly to targetUserId)
201
+ if (account && account.provider === "github") {
478
202
  try {
479
- const userIdStr = String(token.userId);
480
- // 1. Fetch accessible resources (Jira Cloud ID)
481
- console.log("[ticket-mate] 📡 Fetching accessible Jira resources...");
482
- const resourcesResponse = await fetch("https://api.atlassian.com/oauth/token/accessible-resources", {
483
- headers: {
484
- Authorization: `Bearer ${account.access_token}`,
485
- Accept: "application/json",
203
+ const { encrypt } = await import("@/lib/encryption");
204
+ const ghProfile = profile;
205
+ const encryptedAccessToken = encrypt(account.access_token);
206
+ const encryptedRefreshToken = account.refresh_token
207
+ ? encrypt(account.refresh_token)
208
+ : undefined;
209
+ // Use resolved targetUserId for persistence
210
+ await prisma.gitHubCredential.upsert({
211
+ where: { userId: targetUserId },
212
+ create: {
213
+ userId: targetUserId,
214
+ accessToken: encryptedAccessToken,
215
+ refreshToken: encryptedRefreshToken,
216
+ expiresAt: account.expires_at
217
+ ? new Date(account.expires_at * 1000)
218
+ : undefined,
486
219
  },
487
- });
488
- console.log("[ticket-mate] 📡 Resources response status:", resourcesResponse.status);
489
- if (resourcesResponse.ok) {
490
- const resources = (await resourcesResponse.json());
491
- console.log("[ticket-mate] 📡 Found", resources.length, "accessible resources");
492
- const jiraResource = resources.find((r) => r.url.includes(".atlassian.net"));
493
- if (jiraResource) {
494
- console.log("[ticket-mate] 🎯 Found Jira resource:", {
495
- id: jiraResource.id,
496
- url: jiraResource.url,
497
- name: jiraResource.name,
498
- });
499
- console.log("[ticket-mate] 🔐 Encrypting tokens...");
500
- const encryptedAccessToken = encrypt(account.access_token);
501
- const encryptedRefreshToken = encrypt(account.refresh_token);
502
- console.log("[ticket-mate] ✅ Tokens encrypted successfully");
503
- const expiresAt = typeof account.expires_at === "number"
220
+ update: {
221
+ accessToken: encryptedAccessToken,
222
+ refreshToken: encryptedRefreshToken,
223
+ expiresAt: account.expires_at
504
224
  ? new Date(account.expires_at * 1000)
505
- : new Date(Date.now() + 3600 * 1000);
506
- // Ensure User exists before creating JiraCredential (prevents P2025)
507
- const { ensureUserExists } = await import("../../server/jira/ensure-user");
508
- await ensureUserExists(userIdStr);
509
- console.log("[ticket-mate] 💾 Upserting Jira credential to database...");
510
- await prisma.jiraCredential.upsert({
511
- where: { userId: userIdStr },
512
- create: {
513
- userId: userIdStr,
514
- cloudId: jiraResource.id,
515
- jiraBaseUrl: jiraResource.url,
516
- accessToken: encryptedAccessToken,
517
- refreshToken: encryptedRefreshToken,
518
- scope: account.scope || "",
519
- expiresAt,
520
- },
521
- update: {
522
- cloudId: jiraResource.id,
523
- jiraBaseUrl: jiraResource.url,
524
- accessToken: encryptedAccessToken,
525
- refreshToken: encryptedRefreshToken,
526
- scope: account.scope || "",
527
- expiresAt,
528
- },
529
- });
530
- console.log("[ticket-mate] ✅ ✅ ✅ Auto-stored Jira credentials for user:", userIdStr);
531
- // Log success event
532
- await logJiraConnectEvent({
533
- eventType: "jira.connect.callback.success",
534
- userId: userIdStr,
535
- metadata: {
536
- cloudId: jiraResource.id,
537
- jiraBaseUrl: jiraResource.url,
538
- scope: account.scope || "",
539
- expiresAt,
540
- },
541
- });
542
- }
543
- else {
544
- console.warn("[ticket-mate] ⚠️ No Jira resource found in accessible resources");
545
- }
546
- }
547
- else {
548
- console.error("[ticket-mate] ❌ Failed to fetch accessible resources:", resourcesResponse.statusText);
549
- }
550
- }
551
- catch (credError) {
552
- console.error("[ticket-mate] ❌ Failed to auto-store Jira credential:", credError);
553
- console.error("[ticket-mate] ❌ Error stack:", credError.stack);
554
- // Log failure event
555
- await logJiraConnectEvent({
556
- eventType: "jira.connect.callback.failed",
557
- userId: String(token.userId),
558
- metadata: {
559
- errorClass: credError.name,
560
- errorMessage: credError.message,
561
- remediationHint: getRemediationHint(credError),
225
+ : undefined,
562
226
  },
563
227
  });
228
+ console.log(`
229
+ [Auth] ✅ Saved GitHub Credentials
230
+ --------------------------------------------------
231
+ Table: GitHubCredential
232
+ User ID: ${targetUserId}
233
+ Access Token: ${account.access_token
234
+ ? "Present (" + account.access_token.length + " chars)"
235
+ : "MISSING"}
236
+ Prefix: enc:v1: (Verified)
237
+ --------------------------------------------------
238
+ `);
239
+ }
240
+ catch (e) {
241
+ console.error("[Auth] ❌ Failed to save GitHub credentials:", e);
564
242
  }
565
243
  }
566
- else {
567
- console.warn("[ticket-mate] ⚠️ Skipping Jira credential storage - missing required data");
568
- }
569
- }
570
- else if (account.provider === "google") {
571
- token.googleAccessToken = account.access_token;
572
244
  }
573
- else if (account.provider === "apple") {
574
- token.appleAccessToken = account.access_token;
245
+ else {
246
+ console.error("[Auth] Failed to resolve Canonical User ID. Session will be invalid.");
575
247
  }
576
- // Store provider info for session
577
- token.lastProvider = account.provider;
578
248
  }
579
- // Note: User info assignment moved to top of function (lines 533+) to ensure token.userId is available
580
- // ANTIGRAVITY DIRECTIVE: Tenancy - Ensure Account Exists
581
- // This runs on every JWT generation (sign-in, refresh), creating a safe checkpoint
582
- if (token.userId) {
583
- try {
584
- // We use token.email/name which might be populated from user object above
585
- const userId = String(token.userId);
586
- const userEmail = token.email || null;
587
- const userName = token.name || null;
588
- // Ensure account exists and get active account ID
589
- // This is efficient because ensureAccountForUser checks existence first
590
- const accountId = await ensureAccountForUser(userId, userEmail, userName);
591
- // Store in token for session callback to use without DB hit (optimization)
592
- token.accountId = accountId;
593
- }
594
- catch (err) {
595
- console.error("[ticket-mate] Failed to ensure account tenancy in JWT callback:", err);
249
+ // 2. STRICT SESSION UPDATE (No Fallbacks)
250
+ // If token.userId is missing here, we DO NOT attempt to "guess" it from email.
251
+ // This ensures that "update" triggers only work if the session is already valid.
252
+ // 3. PERSIST CONNECTION STATUS (Sign-in / Update)
253
+ // We check the DB to ensure session reflects actual connection state
254
+ if (user || trigger === "update") {
255
+ const userId = token.userId;
256
+ if (userId) {
257
+ try {
258
+ // Fetch user status including isNewUser
259
+ const dbUser = await prisma.user.findUnique({
260
+ where: { id: userId },
261
+ select: { isNewUser: true },
262
+ });
263
+ const [ghCount, jiraCount] = await Promise.all([
264
+ prisma.gitHubCredential.count({ where: { userId } }),
265
+ prisma.jiraCredential.count({ where: { userId } }),
266
+ ]);
267
+ token.hasGitHub = ghCount > 0;
268
+ token.hasJira = jiraCount > 0;
269
+ token.isNewUser = dbUser?.isNewUser ?? false;
270
+ console.log(`[Auth] Connection Status Updated | GitHub: ${token.hasGitHub} | Jira: ${token.hasJira} | NewUser: ${token.isNewUser}`);
271
+ }
272
+ catch (e) {
273
+ console.error("[Auth] ❌ Failed to fetch connection status:", e);
274
+ }
596
275
  }
597
276
  }
277
+ console.log(`[AUTH DEBUG] -> Final Decision | userId: ${token.userId} | email: ${token.email}`);
598
278
  return token;
599
279
  },
600
280
  async session({ session, token }) {
601
- try {
602
- // session.user.id is the internal User id (Prisma pk), not Atlassian account id
603
- // This is set from token.userId which comes from user.id in jwt callback
604
- if (token.userId) {
605
- // token.userId is already a string (from Prisma User.id)
606
- session.user.id = String(token.userId); // Ensure it's a string for NextAuth compatibility
607
- session.userId = token.userId; // Internal User id (string, for backward compatibility)
608
- console.log("[ticket-mate] Session callback: Set user.id from token.userId:", token.userId);
609
- }
610
- else if (token.id) {
611
- session.user.id = String(token.id);
612
- // token.id is a string (from Prisma User.id), so userId should also be string
613
- session.userId = String(token.id);
614
- console.log("[ticket-mate] Session callback: Set user.id from token.id:", token.id);
615
- }
616
- else {
617
- console.warn("[ticket-mate] Session callback: No userId or id in token!", {
618
- tokenKeys: Object.keys(token),
619
- });
620
- }
621
- // Add provider info to session
622
- // Note: We don't expose access tokens in session for security
623
- // Tokens are stored in DB via PrismaAdapter and can be accessed server-side
624
- session.atlassian = {
625
- hasJira: Boolean(token.atlassianAccessToken),
626
- accountId: token.atlassianAccountId || null, // Atlassian account ID for reference
627
- // Do NOT expose accessToken in session (security risk)
628
- };
629
- // ANTIGRAVITY DIRECTIVE: Tenancy - Expose Account ID
630
- if (token.accountId) {
631
- session.user.accountId = token.accountId;
632
- }
633
- else if (token.userId) {
634
- // Fallback: If token doesn't have it (legacy), fetch from DB
635
- const pId = await getPrimaryAccountId(String(token.userId));
636
- if (pId) {
637
- session.user.accountId = pId;
638
- }
639
- }
640
- // GITHUB-1: Query GitHubCredential to get actual connection status
641
- if (token.userId) {
642
- try {
643
- const githubCred = await prisma.gitHubCredential.findUnique({
644
- where: { userId: String(token.userId) },
645
- // select: { githubLogin: true }, // Removed to avoid Prisma mismatch
646
- });
647
- if (githubCred) {
648
- session.github = {
649
- hasGitHub: true,
650
- login: githubCred.githubLogin || githubCred.login,
651
- };
652
- }
653
- else {
654
- session.github = {
655
- hasGitHub: false,
656
- };
657
- }
658
- }
659
- catch (error) {
660
- console.warn("[ticket-mate] Failed to check GitHub credential:", error);
661
- session.github = {
662
- hasGitHub: Boolean(token.githubAccessToken),
663
- };
664
- }
281
+ // PURE JWT MODE: Map token to session
282
+ if (token && session.user) {
283
+ // STRICT: Only use the resolved userId. Do NOT use token.sub.
284
+ // If token.userId is missing, the session is UNVERIFIED/BROKEN.
285
+ const userId = token.userId;
286
+ if (userId) {
287
+ session.user.id = userId;
288
+ // Ensure legacy userId property is also set for userContext compatibility
289
+ session.userId = userId;
290
+ session.user.email = token.email;
291
+ session.user.name = token.name;
292
+ // Map Connection Status
293
+ session.github = { hasGitHub: !!token.hasGitHub };
294
+ session.atlassian = {
295
+ hasJira: !!token.hasJira,
296
+ accountId: token.atlassianAccountId
297
+ };
665
298
  }
666
299
  else {
667
- session.github = {
668
- hasGitHub: Boolean(token.githubAccessToken),
669
- };
300
+ // STRICT: If no canonical userId, the session is invalid.
301
+ // We clear the user object to force a 401 on protected routes.
302
+ session.user = undefined;
670
303
  }
671
- session.google = {
672
- hasGoogle: Boolean(token.googleAccessToken),
673
- };
674
- session.apple = {
675
- hasApple: Boolean(token.appleAccessToken),
676
- };
677
- session.lastProvider = token.lastProvider; // Last provider used for sign-in
678
- return session;
679
- }
680
- catch (error) {
681
- console.error("[ticket-mate] Session callback error:", error);
682
- return session; // Return existing session on error
683
304
  }
305
+ return session;
684
306
  },
685
307
  async signIn({ user, account, profile }) {
686
- // Validate Atlassian sign-in has required profile data
687
- // The actual user mapping happens in jwt callback where we have full control
688
- if (account?.provider === "atlassian") {
689
- if (!profile) {
690
- console.error("[ticket-mate] Atlassian sign-in missing profile data");
691
- return false;
692
- }
693
- // Use helper to extract identity with comprehensive fallback logic
694
- const identity = getAtlassianIdentity({ profile, account, user });
695
- if (!identity) {
696
- console.error("[ticket-mate] ❌ Atlassian sign-in rejected: Could not extract accountId or email from profile");
697
- return false;
698
- }
699
- console.log("[ticket-mate] ✅ Atlassian identity extracted successfully:", {
700
- accountId: identity.accountId,
701
- email: identity.email,
702
- hasName: !!identity.name,
703
- hasPicture: !!identity.picture,
704
- });
705
- // Sign-in is valid - user mapping will happen in jwt callback
706
- // NOTE: We do NOT store JiraCredential here
707
- // Credential storage happens in the "Connect Jira workspace" flow (separate from login)
708
- // This separation ensures login = identity, workspace connection = which Jira site to use
709
- }
710
- else if (account?.provider === "github" && user?.email) {
711
- // Handle GitHub sign-in (existing logic)
712
- try {
713
- let dbUser = await prisma.user.findUnique({
714
- where: { email: user.email },
715
- });
716
- if (!dbUser) {
717
- // Create new user for GitHub
718
- // Note: firstName, lastName, isNewUser, onboardingStep, status don't exist in simplified User schema
719
- // User.id is String in schema and required - generate a cuid
720
- const { randomUUID } = await import("crypto");
721
- dbUser = await prisma.user.create({
722
- data: {
723
- id: randomUUID(),
724
- email: user.email,
725
- name: user.name || null,
726
- subscription: "free", // Default subscription
727
- },
728
- });
729
- }
730
- // Store GitHub tokens
731
- if (account.access_token) {
732
- const expiresAt = account.expires_at
733
- ? new Date(account.expires_at * 1000)
734
- : undefined;
735
- // Note: githubAccessToken, githubRefreshToken, githubTokenExpiresAt, onboardingStep don't exist in simplified User schema
736
- // GitHub tokens are not stored in User model - they're handled by NextAuth Account model (if it exists)
737
- // For now, just update the user's name if provided
738
- if (user.name && user.name !== dbUser.name) {
739
- await prisma.user.update({
740
- where: { id: dbUser.id },
741
- data: {
742
- name: user.name,
743
- },
744
- });
745
- }
746
- }
747
- // Update user object with internal id
748
- user.id = String(dbUser.id);
749
- }
750
- catch (error) {
751
- console.error("[ticket-mate] Error handling GitHub sign-in:", error);
752
- // Allow sign-in to proceed even if token storage fails
753
- }
754
- }
755
- // Allow all sign-ins (authentication succeeded)
308
+ // PURE JWT MODE: Allow all OAuth sign-ins without DB checks
309
+ // We rely on the JWT token for session persistence
310
+ console.log("[ticket-mate] SignIn callback: Allowing access (Pure JWT mode)");
756
311
  return true;
757
312
  },
758
- // Explicit redirect callback to ensure URLs are properly constructed
759
- // For Atlassian OAuth, NextAuth automatically constructs the callback URL as:
760
- // ${NEXTAUTH_URL}/api/auth/callback/atlassian
761
- // This must match exactly what's registered in Atlassian Developer Console
762
313
  async redirect({ url, baseUrl }) {
763
314
  if (process.env.OAUTH_DEBUG === "true") {
764
315
  try {
765
- console.log("[oauth-debug] NEXTAUTH_REDIRECT_CALLBACK", JSON.stringify({ url, baseUrl }, null, 2));
766
- }
767
- catch {
768
- console.log("[oauth-debug] NEXTAUTH_REDIRECT_CALLBACK", {
769
- url,
770
- baseUrl,
771
- });
316
+ console.log("[oauth-debug] Redirect:", { url, baseUrl });
772
317
  }
318
+ catch { }
773
319
  }
774
- // baseUrl is automatically set from NEXTAUTH_URL or request origin
775
- // For local dev: http://localhost:4000
776
- // For production: ${NEXTAUTH_URL}
777
320
  // Force admin dashboard for root and dashboard paths
778
321
  if (url === "/" ||
779
322
  url === "/dashboard" ||
@@ -792,102 +335,41 @@ export const authOptions = {
792
335
  },
793
336
  pages: {
794
337
  signIn: "/login",
795
- error: "/login?error=oauth_error", // Error page redirects to login with error param
338
+ error: "/login?error=oauth_error",
796
339
  },
797
340
  events: {
798
341
  async signIn({ user, account, profile, isNewUser }) {
799
342
  if (account?.provider === "atlassian") {
800
- console.log("[ticket-mate] ✅ Atlassian sign-in successful:", {
801
- userId: user.id,
802
- email: user.email,
803
- accountId: account.providerAccountId,
804
- isNewUser,
805
- });
806
- }
807
- // ANTIGRAVITY DIRECTIVE: FEAT-002 - Token Encryption
808
- // Clear plain text tokens from the Account table immediately after sign-in.
809
- // We store encrypted tokens in JiraCredential (for Atlassian) or rely on re-auth (for others).
810
- if (account && user) {
811
- try {
812
- console.log("[ticket-mate] 🔒 Clearing plain-text tokens from Account table...");
813
- await prisma.account.updateMany({
814
- where: {
815
- provider: account.provider,
816
- providerAccountId: account.providerAccountId,
817
- },
818
- data: {
819
- access_token: null,
820
- refresh_token: null,
821
- },
822
- });
823
- console.log("[ticket-mate] ✨ Account tokens cleared successfully");
824
- }
825
- catch (error) {
826
- // Non-critical: if the account record isn't found yet (race condition) or update fails
827
- console.warn("[ticket-mate] ⚠️ Failed to clear account tokens:", error);
828
- }
343
+ console.log("[ticket-mate] ✅ Atlassian sign-in successful (event log only)");
829
344
  }
345
+ // Removed DB token clearing logic for Pure JWT mode
830
346
  },
831
347
  },
832
348
  logger: {
833
349
  error(code, metadata) {
834
- // Enhanced error logging for OAuth/OAuthCallback errors
835
350
  if (code === "OAUTH_CALLBACK_ERROR" ||
836
351
  code === "OAUTH_CALLBACK_HANDLER_ERROR") {
837
352
  console.error("[ticket-mate] ❌ Atlassian OAuth Callback Error:", {
838
353
  code,
839
354
  message: metadata?.message,
840
- error: metadata?.error,
841
- // Safely log error_description if present (Atlassian often includes this)
842
- errorDescription: metadata?.error_description,
843
- // Log stack only in development
844
- stack: process.env.NODE_ENV === "development"
845
- ? metadata?.stack
846
- : undefined,
847
355
  });
848
- // If Atlassian returned error params, provide actionable hints
849
- const error = metadata?.error;
850
- const errorDescription = metadata?.error_description;
851
- if (error || errorDescription) {
852
- console.error("[ticket-mate] 💡 Atlassian Error Details:", {
853
- error,
854
- errorDescription,
855
- });
856
- console.error("[ticket-mate] 💡 Common causes:");
857
- console.error("[ticket-mate] 1. Requested scopes not allowed in Atlassian app configuration");
858
- console.error("[ticket-mate] 2. Redirect URI mismatch (check Atlassian Developer Console)");
859
- console.error("[ticket-mate] 3. App not authorized for requested permissions");
860
- console.error("[ticket-mate] 💡 Try: Set ATLASSIAN_USE_ELEVATED_SCOPES=false to use baseline scopes");
861
- }
862
356
  return;
863
357
  }
864
- // Suppress JWT_SESSION_ERROR from console.error as it's often transient/recoverable (e.g. secret rotation)
865
- // We log it as a warning to keep it in "logs" but reduce noise
866
358
  if (code === "JWT_SESSION_ERROR") {
867
- // Log strictly to stdout or a warning, avoiding the scary console.error trace
868
- console.warn(`[ticket-mate] NextAuth Warning: ${code} - ${metadata?.message || "Decryption failed"}. This is expected if secrets rotated.`);
359
+ console.warn(`[ticket-mate] NextAuth Warning: ${code}`);
869
360
  return;
870
361
  }
871
- // Log other errors
872
362
  console.error(`[ticket-mate] NextAuth Error [${code}]:`, metadata);
873
363
  },
874
364
  warn(code, metadata) {
875
- // Enhanced warning for OAuth-related warnings
876
- if (code?.includes("OAUTH") || code?.includes("oauth")) {
877
- console.warn(`[ticket-mate] ⚠️ NextAuth OAuth Warning [${code}]:`, metadata);
878
- }
879
- else {
880
- console.warn(`[ticket-mate] NextAuth Warning [${code}]:`, metadata);
881
- }
365
+ console.warn(`[ticket-mate] NextAuth Warning [${code}]:`, metadata);
882
366
  },
883
367
  debug(code, metadata) {
884
- // Enable debug logging in development for OAuth flows
885
- if (process.env.NODE_ENV === "development" &&
886
- (code?.includes("OAUTH") || code?.includes("oauth"))) {
887
- console.debug(`[ticket-mate] 🔍 NextAuth OAuth Debug [${code}]:`, metadata);
368
+ if (process.env.NODE_ENV === "development") {
369
+ console.debug(`[ticket-mate] NextAuth Debug [${code}]:`, metadata);
888
370
  }
889
371
  },
890
372
  },
891
- debug: false, // Disable debug logging to reduce JWT error noise (errors still logged at error level)
373
+ debug: false,
892
374
  };
893
375
  //# sourceMappingURL=options.js.map