@maka/maka-cli 5.1.45 → 5.1.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +3 -1
- package/bundle/typescript/package.json +1 -1
- package/bundle/typescript/src/commands/ai/mcp.sub.cmd.js +118 -5
- package/bundle/typescript/src/commands/ai/mcp.sub.cmd.js.map +1 -1
- package/bundle/typescript/src/commands/create.js +2 -2
- package/bundle/typescript/src/commands/create.js.map +1 -1
- package/bundle/typescript/src/commands/run/run.command.js +0 -5
- package/bundle/typescript/src/commands/run/run.command.js.map +1 -1
- package/bundle/typescript/src/generators/kits/auth/_index.d.ts +1 -0
- package/bundle/typescript/src/generators/kits/auth/_index.js +1 -0
- package/bundle/typescript/src/generators/kits/auth/_index.js.map +1 -1
- package/bundle/typescript/src/generators/kits/auth/auth0-meteor.auth.gen.d.ts +1 -0
- package/bundle/typescript/src/generators/kits/auth/auth0-meteor.auth.gen.js +745 -0
- package/bundle/typescript/src/generators/kits/auth/auth0-meteor.auth.gen.js.map +1 -0
- package/bundle/typescript/src/generators/kits/auth/basic-meteor.auth.gen.js +2 -2
- package/bundle/typescript/src/generators/kits/auth/basic-meteor.auth.gen.js.map +1 -1
- package/bundle/typescript/src/generators/kits/mui/_index.d.ts +1 -0
- package/bundle/typescript/src/generators/kits/mui/_index.js +1 -0
- package/bundle/typescript/src/generators/kits/mui/_index.js.map +1 -1
- package/bundle/typescript/src/generators/kits/mui/auth0.mui.gen.d.ts +2 -0
- package/bundle/typescript/src/generators/kits/mui/auth0.mui.gen.js +158 -0
- package/bundle/typescript/src/generators/kits/mui/auth0.mui.gen.js.map +1 -0
- package/bundle/typescript/src/generators/publish.js +2 -6
- package/bundle/typescript/src/generators/publish.js.map +1 -1
- package/bundle/typescript/src/templates/react/kit/auth/service/auth0-client.js.jsx +163 -0
- package/bundle/typescript/src/templates/react/kit/auth/service/auth0-client.js.tsx +163 -0
- package/bundle/typescript/src/templates/react/kit/auth/service/auth0-config.js.jsx +59 -0
- package/bundle/typescript/src/templates/react/kit/auth/service/auth0-config.js.tsx +67 -0
- package/bundle/typescript/src/templates/react/kit/auth/service/auth0-methods.js.jsx +271 -0
- package/bundle/typescript/src/templates/react/kit/auth/service/auth0-methods.js.tsx +271 -0
- package/bundle/typescript/src/templates/react/kit/auth/service/auth0-validator.js.jsx +179 -0
- package/bundle/typescript/src/templates/react/kit/auth/service/auth0-validator.js.tsx +194 -0
- package/bundle/typescript/src/templates/react/kit/mui/auth0/auth-callback.js.jsx +71 -0
- package/bundle/typescript/src/templates/react/kit/mui/auth0/auth-callback.js.tsx +71 -0
- package/bundle/typescript/src/templates/react/kit/mui/auth0/auth0-account-settings.js.jsx +123 -0
- package/bundle/typescript/src/templates/react/kit/mui/auth0/auth0-account-settings.js.tsx +123 -0
- package/bundle/typescript/src/templates/react/kit/mui/auth0/auth0-login-button.js.jsx +56 -0
- package/bundle/typescript/src/templates/react/kit/mui/auth0/auth0-login-button.js.tsx +74 -0
- package/bundle/typescript/src/tools/scaffold/scaffold.class.d.ts +2 -1
- package/bundle/typescript/src/tools/scaffold/scaffold.class.js +12 -6
- package/bundle/typescript/src/tools/scaffold/scaffold.class.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth0 Configuration
|
|
3
|
+
*
|
|
4
|
+
* This file contains the Auth0 configuration settings.
|
|
5
|
+
* Set your Auth0 credentials in Meteor settings or environment variables.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Normalize Auth0 domain by removing protocol and trailing slashes
|
|
10
|
+
*/
|
|
11
|
+
const normalizeDomain = (domain) => {
|
|
12
|
+
return domain
|
|
13
|
+
.replace(/^https?:\/\//, '') // Remove http:// or https://
|
|
14
|
+
.replace(/\/$/, ''); // Remove trailing slash
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get Auth0 configuration from Meteor settings or environment variables
|
|
19
|
+
*/
|
|
20
|
+
export const getAuth0Config = () => {
|
|
21
|
+
if (Meteor.isClient) {
|
|
22
|
+
const settings = Meteor.settings.public?.auth0;
|
|
23
|
+
|
|
24
|
+
if (!settings?.domain || !settings?.clientId) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
'Auth0 configuration missing. Please add auth0 settings to your Meteor settings.json file.'
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const normalizedDomain = normalizeDomain(settings.domain);
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
domain: normalizedDomain,
|
|
34
|
+
clientId: settings.clientId,
|
|
35
|
+
audience: settings.audience || `https://${normalizedDomain}/api/v2/`,
|
|
36
|
+
redirectUri: settings.redirectUri || window.location.origin + '/auth/callback',
|
|
37
|
+
scope: settings.scope || 'openid profile email',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Server-side configuration
|
|
42
|
+
const settings = Meteor.settings?.auth0;
|
|
43
|
+
|
|
44
|
+
if (!settings?.domain || !settings?.clientId) {
|
|
45
|
+
throw new Error(
|
|
46
|
+
'Auth0 configuration missing. Please add auth0 settings to your Meteor settings.json file.'
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const normalizedDomain = normalizeDomain(settings.domain);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
domain: normalizedDomain,
|
|
54
|
+
clientId: settings.clientId,
|
|
55
|
+
audience: settings.audience || `https://${normalizedDomain}/api/v2/`,
|
|
56
|
+
redirectUri: settings.redirectUri || Meteor.absoluteUrl('auth/callback'),
|
|
57
|
+
scope: settings.scope || 'openid profile email',
|
|
58
|
+
};
|
|
59
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth0 Configuration
|
|
3
|
+
*
|
|
4
|
+
* This file contains the Auth0 configuration settings.
|
|
5
|
+
* Set your Auth0 credentials in Meteor settings or environment variables.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface Auth0Config {
|
|
9
|
+
domain: string;
|
|
10
|
+
clientId: string;
|
|
11
|
+
audience?: string;
|
|
12
|
+
redirectUri: string;
|
|
13
|
+
scope: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Normalize Auth0 domain by removing protocol and trailing slashes
|
|
18
|
+
*/
|
|
19
|
+
const normalizeDomain = (domain: string): string => {
|
|
20
|
+
return domain
|
|
21
|
+
.replace(/^https?:\/\//, '') // Remove http:// or https://
|
|
22
|
+
.replace(/\/$/, ''); // Remove trailing slash
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Get Auth0 configuration from Meteor settings or environment variables
|
|
27
|
+
*/
|
|
28
|
+
export const getAuth0Config = (): Auth0Config => {
|
|
29
|
+
if (Meteor.isClient) {
|
|
30
|
+
const settings = Meteor.settings.public?.auth0;
|
|
31
|
+
|
|
32
|
+
if (!settings?.domain || !settings?.clientId) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
'Auth0 configuration missing. Please add auth0 settings to your Meteor settings.json file.'
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const normalizedDomain = normalizeDomain(settings.domain);
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
domain: normalizedDomain,
|
|
42
|
+
clientId: settings.clientId,
|
|
43
|
+
audience: settings.audience || `https://${normalizedDomain}/api/v2/`,
|
|
44
|
+
redirectUri: settings.redirectUri || window.location.origin + '/auth/callback',
|
|
45
|
+
scope: settings.scope || 'openid profile email',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Server-side configuration
|
|
50
|
+
const settings = Meteor.settings?.auth0;
|
|
51
|
+
|
|
52
|
+
if (!settings?.domain || !settings?.clientId) {
|
|
53
|
+
throw new Error(
|
|
54
|
+
'Auth0 configuration missing. Please add auth0 settings to your Meteor settings.json file.'
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const normalizedDomain = normalizeDomain(settings.domain);
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
domain: normalizedDomain,
|
|
62
|
+
clientId: settings.clientId,
|
|
63
|
+
audience: settings.audience || `https://${normalizedDomain}/api/v2/`,
|
|
64
|
+
redirectUri: settings.redirectUri || Meteor.absoluteUrl('auth/callback'),
|
|
65
|
+
scope: settings.scope || 'openid profile email',
|
|
66
|
+
};
|
|
67
|
+
};
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { Meteor } from 'meteor/meteor';
|
|
2
|
+
import { Accounts } from 'meteor/accounts-base';
|
|
3
|
+
import { verifyAuth0Token, findOrCreateUserFromAuth0 } from '../auth0-validator';
|
|
4
|
+
|
|
5
|
+
// Hook into Meteor logout to clean up Auth0 session
|
|
6
|
+
Accounts.onLogout((options) => {
|
|
7
|
+
const user = options.user;
|
|
8
|
+
|
|
9
|
+
// If user has Auth0 account linked, we log the logout
|
|
10
|
+
// Note: Client-side should handle Auth0 logout via auth0Client.logout()
|
|
11
|
+
if (user?.services?.auth0) {
|
|
12
|
+
console.log(`User ${user._id} logged out (Auth0 ID: ${user.services.auth0.id})`);
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Meteor Methods for Auth0 Authentication
|
|
18
|
+
*
|
|
19
|
+
* These methods handle the server-side Auth0 authentication flow,
|
|
20
|
+
* including token verification and user account creation/linking.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check if the user's Auth0 token has expired
|
|
25
|
+
*/
|
|
26
|
+
const isTokenExpired = (user) => {
|
|
27
|
+
const auth0Data = user?.services?.auth0;
|
|
28
|
+
if (!auth0Data?.expiresAt) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
return new Date() >= new Date(auth0Data.expiresAt);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
Meteor.methods({
|
|
35
|
+
/**
|
|
36
|
+
* Authenticate user with Auth0 ID token
|
|
37
|
+
*
|
|
38
|
+
* @param idToken - The Auth0 ID token received from the client
|
|
39
|
+
* @returns Object containing userId and login token
|
|
40
|
+
*/
|
|
41
|
+
async 'auth0.login'(idToken) {
|
|
42
|
+
// Validate input
|
|
43
|
+
if (!idToken || typeof idToken !== 'string') {
|
|
44
|
+
throw new Meteor.Error('invalid-token', 'Invalid token provided');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// Verify the Auth0 token
|
|
49
|
+
const tokenPayload = await verifyAuth0Token(idToken);
|
|
50
|
+
|
|
51
|
+
// Find or create user from Auth0 data
|
|
52
|
+
const userId = await findOrCreateUserFromAuth0(tokenPayload);
|
|
53
|
+
|
|
54
|
+
// Generate Meteor login token
|
|
55
|
+
const stampedLoginToken = Accounts._generateStampedLoginToken();
|
|
56
|
+
const loginToken = stampedLoginToken.token;
|
|
57
|
+
|
|
58
|
+
// Store login token for the user
|
|
59
|
+
Accounts._insertLoginToken(userId, stampedLoginToken);
|
|
60
|
+
|
|
61
|
+
// Return userId and token for client-side login
|
|
62
|
+
return {
|
|
63
|
+
userId,
|
|
64
|
+
token: loginToken,
|
|
65
|
+
tokenExpires: stampedLoginToken.when,
|
|
66
|
+
};
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error('Auth0 login error:', error);
|
|
69
|
+
|
|
70
|
+
if (error instanceof Meteor.Error) {
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
throw new Meteor.Error(
|
|
75
|
+
'auth0-login-failed',
|
|
76
|
+
'Failed to authenticate with Auth0',
|
|
77
|
+
error.message
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Link Auth0 account to existing Meteor user
|
|
84
|
+
*
|
|
85
|
+
* @param idToken - The Auth0 ID token
|
|
86
|
+
*/
|
|
87
|
+
async 'auth0.linkAccount'(idToken) {
|
|
88
|
+
// Must be logged in to link account
|
|
89
|
+
if (!this.userId) {
|
|
90
|
+
throw new Meteor.Error('not-authorized', 'Must be logged in to link Auth0 account');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!idToken || typeof idToken !== 'string') {
|
|
94
|
+
throw new Meteor.Error('invalid-token', 'Invalid token provided');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
// Verify the Auth0 token
|
|
99
|
+
const tokenPayload = await verifyAuth0Token(idToken);
|
|
100
|
+
const auth0Id = tokenPayload.sub;
|
|
101
|
+
|
|
102
|
+
// Check if Auth0 account is already linked to another user
|
|
103
|
+
const existingUser = await Meteor.users.findOneAsync({
|
|
104
|
+
'services.auth0.id': auth0Id,
|
|
105
|
+
_id: { $ne: this.userId },
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (existingUser) {
|
|
109
|
+
throw new Meteor.Error(
|
|
110
|
+
'auth0-already-linked',
|
|
111
|
+
'This Auth0 account is already linked to another user'
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Link Auth0 account to current user
|
|
116
|
+
await Meteor.users.updateAsync(this.userId, {
|
|
117
|
+
$set: {
|
|
118
|
+
// Profile data (client-accessible)
|
|
119
|
+
'profile.auth0': {
|
|
120
|
+
name: tokenPayload.name,
|
|
121
|
+
picture: tokenPayload.picture,
|
|
122
|
+
email: tokenPayload.email,
|
|
123
|
+
email_verified: tokenPayload.email_verified,
|
|
124
|
+
},
|
|
125
|
+
// Token data (server-only)
|
|
126
|
+
'services.auth0': {
|
|
127
|
+
id: auth0Id,
|
|
128
|
+
accessToken: tokenPayload.aud,
|
|
129
|
+
expiresAt: new Date(tokenPayload.exp * 1000),
|
|
130
|
+
issuedAt: new Date(tokenPayload.iat * 1000),
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return { success: true };
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error('Auth0 link account error:', error);
|
|
138
|
+
|
|
139
|
+
if (error instanceof Meteor.Error) {
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
throw new Meteor.Error(
|
|
144
|
+
'auth0-link-failed',
|
|
145
|
+
'Failed to link Auth0 account',
|
|
146
|
+
error.message
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Unlink Auth0 account from current user
|
|
153
|
+
*/
|
|
154
|
+
async 'auth0.unlinkAccount'() {
|
|
155
|
+
if (!this.userId) {
|
|
156
|
+
throw new Meteor.Error('not-authorized', 'Must be logged in to unlink Auth0 account');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const user = await Meteor.users.findOneAsync(this.userId);
|
|
160
|
+
|
|
161
|
+
if (!user?.services?.auth0) {
|
|
162
|
+
throw new Meteor.Error('no-auth0-account', 'No Auth0 account linked');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Make sure user has another way to login (password or other service)
|
|
166
|
+
if (!user.services.password && Object.keys(user.services).length === 1) {
|
|
167
|
+
throw new Meteor.Error(
|
|
168
|
+
'last-login-method',
|
|
169
|
+
'Cannot unlink Auth0 - it is your only login method. Add a password first.'
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
await Meteor.users.updateAsync(this.userId, {
|
|
174
|
+
$unset: {
|
|
175
|
+
'services.auth0': '',
|
|
176
|
+
'profile.auth0': '',
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
return { success: true };
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Check if the current user's Auth0 token is valid
|
|
185
|
+
* Returns token expiry status
|
|
186
|
+
*/
|
|
187
|
+
async 'auth0.checkTokenValidity'() {
|
|
188
|
+
if (!this.userId) {
|
|
189
|
+
throw new Meteor.Error('not-authorized', 'Must be logged in');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const user = await Meteor.users.findOneAsync(this.userId);
|
|
193
|
+
|
|
194
|
+
if (!user?.services?.auth0) {
|
|
195
|
+
throw new Meteor.Error('no-auth0-account', 'No Auth0 account linked');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const expired = isTokenExpired(user);
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
isValid: !expired,
|
|
202
|
+
expiresAt: user.services.auth0.expiresAt,
|
|
203
|
+
issuedAt: user.services.auth0.issuedAt,
|
|
204
|
+
};
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Refresh Auth0 token and update Meteor user session
|
|
209
|
+
* This method validates the new token and updates the user's token data
|
|
210
|
+
*/
|
|
211
|
+
async 'auth0.refreshToken'(idToken) {
|
|
212
|
+
if (!this.userId) {
|
|
213
|
+
throw new Meteor.Error('not-authorized', 'Must be logged in');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!idToken || typeof idToken !== 'string') {
|
|
217
|
+
throw new Meteor.Error('invalid-token', 'Invalid token provided');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
// Verify the new Auth0 token
|
|
222
|
+
const tokenPayload = await verifyAuth0Token(idToken);
|
|
223
|
+
|
|
224
|
+
// Get current user
|
|
225
|
+
const user = await Meteor.users.findOneAsync(this.userId);
|
|
226
|
+
|
|
227
|
+
if (!user?.services?.auth0) {
|
|
228
|
+
throw new Meteor.Error('no-auth0-account', 'No Auth0 account linked');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Verify the token is for the same Auth0 user
|
|
232
|
+
if (user.services.auth0.id !== tokenPayload.sub) {
|
|
233
|
+
throw new Meteor.Error(
|
|
234
|
+
'token-mismatch',
|
|
235
|
+
'Token does not match current user Auth0 account'
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Update token data in services.auth0 (server-only)
|
|
240
|
+
await Meteor.users.updateAsync(this.userId, {
|
|
241
|
+
$set: {
|
|
242
|
+
'services.auth0.accessToken': tokenPayload.aud,
|
|
243
|
+
'services.auth0.expiresAt': new Date(tokenPayload.exp * 1000),
|
|
244
|
+
'services.auth0.issuedAt': new Date(tokenPayload.iat * 1000),
|
|
245
|
+
// Also update profile data in case it changed
|
|
246
|
+
'profile.auth0.name': tokenPayload.name,
|
|
247
|
+
'profile.auth0.picture': tokenPayload.picture,
|
|
248
|
+
'profile.auth0.email': tokenPayload.email,
|
|
249
|
+
'profile.auth0.email_verified': tokenPayload.email_verified,
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
success: true,
|
|
255
|
+
expiresAt: new Date(tokenPayload.exp * 1000),
|
|
256
|
+
};
|
|
257
|
+
} catch (error) {
|
|
258
|
+
console.error('Auth0 token refresh error:', error);
|
|
259
|
+
|
|
260
|
+
if (error instanceof Meteor.Error) {
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
throw new Meteor.Error(
|
|
265
|
+
'auth0-refresh-failed',
|
|
266
|
+
'Failed to refresh Auth0 token',
|
|
267
|
+
error.message
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
});
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { Meteor } from 'meteor/meteor';
|
|
2
|
+
import { Accounts } from 'meteor/accounts-base';
|
|
3
|
+
import { verifyAuth0Token, findOrCreateUserFromAuth0 } from '../auth0-validator';
|
|
4
|
+
|
|
5
|
+
// Hook into Meteor logout to clean up Auth0 session
|
|
6
|
+
Accounts.onLogout((options: any) => {
|
|
7
|
+
const user = options.user;
|
|
8
|
+
|
|
9
|
+
// If user has Auth0 account linked, we log the logout
|
|
10
|
+
// Note: Client-side should handle Auth0 logout via auth0Client.logout()
|
|
11
|
+
if (user?.services?.auth0) {
|
|
12
|
+
console.log(`User ${user._id} logged out (Auth0 ID: ${user.services.auth0.id})`);
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Meteor Methods for Auth0 Authentication
|
|
18
|
+
*
|
|
19
|
+
* These methods handle the server-side Auth0 authentication flow,
|
|
20
|
+
* including token verification and user account creation/linking.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Check if the user's Auth0 token has expired
|
|
25
|
+
*/
|
|
26
|
+
const isTokenExpired = (user: any): boolean => {
|
|
27
|
+
const auth0Data = user?.services?.auth0;
|
|
28
|
+
if (!auth0Data?.expiresAt) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
return new Date() >= new Date(auth0Data.expiresAt);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
Meteor.methods({
|
|
35
|
+
/**
|
|
36
|
+
* Authenticate user with Auth0 ID token
|
|
37
|
+
*
|
|
38
|
+
* @param idToken - The Auth0 ID token received from the client
|
|
39
|
+
* @returns Object containing userId and login token
|
|
40
|
+
*/
|
|
41
|
+
async 'auth0.login'(idToken: string) {
|
|
42
|
+
// Validate input
|
|
43
|
+
if (!idToken || typeof idToken !== 'string') {
|
|
44
|
+
throw new Meteor.Error('invalid-token', 'Invalid token provided');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
// Verify the Auth0 token
|
|
49
|
+
const tokenPayload = await verifyAuth0Token(idToken);
|
|
50
|
+
|
|
51
|
+
// Find or create user from Auth0 data
|
|
52
|
+
const userId = await findOrCreateUserFromAuth0(tokenPayload);
|
|
53
|
+
|
|
54
|
+
// Generate Meteor login token
|
|
55
|
+
const stampedLoginToken = Accounts._generateStampedLoginToken();
|
|
56
|
+
const loginToken = stampedLoginToken.token;
|
|
57
|
+
|
|
58
|
+
// Store login token for the user
|
|
59
|
+
Accounts._insertLoginToken(userId, stampedLoginToken);
|
|
60
|
+
|
|
61
|
+
// Return userId and token for client-side login
|
|
62
|
+
return {
|
|
63
|
+
userId,
|
|
64
|
+
token: loginToken,
|
|
65
|
+
tokenExpires: stampedLoginToken.when,
|
|
66
|
+
};
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error('Auth0 login error:', error);
|
|
69
|
+
|
|
70
|
+
if (error instanceof Meteor.Error) {
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
throw new Meteor.Error(
|
|
75
|
+
'auth0-login-failed',
|
|
76
|
+
'Failed to authenticate with Auth0',
|
|
77
|
+
error.message
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Link Auth0 account to existing Meteor user
|
|
84
|
+
*
|
|
85
|
+
* @param idToken - The Auth0 ID token
|
|
86
|
+
*/
|
|
87
|
+
async 'auth0.linkAccount'(idToken: string) {
|
|
88
|
+
// Must be logged in to link account
|
|
89
|
+
if (!this.userId) {
|
|
90
|
+
throw new Meteor.Error('not-authorized', 'Must be logged in to link Auth0 account');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!idToken || typeof idToken !== 'string') {
|
|
94
|
+
throw new Meteor.Error('invalid-token', 'Invalid token provided');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
// Verify the Auth0 token
|
|
99
|
+
const tokenPayload = await verifyAuth0Token(idToken);
|
|
100
|
+
const auth0Id = tokenPayload.sub;
|
|
101
|
+
|
|
102
|
+
// Check if Auth0 account is already linked to another user
|
|
103
|
+
const existingUser = await Meteor.users.findOneAsync({
|
|
104
|
+
'services.auth0.id': auth0Id,
|
|
105
|
+
_id: { $ne: this.userId },
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (existingUser) {
|
|
109
|
+
throw new Meteor.Error(
|
|
110
|
+
'auth0-already-linked',
|
|
111
|
+
'This Auth0 account is already linked to another user'
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Link Auth0 account to current user
|
|
116
|
+
await Meteor.users.updateAsync(this.userId, {
|
|
117
|
+
$set: {
|
|
118
|
+
// Profile data (client-accessible)
|
|
119
|
+
'profile.auth0': {
|
|
120
|
+
name: tokenPayload.name,
|
|
121
|
+
picture: tokenPayload.picture,
|
|
122
|
+
email: tokenPayload.email,
|
|
123
|
+
email_verified: tokenPayload.email_verified,
|
|
124
|
+
},
|
|
125
|
+
// Token data (server-only)
|
|
126
|
+
'services.auth0': {
|
|
127
|
+
id: auth0Id,
|
|
128
|
+
accessToken: tokenPayload.aud,
|
|
129
|
+
expiresAt: new Date(tokenPayload.exp * 1000),
|
|
130
|
+
issuedAt: new Date(tokenPayload.iat * 1000),
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return { success: true };
|
|
136
|
+
} catch (error) {
|
|
137
|
+
console.error('Auth0 link account error:', error);
|
|
138
|
+
|
|
139
|
+
if (error instanceof Meteor.Error) {
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
throw new Meteor.Error(
|
|
144
|
+
'auth0-link-failed',
|
|
145
|
+
'Failed to link Auth0 account',
|
|
146
|
+
error.message
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Unlink Auth0 account from current user
|
|
153
|
+
*/
|
|
154
|
+
async 'auth0.unlinkAccount'() {
|
|
155
|
+
if (!this.userId) {
|
|
156
|
+
throw new Meteor.Error('not-authorized', 'Must be logged in to unlink Auth0 account');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const user = await Meteor.users.findOneAsync(this.userId);
|
|
160
|
+
|
|
161
|
+
if (!user?.services?.auth0) {
|
|
162
|
+
throw new Meteor.Error('no-auth0-account', 'No Auth0 account linked');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Make sure user has another way to login (password or other service)
|
|
166
|
+
if (!user.services.password && Object.keys(user.services).length === 1) {
|
|
167
|
+
throw new Meteor.Error(
|
|
168
|
+
'last-login-method',
|
|
169
|
+
'Cannot unlink Auth0 - it is your only login method. Add a password first.'
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
await Meteor.users.updateAsync(this.userId, {
|
|
174
|
+
$unset: {
|
|
175
|
+
'services.auth0': '',
|
|
176
|
+
'profile.auth0': '',
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
return { success: true };
|
|
181
|
+
},
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Check if the current user's Auth0 token is valid
|
|
185
|
+
* Returns token expiry status
|
|
186
|
+
*/
|
|
187
|
+
async 'auth0.checkTokenValidity'() {
|
|
188
|
+
if (!this.userId) {
|
|
189
|
+
throw new Meteor.Error('not-authorized', 'Must be logged in');
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const user = await Meteor.users.findOneAsync(this.userId);
|
|
193
|
+
|
|
194
|
+
if (!user?.services?.auth0) {
|
|
195
|
+
throw new Meteor.Error('no-auth0-account', 'No Auth0 account linked');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const expired = isTokenExpired(user);
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
isValid: !expired,
|
|
202
|
+
expiresAt: user.services.auth0.expiresAt,
|
|
203
|
+
issuedAt: user.services.auth0.issuedAt,
|
|
204
|
+
};
|
|
205
|
+
},
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Refresh Auth0 token and update Meteor user session
|
|
209
|
+
* This method validates the new token and updates the user's token data
|
|
210
|
+
*/
|
|
211
|
+
async 'auth0.refreshToken'(idToken: string) {
|
|
212
|
+
if (!this.userId) {
|
|
213
|
+
throw new Meteor.Error('not-authorized', 'Must be logged in');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!idToken || typeof idToken !== 'string') {
|
|
217
|
+
throw new Meteor.Error('invalid-token', 'Invalid token provided');
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
try {
|
|
221
|
+
// Verify the new Auth0 token
|
|
222
|
+
const tokenPayload = await verifyAuth0Token(idToken);
|
|
223
|
+
|
|
224
|
+
// Get current user
|
|
225
|
+
const user = await Meteor.users.findOneAsync(this.userId);
|
|
226
|
+
|
|
227
|
+
if (!user?.services?.auth0) {
|
|
228
|
+
throw new Meteor.Error('no-auth0-account', 'No Auth0 account linked');
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Verify the token is for the same Auth0 user
|
|
232
|
+
if (user.services.auth0.id !== tokenPayload.sub) {
|
|
233
|
+
throw new Meteor.Error(
|
|
234
|
+
'token-mismatch',
|
|
235
|
+
'Token does not match current user Auth0 account'
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Update token data in services.auth0 (server-only)
|
|
240
|
+
await Meteor.users.updateAsync(this.userId, {
|
|
241
|
+
$set: {
|
|
242
|
+
'services.auth0.accessToken': tokenPayload.aud,
|
|
243
|
+
'services.auth0.expiresAt': new Date(tokenPayload.exp * 1000),
|
|
244
|
+
'services.auth0.issuedAt': new Date(tokenPayload.iat * 1000),
|
|
245
|
+
// Also update profile data in case it changed
|
|
246
|
+
'profile.auth0.name': tokenPayload.name,
|
|
247
|
+
'profile.auth0.picture': tokenPayload.picture,
|
|
248
|
+
'profile.auth0.email': tokenPayload.email,
|
|
249
|
+
'profile.auth0.email_verified': tokenPayload.email_verified,
|
|
250
|
+
},
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
success: true,
|
|
255
|
+
expiresAt: new Date(tokenPayload.exp * 1000),
|
|
256
|
+
};
|
|
257
|
+
} catch (error) {
|
|
258
|
+
console.error('Auth0 token refresh error:', error);
|
|
259
|
+
|
|
260
|
+
if (error instanceof Meteor.Error) {
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
throw new Meteor.Error(
|
|
265
|
+
'auth0-refresh-failed',
|
|
266
|
+
'Failed to refresh Auth0 token',
|
|
267
|
+
error.message
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
});
|