@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.
- package/package.json +2 -1
- package/public/acceptance-criteria/file-discovery.js +1 -1
- package/public/acceptance-criteria/file-discovery.js.map +1 -1
- package/public/cli/commands/env-debug.js +1 -1
- package/public/cli/commands/env-debug.js.map +1 -1
- package/public/cli/commands/init-repo.js +1 -1
- package/public/cli/commands/init-repo.js.map +1 -1
- package/public/cli/commands/status.js +1 -1
- package/public/cli/commands/status.js.map +1 -1
- package/public/cli/commands/superadmin.d.ts +9 -0
- package/public/cli/commands/superadmin.d.ts.map +1 -0
- package/public/cli/commands/superadmin.js +17 -0
- package/public/cli/commands/superadmin.js.map +1 -0
- package/public/cli/commands/test-workflow.js +1 -1
- package/public/cli/commands/test-workflow.js.map +1 -1
- package/public/cli/commands/webhook-setup.d.ts +8 -0
- package/public/cli/commands/webhook-setup.d.ts.map +1 -0
- package/public/cli/commands/webhook-setup.js +92 -0
- package/public/cli/commands/webhook-setup.js.map +1 -0
- package/public/cli/commands/webhooks-debug.js +1 -1
- package/public/cli/commands/webhooks-debug.js.map +1 -1
- package/public/cli/index.js +5 -0
- package/public/cli/index.js.map +1 -1
- package/public/cli/utils/get-cli-client.d.ts.map +1 -1
- package/public/cli/utils/get-cli-client.js +2 -1
- package/public/cli/utils/get-cli-client.js.map +1 -1
- package/public/config/config-loader.js +2 -2
- package/public/config/config-loader.js.map +1 -1
- package/public/config/config-wizard.js +1 -1
- package/public/config/config-wizard.js.map +1 -1
- package/public/config/config.js +1 -1
- package/public/config/config.js.map +1 -1
- package/public/config/jiraConnection.d.ts +3 -3
- package/public/config/jiraConnection.d.ts.map +1 -1
- package/public/config/jiraConnection.js +47 -41
- package/public/config/jiraConnection.js.map +1 -1
- package/public/config/pricing.d.ts +2 -2
- package/public/config/pricing.d.ts.map +1 -1
- package/public/config/pricing.js +164 -50
- package/public/config/pricing.js.map +1 -1
- package/public/config/project-mapping.js +1 -1
- package/public/config/project-mapping.js.map +1 -1
- package/public/fast-tasks/storage.d.ts +3 -2
- package/public/fast-tasks/storage.d.ts.map +1 -1
- package/public/fast-tasks/storage.js +24 -17
- package/public/fast-tasks/storage.js.map +1 -1
- package/public/jira-management/projects.d.ts.map +1 -1
- package/public/jira-management/projects.js +15 -13
- package/public/jira-management/projects.js.map +1 -1
- package/public/lib/auth/options.d.ts +0 -11
- package/public/lib/auth/options.d.ts.map +1 -1
- package/public/lib/auth/options.js +180 -698
- package/public/lib/auth/options.js.map +1 -1
- package/public/lib/dashboard-widgets.d.ts +2 -2
- package/public/lib/dashboard-widgets.d.ts.map +1 -1
- package/public/lib/dashboard-widgets.js +33 -33
- package/public/lib/dashboard-widgets.js.map +1 -1
- package/public/lib/docs.d.ts.map +1 -1
- package/public/lib/docs.js +5 -2
- package/public/lib/docs.js.map +1 -1
- package/public/lib/encryption.d.ts +2 -1
- package/public/lib/encryption.d.ts.map +1 -1
- package/public/lib/encryption.js +27 -20
- package/public/lib/encryption.js.map +1 -1
- package/public/lib/integrations/teams/auth.d.ts +1 -1
- package/public/lib/integrations/teams/auth.d.ts.map +1 -1
- package/public/lib/integrations/teams/auth.js +15 -15
- package/public/lib/integrations/teams/auth.js.map +1 -1
- package/public/lib/integrations/teams/transformer.d.ts.map +1 -1
- package/public/lib/integrations/teams/transformer.js +3 -1
- package/public/lib/integrations/teams/transformer.js.map +1 -1
- package/public/lib/invite/accept.d.ts +1 -0
- package/public/lib/invite/accept.d.ts.map +1 -1
- package/public/lib/invite/accept.js +22 -33
- package/public/lib/invite/accept.js.map +1 -1
- package/public/lib/invite/service.d.ts.map +1 -1
- package/public/lib/invite/service.js +7 -0
- package/public/lib/invite/service.js.map +1 -1
- package/public/lib/invite/types.d.ts +1 -0
- package/public/lib/invite/types.d.ts.map +1 -1
- package/public/lib/markdown/fallback-handler.js +2 -2
- package/public/lib/markdown/fallback-handler.js.map +1 -1
- package/public/lib/notifications.d.ts +1 -1
- package/public/lib/notifications.d.ts.map +1 -1
- package/public/lib/notifications.js +24 -7
- package/public/lib/notifications.js.map +1 -1
- package/public/lib/prisma.d.ts +5 -22
- package/public/lib/prisma.d.ts.map +1 -1
- package/public/lib/prisma.js +14 -132
- package/public/lib/prisma.js.map +1 -1
- package/public/lib/projects.d.ts +2 -2
- package/public/lib/webhook-events-store.d.ts +5 -6
- package/public/lib/webhook-events-store.d.ts.map +1 -1
- package/public/lib/webhook-events-store.js +63 -18
- package/public/lib/webhook-events-store.js.map +1 -1
- package/public/server/account/account.service.d.ts +217 -0
- package/public/server/account/account.service.d.ts.map +1 -0
- package/public/server/account/account.service.js +307 -0
- package/public/server/account/account.service.js.map +1 -0
- package/public/server/jira/jiraClient.d.ts +2 -1
- package/public/server/jira/jiraClient.d.ts.map +1 -1
- package/public/server/jira/jiraClient.js +4 -3
- package/public/server/jira/jiraClient.js.map +1 -1
- package/public/server/jira/tokens.d.ts.map +1 -1
- package/public/server/jira/tokens.js +0 -9
- package/public/server/jira/tokens.js.map +1 -1
- package/public/types/github.d.ts +18 -0
- package/public/types/github.d.ts.map +1 -0
- package/public/types/github.js +2 -0
- package/public/types/github.js.map +1 -0
- package/public/utils/adf.d.ts.map +1 -1
- package/public/utils/adf.js +2 -1
- package/public/utils/adf.js.map +1 -1
- package/public/utils/parseJiraError.d.ts +39 -0
- package/public/utils/parseJiraError.d.ts.map +1 -0
- package/public/utils/parseJiraError.js +70 -0
- package/public/utils/parseJiraError.js.map +1 -0
- package/public/utils/validation.d.ts.map +1 -1
- package/public/utils/validation.js +20 -20
- package/public/utils/validation.js.map +1 -1
- package/scripts/cli-wrapper.mjs +1 -0
- package/public/server/auth/account-provisioning.d.ts +0 -16
- package/public/server/auth/account-provisioning.d.ts.map +0 -1
- package/public/server/auth/account-provisioning.js +0 -109
- package/public/server/auth/account-provisioning.js.map +0 -1
- package/public/server/auth/atlassian-user-mapping.d.ts +0 -36
- package/public/server/auth/atlassian-user-mapping.d.ts.map +0 -1
- package/public/server/auth/atlassian-user-mapping.js +0 -97
- 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
|
|
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
|
-
|
|
76
|
-
|
|
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",
|
|
103
|
-
"read:me",
|
|
104
|
-
"read:account",
|
|
105
|
-
"read:jira-user",
|
|
106
|
-
"read:jira-work",
|
|
107
|
-
"write:jira-work",
|
|
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",
|
|
114
|
-
"manage:jira-project",
|
|
115
|
-
"manage:jira-configuration",
|
|
116
|
-
"manage:jira-webhook",
|
|
117
|
-
"manage:jira-data-provider",
|
|
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
|
|
198
|
-
secret: process.env.NEXTAUTH_SECRET,
|
|
75
|
+
// Pure JWT authentication - no database adapter
|
|
76
|
+
secret: process.env.NEXTAUTH_SECRET,
|
|
199
77
|
session: {
|
|
200
|
-
strategy: "jwt",
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
|
263
|
-
email:
|
|
264
|
-
name:
|
|
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
|
-
|
|
268
|
-
|
|
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,
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
token.
|
|
453
|
-
|
|
454
|
-
|
|
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
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
const
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
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
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
-
:
|
|
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
|
|
574
|
-
|
|
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
|
-
//
|
|
580
|
-
//
|
|
581
|
-
// This
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
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
|
-
|
|
602
|
-
|
|
603
|
-
//
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
session.
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
session.user.
|
|
612
|
-
//
|
|
613
|
-
session.
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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
|
|
668
|
-
|
|
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
|
-
//
|
|
687
|
-
//
|
|
688
|
-
|
|
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]
|
|
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",
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
885
|
-
|
|
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,
|
|
373
|
+
debug: false,
|
|
892
374
|
};
|
|
893
375
|
//# sourceMappingURL=options.js.map
|