@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,179 @@
|
|
|
1
|
+
import { Meteor } from 'meteor/meteor';
|
|
2
|
+
import { Accounts } from 'meteor/accounts-base';
|
|
3
|
+
import jwt from 'jsonwebtoken';
|
|
4
|
+
import jwksClient from 'jwks-rsa';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Auth0 JWT Token Validator
|
|
8
|
+
*
|
|
9
|
+
* This module handles server-side validation of Auth0 JWT tokens
|
|
10
|
+
* and creates/updates Meteor user accounts based on Auth0 user data.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Normalize Auth0 domain by removing protocol and trailing slashes
|
|
15
|
+
*/
|
|
16
|
+
const normalizeDomain = (domain) => {
|
|
17
|
+
return domain
|
|
18
|
+
.replace(/^https?:\/\//, '') // Remove http:// or https://
|
|
19
|
+
.replace(/\/$/, ''); // Remove trailing slash
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Create JWKS client for verifying Auth0 tokens
|
|
24
|
+
*/
|
|
25
|
+
const getJwksClient = () => {
|
|
26
|
+
const auth0Domain = Meteor.settings?.auth0?.domain;
|
|
27
|
+
|
|
28
|
+
if (!auth0Domain) {
|
|
29
|
+
throw new Meteor.Error('auth0-config-missing', 'Auth0 domain not configured');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const normalizedDomain = normalizeDomain(auth0Domain);
|
|
33
|
+
|
|
34
|
+
return jwksClient({
|
|
35
|
+
jwksUri: `https://${normalizedDomain}/.well-known/jwks.json`,
|
|
36
|
+
cache: true,
|
|
37
|
+
cacheMaxAge: 86400000, // 24 hours
|
|
38
|
+
});
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get signing key for JWT verification
|
|
43
|
+
*/
|
|
44
|
+
const getKey = (header, callback) => {
|
|
45
|
+
const client = getJwksClient();
|
|
46
|
+
|
|
47
|
+
client.getSigningKey(header.kid, (err, key) => {
|
|
48
|
+
if (err) {
|
|
49
|
+
callback(err);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const signingKey = key?.getPublicKey();
|
|
53
|
+
callback(null, signingKey);
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Verify Auth0 JWT token
|
|
59
|
+
*/
|
|
60
|
+
export const verifyAuth0Token = (token) => {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const auth0Config = Meteor.settings?.auth0;
|
|
63
|
+
|
|
64
|
+
if (!auth0Config?.domain || !auth0Config?.clientId) {
|
|
65
|
+
reject(new Meteor.Error('auth0-config-missing', 'Auth0 configuration missing'));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const normalizedDomain = normalizeDomain(auth0Config.domain);
|
|
70
|
+
|
|
71
|
+
jwt.verify(
|
|
72
|
+
token,
|
|
73
|
+
getKey,
|
|
74
|
+
{
|
|
75
|
+
audience: auth0Config.clientId,
|
|
76
|
+
issuer: `https://${normalizedDomain}/`,
|
|
77
|
+
algorithms: ['RS256'],
|
|
78
|
+
},
|
|
79
|
+
(err, decoded) => {
|
|
80
|
+
if (err) {
|
|
81
|
+
reject(new Meteor.Error('auth0-invalid-token', 'Invalid Auth0 token', err));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
resolve(decoded);
|
|
85
|
+
}
|
|
86
|
+
);
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Find or create a Meteor user from Auth0 data
|
|
92
|
+
*/
|
|
93
|
+
export const findOrCreateUserFromAuth0 = async (tokenPayload) => {
|
|
94
|
+
const auth0Id = tokenPayload.sub;
|
|
95
|
+
const email = tokenPayload.email;
|
|
96
|
+
const now = new Date();
|
|
97
|
+
|
|
98
|
+
// Try to find existing user by Auth0 ID
|
|
99
|
+
let user = await Meteor.users.findOneAsync({
|
|
100
|
+
'services.auth0.id': auth0Id,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
if (user) {
|
|
104
|
+
// Update user profile and token data
|
|
105
|
+
await Meteor.users.updateAsync(user._id, {
|
|
106
|
+
$set: {
|
|
107
|
+
// Profile data (client-accessible)
|
|
108
|
+
'profile.auth0': {
|
|
109
|
+
name: tokenPayload.name,
|
|
110
|
+
picture: tokenPayload.picture,
|
|
111
|
+
email: email,
|
|
112
|
+
email_verified: tokenPayload.email_verified,
|
|
113
|
+
},
|
|
114
|
+
// Token data (server-only)
|
|
115
|
+
'services.auth0.id': auth0Id,
|
|
116
|
+
'services.auth0.accessToken': tokenPayload.aud,
|
|
117
|
+
'services.auth0.expiresAt': new Date(tokenPayload.exp * 1000),
|
|
118
|
+
'services.auth0.issuedAt': new Date(tokenPayload.iat * 1000),
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
return user._id;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Try to find by email if email exists
|
|
125
|
+
if (email) {
|
|
126
|
+
user = await Meteor.users.findOneAsync({
|
|
127
|
+
'emails.address': email,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (user) {
|
|
131
|
+
// Link Auth0 account to existing user
|
|
132
|
+
await Meteor.users.updateAsync(user._id, {
|
|
133
|
+
$set: {
|
|
134
|
+
// Profile data (client-accessible)
|
|
135
|
+
'profile.auth0': {
|
|
136
|
+
name: tokenPayload.name,
|
|
137
|
+
picture: tokenPayload.picture,
|
|
138
|
+
email: email,
|
|
139
|
+
email_verified: tokenPayload.email_verified,
|
|
140
|
+
},
|
|
141
|
+
// Token data (server-only)
|
|
142
|
+
'services.auth0': {
|
|
143
|
+
id: auth0Id,
|
|
144
|
+
accessToken: tokenPayload.aud,
|
|
145
|
+
expiresAt: new Date(tokenPayload.exp * 1000),
|
|
146
|
+
issuedAt: new Date(tokenPayload.iat * 1000),
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
return user._id;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Create new user
|
|
155
|
+
const userId = Accounts.insertUserDoc({}, {
|
|
156
|
+
emails: email ? [{ address: email, verified: tokenPayload.email_verified || false }] : [],
|
|
157
|
+
profile: {
|
|
158
|
+
name: tokenPayload.name || tokenPayload.nickname || 'Auth0 User',
|
|
159
|
+
// Auth0 profile data (client-accessible)
|
|
160
|
+
auth0: {
|
|
161
|
+
name: tokenPayload.name,
|
|
162
|
+
picture: tokenPayload.picture,
|
|
163
|
+
email: email,
|
|
164
|
+
email_verified: tokenPayload.email_verified,
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
services: {
|
|
168
|
+
// Token data (server-only)
|
|
169
|
+
auth0: {
|
|
170
|
+
id: auth0Id,
|
|
171
|
+
accessToken: tokenPayload.aud,
|
|
172
|
+
expiresAt: new Date(tokenPayload.exp * 1000),
|
|
173
|
+
issuedAt: new Date(tokenPayload.iat * 1000),
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return userId;
|
|
179
|
+
};
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { Meteor } from 'meteor/meteor';
|
|
2
|
+
import { Accounts } from 'meteor/accounts-base';
|
|
3
|
+
import jwt from 'jsonwebtoken';
|
|
4
|
+
import jwksClient from 'jwks-rsa';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Auth0 JWT Token Validator
|
|
8
|
+
*
|
|
9
|
+
* This module handles server-side validation of Auth0 JWT tokens
|
|
10
|
+
* and creates/updates Meteor user accounts based on Auth0 user data.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
interface Auth0TokenPayload {
|
|
14
|
+
sub: string; // Auth0 user ID
|
|
15
|
+
email?: string;
|
|
16
|
+
email_verified?: boolean;
|
|
17
|
+
name?: string;
|
|
18
|
+
nickname?: string;
|
|
19
|
+
picture?: string;
|
|
20
|
+
aud: string | string[];
|
|
21
|
+
iss: string;
|
|
22
|
+
exp: number;
|
|
23
|
+
iat: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Normalize Auth0 domain by removing protocol and trailing slashes
|
|
28
|
+
*/
|
|
29
|
+
const normalizeDomain = (domain: string): string => {
|
|
30
|
+
return domain
|
|
31
|
+
.replace(/^https?:\/\//, '') // Remove http:// or https://
|
|
32
|
+
.replace(/\/$/, ''); // Remove trailing slash
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Create JWKS client for verifying Auth0 tokens
|
|
37
|
+
*/
|
|
38
|
+
const getJwksClient = () => {
|
|
39
|
+
const auth0Domain = Meteor.settings?.auth0?.domain;
|
|
40
|
+
|
|
41
|
+
if (!auth0Domain) {
|
|
42
|
+
throw new Meteor.Error('auth0-config-missing', 'Auth0 domain not configured');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const normalizedDomain = normalizeDomain(auth0Domain);
|
|
46
|
+
|
|
47
|
+
return jwksClient({
|
|
48
|
+
jwksUri: `https://${normalizedDomain}/.well-known/jwks.json`,
|
|
49
|
+
cache: true,
|
|
50
|
+
cacheMaxAge: 86400000, // 24 hours
|
|
51
|
+
});
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get signing key for JWT verification
|
|
56
|
+
*/
|
|
57
|
+
const getKey = (header: any, callback: any) => {
|
|
58
|
+
const client = getJwksClient();
|
|
59
|
+
|
|
60
|
+
client.getSigningKey(header.kid, (err, key) => {
|
|
61
|
+
if (err) {
|
|
62
|
+
callback(err);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const signingKey = key?.getPublicKey();
|
|
66
|
+
callback(null, signingKey);
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Verify Auth0 JWT token
|
|
72
|
+
*/
|
|
73
|
+
export const verifyAuth0Token = (token: string): Promise<Auth0TokenPayload> => {
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
const auth0Config = Meteor.settings?.auth0;
|
|
76
|
+
|
|
77
|
+
if (!auth0Config?.domain || !auth0Config?.clientId) {
|
|
78
|
+
reject(new Meteor.Error('auth0-config-missing', 'Auth0 configuration missing'));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const normalizedDomain = normalizeDomain(auth0Config.domain);
|
|
83
|
+
|
|
84
|
+
jwt.verify(
|
|
85
|
+
token,
|
|
86
|
+
getKey,
|
|
87
|
+
{
|
|
88
|
+
audience: auth0Config.clientId,
|
|
89
|
+
issuer: `https://${normalizedDomain}/`,
|
|
90
|
+
algorithms: ['RS256'],
|
|
91
|
+
},
|
|
92
|
+
(err, decoded) => {
|
|
93
|
+
if (err) {
|
|
94
|
+
reject(new Meteor.Error('auth0-invalid-token', 'Invalid Auth0 token', err));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
resolve(decoded as Auth0TokenPayload);
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Find or create a Meteor user from Auth0 data
|
|
105
|
+
*/
|
|
106
|
+
export const findOrCreateUserFromAuth0 = async (
|
|
107
|
+
tokenPayload: Auth0TokenPayload
|
|
108
|
+
): Promise<string> => {
|
|
109
|
+
const auth0Id = tokenPayload.sub;
|
|
110
|
+
const email = tokenPayload.email;
|
|
111
|
+
const now = new Date();
|
|
112
|
+
|
|
113
|
+
// Try to find existing user by Auth0 ID
|
|
114
|
+
let user = await Meteor.users.findOneAsync({
|
|
115
|
+
'services.auth0.id': auth0Id,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
if (user) {
|
|
119
|
+
// Update user profile and token data
|
|
120
|
+
await Meteor.users.updateAsync(user._id, {
|
|
121
|
+
$set: {
|
|
122
|
+
// Profile data (client-accessible)
|
|
123
|
+
'profile.auth0': {
|
|
124
|
+
name: tokenPayload.name,
|
|
125
|
+
picture: tokenPayload.picture,
|
|
126
|
+
email: email,
|
|
127
|
+
email_verified: tokenPayload.email_verified,
|
|
128
|
+
},
|
|
129
|
+
// Token data (server-only)
|
|
130
|
+
'services.auth0.id': auth0Id,
|
|
131
|
+
'services.auth0.accessToken': tokenPayload.aud,
|
|
132
|
+
'services.auth0.expiresAt': new Date(tokenPayload.exp * 1000),
|
|
133
|
+
'services.auth0.issuedAt': new Date(tokenPayload.iat * 1000),
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
return user._id;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Try to find by email if email exists
|
|
140
|
+
if (email) {
|
|
141
|
+
user = await Meteor.users.findOneAsync({
|
|
142
|
+
'emails.address': email,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (user) {
|
|
146
|
+
// Link Auth0 account to existing user
|
|
147
|
+
await Meteor.users.updateAsync(user._id, {
|
|
148
|
+
$set: {
|
|
149
|
+
// Profile data (client-accessible)
|
|
150
|
+
'profile.auth0': {
|
|
151
|
+
name: tokenPayload.name,
|
|
152
|
+
picture: tokenPayload.picture,
|
|
153
|
+
email: email,
|
|
154
|
+
email_verified: tokenPayload.email_verified,
|
|
155
|
+
},
|
|
156
|
+
// Token data (server-only)
|
|
157
|
+
'services.auth0': {
|
|
158
|
+
id: auth0Id,
|
|
159
|
+
accessToken: tokenPayload.aud,
|
|
160
|
+
expiresAt: new Date(tokenPayload.exp * 1000),
|
|
161
|
+
issuedAt: new Date(tokenPayload.iat * 1000),
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
return user._id;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Create new user
|
|
170
|
+
const userId = Accounts.insertUserDoc({}, {
|
|
171
|
+
emails: email ? [{ address: email, verified: tokenPayload.email_verified || false }] : [],
|
|
172
|
+
profile: {
|
|
173
|
+
name: tokenPayload.name || tokenPayload.nickname || 'Auth0 User',
|
|
174
|
+
// Auth0 profile data (client-accessible)
|
|
175
|
+
auth0: {
|
|
176
|
+
name: tokenPayload.name,
|
|
177
|
+
picture: tokenPayload.picture,
|
|
178
|
+
email: email,
|
|
179
|
+
email_verified: tokenPayload.email_verified,
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
services: {
|
|
183
|
+
// Token data (server-only)
|
|
184
|
+
auth0: {
|
|
185
|
+
id: auth0Id,
|
|
186
|
+
accessToken: tokenPayload.aud,
|
|
187
|
+
expiresAt: new Date(tokenPayload.exp * 1000),
|
|
188
|
+
issuedAt: new Date(tokenPayload.iat * 1000),
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return userId;
|
|
194
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { useNavigate } from 'react-router-dom';
|
|
3
|
+
import { Box, CircularProgress, Typography, Alert } from '@mui/material';
|
|
4
|
+
import AuthService from '<%= authServicePath %>';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Auth0 Callback Component
|
|
8
|
+
*
|
|
9
|
+
* This component handles the OAuth callback from Auth0.
|
|
10
|
+
* It exchanges the authorization code for tokens and logs the user into Meteor.
|
|
11
|
+
*/
|
|
12
|
+
export const AuthCallback = () => {
|
|
13
|
+
const navigate = useNavigate();
|
|
14
|
+
const [error, setError] = useState(null);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const handleCallback = async () => {
|
|
18
|
+
try {
|
|
19
|
+
// Handle the Auth0 callback and login to Meteor
|
|
20
|
+
await AuthService.handleAuth0Callback();
|
|
21
|
+
|
|
22
|
+
// Redirect to home page after successful login
|
|
23
|
+
navigate('/', { replace: true });
|
|
24
|
+
} catch (err) {
|
|
25
|
+
console.error('Auth0 callback error:', err);
|
|
26
|
+
setError(err instanceof Error ? err.message : 'Authentication failed');
|
|
27
|
+
|
|
28
|
+
// Redirect to login page after a delay
|
|
29
|
+
setTimeout(() => {
|
|
30
|
+
navigate('/login', { replace: true });
|
|
31
|
+
}, 3000);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
handleCallback();
|
|
36
|
+
}, [navigate]);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Box
|
|
40
|
+
sx={{
|
|
41
|
+
display: 'flex',
|
|
42
|
+
flexDirection: 'column',
|
|
43
|
+
alignItems: 'center',
|
|
44
|
+
justifyContent: 'center',
|
|
45
|
+
minHeight: '100vh',
|
|
46
|
+
gap: 2,
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
{error ? (
|
|
50
|
+
<>
|
|
51
|
+
<Alert severity="error" sx={{ maxWidth: 600 }}>
|
|
52
|
+
{error}
|
|
53
|
+
</Alert>
|
|
54
|
+
<Typography variant="body2" color="text.secondary">
|
|
55
|
+
Redirecting to login page...
|
|
56
|
+
</Typography>
|
|
57
|
+
</>
|
|
58
|
+
) : (
|
|
59
|
+
<>
|
|
60
|
+
<CircularProgress size={60} />
|
|
61
|
+
<Typography variant="h6" sx={{ mt: 2 }}>
|
|
62
|
+
Completing authentication...
|
|
63
|
+
</Typography>
|
|
64
|
+
<Typography variant="body2" color="text.secondary">
|
|
65
|
+
Please wait while we log you in.
|
|
66
|
+
</Typography>
|
|
67
|
+
</>
|
|
68
|
+
)}
|
|
69
|
+
</Box>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import { useNavigate } from 'react-router-dom';
|
|
3
|
+
import { Box, CircularProgress, Typography, Alert } from '@mui/material';
|
|
4
|
+
import AuthService from '<%= authServicePath %>';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Auth0 Callback Component
|
|
8
|
+
*
|
|
9
|
+
* This component handles the OAuth callback from Auth0.
|
|
10
|
+
* It exchanges the authorization code for tokens and logs the user into Meteor.
|
|
11
|
+
*/
|
|
12
|
+
export const AuthCallback = () => {
|
|
13
|
+
const navigate = useNavigate();
|
|
14
|
+
const [error, setError] = useState<string | null>(null);
|
|
15
|
+
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
const handleCallback = async () => {
|
|
18
|
+
try {
|
|
19
|
+
// Handle the Auth0 callback and login to Meteor
|
|
20
|
+
await AuthService.handleAuth0Callback();
|
|
21
|
+
|
|
22
|
+
// Redirect to home page after successful login
|
|
23
|
+
navigate('/', { replace: true });
|
|
24
|
+
} catch (err) {
|
|
25
|
+
console.error('Auth0 callback error:', err);
|
|
26
|
+
setError(err instanceof Error ? err.message : 'Authentication failed');
|
|
27
|
+
|
|
28
|
+
// Redirect to login page after a delay
|
|
29
|
+
setTimeout(() => {
|
|
30
|
+
navigate('/login', { replace: true });
|
|
31
|
+
}, 3000);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
handleCallback();
|
|
36
|
+
}, [navigate]);
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Box
|
|
40
|
+
sx={{
|
|
41
|
+
display: 'flex',
|
|
42
|
+
flexDirection: 'column',
|
|
43
|
+
alignItems: 'center',
|
|
44
|
+
justifyContent: 'center',
|
|
45
|
+
minHeight: '100vh',
|
|
46
|
+
gap: 2,
|
|
47
|
+
}}
|
|
48
|
+
>
|
|
49
|
+
{error ? (
|
|
50
|
+
<>
|
|
51
|
+
<Alert severity="error" sx={{ maxWidth: 600 }}>
|
|
52
|
+
{error}
|
|
53
|
+
</Alert>
|
|
54
|
+
<Typography variant="body2" color="text.secondary">
|
|
55
|
+
Redirecting to login page...
|
|
56
|
+
</Typography>
|
|
57
|
+
</>
|
|
58
|
+
) : (
|
|
59
|
+
<>
|
|
60
|
+
<CircularProgress size={60} />
|
|
61
|
+
<Typography variant="h6" sx={{ mt: 2 }}>
|
|
62
|
+
Completing authentication...
|
|
63
|
+
</Typography>
|
|
64
|
+
<Typography variant="body2" color="text.secondary">
|
|
65
|
+
Please wait while we log you in.
|
|
66
|
+
</Typography>
|
|
67
|
+
</>
|
|
68
|
+
)}
|
|
69
|
+
</Box>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import React, { useContext, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Box,
|
|
4
|
+
Card,
|
|
5
|
+
CardContent,
|
|
6
|
+
Typography,
|
|
7
|
+
Button,
|
|
8
|
+
Alert,
|
|
9
|
+
CircularProgress,
|
|
10
|
+
} from '@mui/material';
|
|
11
|
+
import { Meteor } from 'meteor/meteor';
|
|
12
|
+
import { AuthContext } from '<%= authContextPath %>';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Auth0 Account Settings Component
|
|
16
|
+
*
|
|
17
|
+
* Displays Auth0 account status and allows linking/unlinking
|
|
18
|
+
* Auth0 account to/from the current Meteor user.
|
|
19
|
+
*/
|
|
20
|
+
export const Auth0AccountSettings = () => {
|
|
21
|
+
const auth = useContext(AuthContext);
|
|
22
|
+
const [loading, setLoading] = useState(false);
|
|
23
|
+
const [message, setMessage] = useState(null);
|
|
24
|
+
|
|
25
|
+
if (!auth?.currentUser) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const user = Meteor.user();
|
|
30
|
+
const hasAuth0Account = Boolean(user?.services?.auth0);
|
|
31
|
+
|
|
32
|
+
const handleLinkAccount = async () => {
|
|
33
|
+
setLoading(true);
|
|
34
|
+
setMessage(null);
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const success = await auth.linkAuth0Account?.();
|
|
38
|
+
if (success) {
|
|
39
|
+
setMessage({ type: 'success', text: 'Auth0 account linked successfully!' });
|
|
40
|
+
}
|
|
41
|
+
} catch (error) {
|
|
42
|
+
setMessage({
|
|
43
|
+
type: 'error',
|
|
44
|
+
text: error instanceof Error ? error.message : 'Failed to link Auth0 account',
|
|
45
|
+
});
|
|
46
|
+
} finally {
|
|
47
|
+
setLoading(false);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const handleUnlinkAccount = async () => {
|
|
52
|
+
setLoading(true);
|
|
53
|
+
setMessage(null);
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const success = await auth.unlinkAuth0Account?.();
|
|
57
|
+
if (success) {
|
|
58
|
+
setMessage({ type: 'success', text: 'Auth0 account unlinked successfully!' });
|
|
59
|
+
}
|
|
60
|
+
} catch (error) {
|
|
61
|
+
setMessage({
|
|
62
|
+
type: 'error',
|
|
63
|
+
text: error instanceof Error ? error.message : 'Failed to unlink Auth0 account',
|
|
64
|
+
});
|
|
65
|
+
} finally {
|
|
66
|
+
setLoading(false);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<Card>
|
|
72
|
+
<CardContent>
|
|
73
|
+
<Typography variant="h6" gutterBottom>
|
|
74
|
+
Auth0 Integration
|
|
75
|
+
</Typography>
|
|
76
|
+
|
|
77
|
+
{message && (
|
|
78
|
+
<Alert severity={message.type} sx={{ mb: 2 }}>
|
|
79
|
+
{message.text}
|
|
80
|
+
</Alert>
|
|
81
|
+
)}
|
|
82
|
+
|
|
83
|
+
{hasAuth0Account ? (
|
|
84
|
+
<Box>
|
|
85
|
+
<Alert severity="success" sx={{ mb: 2 }}>
|
|
86
|
+
Your account is linked with Auth0
|
|
87
|
+
</Alert>
|
|
88
|
+
<Typography variant="body2" color="text.secondary" gutterBottom>
|
|
89
|
+
Email: {user?.services?.auth0?.email || 'N/A'}
|
|
90
|
+
</Typography>
|
|
91
|
+
<Typography variant="body2" color="text.secondary" gutterBottom>
|
|
92
|
+
Name: {user?.services?.auth0?.name || 'N/A'}
|
|
93
|
+
</Typography>
|
|
94
|
+
<Button
|
|
95
|
+
variant="outlined"
|
|
96
|
+
color="error"
|
|
97
|
+
onClick={handleUnlinkAccount}
|
|
98
|
+
disabled={loading}
|
|
99
|
+
sx={{ mt: 2 }}
|
|
100
|
+
startIcon={loading ? <CircularProgress size={20} /> : undefined}
|
|
101
|
+
>
|
|
102
|
+
{loading ? 'Unlinking...' : 'Unlink Auth0 Account'}
|
|
103
|
+
</Button>
|
|
104
|
+
</Box>
|
|
105
|
+
) : (
|
|
106
|
+
<Box>
|
|
107
|
+
<Alert severity="info" sx={{ mb: 2 }}>
|
|
108
|
+
Link your account with Auth0 for easier login
|
|
109
|
+
</Alert>
|
|
110
|
+
<Button
|
|
111
|
+
variant="contained"
|
|
112
|
+
onClick={handleLinkAccount}
|
|
113
|
+
disabled={loading}
|
|
114
|
+
startIcon={loading ? <CircularProgress size={20} /> : undefined}
|
|
115
|
+
>
|
|
116
|
+
{loading ? 'Linking...' : 'Link Auth0 Account'}
|
|
117
|
+
</Button>
|
|
118
|
+
</Box>
|
|
119
|
+
)}
|
|
120
|
+
</CardContent>
|
|
121
|
+
</Card>
|
|
122
|
+
);
|
|
123
|
+
};
|