@rblez/authly 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/commands/init.js +24 -29
- package/src/commands/serve.js +102 -206
package/package.json
CHANGED
package/src/commands/init.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import chalk from "chalk";
|
|
4
4
|
import ora from "ora";
|
|
5
5
|
import { detectFramework } from "../lib/framework.js";
|
|
6
|
+
import { scanSupabase } from "../integrations/supabase.js";
|
|
6
7
|
import { generateEnv } from "../generators/env.js";
|
|
7
8
|
|
|
8
9
|
export async function cmdInit() {
|
|
@@ -11,56 +12,50 @@ export async function cmdInit() {
|
|
|
11
12
|
// Detect framework
|
|
12
13
|
const framework = detectFramework();
|
|
13
14
|
if (!framework) {
|
|
14
|
-
console.log(chalk.yellow(" No Next.js project detected. Run authly init from your Next.js project root.\n"));
|
|
15
|
+
console.log(chalk.yellow(" × No Next.js project detected. Run authly init from your Next.js project root.\n"));
|
|
15
16
|
process.exit(1);
|
|
16
17
|
}
|
|
17
18
|
console.log(`${chalk.green("✔")} Detected framework: ${chalk.cyan(framework.name)}`);
|
|
18
19
|
|
|
19
|
-
//
|
|
20
|
-
const
|
|
21
|
-
const
|
|
20
|
+
// Scan for existing Supabase credentials
|
|
21
|
+
const projectRoot = path.resolve(process.cwd());
|
|
22
|
+
const scan = scanSupabase(projectRoot);
|
|
22
23
|
|
|
23
|
-
if (
|
|
24
|
-
console.log(`${chalk.
|
|
24
|
+
if (scan.detected) {
|
|
25
|
+
console.log(`${chalk.green("✔")} Found existing Supabase credentials`);
|
|
25
26
|
}
|
|
26
|
-
if (hasAuthlyConfig) {
|
|
27
|
-
console.log(`${chalk.yellow("•")} authly.config.json already exists`);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// Generate .env.local template
|
|
31
|
-
const spinner = ora("Generating .env.local").start();
|
|
32
|
-
const envPath = path.join(process.cwd(), ".env.local");
|
|
33
27
|
|
|
34
|
-
if
|
|
28
|
+
// Generate .env.local if missing
|
|
29
|
+
const envPath = path.join(projectRoot, ".env.local");
|
|
30
|
+
if (!fs.existsSync(envPath)) {
|
|
31
|
+
const spinner = ora("Generating .env.local").start();
|
|
35
32
|
await generateEnv(envPath);
|
|
36
|
-
spinner.succeed("Generated .env.local
|
|
33
|
+
spinner.succeed("Generated .env.local");
|
|
37
34
|
} else {
|
|
38
|
-
|
|
35
|
+
console.log(`${chalk.yellow("·")} .env.local already exists`);
|
|
39
36
|
}
|
|
40
37
|
|
|
41
|
-
// Generate authly
|
|
42
|
-
const
|
|
43
|
-
if (!
|
|
38
|
+
// Generate authly.config.json if missing
|
|
39
|
+
const configPath = path.join(projectRoot, "authly.config.json");
|
|
40
|
+
if (!fs.existsSync(configPath)) {
|
|
41
|
+
const configSpinner = ora("Creating authly.config.json").start();
|
|
44
42
|
const config = {
|
|
45
43
|
$schema: "https://raw.githubusercontent.com/rblez/authly/main/schema/config.json",
|
|
46
|
-
framework,
|
|
44
|
+
framework: framework.name,
|
|
47
45
|
port: 1284,
|
|
48
46
|
supabase: {
|
|
49
|
-
url: "",
|
|
50
|
-
anonKey: "",
|
|
51
|
-
serviceRoleKey: "",
|
|
47
|
+
url: scan.url || "",
|
|
48
|
+
anonKey: scan.anonKey ? "set" : "",
|
|
49
|
+
serviceRoleKey: scan.serviceKey ? "set" : "",
|
|
50
|
+
projectRef: scan.projectRef || "",
|
|
52
51
|
},
|
|
53
52
|
providers: {},
|
|
54
53
|
roles: ["admin", "user", "guest"],
|
|
55
54
|
};
|
|
56
|
-
fs.writeFileSync(
|
|
57
|
-
fs.writeFileSync(
|
|
58
|
-
"authly.config.json",
|
|
59
|
-
JSON.stringify(config, null, 2) + "\n",
|
|
60
|
-
);
|
|
55
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
61
56
|
configSpinner.succeed("Created authly.config.json");
|
|
62
57
|
} else {
|
|
63
|
-
|
|
58
|
+
console.log(`${chalk.yellow("·")} authly.config.json already exists`);
|
|
64
59
|
}
|
|
65
60
|
|
|
66
61
|
console.log(chalk.dim("\n Next: run `npx @rblez/authly serve` to start the dashboard\n"));
|
package/src/commands/serve.js
CHANGED
|
@@ -18,13 +18,10 @@ import {
|
|
|
18
18
|
listRoles,
|
|
19
19
|
} from "../generators/roles.js";
|
|
20
20
|
import { listMigrations, getMigration, migrations } from "../generators/migrations.js";
|
|
21
|
-
import { detectFramework } from "../lib/framework.js";
|
|
22
21
|
import { scaffoldAuth, previewGenerated } from "../generators/ui.js";
|
|
23
|
-
import { generateEnv } from "../generators/env.js";
|
|
24
22
|
import { mountMcp } from "../mcp/server.js";
|
|
25
|
-
import { scanSupabase } from "../integrations/supabase.js";
|
|
26
23
|
import { generatePKCE, buildSupabaseAuthorizeUrl, exchangeSupabaseToken, saveSupabaseTokens } from "../lib/supabase-oauth.js";
|
|
27
|
-
import { ensureValidAccessToken,
|
|
24
|
+
import { ensureValidAccessToken, getProjects, getProjectApiKeys } from "../lib/supabase-api.js";
|
|
28
25
|
|
|
29
26
|
const PORT = process.env.PORT || process.env.AUTHLY_PORT || 1284;
|
|
30
27
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -41,7 +38,7 @@ export async function cmdServe() {
|
|
|
41
38
|
|
|
42
39
|
const app = new Hono();
|
|
43
40
|
|
|
44
|
-
// ── CORS — allow localhost to call
|
|
41
|
+
// ── CORS — allow localhost to call hosted API ──
|
|
45
42
|
app.use("/api/*", (c) => {
|
|
46
43
|
c.header("Access-Control-Allow-Origin", "*");
|
|
47
44
|
c.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
|
@@ -70,13 +67,6 @@ export async function cmdServe() {
|
|
|
70
67
|
// ── Public API ─────────────────────────────────────
|
|
71
68
|
app.get("/api/health", (c) => c.json({ status: "ok", uptime: process.uptime() }));
|
|
72
69
|
|
|
73
|
-
/** GET /api/integrations/supabase/scan — scan local project */
|
|
74
|
-
app.get("/api/integrations/supabase/scan", (c) => {
|
|
75
|
-
const projectRoot = path.resolve(process.cwd());
|
|
76
|
-
const scan = scanSupabase(projectRoot);
|
|
77
|
-
return c.json({ success: true, ...scan });
|
|
78
|
-
});
|
|
79
|
-
|
|
80
70
|
/** GET /api/users — list all users from Supabase */
|
|
81
71
|
app.get("/api/users", async (c) => {
|
|
82
72
|
const { client, errors } = getSupabaseClient();
|
|
@@ -101,7 +91,6 @@ export async function cmdServe() {
|
|
|
101
91
|
const result = buildAuthorizeUrl({ provider, redirectUri, state: query.state, scope: query.scope });
|
|
102
92
|
return c.redirect(result.url);
|
|
103
93
|
} catch (e) {
|
|
104
|
-
// Provider not found — redirect to authorize page
|
|
105
94
|
return c.redirect("/authorize");
|
|
106
95
|
}
|
|
107
96
|
});
|
|
@@ -115,6 +104,33 @@ export async function cmdServe() {
|
|
|
115
104
|
return c.json({ providers: listProviderStatus() });
|
|
116
105
|
});
|
|
117
106
|
|
|
107
|
+
/** POST /api/auth/:provider/authorize — get OAuth URL as JSON */
|
|
108
|
+
app.post("/api/auth/:provider/authorize", async (c) => {
|
|
109
|
+
const { provider } = c.req.param();
|
|
110
|
+
const body = await c.req.json();
|
|
111
|
+
const result = buildAuthorizeUrl({
|
|
112
|
+
provider,
|
|
113
|
+
redirectUri: body.redirectUri,
|
|
114
|
+
state: body.state,
|
|
115
|
+
scope: body.scope,
|
|
116
|
+
});
|
|
117
|
+
if (result.error) return c.json({ error: result.error }, 400);
|
|
118
|
+
return c.json(result);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
/** POST /api/auth/:provider/callback — exchange code for session */
|
|
122
|
+
app.post("/api/auth/:provider/callback", async (c) => {
|
|
123
|
+
const { provider } = c.req.param();
|
|
124
|
+
const body = await c.req.json();
|
|
125
|
+
const { user, token, error } = await handleOAuthCallback({
|
|
126
|
+
provider,
|
|
127
|
+
code: body.code,
|
|
128
|
+
redirectUri: body.redirectUri,
|
|
129
|
+
});
|
|
130
|
+
if (error) return c.json({ success: false, error }, 400);
|
|
131
|
+
return c.json({ success: true, user, token });
|
|
132
|
+
});
|
|
133
|
+
|
|
118
134
|
// ── Supabase Platform OAuth with PKCE ───────────────
|
|
119
135
|
|
|
120
136
|
/** GET /api/auth/supabase/authorize — start Supabase OAuth flow */
|
|
@@ -122,11 +138,7 @@ export async function cmdServe() {
|
|
|
122
138
|
const query = c.req.query();
|
|
123
139
|
const { verifier, challenge } = generatePKCE();
|
|
124
140
|
const state = randomBytes(16).toString("hex");
|
|
125
|
-
|
|
126
|
-
// Store verifier for later exchange
|
|
127
141
|
pkceState.set(state, { verifier, challenge });
|
|
128
|
-
|
|
129
|
-
// Auto-expire state after 10 min
|
|
130
142
|
setTimeout(() => pkceState.delete(state), 10 * 60 * 1000);
|
|
131
143
|
|
|
132
144
|
const result = buildSupabaseAuthorizeUrl({
|
|
@@ -134,65 +146,85 @@ export async function cmdServe() {
|
|
|
134
146
|
codeChallenge: challenge,
|
|
135
147
|
organizationSlug: query.organization,
|
|
136
148
|
});
|
|
137
|
-
|
|
138
149
|
return c.redirect(result.url);
|
|
139
150
|
});
|
|
140
151
|
|
|
141
|
-
/** GET /api/auth/supabase/callback — OAuth callback with PKCE
|
|
152
|
+
/** GET /api/auth/supabase/callback — OAuth callback with PKCE
|
|
153
|
+
* Saves tokens to DB. Shows project + API keys directly to user
|
|
154
|
+
* for manual copy-paste into .env.local. */
|
|
142
155
|
app.get("/api/auth/supabase/callback", async (c) => {
|
|
143
156
|
const query = c.req.query();
|
|
144
157
|
const code = query.code;
|
|
145
158
|
const state = query.state;
|
|
146
159
|
|
|
147
|
-
// Validate state and get stored verifier
|
|
148
160
|
const stored = pkceState.get(state);
|
|
149
161
|
if (!stored) {
|
|
150
|
-
return c.html(`<h1>Invalid or expired state token</h1>
|
|
151
|
-
<p>Return to <a href="/">dashboard</a></p>`, 400);
|
|
162
|
+
return c.html(`<h1>Invalid or expired state token</h1><p><a href="/">dashboard</a></p>`, 400);
|
|
152
163
|
}
|
|
153
|
-
|
|
154
164
|
pkceState.delete(state);
|
|
155
|
-
|
|
156
165
|
if (!code) {
|
|
157
|
-
return c.html(`<h1>Authorization failed</h1>
|
|
158
|
-
<p>Return to <a href="/">dashboard</a></p>`, 400);
|
|
166
|
+
return c.html(`<h1>Authorization failed</h1><p><a href="/">dashboard</a></p>`, 400);
|
|
159
167
|
}
|
|
160
168
|
|
|
161
169
|
try {
|
|
162
|
-
|
|
163
|
-
const tokens = await exchangeSupabaseToken({
|
|
164
|
-
code,
|
|
165
|
-
verifier: stored.verifier,
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
// Save tokens to DB
|
|
170
|
+
const tokens = await exchangeSupabaseToken({ code, verifier: stored.verifier });
|
|
169
171
|
await saveSupabaseTokens({
|
|
170
172
|
accessToken: tokens.access_token,
|
|
171
173
|
refreshToken: tokens.refresh_token,
|
|
172
174
|
expiresIn: tokens.expires_in,
|
|
173
175
|
});
|
|
174
176
|
|
|
175
|
-
//
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
177
|
+
// Get project info for success page
|
|
178
|
+
const projects = await getProjects(tokens.access_token);
|
|
179
|
+
const project = projects?.[0];
|
|
180
|
+
|
|
181
|
+
if (project) {
|
|
182
|
+
const keys = await getProjectApiKeys(tokens.access_token, project.id);
|
|
183
|
+
const anon = keys.find(k => k.type === "anon")?.api_key || "";
|
|
184
|
+
const svc = keys.find(k => k.type === "service_role")?.api_key || "";
|
|
185
|
+
|
|
186
|
+
// Save project ref in tokens table
|
|
187
|
+
const { client } = getSupabaseClient();
|
|
188
|
+
if (client) {
|
|
189
|
+
await client
|
|
190
|
+
.from("authly_supabase_tokens")
|
|
191
|
+
.update({ project_ref: project.id, project_name: project.name })
|
|
192
|
+
.eq("user_id", "00000000-0000-0000-0000-000000000000");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return c.html(`<!DOCTYPE html><html><head><meta charset="utf-8"><title>Authly — Connected</title>
|
|
196
|
+
<style>
|
|
197
|
+
body{font-family:sans-serif;background:#0a0a0a;color:#fff;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0}
|
|
198
|
+
.card{background:#111;border:1px solid #222;border-radius:12px;padding:40px 32px;max-width:540px;width:100%;text-align:center}
|
|
199
|
+
h1{color:#22c55e;margin:0 0 12px;font-size:1.5rem}
|
|
200
|
+
p{color:#888;font-size:.9rem;margin:0 0 8px}
|
|
201
|
+
.code{background:#0a0a0a;border:1px solid #333;border-radius:8px;padding:16px;text-align:left;font-size:.8rem;font-family:monospace;overflow-x:auto;color:#ccc;margin:12px 0;line-height:1.8;word-break:break-all}
|
|
202
|
+
.key-val{color:#22c55e}
|
|
203
|
+
.key-label{color:#666}
|
|
204
|
+
a{display:inline-block;margin-top:16px;padding:10px 24px;background:#444;color:#fff;text-decoration:none;border-radius:6px;font-size:.9rem}
|
|
205
|
+
a:hover{background:#555}
|
|
206
|
+
</style></head><body><div class="card">
|
|
207
|
+
<h1>Connected to Supabase</h1>
|
|
208
|
+
<p>Project: <strong>${project.name}</strong></p>
|
|
209
|
+
<p><code style="color:#58a6ff">${project.id}</code></p>
|
|
210
|
+
<p style="margin-top:20px;text-align:left;color:#aaa;">Add these to your project's <strong>.env.local</strong>:</p>
|
|
211
|
+
<div class="code"><span class="key-label">SUPABASE_URL=</span>"<span class="key-val">https://${project.id}.supabase.co</span>"<br>
|
|
212
|
+
<span class="key-label">SUPABASE_ANON_KEY=</span>"<span class="key-val">${anon}</span>"<br>
|
|
213
|
+
<span class="key-label">SUPABASE_SERVICE_ROLE_KEY=</span>"<span class="key-val">${svc}</span>"<br>
|
|
214
|
+
<span class="key-label">AUTHLY_SECRET=</span>"<span class="key-val">generate-with-openssl-rand-hex-32</span>"</div>
|
|
215
|
+
<a href="/">Back to Dashboard</a>
|
|
216
|
+
</div></body></html>`);
|
|
186
217
|
}
|
|
187
218
|
|
|
188
|
-
return c.html(
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
219
|
+
return c.html(`<!DOCTYPE html><html><head><meta charset="utf-8"><title>Authly</title>
|
|
220
|
+
<style>body{font-family:sans-serif;background:#0a0a0a;color:#fff;display:flex;align-items:center;justify-content:center;min-height:100vh;margin:0}
|
|
221
|
+
.card{background:#111;border:1px solid #222;border-radius:12px;padding:40px 32px;max-width:540px;width:100%;text-align:center}
|
|
222
|
+
h1{color:#22c55e;margin:0 0 12px}
|
|
223
|
+
a{color:#58a6ff}</style></head><body><div class="card">
|
|
224
|
+
<h1>Authenticated</h1><p>Supabase tokens saved, no projects found.</p>
|
|
225
|
+
<a href="/">Back to Dashboard</a></div></body></html>`);
|
|
193
226
|
} catch (e) {
|
|
194
|
-
return c.html(`<h1>Token exchange failed</h1><p>${e.message}</p>
|
|
195
|
-
<p>Return to <a href="/">dashboard</a></p>`, 400);
|
|
227
|
+
return c.html(`<h1>Token exchange failed</h1><p>${e.message}</p><p><a href="/">dashboard</a></p>`, 400);
|
|
196
228
|
}
|
|
197
229
|
});
|
|
198
230
|
|
|
@@ -200,7 +232,7 @@ export async function cmdServe() {
|
|
|
200
232
|
app.get("/api/supabase/projects", async (c) => {
|
|
201
233
|
try {
|
|
202
234
|
const token = await ensureValidAccessToken();
|
|
203
|
-
if (!token) return c.json({ error: "Not connected to Supabase
|
|
235
|
+
if (!token) return c.json({ error: "Not connected to Supabase" }, 401);
|
|
204
236
|
const projects = await getProjects(token);
|
|
205
237
|
return c.json({ success: true, projects });
|
|
206
238
|
} catch (e) {
|
|
@@ -215,7 +247,7 @@ export async function cmdServe() {
|
|
|
215
247
|
const token = await ensureValidAccessToken();
|
|
216
248
|
if (!token) return c.json({ error: "Not connected to Supabase" }, 401);
|
|
217
249
|
const keys = await getProjectApiKeys(token, ref);
|
|
218
|
-
return c.json({ success: true, keys });
|
|
250
|
+
return c.json({ success: true, keys: keys.filter(k => ["anon", "service_role"].includes(k.type)) });
|
|
219
251
|
} catch (e) {
|
|
220
252
|
return c.json({ error: e.message }, 500);
|
|
221
253
|
}
|
|
@@ -232,57 +264,7 @@ export async function cmdServe() {
|
|
|
232
264
|
}
|
|
233
265
|
});
|
|
234
266
|
|
|
235
|
-
/** POST /api/auth/:provider/authorize — get OAuth URL as JSON */
|
|
236
|
-
app.post("/api/auth/:provider/authorize", async (c) => {
|
|
237
|
-
const { provider } = c.req.param();
|
|
238
|
-
const body = await c.req.json();
|
|
239
|
-
const result = buildAuthorizeUrl({
|
|
240
|
-
provider,
|
|
241
|
-
redirectUri: body.redirectUri,
|
|
242
|
-
state: body.state,
|
|
243
|
-
scope: body.scope,
|
|
244
|
-
});
|
|
245
|
-
if (result.error) return c.json({ error: result.error }, 400);
|
|
246
|
-
return c.json(result);
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
/** POST /api/auth/ :provider/callback — exchange code for session */
|
|
250
|
-
app.post("/api/auth/:provider/callback", async (c) => {
|
|
251
|
-
const { provider } = c.req.param();
|
|
252
|
-
const body = await c.req.json();
|
|
253
|
-
const { session, error } = await exchangeOAuthCode({
|
|
254
|
-
provider,
|
|
255
|
-
code: body.code,
|
|
256
|
-
redirectUri: body.redirectUri,
|
|
257
|
-
});
|
|
258
|
-
if (error) return c.json({ success: false, error }, 400);
|
|
259
|
-
|
|
260
|
-
// Create an internal Authly session token too
|
|
261
|
-
const token = await createSessionToken({
|
|
262
|
-
sub: session.user?.id ?? "unknown",
|
|
263
|
-
role: session.user?.user_metadata?.role ?? "user",
|
|
264
|
-
});
|
|
265
|
-
return c.json({ success: true, session, token });
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
/** POST /api/auth/session — create a session token from credentials */
|
|
269
|
-
app.post("/api/auth/session", async (c) => {
|
|
270
|
-
const body = await c.req.json();
|
|
271
|
-
if (!body.sub) return c.json({ error: "Missing 'sub' field" }, 400);
|
|
272
|
-
const token = await createSessionToken({
|
|
273
|
-
sub: body.sub,
|
|
274
|
-
role: body.role ?? "user",
|
|
275
|
-
});
|
|
276
|
-
return c.json({ success: true, token });
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
/** GET /api/auth/me — verify session token from Authorization header */
|
|
280
|
-
app.get("/api/auth/me", authMiddleware(), (c) =>
|
|
281
|
-
c.json({ success: true, session: c.get("session") }),
|
|
282
|
-
);
|
|
283
|
-
|
|
284
267
|
// ── Password Auth ──────────────────────────────────
|
|
285
|
-
/** POST /api/auth/register — sign up with email + password */
|
|
286
268
|
app.post("/api/auth/register", async (c) => {
|
|
287
269
|
const body = await c.req.json();
|
|
288
270
|
const { user, token, error } = await signUp({
|
|
@@ -294,7 +276,6 @@ export async function cmdServe() {
|
|
|
294
276
|
return c.json({ success: true, user, token });
|
|
295
277
|
});
|
|
296
278
|
|
|
297
|
-
/** POST /api/auth/login — sign in with email + password */
|
|
298
279
|
app.post("/api/auth/login", async (c) => {
|
|
299
280
|
const body = await c.req.json();
|
|
300
281
|
const { user, token, error } = await signIn({
|
|
@@ -305,13 +286,25 @@ export async function cmdServe() {
|
|
|
305
286
|
return c.json({ success: true, user, token });
|
|
306
287
|
});
|
|
307
288
|
|
|
308
|
-
/** GET /api/auth/providers — list available and enabled providers */
|
|
309
289
|
app.get("/api/auth/providers", (c) => {
|
|
310
290
|
const providers = getProviders();
|
|
311
291
|
return c.json({ providers });
|
|
312
292
|
});
|
|
313
293
|
|
|
314
|
-
|
|
294
|
+
app.post("/api/auth/session", async (c) => {
|
|
295
|
+
const body = await c.req.json();
|
|
296
|
+
if (!body.sub) return c.json({ error: "Missing 'sub' field" }, 400);
|
|
297
|
+
const token = await createSessionToken({
|
|
298
|
+
sub: body.sub,
|
|
299
|
+
role: body.role ?? "user",
|
|
300
|
+
});
|
|
301
|
+
return c.json({ success: true, token });
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
app.get("/api/auth/me", authMiddleware(), (c) =>
|
|
305
|
+
c.json({ success: true, session: c.get("session") }),
|
|
306
|
+
);
|
|
307
|
+
|
|
315
308
|
app.post("/api/auth/magic-link/send", async (c) => {
|
|
316
309
|
const body = await c.req.json();
|
|
317
310
|
const result = await sendMagicLink({ email: body.email, callbackUrl: body.callbackUrl || "/" });
|
|
@@ -319,7 +312,6 @@ export async function cmdServe() {
|
|
|
319
312
|
return c.json({ success: true, sent: true });
|
|
320
313
|
});
|
|
321
314
|
|
|
322
|
-
/** POST /api/auth/magic-link/verify — verify magic link token */
|
|
323
315
|
app.post("/api/auth/magic-link/verify", async (c) => {
|
|
324
316
|
const body = await c.req.json();
|
|
325
317
|
const { user, token, error } = await verifyMagicLink({ token: body.token });
|
|
@@ -327,26 +319,13 @@ export async function cmdServe() {
|
|
|
327
319
|
return c.json({ success: true, user, token });
|
|
328
320
|
});
|
|
329
321
|
|
|
330
|
-
|
|
331
|
-
app.get("/api/config", async (c) => {
|
|
332
|
-
const fw = detectFramework();
|
|
333
|
-
const { roles = [], error } = await listRoles();
|
|
334
|
-
return c.json({
|
|
335
|
-
framework: fw ?? null,
|
|
336
|
-
providers: listProviderStatus(),
|
|
337
|
-
roles,
|
|
338
|
-
});
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
// ── Role management (protected) ────────────────────
|
|
342
|
-
/** GET /api/roles — list all roles */
|
|
322
|
+
// ── Role management ────────────────────────────────
|
|
343
323
|
app.get("/api/roles", async (c) => {
|
|
344
324
|
const { roles, error } = await listRoles();
|
|
345
325
|
if (error) return c.json({ success: false, error }, 500);
|
|
346
326
|
return c.json({ success: true, roles });
|
|
347
327
|
});
|
|
348
328
|
|
|
349
|
-
/** POST /api/roles — create a new role */
|
|
350
329
|
app.post("/api/roles", async (c) => {
|
|
351
330
|
const { name, description } = await c.req.json();
|
|
352
331
|
if (!name) return c.json({ error: "'name' is required" }, 400);
|
|
@@ -355,7 +334,6 @@ export async function cmdServe() {
|
|
|
355
334
|
return c.json(result);
|
|
356
335
|
});
|
|
357
336
|
|
|
358
|
-
/** POST /api/roles/:roleId/users/:userId/assign — assign role to user */
|
|
359
337
|
app.post("/api/roles/:roleName/users/:userId/assign", async (c) => {
|
|
360
338
|
const { roleName, userId } = c.req.param();
|
|
361
339
|
const result = await assignRoleToUser(userId, roleName);
|
|
@@ -363,7 +341,6 @@ export async function cmdServe() {
|
|
|
363
341
|
return c.json(result);
|
|
364
342
|
});
|
|
365
343
|
|
|
366
|
-
/** DELETE /api/roles/:roleId/users/:userId/revoke — revoke role from user */
|
|
367
344
|
app.delete("/api/roles/:roleName/users/:userId/revoke", async (c) => {
|
|
368
345
|
const { roleName, userId } = c.req.param();
|
|
369
346
|
const result = await revokeRoleFromUser(userId, roleName);
|
|
@@ -371,7 +348,6 @@ export async function cmdServe() {
|
|
|
371
348
|
return c.json(result);
|
|
372
349
|
});
|
|
373
350
|
|
|
374
|
-
/** GET /api/users/:userId/roles — get roles for a user */
|
|
375
351
|
app.get("/api/users/:userId/roles", async (c) => {
|
|
376
352
|
const { userId } = c.req.param();
|
|
377
353
|
const { roles, error } = await getUserRoles(userId);
|
|
@@ -380,7 +356,6 @@ export async function cmdServe() {
|
|
|
380
356
|
});
|
|
381
357
|
|
|
382
358
|
// ── API Keys ───────────────────────────────────────
|
|
383
|
-
/** POST /api/keys — generate a new API key */
|
|
384
359
|
app.post("/api/keys", async (c) => {
|
|
385
360
|
const { client, errors } = getSupabaseClient();
|
|
386
361
|
if (!client) return c.json({ success: false, errors }, 503);
|
|
@@ -400,17 +375,14 @@ export async function cmdServe() {
|
|
|
400
375
|
});
|
|
401
376
|
if (error) return c.json({ success: false, error: error.message }, 400);
|
|
402
377
|
|
|
403
|
-
// Return raw key ONCE — it cannot be retrieved later
|
|
404
378
|
return c.json({ success: true, key: rawKey, hashesTo: keyHash });
|
|
405
379
|
});
|
|
406
380
|
|
|
407
381
|
// ── Migrations ─────────────────────────────────────
|
|
408
|
-
/** GET /api/migrations — list available migrations */
|
|
409
382
|
app.get("/api/migrations", (c) =>
|
|
410
383
|
c.json({ success: true, migrations: listMigrations() }),
|
|
411
384
|
);
|
|
412
385
|
|
|
413
|
-
/** GET /api/migrations/:name/sql — get SQL for a migration */
|
|
414
386
|
app.get("/api/migrations/:name/sql", (c) => {
|
|
415
387
|
const { name } = c.req.param();
|
|
416
388
|
const sql = getMigration(name);
|
|
@@ -418,7 +390,6 @@ export async function cmdServe() {
|
|
|
418
390
|
return c.json({ success: true, name, sql });
|
|
419
391
|
});
|
|
420
392
|
|
|
421
|
-
/** POST /api/migrations/:name/run — execute a migration against Supabase */
|
|
422
393
|
app.post("/api/migrations/:name/run", async (c) => {
|
|
423
394
|
const { client, errors } = getSupabaseClient();
|
|
424
395
|
if (!client) return c.json({ success: false, errors }, 503);
|
|
@@ -432,8 +403,7 @@ export async function cmdServe() {
|
|
|
432
403
|
return c.json({ success: true, migration: name });
|
|
433
404
|
});
|
|
434
405
|
|
|
435
|
-
// ── Scaffold
|
|
436
|
-
/** POST /api/scaffold/preview — preview generated code without writing */
|
|
406
|
+
// ── Scaffold preview only ─────────────────────────
|
|
437
407
|
app.post("/api/scaffold/preview", async (c) => {
|
|
438
408
|
const { type } = await c.req.json();
|
|
439
409
|
if (!["login", "signup", "middleware", "route-login", "route-signup"].includes(type)) {
|
|
@@ -442,78 +412,6 @@ export async function cmdServe() {
|
|
|
442
412
|
return c.json({ success: true, type, code: previewGenerated(type) });
|
|
443
413
|
});
|
|
444
414
|
|
|
445
|
-
/** POST /api/scaffold/generate — write auth files to a project */
|
|
446
|
-
app.post("/api/scaffold/generate", async (c) => {
|
|
447
|
-
const { targetDir } = await c.req.json();
|
|
448
|
-
if (!targetDir) return c.json({ error: "'targetDir' is required" }, 400);
|
|
449
|
-
try {
|
|
450
|
-
const { files } = await scaffoldAuth(targetDir, { apiRoutes: true });
|
|
451
|
-
c.json({ success: true, files });
|
|
452
|
-
} catch (e) {
|
|
453
|
-
c.json({ success: false, error: e.message }, 500);
|
|
454
|
-
}
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
// ── Init ──────────────────────────────────────────
|
|
458
|
-
/** GET /api/init/scan — autodetect Supabase config in current project */
|
|
459
|
-
app.get("/api/init/scan", (c) => {
|
|
460
|
-
const projectRoot = path.resolve(process.cwd());
|
|
461
|
-
const scan = scanSupabase(projectRoot);
|
|
462
|
-
return c.json(scan);
|
|
463
|
-
});
|
|
464
|
-
|
|
465
|
-
/** POST /api/init/connect — connect with autodetected or manual config */
|
|
466
|
-
app.post("/api/init/connect", async (c) => {
|
|
467
|
-
const body = await c.req.json().catch(() => ({}));
|
|
468
|
-
|
|
469
|
-
// Auto-detect first
|
|
470
|
-
const projectRoot = path.resolve(process.cwd());
|
|
471
|
-
const scan = scanSupabase(projectRoot);
|
|
472
|
-
|
|
473
|
-
// Merge scan results with any manually provided values
|
|
474
|
-
const url = scan.url || body.supabaseUrl || process.env.SUPABASE_URL || "";
|
|
475
|
-
const anonKey = scan.anonKey || body.supabaseAnonKey || process.env.SUPABASE_ANON_KEY || "";
|
|
476
|
-
const serviceKey = scan.serviceKey || body.supabaseServiceKey || process.env.SUPABASE_SERVICE_ROLE_KEY || "";
|
|
477
|
-
|
|
478
|
-
// Set env vars from detected config
|
|
479
|
-
if (url) process.env.SUPABASE_URL = url;
|
|
480
|
-
if (anonKey) process.env.SUPABASE_ANON_KEY = anonKey;
|
|
481
|
-
if (serviceKey) process.env.SUPABASE_SERVICE_ROLE_KEY = serviceKey;
|
|
482
|
-
|
|
483
|
-
const fw = scan.framework || detectFramework();
|
|
484
|
-
if (!fw && !body.supabaseUrl) return c.json({ success: false, error: "No Next.js project detected" }, 400);
|
|
485
|
-
|
|
486
|
-
// Generate .env.local if needed
|
|
487
|
-
if (!fs.existsSync(".env.local")) {
|
|
488
|
-
await generateEnv(".env.local");
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
// Write authly.config.json
|
|
492
|
-
const config = {
|
|
493
|
-
framework: fw || "unknown",
|
|
494
|
-
supabase: {
|
|
495
|
-
url,
|
|
496
|
-
projectRef: scan.projectRef || "",
|
|
497
|
-
anonKey: anonKey ? "set" : "",
|
|
498
|
-
serviceKey: serviceKey ? "set" : "",
|
|
499
|
-
autoDetected: scan.detected,
|
|
500
|
-
sources: scan.sources,
|
|
501
|
-
},
|
|
502
|
-
};
|
|
503
|
-
fs.writeFileSync("authly.config.json", JSON.stringify(config, null, 2) + "\n");
|
|
504
|
-
|
|
505
|
-
return c.json({
|
|
506
|
-
success: true,
|
|
507
|
-
framework: fw,
|
|
508
|
-
supabase: {
|
|
509
|
-
url,
|
|
510
|
-
detected: scan.detected,
|
|
511
|
-
canConnect: scan.canConnect,
|
|
512
|
-
sources: scan.sources,
|
|
513
|
-
},
|
|
514
|
-
});
|
|
515
|
-
});
|
|
516
|
-
|
|
517
415
|
// ── MCP (beta) ─────────────────────────────────────
|
|
518
416
|
mountMcp(app);
|
|
519
417
|
|
|
@@ -521,13 +419,11 @@ export async function cmdServe() {
|
|
|
521
419
|
app.post("/api/audit", async (c) => {
|
|
522
420
|
const issues = [];
|
|
523
421
|
|
|
524
|
-
// Env vars
|
|
525
422
|
for (const key of ["SUPABASE_URL", "SUPABASE_ANON_KEY", "AUTHLY_SECRET"]) {
|
|
526
423
|
if (!process.env[key]) issues.push({ check: key, status: "fail", detail: "not set" });
|
|
527
424
|
else issues.push({ check: key, status: "ok" });
|
|
528
425
|
}
|
|
529
426
|
|
|
530
|
-
// Supabase connection
|
|
531
427
|
const { client } = getSupabaseClient();
|
|
532
428
|
if (client) {
|
|
533
429
|
const { error } = await client
|
|
@@ -553,7 +449,7 @@ export async function cmdServe() {
|
|
|
553
449
|
}
|
|
554
450
|
return c.json({
|
|
555
451
|
name: "authly",
|
|
556
|
-
version: "0.
|
|
452
|
+
version: "0.4.0",
|
|
557
453
|
docs: "/api/health",
|
|
558
454
|
});
|
|
559
455
|
});
|