@drmhse/authos-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.d.ts +2 -0
- package/dist/bin.js +1170 -0
- package/dist/index.d.ts +51 -0
- package/dist/index.js +1157 -0
- package/package.json +55 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1157 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var path2 = require('path');
|
|
5
|
+
var prompts2 = require('prompts');
|
|
6
|
+
var child_process = require('child_process');
|
|
7
|
+
var fs = require('fs');
|
|
8
|
+
var pc = require('picocolors');
|
|
9
|
+
|
|
10
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
|
+
|
|
12
|
+
function _interopNamespace(e) {
|
|
13
|
+
if (e && e.__esModule) return e;
|
|
14
|
+
var n = Object.create(null);
|
|
15
|
+
if (e) {
|
|
16
|
+
Object.keys(e).forEach(function (k) {
|
|
17
|
+
if (k !== 'default') {
|
|
18
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
19
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
20
|
+
enumerable: true,
|
|
21
|
+
get: function () { return e[k]; }
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
n.default = e;
|
|
27
|
+
return Object.freeze(n);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
var path2__namespace = /*#__PURE__*/_interopNamespace(path2);
|
|
31
|
+
var prompts2__default = /*#__PURE__*/_interopDefault(prompts2);
|
|
32
|
+
var fs__namespace = /*#__PURE__*/_interopNamespace(fs);
|
|
33
|
+
var pc__default = /*#__PURE__*/_interopDefault(pc);
|
|
34
|
+
|
|
35
|
+
function detectFramework(cwd) {
|
|
36
|
+
const packageJsonPath = path2__namespace.join(cwd, "package.json");
|
|
37
|
+
if (!fs__namespace.existsSync(packageJsonPath)) {
|
|
38
|
+
return "unknown";
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const content = fs__namespace.readFileSync(packageJsonPath, "utf8");
|
|
42
|
+
const pkg = JSON.parse(content);
|
|
43
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
44
|
+
if (deps["nuxt"] || deps["nuxt3"]) {
|
|
45
|
+
return "nuxt";
|
|
46
|
+
}
|
|
47
|
+
if (deps["next"]) {
|
|
48
|
+
return "next";
|
|
49
|
+
}
|
|
50
|
+
if (deps["vue"]) {
|
|
51
|
+
return "vue";
|
|
52
|
+
}
|
|
53
|
+
if (deps["react"]) {
|
|
54
|
+
return "react";
|
|
55
|
+
}
|
|
56
|
+
return "unknown";
|
|
57
|
+
} catch {
|
|
58
|
+
return "unknown";
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function getAdapterPackage(framework) {
|
|
62
|
+
switch (framework) {
|
|
63
|
+
case "react":
|
|
64
|
+
case "next":
|
|
65
|
+
return "@drmhse/authos-react";
|
|
66
|
+
case "vue":
|
|
67
|
+
case "nuxt":
|
|
68
|
+
return "@drmhse/authos-vue";
|
|
69
|
+
default:
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function writeFile(filePath, content) {
|
|
74
|
+
const dir = path2__namespace.dirname(filePath);
|
|
75
|
+
if (!fs__namespace.existsSync(dir)) {
|
|
76
|
+
fs__namespace.mkdirSync(dir, { recursive: true });
|
|
77
|
+
}
|
|
78
|
+
fs__namespace.writeFileSync(filePath, content, "utf8");
|
|
79
|
+
}
|
|
80
|
+
function fileExists(filePath) {
|
|
81
|
+
return fs__namespace.existsSync(filePath);
|
|
82
|
+
}
|
|
83
|
+
function appendToFile(filePath, content) {
|
|
84
|
+
if (fs__namespace.existsSync(filePath)) {
|
|
85
|
+
const existing = fs__namespace.readFileSync(filePath, "utf8");
|
|
86
|
+
if (!existing.includes(content.trim())) {
|
|
87
|
+
fs__namespace.appendFileSync(filePath, "\n" + content);
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
writeFile(filePath, content);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function findComponentsDir(cwd) {
|
|
94
|
+
const candidates = [
|
|
95
|
+
"src/components",
|
|
96
|
+
"components",
|
|
97
|
+
"app/components",
|
|
98
|
+
"src/app/components"
|
|
99
|
+
];
|
|
100
|
+
for (const candidate of candidates) {
|
|
101
|
+
const fullPath = path2__namespace.join(cwd, candidate);
|
|
102
|
+
if (fs__namespace.existsSync(fullPath)) {
|
|
103
|
+
return fullPath;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return path2__namespace.join(cwd, "src", "components");
|
|
107
|
+
}
|
|
108
|
+
function getFileExtension(framework) {
|
|
109
|
+
switch (framework) {
|
|
110
|
+
case "vue":
|
|
111
|
+
case "nuxt":
|
|
112
|
+
return ".vue";
|
|
113
|
+
case "react":
|
|
114
|
+
case "next":
|
|
115
|
+
default:
|
|
116
|
+
return ".tsx";
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
var log = {
|
|
120
|
+
info: (msg) => console.log(pc__default.default.blue("i"), msg),
|
|
121
|
+
success: (msg) => console.log(pc__default.default.green("\u2713"), msg),
|
|
122
|
+
warn: (msg) => console.log(pc__default.default.yellow("!"), msg),
|
|
123
|
+
error: (msg) => console.log(pc__default.default.red("\u2717"), msg),
|
|
124
|
+
step: (msg) => console.log(pc__default.default.cyan("\u2192"), msg)
|
|
125
|
+
};
|
|
126
|
+
function getFrameworkName(framework) {
|
|
127
|
+
switch (framework) {
|
|
128
|
+
case "react":
|
|
129
|
+
return "React";
|
|
130
|
+
case "vue":
|
|
131
|
+
return "Vue";
|
|
132
|
+
case "next":
|
|
133
|
+
return "Next.js";
|
|
134
|
+
case "nuxt":
|
|
135
|
+
return "Nuxt";
|
|
136
|
+
default:
|
|
137
|
+
return "Unknown";
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/commands/init.ts
|
|
142
|
+
async function initCommand(options = {}) {
|
|
143
|
+
const cwd = options.cwd || process.cwd();
|
|
144
|
+
log.info("Initializing AuthOS...\n");
|
|
145
|
+
let framework = detectFramework(cwd);
|
|
146
|
+
if (framework === "unknown") {
|
|
147
|
+
log.warn("Could not detect project framework from package.json.");
|
|
148
|
+
const response = await prompts2__default.default({
|
|
149
|
+
type: "select",
|
|
150
|
+
name: "framework",
|
|
151
|
+
message: "Select your framework:",
|
|
152
|
+
choices: [
|
|
153
|
+
{ title: "React", value: "react" },
|
|
154
|
+
{ title: "Next.js", value: "next" },
|
|
155
|
+
{ title: "Vue", value: "vue" },
|
|
156
|
+
{ title: "Nuxt", value: "nuxt" }
|
|
157
|
+
]
|
|
158
|
+
});
|
|
159
|
+
if (!response.framework) {
|
|
160
|
+
log.error("Initialization cancelled.");
|
|
161
|
+
process.exit(1);
|
|
162
|
+
}
|
|
163
|
+
framework = response.framework;
|
|
164
|
+
} else {
|
|
165
|
+
log.success(`Detected ${getFrameworkName(framework)} project`);
|
|
166
|
+
}
|
|
167
|
+
const adapterPackage = getAdapterPackage(framework);
|
|
168
|
+
if (!adapterPackage) {
|
|
169
|
+
log.error("Unsupported framework.");
|
|
170
|
+
process.exit(1);
|
|
171
|
+
}
|
|
172
|
+
const urlResponse = await prompts2__default.default({
|
|
173
|
+
type: "text",
|
|
174
|
+
name: "baseUrl",
|
|
175
|
+
message: "Enter your AuthOS base URL:",
|
|
176
|
+
initial: "https://auth.example.com",
|
|
177
|
+
validate: (value) => {
|
|
178
|
+
try {
|
|
179
|
+
new URL(value);
|
|
180
|
+
return true;
|
|
181
|
+
} catch {
|
|
182
|
+
return "Please enter a valid URL";
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
if (!urlResponse.baseUrl) {
|
|
187
|
+
log.error("Initialization cancelled.");
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
const baseUrl = urlResponse.baseUrl;
|
|
191
|
+
if (!options.skipInstall) {
|
|
192
|
+
log.step(`Installing ${adapterPackage}...`);
|
|
193
|
+
try {
|
|
194
|
+
const packageManager = detectPackageManager(cwd);
|
|
195
|
+
const installCmd = getInstallCommand(packageManager, adapterPackage);
|
|
196
|
+
child_process.execSync(installCmd, { cwd, stdio: "inherit" });
|
|
197
|
+
log.success(`Installed ${adapterPackage}`);
|
|
198
|
+
} catch (error) {
|
|
199
|
+
log.error(`Failed to install ${adapterPackage}`);
|
|
200
|
+
log.info(`You can install it manually: npm install ${adapterPackage}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const envPath = path2__namespace.join(cwd, ".env");
|
|
204
|
+
const envLocalPath = path2__namespace.join(cwd, ".env.local");
|
|
205
|
+
const envContent = `AUTHOS_BASE_URL=${baseUrl}
|
|
206
|
+
`;
|
|
207
|
+
const targetEnvPath = framework === "next" || framework === "nuxt" ? envLocalPath : envPath;
|
|
208
|
+
if (fileExists(targetEnvPath)) {
|
|
209
|
+
appendToFile(targetEnvPath, envContent);
|
|
210
|
+
log.success(`Updated ${path2__namespace.basename(targetEnvPath)}`);
|
|
211
|
+
} else {
|
|
212
|
+
appendToFile(targetEnvPath, envContent);
|
|
213
|
+
log.success(`Created ${path2__namespace.basename(targetEnvPath)}`);
|
|
214
|
+
}
|
|
215
|
+
console.log("\n");
|
|
216
|
+
log.success("AuthOS initialized successfully!\n");
|
|
217
|
+
console.log("Next steps:\n");
|
|
218
|
+
if (framework === "react" || framework === "next") {
|
|
219
|
+
console.log(" 1. Wrap your app with AuthOSProvider:\n");
|
|
220
|
+
console.log(' import { AuthOSProvider } from "@drmhse/authos-react";');
|
|
221
|
+
console.log("");
|
|
222
|
+
console.log(" <AuthOSProvider baseURL={process.env.AUTHOS_BASE_URL}>");
|
|
223
|
+
console.log(" <App />");
|
|
224
|
+
console.log(" </AuthOSProvider>\n");
|
|
225
|
+
} else if (framework === "vue" || framework === "nuxt") {
|
|
226
|
+
console.log(" 1. Install the Vue plugin:\n");
|
|
227
|
+
console.log(' import { createAuthOS } from "@drmhse/authos-vue";');
|
|
228
|
+
console.log("");
|
|
229
|
+
console.log(" app.use(createAuthOS({");
|
|
230
|
+
console.log(" baseURL: import.meta.env.VITE_AUTHOS_BASE_URL,");
|
|
231
|
+
console.log(" }));\n");
|
|
232
|
+
}
|
|
233
|
+
console.log(" 2. Add authentication components:");
|
|
234
|
+
console.log(" npx authos add login-form");
|
|
235
|
+
console.log(" npx authos add user-profile\n");
|
|
236
|
+
console.log(" 3. Check out the docs: https://authos.dev/docs\n");
|
|
237
|
+
}
|
|
238
|
+
function detectPackageManager(cwd) {
|
|
239
|
+
if (fileExists(path2__namespace.join(cwd, "bun.lockb"))) {
|
|
240
|
+
return "bun";
|
|
241
|
+
}
|
|
242
|
+
if (fileExists(path2__namespace.join(cwd, "pnpm-lock.yaml"))) {
|
|
243
|
+
return "pnpm";
|
|
244
|
+
}
|
|
245
|
+
if (fileExists(path2__namespace.join(cwd, "yarn.lock"))) {
|
|
246
|
+
return "yarn";
|
|
247
|
+
}
|
|
248
|
+
return "npm";
|
|
249
|
+
}
|
|
250
|
+
function getInstallCommand(pm, pkg) {
|
|
251
|
+
switch (pm) {
|
|
252
|
+
case "yarn":
|
|
253
|
+
return `yarn add ${pkg}`;
|
|
254
|
+
case "pnpm":
|
|
255
|
+
return `pnpm add ${pkg}`;
|
|
256
|
+
case "bun":
|
|
257
|
+
return `bun add ${pkg}`;
|
|
258
|
+
default:
|
|
259
|
+
return `npm install ${pkg}`;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/templates/registry.ts
|
|
264
|
+
var loginFormReact = `"use client";
|
|
265
|
+
|
|
266
|
+
import { useState } from "react";
|
|
267
|
+
import { useAuthOS } from "@drmhse/authos-react";
|
|
268
|
+
import { AuthErrorCodes } from "@drmhse/sso-sdk";
|
|
269
|
+
|
|
270
|
+
interface LoginFormProps {
|
|
271
|
+
onSuccess?: () => void;
|
|
272
|
+
redirectTo?: string;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function LoginForm({ onSuccess, redirectTo }: LoginFormProps) {
|
|
276
|
+
const { client } = useAuthOS();
|
|
277
|
+
const [email, setEmail] = useState("");
|
|
278
|
+
const [password, setPassword] = useState("");
|
|
279
|
+
const [error, setError] = useState<string | null>(null);
|
|
280
|
+
const [loading, setLoading] = useState(false);
|
|
281
|
+
const [mfaRequired, setMfaRequired] = useState(false);
|
|
282
|
+
const [mfaCode, setMfaCode] = useState("");
|
|
283
|
+
const [mfaToken, setMfaToken] = useState<string | null>(null);
|
|
284
|
+
|
|
285
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
286
|
+
e.preventDefault();
|
|
287
|
+
setError(null);
|
|
288
|
+
setLoading(true);
|
|
289
|
+
|
|
290
|
+
try {
|
|
291
|
+
const response = await client.auth.login({ email, password });
|
|
292
|
+
|
|
293
|
+
if (response.mfa_required) {
|
|
294
|
+
setMfaRequired(true);
|
|
295
|
+
setMfaToken(response.mfa_token || null);
|
|
296
|
+
} else {
|
|
297
|
+
onSuccess?.();
|
|
298
|
+
if (redirectTo) {
|
|
299
|
+
window.location.href = redirectTo;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
} catch (err: unknown) {
|
|
303
|
+
if (err && typeof err === "object" && "errorCode" in err) {
|
|
304
|
+
const apiError = err as { errorCode: string; message: string };
|
|
305
|
+
if (apiError.errorCode === AuthErrorCodes.MFA_REQUIRED) {
|
|
306
|
+
setMfaRequired(true);
|
|
307
|
+
} else {
|
|
308
|
+
setError(apiError.message || "Login failed");
|
|
309
|
+
}
|
|
310
|
+
} else {
|
|
311
|
+
setError("An unexpected error occurred");
|
|
312
|
+
}
|
|
313
|
+
} finally {
|
|
314
|
+
setLoading(false);
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
const handleMfaSubmit = async (e: React.FormEvent) => {
|
|
319
|
+
e.preventDefault();
|
|
320
|
+
setError(null);
|
|
321
|
+
setLoading(true);
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
await client.auth.verifyMfa({ code: mfaCode, mfa_token: mfaToken! });
|
|
325
|
+
onSuccess?.();
|
|
326
|
+
if (redirectTo) {
|
|
327
|
+
window.location.href = redirectTo;
|
|
328
|
+
}
|
|
329
|
+
} catch (err: unknown) {
|
|
330
|
+
if (err && typeof err === "object" && "message" in err) {
|
|
331
|
+
setError((err as { message: string }).message || "Invalid MFA code");
|
|
332
|
+
} else {
|
|
333
|
+
setError("Invalid MFA code");
|
|
334
|
+
}
|
|
335
|
+
} finally {
|
|
336
|
+
setLoading(false);
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
if (mfaRequired) {
|
|
341
|
+
return (
|
|
342
|
+
<form onSubmit={handleMfaSubmit} className="space-y-4 w-full max-w-sm">
|
|
343
|
+
<div className="text-center mb-6">
|
|
344
|
+
<h2 className="text-xl font-semibold">Two-Factor Authentication</h2>
|
|
345
|
+
<p className="text-gray-600 text-sm mt-1">
|
|
346
|
+
Enter the code from your authenticator app
|
|
347
|
+
</p>
|
|
348
|
+
</div>
|
|
349
|
+
|
|
350
|
+
{error && (
|
|
351
|
+
<div className="bg-red-50 text-red-600 px-4 py-2 rounded-md text-sm">
|
|
352
|
+
{error}
|
|
353
|
+
</div>
|
|
354
|
+
)}
|
|
355
|
+
|
|
356
|
+
<div>
|
|
357
|
+
<label htmlFor="mfa-code" className="block text-sm font-medium mb-1">
|
|
358
|
+
Verification Code
|
|
359
|
+
</label>
|
|
360
|
+
<input
|
|
361
|
+
id="mfa-code"
|
|
362
|
+
type="text"
|
|
363
|
+
inputMode="numeric"
|
|
364
|
+
autoComplete="one-time-code"
|
|
365
|
+
value={mfaCode}
|
|
366
|
+
onChange={(e) => setMfaCode(e.target.value)}
|
|
367
|
+
className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
368
|
+
placeholder="000000"
|
|
369
|
+
maxLength={6}
|
|
370
|
+
required
|
|
371
|
+
/>
|
|
372
|
+
</div>
|
|
373
|
+
|
|
374
|
+
<button
|
|
375
|
+
type="submit"
|
|
376
|
+
disabled={loading}
|
|
377
|
+
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
378
|
+
>
|
|
379
|
+
{loading ? "Verifying..." : "Verify"}
|
|
380
|
+
</button>
|
|
381
|
+
|
|
382
|
+
<button
|
|
383
|
+
type="button"
|
|
384
|
+
onClick={() => setMfaRequired(false)}
|
|
385
|
+
className="w-full text-gray-600 text-sm hover:underline"
|
|
386
|
+
>
|
|
387
|
+
Back to login
|
|
388
|
+
</button>
|
|
389
|
+
</form>
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return (
|
|
394
|
+
<form onSubmit={handleSubmit} className="space-y-4 w-full max-w-sm">
|
|
395
|
+
<div className="text-center mb-6">
|
|
396
|
+
<h2 className="text-xl font-semibold">Sign In</h2>
|
|
397
|
+
<p className="text-gray-600 text-sm mt-1">
|
|
398
|
+
Enter your credentials to continue
|
|
399
|
+
</p>
|
|
400
|
+
</div>
|
|
401
|
+
|
|
402
|
+
{error && (
|
|
403
|
+
<div className="bg-red-50 text-red-600 px-4 py-2 rounded-md text-sm">
|
|
404
|
+
{error}
|
|
405
|
+
</div>
|
|
406
|
+
)}
|
|
407
|
+
|
|
408
|
+
<div>
|
|
409
|
+
<label htmlFor="email" className="block text-sm font-medium mb-1">
|
|
410
|
+
Email
|
|
411
|
+
</label>
|
|
412
|
+
<input
|
|
413
|
+
id="email"
|
|
414
|
+
type="email"
|
|
415
|
+
autoComplete="email"
|
|
416
|
+
value={email}
|
|
417
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
418
|
+
className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
419
|
+
placeholder="you@example.com"
|
|
420
|
+
required
|
|
421
|
+
/>
|
|
422
|
+
</div>
|
|
423
|
+
|
|
424
|
+
<div>
|
|
425
|
+
<label htmlFor="password" className="block text-sm font-medium mb-1">
|
|
426
|
+
Password
|
|
427
|
+
</label>
|
|
428
|
+
<input
|
|
429
|
+
id="password"
|
|
430
|
+
type="password"
|
|
431
|
+
autoComplete="current-password"
|
|
432
|
+
value={password}
|
|
433
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
434
|
+
className="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
435
|
+
placeholder="Your password"
|
|
436
|
+
required
|
|
437
|
+
/>
|
|
438
|
+
</div>
|
|
439
|
+
|
|
440
|
+
<button
|
|
441
|
+
type="submit"
|
|
442
|
+
disabled={loading}
|
|
443
|
+
className="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
444
|
+
>
|
|
445
|
+
{loading ? "Signing in..." : "Sign In"}
|
|
446
|
+
</button>
|
|
447
|
+
</form>
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
`;
|
|
451
|
+
var loginFormVue = `<script setup lang="ts">
|
|
452
|
+
import { ref } from "vue";
|
|
453
|
+
import { useAuthOS } from "@drmhse/authos-vue";
|
|
454
|
+
import { AuthErrorCodes } from "@drmhse/sso-sdk";
|
|
455
|
+
|
|
456
|
+
interface Props {
|
|
457
|
+
redirectTo?: string;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const props = defineProps<Props>();
|
|
461
|
+
const emit = defineEmits<{
|
|
462
|
+
success: [];
|
|
463
|
+
}>();
|
|
464
|
+
|
|
465
|
+
const { client } = useAuthOS();
|
|
466
|
+
|
|
467
|
+
const email = ref("");
|
|
468
|
+
const password = ref("");
|
|
469
|
+
const error = ref<string | null>(null);
|
|
470
|
+
const loading = ref(false);
|
|
471
|
+
const mfaRequired = ref(false);
|
|
472
|
+
const mfaCode = ref("");
|
|
473
|
+
const mfaToken = ref<string | null>(null);
|
|
474
|
+
|
|
475
|
+
async function handleSubmit() {
|
|
476
|
+
error.value = null;
|
|
477
|
+
loading.value = true;
|
|
478
|
+
|
|
479
|
+
try {
|
|
480
|
+
const response = await client.auth.login({
|
|
481
|
+
email: email.value,
|
|
482
|
+
password: password.value,
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
if (response.mfa_required) {
|
|
486
|
+
mfaRequired.value = true;
|
|
487
|
+
mfaToken.value = response.mfa_token || null;
|
|
488
|
+
} else {
|
|
489
|
+
emit("success");
|
|
490
|
+
if (props.redirectTo) {
|
|
491
|
+
window.location.href = props.redirectTo;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
} catch (err: unknown) {
|
|
495
|
+
if (err && typeof err === "object" && "errorCode" in err) {
|
|
496
|
+
const apiError = err as { errorCode: string; message: string };
|
|
497
|
+
if (apiError.errorCode === AuthErrorCodes.MFA_REQUIRED) {
|
|
498
|
+
mfaRequired.value = true;
|
|
499
|
+
} else {
|
|
500
|
+
error.value = apiError.message || "Login failed";
|
|
501
|
+
}
|
|
502
|
+
} else {
|
|
503
|
+
error.value = "An unexpected error occurred";
|
|
504
|
+
}
|
|
505
|
+
} finally {
|
|
506
|
+
loading.value = false;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
async function handleMfaSubmit() {
|
|
511
|
+
error.value = null;
|
|
512
|
+
loading.value = true;
|
|
513
|
+
|
|
514
|
+
try {
|
|
515
|
+
await client.auth.verifyMfa({
|
|
516
|
+
code: mfaCode.value,
|
|
517
|
+
mfa_token: mfaToken.value!,
|
|
518
|
+
});
|
|
519
|
+
emit("success");
|
|
520
|
+
if (props.redirectTo) {
|
|
521
|
+
window.location.href = props.redirectTo;
|
|
522
|
+
}
|
|
523
|
+
} catch (err: unknown) {
|
|
524
|
+
if (err && typeof err === "object" && "message" in err) {
|
|
525
|
+
error.value = (err as { message: string }).message || "Invalid MFA code";
|
|
526
|
+
} else {
|
|
527
|
+
error.value = "Invalid MFA code";
|
|
528
|
+
}
|
|
529
|
+
} finally {
|
|
530
|
+
loading.value = false;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function backToLogin() {
|
|
535
|
+
mfaRequired.value = false;
|
|
536
|
+
mfaCode.value = "";
|
|
537
|
+
error.value = null;
|
|
538
|
+
}
|
|
539
|
+
</script>
|
|
540
|
+
|
|
541
|
+
<template>
|
|
542
|
+
<form
|
|
543
|
+
v-if="mfaRequired"
|
|
544
|
+
@submit.prevent="handleMfaSubmit"
|
|
545
|
+
class="space-y-4 w-full max-w-sm"
|
|
546
|
+
>
|
|
547
|
+
<div class="text-center mb-6">
|
|
548
|
+
<h2 class="text-xl font-semibold">Two-Factor Authentication</h2>
|
|
549
|
+
<p class="text-gray-600 text-sm mt-1">
|
|
550
|
+
Enter the code from your authenticator app
|
|
551
|
+
</p>
|
|
552
|
+
</div>
|
|
553
|
+
|
|
554
|
+
<div
|
|
555
|
+
v-if="error"
|
|
556
|
+
class="bg-red-50 text-red-600 px-4 py-2 rounded-md text-sm"
|
|
557
|
+
>
|
|
558
|
+
{{ error }}
|
|
559
|
+
</div>
|
|
560
|
+
|
|
561
|
+
<div>
|
|
562
|
+
<label for="mfa-code" class="block text-sm font-medium mb-1">
|
|
563
|
+
Verification Code
|
|
564
|
+
</label>
|
|
565
|
+
<input
|
|
566
|
+
id="mfa-code"
|
|
567
|
+
v-model="mfaCode"
|
|
568
|
+
type="text"
|
|
569
|
+
inputmode="numeric"
|
|
570
|
+
autocomplete="one-time-code"
|
|
571
|
+
class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
572
|
+
placeholder="000000"
|
|
573
|
+
maxlength="6"
|
|
574
|
+
required
|
|
575
|
+
/>
|
|
576
|
+
</div>
|
|
577
|
+
|
|
578
|
+
<button
|
|
579
|
+
type="submit"
|
|
580
|
+
:disabled="loading"
|
|
581
|
+
class="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
582
|
+
>
|
|
583
|
+
{{ loading ? "Verifying..." : "Verify" }}
|
|
584
|
+
</button>
|
|
585
|
+
|
|
586
|
+
<button
|
|
587
|
+
type="button"
|
|
588
|
+
@click="backToLogin"
|
|
589
|
+
class="w-full text-gray-600 text-sm hover:underline"
|
|
590
|
+
>
|
|
591
|
+
Back to login
|
|
592
|
+
</button>
|
|
593
|
+
</form>
|
|
594
|
+
|
|
595
|
+
<form v-else @submit.prevent="handleSubmit" class="space-y-4 w-full max-w-sm">
|
|
596
|
+
<div class="text-center mb-6">
|
|
597
|
+
<h2 class="text-xl font-semibold">Sign In</h2>
|
|
598
|
+
<p class="text-gray-600 text-sm mt-1">
|
|
599
|
+
Enter your credentials to continue
|
|
600
|
+
</p>
|
|
601
|
+
</div>
|
|
602
|
+
|
|
603
|
+
<div
|
|
604
|
+
v-if="error"
|
|
605
|
+
class="bg-red-50 text-red-600 px-4 py-2 rounded-md text-sm"
|
|
606
|
+
>
|
|
607
|
+
{{ error }}
|
|
608
|
+
</div>
|
|
609
|
+
|
|
610
|
+
<div>
|
|
611
|
+
<label for="email" class="block text-sm font-medium mb-1"> Email </label>
|
|
612
|
+
<input
|
|
613
|
+
id="email"
|
|
614
|
+
v-model="email"
|
|
615
|
+
type="email"
|
|
616
|
+
autocomplete="email"
|
|
617
|
+
class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
618
|
+
placeholder="you@example.com"
|
|
619
|
+
required
|
|
620
|
+
/>
|
|
621
|
+
</div>
|
|
622
|
+
|
|
623
|
+
<div>
|
|
624
|
+
<label for="password" class="block text-sm font-medium mb-1">
|
|
625
|
+
Password
|
|
626
|
+
</label>
|
|
627
|
+
<input
|
|
628
|
+
id="password"
|
|
629
|
+
v-model="password"
|
|
630
|
+
type="password"
|
|
631
|
+
autocomplete="current-password"
|
|
632
|
+
class="w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
|
633
|
+
placeholder="Your password"
|
|
634
|
+
required
|
|
635
|
+
/>
|
|
636
|
+
</div>
|
|
637
|
+
|
|
638
|
+
<button
|
|
639
|
+
type="submit"
|
|
640
|
+
:disabled="loading"
|
|
641
|
+
class="w-full bg-blue-600 text-white py-2 px-4 rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
642
|
+
>
|
|
643
|
+
{{ loading ? "Signing in..." : "Sign In" }}
|
|
644
|
+
</button>
|
|
645
|
+
</form>
|
|
646
|
+
</template>
|
|
647
|
+
`;
|
|
648
|
+
var orgSwitcherReact = `"use client";
|
|
649
|
+
|
|
650
|
+
import { useState, useRef, useEffect } from "react";
|
|
651
|
+
import { useOrganization, useUser } from "@drmhse/authos-react";
|
|
652
|
+
|
|
653
|
+
export function OrganizationSwitcher() {
|
|
654
|
+
const { organization, organizations, switchOrganization, loading } = useOrganization();
|
|
655
|
+
const { user } = useUser();
|
|
656
|
+
const [open, setOpen] = useState(false);
|
|
657
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
658
|
+
|
|
659
|
+
useEffect(() => {
|
|
660
|
+
function handleClickOutside(event: MouseEvent) {
|
|
661
|
+
if (ref.current && !ref.current.contains(event.target as Node)) {
|
|
662
|
+
setOpen(false);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
667
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
668
|
+
}, []);
|
|
669
|
+
|
|
670
|
+
if (!user || organizations.length === 0) {
|
|
671
|
+
return null;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
const handleSwitch = async (slug: string) => {
|
|
675
|
+
await switchOrganization(slug);
|
|
676
|
+
setOpen(false);
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
return (
|
|
680
|
+
<div ref={ref} className="relative">
|
|
681
|
+
<button
|
|
682
|
+
onClick={() => setOpen(!open)}
|
|
683
|
+
disabled={loading}
|
|
684
|
+
className="flex items-center gap-2 px-3 py-2 text-sm border rounded-md hover:bg-gray-50 disabled:opacity-50"
|
|
685
|
+
>
|
|
686
|
+
<span className="font-medium">
|
|
687
|
+
{organization?.name || "Select Organization"}
|
|
688
|
+
</span>
|
|
689
|
+
<svg
|
|
690
|
+
className={\`w-4 h-4 transition-transform \${open ? "rotate-180" : ""}\`}
|
|
691
|
+
fill="none"
|
|
692
|
+
stroke="currentColor"
|
|
693
|
+
viewBox="0 0 24 24"
|
|
694
|
+
>
|
|
695
|
+
<path
|
|
696
|
+
strokeLinecap="round"
|
|
697
|
+
strokeLinejoin="round"
|
|
698
|
+
strokeWidth={2}
|
|
699
|
+
d="M19 9l-7 7-7-7"
|
|
700
|
+
/>
|
|
701
|
+
</svg>
|
|
702
|
+
</button>
|
|
703
|
+
|
|
704
|
+
{open && (
|
|
705
|
+
<div className="absolute right-0 mt-2 w-56 bg-white border rounded-md shadow-lg z-50">
|
|
706
|
+
<div className="py-1">
|
|
707
|
+
{organizations.map((org) => (
|
|
708
|
+
<button
|
|
709
|
+
key={org.slug}
|
|
710
|
+
onClick={() => handleSwitch(org.slug)}
|
|
711
|
+
className={\`w-full text-left px-4 py-2 text-sm hover:bg-gray-100 flex items-center justify-between \${
|
|
712
|
+
org.slug === organization?.slug ? "bg-blue-50" : ""
|
|
713
|
+
}\`}
|
|
714
|
+
>
|
|
715
|
+
<span>{org.name}</span>
|
|
716
|
+
{org.slug === organization?.slug && (
|
|
717
|
+
<svg
|
|
718
|
+
className="w-4 h-4 text-blue-600"
|
|
719
|
+
fill="currentColor"
|
|
720
|
+
viewBox="0 0 20 20"
|
|
721
|
+
>
|
|
722
|
+
<path
|
|
723
|
+
fillRule="evenodd"
|
|
724
|
+
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
|
725
|
+
clipRule="evenodd"
|
|
726
|
+
/>
|
|
727
|
+
</svg>
|
|
728
|
+
)}
|
|
729
|
+
</button>
|
|
730
|
+
))}
|
|
731
|
+
</div>
|
|
732
|
+
</div>
|
|
733
|
+
)}
|
|
734
|
+
</div>
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
`;
|
|
738
|
+
var orgSwitcherVue = `<script setup lang="ts">
|
|
739
|
+
import { ref, onMounted, onUnmounted } from "vue";
|
|
740
|
+
import { useOrganization, useUser } from "@drmhse/authos-vue";
|
|
741
|
+
|
|
742
|
+
const { organization, organizations, switchOrganization, loading } = useOrganization();
|
|
743
|
+
const { user } = useUser();
|
|
744
|
+
|
|
745
|
+
const open = ref(false);
|
|
746
|
+
const dropdownRef = ref<HTMLDivElement | null>(null);
|
|
747
|
+
|
|
748
|
+
function handleClickOutside(event: MouseEvent) {
|
|
749
|
+
if (dropdownRef.value && !dropdownRef.value.contains(event.target as Node)) {
|
|
750
|
+
open.value = false;
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
onMounted(() => {
|
|
755
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
onUnmounted(() => {
|
|
759
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
async function handleSwitch(slug: string) {
|
|
763
|
+
await switchOrganization(slug);
|
|
764
|
+
open.value = false;
|
|
765
|
+
}
|
|
766
|
+
</script>
|
|
767
|
+
|
|
768
|
+
<template>
|
|
769
|
+
<div v-if="user && organizations.length > 0" ref="dropdownRef" class="relative">
|
|
770
|
+
<button
|
|
771
|
+
@click="open = !open"
|
|
772
|
+
:disabled="loading"
|
|
773
|
+
class="flex items-center gap-2 px-3 py-2 text-sm border rounded-md hover:bg-gray-50 disabled:opacity-50"
|
|
774
|
+
>
|
|
775
|
+
<span class="font-medium">
|
|
776
|
+
{{ organization?.name || "Select Organization" }}
|
|
777
|
+
</span>
|
|
778
|
+
<svg
|
|
779
|
+
:class="['w-4 h-4 transition-transform', open ? 'rotate-180' : '']"
|
|
780
|
+
fill="none"
|
|
781
|
+
stroke="currentColor"
|
|
782
|
+
viewBox="0 0 24 24"
|
|
783
|
+
>
|
|
784
|
+
<path
|
|
785
|
+
stroke-linecap="round"
|
|
786
|
+
stroke-linejoin="round"
|
|
787
|
+
stroke-width="2"
|
|
788
|
+
d="M19 9l-7 7-7-7"
|
|
789
|
+
/>
|
|
790
|
+
</svg>
|
|
791
|
+
</button>
|
|
792
|
+
|
|
793
|
+
<div
|
|
794
|
+
v-if="open"
|
|
795
|
+
class="absolute right-0 mt-2 w-56 bg-white border rounded-md shadow-lg z-50"
|
|
796
|
+
>
|
|
797
|
+
<div class="py-1">
|
|
798
|
+
<button
|
|
799
|
+
v-for="org in organizations"
|
|
800
|
+
:key="org.slug"
|
|
801
|
+
@click="handleSwitch(org.slug)"
|
|
802
|
+
:class="[
|
|
803
|
+
'w-full text-left px-4 py-2 text-sm hover:bg-gray-100 flex items-center justify-between',
|
|
804
|
+
org.slug === organization?.slug ? 'bg-blue-50' : '',
|
|
805
|
+
]"
|
|
806
|
+
>
|
|
807
|
+
<span>{{ org.name }}</span>
|
|
808
|
+
<svg
|
|
809
|
+
v-if="org.slug === organization?.slug"
|
|
810
|
+
class="w-4 h-4 text-blue-600"
|
|
811
|
+
fill="currentColor"
|
|
812
|
+
viewBox="0 0 20 20"
|
|
813
|
+
>
|
|
814
|
+
<path
|
|
815
|
+
fill-rule="evenodd"
|
|
816
|
+
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
|
817
|
+
clip-rule="evenodd"
|
|
818
|
+
/>
|
|
819
|
+
</svg>
|
|
820
|
+
</button>
|
|
821
|
+
</div>
|
|
822
|
+
</div>
|
|
823
|
+
</div>
|
|
824
|
+
</template>
|
|
825
|
+
`;
|
|
826
|
+
var userProfileReact = `"use client";
|
|
827
|
+
|
|
828
|
+
import { useState, useRef, useEffect } from "react";
|
|
829
|
+
import { useUser, useAuthOS } from "@drmhse/authos-react";
|
|
830
|
+
|
|
831
|
+
interface UserProfileProps {
|
|
832
|
+
onSignOut?: () => void;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
export function UserProfile({ onSignOut }: UserProfileProps) {
|
|
836
|
+
const { user } = useUser();
|
|
837
|
+
const { client } = useAuthOS();
|
|
838
|
+
const [open, setOpen] = useState(false);
|
|
839
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
840
|
+
|
|
841
|
+
useEffect(() => {
|
|
842
|
+
function handleClickOutside(event: MouseEvent) {
|
|
843
|
+
if (ref.current && !ref.current.contains(event.target as Node)) {
|
|
844
|
+
setOpen(false);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
849
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
850
|
+
}, []);
|
|
851
|
+
|
|
852
|
+
if (!user) {
|
|
853
|
+
return null;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
const handleSignOut = async () => {
|
|
857
|
+
await client.auth.logout();
|
|
858
|
+
onSignOut?.();
|
|
859
|
+
setOpen(false);
|
|
860
|
+
};
|
|
861
|
+
|
|
862
|
+
const initials = user.name
|
|
863
|
+
? user.name
|
|
864
|
+
.split(" ")
|
|
865
|
+
.map((n) => n[0])
|
|
866
|
+
.join("")
|
|
867
|
+
.toUpperCase()
|
|
868
|
+
.slice(0, 2)
|
|
869
|
+
: user.email[0].toUpperCase();
|
|
870
|
+
|
|
871
|
+
return (
|
|
872
|
+
<div ref={ref} className="relative">
|
|
873
|
+
<button
|
|
874
|
+
onClick={() => setOpen(!open)}
|
|
875
|
+
className="flex items-center gap-2 p-1 rounded-full hover:bg-gray-100"
|
|
876
|
+
>
|
|
877
|
+
{user.avatar_url ? (
|
|
878
|
+
<img
|
|
879
|
+
src={user.avatar_url}
|
|
880
|
+
alt={user.name || user.email}
|
|
881
|
+
className="w-8 h-8 rounded-full object-cover"
|
|
882
|
+
/>
|
|
883
|
+
) : (
|
|
884
|
+
<div className="w-8 h-8 rounded-full bg-blue-600 text-white flex items-center justify-center text-sm font-medium">
|
|
885
|
+
{initials}
|
|
886
|
+
</div>
|
|
887
|
+
)}
|
|
888
|
+
</button>
|
|
889
|
+
|
|
890
|
+
{open && (
|
|
891
|
+
<div className="absolute right-0 mt-2 w-64 bg-white border rounded-md shadow-lg z-50">
|
|
892
|
+
<div className="p-4 border-b">
|
|
893
|
+
<div className="font-medium">{user.name || "User"}</div>
|
|
894
|
+
<div className="text-sm text-gray-600">{user.email}</div>
|
|
895
|
+
</div>
|
|
896
|
+
<div className="py-1">
|
|
897
|
+
<button
|
|
898
|
+
onClick={handleSignOut}
|
|
899
|
+
className="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50"
|
|
900
|
+
>
|
|
901
|
+
Sign out
|
|
902
|
+
</button>
|
|
903
|
+
</div>
|
|
904
|
+
</div>
|
|
905
|
+
)}
|
|
906
|
+
</div>
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
`;
|
|
910
|
+
var userProfileVue = `<script setup lang="ts">
|
|
911
|
+
import { ref, computed, onMounted, onUnmounted } from "vue";
|
|
912
|
+
import { useUser, useAuthOS } from "@drmhse/authos-vue";
|
|
913
|
+
|
|
914
|
+
const emit = defineEmits<{
|
|
915
|
+
signOut: [];
|
|
916
|
+
}>();
|
|
917
|
+
|
|
918
|
+
const { user } = useUser();
|
|
919
|
+
const { client } = useAuthOS();
|
|
920
|
+
|
|
921
|
+
const open = ref(false);
|
|
922
|
+
const dropdownRef = ref<HTMLDivElement | null>(null);
|
|
923
|
+
|
|
924
|
+
const initials = computed(() => {
|
|
925
|
+
if (!user.value) return "";
|
|
926
|
+
if (user.value.name) {
|
|
927
|
+
return user.value.name
|
|
928
|
+
.split(" ")
|
|
929
|
+
.map((n) => n[0])
|
|
930
|
+
.join("")
|
|
931
|
+
.toUpperCase()
|
|
932
|
+
.slice(0, 2);
|
|
933
|
+
}
|
|
934
|
+
return user.value.email[0].toUpperCase();
|
|
935
|
+
});
|
|
936
|
+
|
|
937
|
+
function handleClickOutside(event: MouseEvent) {
|
|
938
|
+
if (dropdownRef.value && !dropdownRef.value.contains(event.target as Node)) {
|
|
939
|
+
open.value = false;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
onMounted(() => {
|
|
944
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
945
|
+
});
|
|
946
|
+
|
|
947
|
+
onUnmounted(() => {
|
|
948
|
+
document.removeEventListener("mousedown", handleClickOutside);
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
async function handleSignOut() {
|
|
952
|
+
await client.auth.logout();
|
|
953
|
+
emit("signOut");
|
|
954
|
+
open.value = false;
|
|
955
|
+
}
|
|
956
|
+
</script>
|
|
957
|
+
|
|
958
|
+
<template>
|
|
959
|
+
<div v-if="user" ref="dropdownRef" class="relative">
|
|
960
|
+
<button
|
|
961
|
+
@click="open = !open"
|
|
962
|
+
class="flex items-center gap-2 p-1 rounded-full hover:bg-gray-100"
|
|
963
|
+
>
|
|
964
|
+
<img
|
|
965
|
+
v-if="user.avatar_url"
|
|
966
|
+
:src="user.avatar_url"
|
|
967
|
+
:alt="user.name || user.email"
|
|
968
|
+
class="w-8 h-8 rounded-full object-cover"
|
|
969
|
+
/>
|
|
970
|
+
<div
|
|
971
|
+
v-else
|
|
972
|
+
class="w-8 h-8 rounded-full bg-blue-600 text-white flex items-center justify-center text-sm font-medium"
|
|
973
|
+
>
|
|
974
|
+
{{ initials }}
|
|
975
|
+
</div>
|
|
976
|
+
</button>
|
|
977
|
+
|
|
978
|
+
<div
|
|
979
|
+
v-if="open"
|
|
980
|
+
class="absolute right-0 mt-2 w-64 bg-white border rounded-md shadow-lg z-50"
|
|
981
|
+
>
|
|
982
|
+
<div class="p-4 border-b">
|
|
983
|
+
<div class="font-medium">{{ user.name || "User" }}</div>
|
|
984
|
+
<div class="text-sm text-gray-600">{{ user.email }}</div>
|
|
985
|
+
</div>
|
|
986
|
+
<div class="py-1">
|
|
987
|
+
<button
|
|
988
|
+
@click="handleSignOut"
|
|
989
|
+
class="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50"
|
|
990
|
+
>
|
|
991
|
+
Sign out
|
|
992
|
+
</button>
|
|
993
|
+
</div>
|
|
994
|
+
</div>
|
|
995
|
+
</div>
|
|
996
|
+
</template>
|
|
997
|
+
`;
|
|
998
|
+
var templates = {
|
|
999
|
+
"login-form": {
|
|
1000
|
+
name: "Login Form",
|
|
1001
|
+
description: "A styled login form with email/password and MFA support",
|
|
1002
|
+
files: [
|
|
1003
|
+
{
|
|
1004
|
+
name: "LoginForm",
|
|
1005
|
+
content: {
|
|
1006
|
+
react: loginFormReact,
|
|
1007
|
+
next: loginFormReact,
|
|
1008
|
+
vue: loginFormVue,
|
|
1009
|
+
nuxt: loginFormVue,
|
|
1010
|
+
unknown: loginFormReact
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
]
|
|
1014
|
+
},
|
|
1015
|
+
"org-switcher": {
|
|
1016
|
+
name: "Organization Switcher",
|
|
1017
|
+
description: "A dropdown component for switching between organizations",
|
|
1018
|
+
files: [
|
|
1019
|
+
{
|
|
1020
|
+
name: "OrganizationSwitcher",
|
|
1021
|
+
content: {
|
|
1022
|
+
react: orgSwitcherReact,
|
|
1023
|
+
next: orgSwitcherReact,
|
|
1024
|
+
vue: orgSwitcherVue,
|
|
1025
|
+
nuxt: orgSwitcherVue,
|
|
1026
|
+
unknown: orgSwitcherReact
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
1029
|
+
]
|
|
1030
|
+
},
|
|
1031
|
+
"user-profile": {
|
|
1032
|
+
name: "User Profile",
|
|
1033
|
+
description: "A user avatar button with dropdown menu and sign out",
|
|
1034
|
+
files: [
|
|
1035
|
+
{
|
|
1036
|
+
name: "UserProfile",
|
|
1037
|
+
content: {
|
|
1038
|
+
react: userProfileReact,
|
|
1039
|
+
next: userProfileReact,
|
|
1040
|
+
vue: userProfileVue,
|
|
1041
|
+
nuxt: userProfileVue,
|
|
1042
|
+
unknown: userProfileReact
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
]
|
|
1046
|
+
}
|
|
1047
|
+
};
|
|
1048
|
+
function getAvailableTemplates() {
|
|
1049
|
+
return Object.keys(templates);
|
|
1050
|
+
}
|
|
1051
|
+
function getTemplate(name) {
|
|
1052
|
+
return templates[name] || null;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// src/commands/add.ts
|
|
1056
|
+
async function addCommand(templateName, options = {}) {
|
|
1057
|
+
const cwd = options.cwd || process.cwd();
|
|
1058
|
+
let framework = detectFramework(cwd);
|
|
1059
|
+
if (framework === "unknown") {
|
|
1060
|
+
log.warn("Could not detect project framework from package.json.");
|
|
1061
|
+
const response = await prompts2__default.default({
|
|
1062
|
+
type: "select",
|
|
1063
|
+
name: "framework",
|
|
1064
|
+
message: "Select your framework:",
|
|
1065
|
+
choices: [
|
|
1066
|
+
{ title: "React", value: "react" },
|
|
1067
|
+
{ title: "Next.js", value: "next" },
|
|
1068
|
+
{ title: "Vue", value: "vue" },
|
|
1069
|
+
{ title: "Nuxt", value: "nuxt" }
|
|
1070
|
+
]
|
|
1071
|
+
});
|
|
1072
|
+
if (!response.framework) {
|
|
1073
|
+
log.error("Command cancelled.");
|
|
1074
|
+
process.exit(1);
|
|
1075
|
+
}
|
|
1076
|
+
framework = response.framework;
|
|
1077
|
+
} else {
|
|
1078
|
+
log.info(`Detected ${getFrameworkName(framework)} project`);
|
|
1079
|
+
}
|
|
1080
|
+
if (!templateName) {
|
|
1081
|
+
const availableTemplates = getAvailableTemplates();
|
|
1082
|
+
const choices = availableTemplates.map((name) => {
|
|
1083
|
+
const template2 = getTemplate(name);
|
|
1084
|
+
return {
|
|
1085
|
+
title: template2.name,
|
|
1086
|
+
description: template2.description,
|
|
1087
|
+
value: name
|
|
1088
|
+
};
|
|
1089
|
+
});
|
|
1090
|
+
const response = await prompts2__default.default({
|
|
1091
|
+
type: "select",
|
|
1092
|
+
name: "template",
|
|
1093
|
+
message: "Select a component to add:",
|
|
1094
|
+
choices
|
|
1095
|
+
});
|
|
1096
|
+
if (!response.template) {
|
|
1097
|
+
log.error("Command cancelled.");
|
|
1098
|
+
process.exit(1);
|
|
1099
|
+
}
|
|
1100
|
+
templateName = response.template;
|
|
1101
|
+
}
|
|
1102
|
+
const template = getTemplate(templateName);
|
|
1103
|
+
if (!template) {
|
|
1104
|
+
log.error(`Unknown template: ${templateName}`);
|
|
1105
|
+
console.log("\nAvailable templates:");
|
|
1106
|
+
for (const name of getAvailableTemplates()) {
|
|
1107
|
+
const t = getTemplate(name);
|
|
1108
|
+
console.log(` - ${name}: ${t.description}`);
|
|
1109
|
+
}
|
|
1110
|
+
process.exit(1);
|
|
1111
|
+
}
|
|
1112
|
+
const componentsDir = findComponentsDir(cwd);
|
|
1113
|
+
const extension = getFileExtension(framework);
|
|
1114
|
+
log.info(`Adding ${template.name}...`);
|
|
1115
|
+
for (const file of template.files) {
|
|
1116
|
+
const fileName = `${file.name}${extension}`;
|
|
1117
|
+
const filePath = path2__namespace.join(componentsDir, fileName);
|
|
1118
|
+
if (fileExists(filePath) && !options.force) {
|
|
1119
|
+
const response = await prompts2__default.default({
|
|
1120
|
+
type: "confirm",
|
|
1121
|
+
name: "overwrite",
|
|
1122
|
+
message: `${fileName} already exists. Overwrite?`,
|
|
1123
|
+
initial: false
|
|
1124
|
+
});
|
|
1125
|
+
if (!response.overwrite) {
|
|
1126
|
+
log.warn(`Skipped ${fileName}`);
|
|
1127
|
+
continue;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
const content = file.content[framework];
|
|
1131
|
+
writeFile(filePath, content);
|
|
1132
|
+
log.success(`Created ${path2__namespace.relative(cwd, filePath)}`);
|
|
1133
|
+
}
|
|
1134
|
+
console.log("\n");
|
|
1135
|
+
log.success(`Added ${template.name} component!`);
|
|
1136
|
+
console.log("\nUsage:\n");
|
|
1137
|
+
if (framework === "react" || framework === "next") {
|
|
1138
|
+
console.log(` import { ${template.files[0].name} } from "@/components/${template.files[0].name}";`);
|
|
1139
|
+
console.log("");
|
|
1140
|
+
console.log(` <${template.files[0].name} />`);
|
|
1141
|
+
} else {
|
|
1142
|
+
console.log(` <script setup>`);
|
|
1143
|
+
console.log(` import ${template.files[0].name} from "@/components/${template.files[0].name}.vue";`);
|
|
1144
|
+
console.log(` </script>`);
|
|
1145
|
+
console.log("");
|
|
1146
|
+
console.log(` <${template.files[0].name} />`);
|
|
1147
|
+
}
|
|
1148
|
+
console.log("\n");
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
exports.addCommand = addCommand;
|
|
1152
|
+
exports.detectFramework = detectFramework;
|
|
1153
|
+
exports.getAdapterPackage = getAdapterPackage;
|
|
1154
|
+
exports.getAvailableTemplates = getAvailableTemplates;
|
|
1155
|
+
exports.getFrameworkName = getFrameworkName;
|
|
1156
|
+
exports.getTemplate = getTemplate;
|
|
1157
|
+
exports.initCommand = initCommand;
|