@rblez/authly 0.3.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/bin/authly.js +1 -1
- package/dist/dashboard/app.js +5 -2
- package/dist/dashboard/authorize.html +7 -8
- package/dist/dashboard/index.html +1 -1
- package/package.json +1 -1
- package/src/commands/init.js +24 -29
- package/src/commands/serve.js +112 -205
package/bin/authly.js
CHANGED
|
@@ -11,7 +11,7 @@ const COMMANDS = {
|
|
|
11
11
|
init: { description: "Initialize authly in your project", handler: cmdInit },
|
|
12
12
|
ext: { description: "Manage extensions (add, remove)", handler: cmdExt },
|
|
13
13
|
audit: { description: "Check auth configuration for issues", handler: cmdAudit },
|
|
14
|
-
version: { description: "Show version", handler: () => console.log("0.
|
|
14
|
+
version: { description: "Show version", handler: () => console.log("0.4.0") },
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
async function main() {
|
package/dist/dashboard/app.js
CHANGED
|
@@ -7,11 +7,14 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
7
7
|
const statusText = document.getElementById("statusText");
|
|
8
8
|
const statusDot = document.querySelector(".header__dot");
|
|
9
9
|
|
|
10
|
+
// All API calls go to the hosted authly instance
|
|
11
|
+
const API_URL = "https://authly.rblez.com/api";
|
|
12
|
+
|
|
10
13
|
checkHealth();
|
|
11
14
|
|
|
12
15
|
// ── Helpers ─────────────────────────────────────────
|
|
13
16
|
async function api(endpoint, opts = {}) {
|
|
14
|
-
const res = await fetch(
|
|
17
|
+
const res = await fetch(`${API_URL}${endpoint}`, {
|
|
15
18
|
headers: { "Content-Type": "application/json" },
|
|
16
19
|
...opts,
|
|
17
20
|
});
|
|
@@ -40,7 +43,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
40
43
|
// ── Health ──────────────────────────────────────────
|
|
41
44
|
async function checkHealth() {
|
|
42
45
|
try {
|
|
43
|
-
const res = await fetch(
|
|
46
|
+
const res = await fetch(`${API_URL}/health`);
|
|
44
47
|
if (res.ok) {
|
|
45
48
|
statusText.textContent = "Connected";
|
|
46
49
|
statusDot.style.background = "#22c55e";
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
|
|
55
55
|
<!-- Platform: Connect Supabase (OAuth to Supabase API) -->
|
|
56
56
|
<div class="section-label">Platform connection</div>
|
|
57
|
-
<a href="/api/auth/supabase/authorize" id="supabasePlatformBtn" class="platform-connect">
|
|
57
|
+
<a href="https://authly.rblez.com/api/auth/supabase/authorize" id="supabasePlatformBtn" class="platform-connect">
|
|
58
58
|
<img src="https://cdn.simpleicons.org/supabase/fff" width="20" height="20" alt="Supabase" />
|
|
59
59
|
<span class="label">Connect Supabase</span>
|
|
60
60
|
<span class="status" id="sbStatus">Not connected</span>
|
|
@@ -68,10 +68,12 @@
|
|
|
68
68
|
</div>
|
|
69
69
|
|
|
70
70
|
<script>
|
|
71
|
+
const API_URL = "https://authly.rblez.com/api";
|
|
72
|
+
|
|
71
73
|
async function loadProviders() {
|
|
72
74
|
const container = document.getElementById("providerList");
|
|
73
75
|
try {
|
|
74
|
-
const res = await fetch(
|
|
76
|
+
const res = await fetch(`${API_URL}/providers`);
|
|
75
77
|
const data = await res.json();
|
|
76
78
|
if (!data.providers) { container.innerHTML = "<p style='color:#555'>—</p>"; return; }
|
|
77
79
|
|
|
@@ -86,11 +88,10 @@
|
|
|
86
88
|
</div>`;
|
|
87
89
|
}).join("");
|
|
88
90
|
|
|
89
|
-
// Attach click handlers
|
|
90
91
|
container.querySelectorAll(".provider-btn[data-enabled='true']").forEach(el => {
|
|
91
92
|
el.addEventListener("click", () => {
|
|
92
93
|
const provider = el.dataset.provider;
|
|
93
|
-
window.location.href =
|
|
94
|
+
window.location.href = `${API_URL}/auth/${provider}/authorize`;
|
|
94
95
|
});
|
|
95
96
|
});
|
|
96
97
|
} catch {
|
|
@@ -98,14 +99,12 @@
|
|
|
98
99
|
}
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
// Check if Supabase is already connected
|
|
102
102
|
async function checkSupabaseConnected() {
|
|
103
103
|
try {
|
|
104
|
-
const res = await fetch(
|
|
104
|
+
const res = await fetch(`${API_URL}/health`);
|
|
105
105
|
if (res.ok) {
|
|
106
106
|
document.getElementById("sbStatus").textContent = "Connected";
|
|
107
|
-
|
|
108
|
-
btn.classList.add("connected");
|
|
107
|
+
document.getElementById("supabasePlatformBtn").classList.add("connected");
|
|
109
108
|
}
|
|
110
109
|
} catch {}
|
|
111
110
|
}
|
|
@@ -187,7 +187,7 @@
|
|
|
187
187
|
<div id="integrationDetail" class="hidden" style="margin-top:12px"></div>
|
|
188
188
|
<div style="margin-top:16px;display:flex;gap:8px;flex-wrap:wrap">
|
|
189
189
|
<button class="btn btn--primary btn--sm" id="reconnectBtn"><i class="ri-refresh-line"></i> Re-scan & reconnect</button>
|
|
190
|
-
<a href="/api/auth/supabase/authorize" class="btn btn--sm" style="background:#0d1b3e;border:1px solid #1d355e;color:#58a6ff;text-decoration:none" id="connectSbPlatformBtn"><i class="ri-plug-line"></i> Connect via OAuth</a>
|
|
190
|
+
<a href="https://authly.rblez.com/api/auth/supabase/authorize" target="_blank" class="btn btn--sm" style="background:#0d1b3e;border:1px solid #1d355e;color:#58a6ff;text-decoration:none" id="connectSbPlatformBtn"><i class="ri-plug-line"></i> Connect via OAuth</a>
|
|
191
191
|
</div>
|
|
192
192
|
</div>
|
|
193
193
|
</section>
|
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,6 +38,17 @@ export async function cmdServe() {
|
|
|
41
38
|
|
|
42
39
|
const app = new Hono();
|
|
43
40
|
|
|
41
|
+
// ── CORS — allow localhost to call hosted API ──
|
|
42
|
+
app.use("/api/*", (c) => {
|
|
43
|
+
c.header("Access-Control-Allow-Origin", "*");
|
|
44
|
+
c.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
|
45
|
+
c.header("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
46
|
+
if (c.req.method === "OPTIONS") {
|
|
47
|
+
return new Response(null, { status: 204 });
|
|
48
|
+
}
|
|
49
|
+
return c.next();
|
|
50
|
+
});
|
|
51
|
+
|
|
44
52
|
// ── Static file serving ────────────────────────────
|
|
45
53
|
const dashboardPath = path.join(__dirname, "../../dist/dashboard");
|
|
46
54
|
const hasDashboard = fs.existsSync(dashboardPath);
|
|
@@ -59,13 +67,6 @@ export async function cmdServe() {
|
|
|
59
67
|
// ── Public API ─────────────────────────────────────
|
|
60
68
|
app.get("/api/health", (c) => c.json({ status: "ok", uptime: process.uptime() }));
|
|
61
69
|
|
|
62
|
-
/** GET /api/integrations/supabase/scan — scan local project */
|
|
63
|
-
app.get("/api/integrations/supabase/scan", (c) => {
|
|
64
|
-
const projectRoot = path.resolve(process.cwd());
|
|
65
|
-
const scan = scanSupabase(projectRoot);
|
|
66
|
-
return c.json({ success: true, ...scan });
|
|
67
|
-
});
|
|
68
|
-
|
|
69
70
|
/** GET /api/users — list all users from Supabase */
|
|
70
71
|
app.get("/api/users", async (c) => {
|
|
71
72
|
const { client, errors } = getSupabaseClient();
|
|
@@ -90,7 +91,6 @@ export async function cmdServe() {
|
|
|
90
91
|
const result = buildAuthorizeUrl({ provider, redirectUri, state: query.state, scope: query.scope });
|
|
91
92
|
return c.redirect(result.url);
|
|
92
93
|
} catch (e) {
|
|
93
|
-
// Provider not found — redirect to authorize page
|
|
94
94
|
return c.redirect("/authorize");
|
|
95
95
|
}
|
|
96
96
|
});
|
|
@@ -104,6 +104,33 @@ export async function cmdServe() {
|
|
|
104
104
|
return c.json({ providers: listProviderStatus() });
|
|
105
105
|
});
|
|
106
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
|
+
|
|
107
134
|
// ── Supabase Platform OAuth with PKCE ───────────────
|
|
108
135
|
|
|
109
136
|
/** GET /api/auth/supabase/authorize — start Supabase OAuth flow */
|
|
@@ -111,11 +138,7 @@ export async function cmdServe() {
|
|
|
111
138
|
const query = c.req.query();
|
|
112
139
|
const { verifier, challenge } = generatePKCE();
|
|
113
140
|
const state = randomBytes(16).toString("hex");
|
|
114
|
-
|
|
115
|
-
// Store verifier for later exchange
|
|
116
141
|
pkceState.set(state, { verifier, challenge });
|
|
117
|
-
|
|
118
|
-
// Auto-expire state after 10 min
|
|
119
142
|
setTimeout(() => pkceState.delete(state), 10 * 60 * 1000);
|
|
120
143
|
|
|
121
144
|
const result = buildSupabaseAuthorizeUrl({
|
|
@@ -123,65 +146,85 @@ export async function cmdServe() {
|
|
|
123
146
|
codeChallenge: challenge,
|
|
124
147
|
organizationSlug: query.organization,
|
|
125
148
|
});
|
|
126
|
-
|
|
127
149
|
return c.redirect(result.url);
|
|
128
150
|
});
|
|
129
151
|
|
|
130
|
-
/** 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. */
|
|
131
155
|
app.get("/api/auth/supabase/callback", async (c) => {
|
|
132
156
|
const query = c.req.query();
|
|
133
157
|
const code = query.code;
|
|
134
158
|
const state = query.state;
|
|
135
159
|
|
|
136
|
-
// Validate state and get stored verifier
|
|
137
160
|
const stored = pkceState.get(state);
|
|
138
161
|
if (!stored) {
|
|
139
|
-
return c.html(`<h1>Invalid or expired state token</h1>
|
|
140
|
-
<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);
|
|
141
163
|
}
|
|
142
|
-
|
|
143
164
|
pkceState.delete(state);
|
|
144
|
-
|
|
145
165
|
if (!code) {
|
|
146
|
-
return c.html(`<h1>Authorization failed</h1>
|
|
147
|
-
<p>Return to <a href="/">dashboard</a></p>`, 400);
|
|
166
|
+
return c.html(`<h1>Authorization failed</h1><p><a href="/">dashboard</a></p>`, 400);
|
|
148
167
|
}
|
|
149
168
|
|
|
150
169
|
try {
|
|
151
|
-
|
|
152
|
-
const tokens = await exchangeSupabaseToken({
|
|
153
|
-
code,
|
|
154
|
-
verifier: stored.verifier,
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
// Save tokens to DB
|
|
170
|
+
const tokens = await exchangeSupabaseToken({ code, verifier: stored.verifier });
|
|
158
171
|
await saveSupabaseTokens({
|
|
159
172
|
accessToken: tokens.access_token,
|
|
160
173
|
refreshToken: tokens.refresh_token,
|
|
161
174
|
expiresIn: tokens.expires_in,
|
|
162
175
|
});
|
|
163
176
|
|
|
164
|
-
//
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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>`);
|
|
175
217
|
}
|
|
176
218
|
|
|
177
|
-
return c.html(
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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>`);
|
|
182
226
|
} catch (e) {
|
|
183
|
-
return c.html(`<h1>Token exchange failed</h1><p>${e.message}</p>
|
|
184
|
-
<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);
|
|
185
228
|
}
|
|
186
229
|
});
|
|
187
230
|
|
|
@@ -189,7 +232,7 @@ export async function cmdServe() {
|
|
|
189
232
|
app.get("/api/supabase/projects", async (c) => {
|
|
190
233
|
try {
|
|
191
234
|
const token = await ensureValidAccessToken();
|
|
192
|
-
if (!token) return c.json({ error: "Not connected to Supabase
|
|
235
|
+
if (!token) return c.json({ error: "Not connected to Supabase" }, 401);
|
|
193
236
|
const projects = await getProjects(token);
|
|
194
237
|
return c.json({ success: true, projects });
|
|
195
238
|
} catch (e) {
|
|
@@ -204,7 +247,7 @@ export async function cmdServe() {
|
|
|
204
247
|
const token = await ensureValidAccessToken();
|
|
205
248
|
if (!token) return c.json({ error: "Not connected to Supabase" }, 401);
|
|
206
249
|
const keys = await getProjectApiKeys(token, ref);
|
|
207
|
-
return c.json({ success: true, keys });
|
|
250
|
+
return c.json({ success: true, keys: keys.filter(k => ["anon", "service_role"].includes(k.type)) });
|
|
208
251
|
} catch (e) {
|
|
209
252
|
return c.json({ error: e.message }, 500);
|
|
210
253
|
}
|
|
@@ -221,57 +264,7 @@ export async function cmdServe() {
|
|
|
221
264
|
}
|
|
222
265
|
});
|
|
223
266
|
|
|
224
|
-
/** POST /api/auth/:provider/authorize — get OAuth URL as JSON */
|
|
225
|
-
app.post("/api/auth/:provider/authorize", async (c) => {
|
|
226
|
-
const { provider } = c.req.param();
|
|
227
|
-
const body = await c.req.json();
|
|
228
|
-
const result = buildAuthorizeUrl({
|
|
229
|
-
provider,
|
|
230
|
-
redirectUri: body.redirectUri,
|
|
231
|
-
state: body.state,
|
|
232
|
-
scope: body.scope,
|
|
233
|
-
});
|
|
234
|
-
if (result.error) return c.json({ error: result.error }, 400);
|
|
235
|
-
return c.json(result);
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
/** POST /api/auth/ :provider/callback — exchange code for session */
|
|
239
|
-
app.post("/api/auth/:provider/callback", async (c) => {
|
|
240
|
-
const { provider } = c.req.param();
|
|
241
|
-
const body = await c.req.json();
|
|
242
|
-
const { session, error } = await exchangeOAuthCode({
|
|
243
|
-
provider,
|
|
244
|
-
code: body.code,
|
|
245
|
-
redirectUri: body.redirectUri,
|
|
246
|
-
});
|
|
247
|
-
if (error) return c.json({ success: false, error }, 400);
|
|
248
|
-
|
|
249
|
-
// Create an internal Authly session token too
|
|
250
|
-
const token = await createSessionToken({
|
|
251
|
-
sub: session.user?.id ?? "unknown",
|
|
252
|
-
role: session.user?.user_metadata?.role ?? "user",
|
|
253
|
-
});
|
|
254
|
-
return c.json({ success: true, session, token });
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
/** POST /api/auth/session — create a session token from credentials */
|
|
258
|
-
app.post("/api/auth/session", async (c) => {
|
|
259
|
-
const body = await c.req.json();
|
|
260
|
-
if (!body.sub) return c.json({ error: "Missing 'sub' field" }, 400);
|
|
261
|
-
const token = await createSessionToken({
|
|
262
|
-
sub: body.sub,
|
|
263
|
-
role: body.role ?? "user",
|
|
264
|
-
});
|
|
265
|
-
return c.json({ success: true, token });
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
/** GET /api/auth/me — verify session token from Authorization header */
|
|
269
|
-
app.get("/api/auth/me", authMiddleware(), (c) =>
|
|
270
|
-
c.json({ success: true, session: c.get("session") }),
|
|
271
|
-
);
|
|
272
|
-
|
|
273
267
|
// ── Password Auth ──────────────────────────────────
|
|
274
|
-
/** POST /api/auth/register — sign up with email + password */
|
|
275
268
|
app.post("/api/auth/register", async (c) => {
|
|
276
269
|
const body = await c.req.json();
|
|
277
270
|
const { user, token, error } = await signUp({
|
|
@@ -283,7 +276,6 @@ export async function cmdServe() {
|
|
|
283
276
|
return c.json({ success: true, user, token });
|
|
284
277
|
});
|
|
285
278
|
|
|
286
|
-
/** POST /api/auth/login — sign in with email + password */
|
|
287
279
|
app.post("/api/auth/login", async (c) => {
|
|
288
280
|
const body = await c.req.json();
|
|
289
281
|
const { user, token, error } = await signIn({
|
|
@@ -294,13 +286,25 @@ export async function cmdServe() {
|
|
|
294
286
|
return c.json({ success: true, user, token });
|
|
295
287
|
});
|
|
296
288
|
|
|
297
|
-
/** GET /api/auth/providers — list available and enabled providers */
|
|
298
289
|
app.get("/api/auth/providers", (c) => {
|
|
299
290
|
const providers = getProviders();
|
|
300
291
|
return c.json({ providers });
|
|
301
292
|
});
|
|
302
293
|
|
|
303
|
-
|
|
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
|
+
|
|
304
308
|
app.post("/api/auth/magic-link/send", async (c) => {
|
|
305
309
|
const body = await c.req.json();
|
|
306
310
|
const result = await sendMagicLink({ email: body.email, callbackUrl: body.callbackUrl || "/" });
|
|
@@ -308,7 +312,6 @@ export async function cmdServe() {
|
|
|
308
312
|
return c.json({ success: true, sent: true });
|
|
309
313
|
});
|
|
310
314
|
|
|
311
|
-
/** POST /api/auth/magic-link/verify — verify magic link token */
|
|
312
315
|
app.post("/api/auth/magic-link/verify", async (c) => {
|
|
313
316
|
const body = await c.req.json();
|
|
314
317
|
const { user, token, error } = await verifyMagicLink({ token: body.token });
|
|
@@ -316,26 +319,13 @@ export async function cmdServe() {
|
|
|
316
319
|
return c.json({ success: true, user, token });
|
|
317
320
|
});
|
|
318
321
|
|
|
319
|
-
|
|
320
|
-
app.get("/api/config", async (c) => {
|
|
321
|
-
const fw = detectFramework();
|
|
322
|
-
const { roles = [], error } = await listRoles();
|
|
323
|
-
return c.json({
|
|
324
|
-
framework: fw ?? null,
|
|
325
|
-
providers: listProviderStatus(),
|
|
326
|
-
roles,
|
|
327
|
-
});
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
// ── Role management (protected) ────────────────────
|
|
331
|
-
/** GET /api/roles — list all roles */
|
|
322
|
+
// ── Role management ────────────────────────────────
|
|
332
323
|
app.get("/api/roles", async (c) => {
|
|
333
324
|
const { roles, error } = await listRoles();
|
|
334
325
|
if (error) return c.json({ success: false, error }, 500);
|
|
335
326
|
return c.json({ success: true, roles });
|
|
336
327
|
});
|
|
337
328
|
|
|
338
|
-
/** POST /api/roles — create a new role */
|
|
339
329
|
app.post("/api/roles", async (c) => {
|
|
340
330
|
const { name, description } = await c.req.json();
|
|
341
331
|
if (!name) return c.json({ error: "'name' is required" }, 400);
|
|
@@ -344,7 +334,6 @@ export async function cmdServe() {
|
|
|
344
334
|
return c.json(result);
|
|
345
335
|
});
|
|
346
336
|
|
|
347
|
-
/** POST /api/roles/:roleId/users/:userId/assign — assign role to user */
|
|
348
337
|
app.post("/api/roles/:roleName/users/:userId/assign", async (c) => {
|
|
349
338
|
const { roleName, userId } = c.req.param();
|
|
350
339
|
const result = await assignRoleToUser(userId, roleName);
|
|
@@ -352,7 +341,6 @@ export async function cmdServe() {
|
|
|
352
341
|
return c.json(result);
|
|
353
342
|
});
|
|
354
343
|
|
|
355
|
-
/** DELETE /api/roles/:roleId/users/:userId/revoke — revoke role from user */
|
|
356
344
|
app.delete("/api/roles/:roleName/users/:userId/revoke", async (c) => {
|
|
357
345
|
const { roleName, userId } = c.req.param();
|
|
358
346
|
const result = await revokeRoleFromUser(userId, roleName);
|
|
@@ -360,7 +348,6 @@ export async function cmdServe() {
|
|
|
360
348
|
return c.json(result);
|
|
361
349
|
});
|
|
362
350
|
|
|
363
|
-
/** GET /api/users/:userId/roles — get roles for a user */
|
|
364
351
|
app.get("/api/users/:userId/roles", async (c) => {
|
|
365
352
|
const { userId } = c.req.param();
|
|
366
353
|
const { roles, error } = await getUserRoles(userId);
|
|
@@ -369,7 +356,6 @@ export async function cmdServe() {
|
|
|
369
356
|
});
|
|
370
357
|
|
|
371
358
|
// ── API Keys ───────────────────────────────────────
|
|
372
|
-
/** POST /api/keys — generate a new API key */
|
|
373
359
|
app.post("/api/keys", async (c) => {
|
|
374
360
|
const { client, errors } = getSupabaseClient();
|
|
375
361
|
if (!client) return c.json({ success: false, errors }, 503);
|
|
@@ -389,17 +375,14 @@ export async function cmdServe() {
|
|
|
389
375
|
});
|
|
390
376
|
if (error) return c.json({ success: false, error: error.message }, 400);
|
|
391
377
|
|
|
392
|
-
// Return raw key ONCE — it cannot be retrieved later
|
|
393
378
|
return c.json({ success: true, key: rawKey, hashesTo: keyHash });
|
|
394
379
|
});
|
|
395
380
|
|
|
396
381
|
// ── Migrations ─────────────────────────────────────
|
|
397
|
-
/** GET /api/migrations — list available migrations */
|
|
398
382
|
app.get("/api/migrations", (c) =>
|
|
399
383
|
c.json({ success: true, migrations: listMigrations() }),
|
|
400
384
|
);
|
|
401
385
|
|
|
402
|
-
/** GET /api/migrations/:name/sql — get SQL for a migration */
|
|
403
386
|
app.get("/api/migrations/:name/sql", (c) => {
|
|
404
387
|
const { name } = c.req.param();
|
|
405
388
|
const sql = getMigration(name);
|
|
@@ -407,7 +390,6 @@ export async function cmdServe() {
|
|
|
407
390
|
return c.json({ success: true, name, sql });
|
|
408
391
|
});
|
|
409
392
|
|
|
410
|
-
/** POST /api/migrations/:name/run — execute a migration against Supabase */
|
|
411
393
|
app.post("/api/migrations/:name/run", async (c) => {
|
|
412
394
|
const { client, errors } = getSupabaseClient();
|
|
413
395
|
if (!client) return c.json({ success: false, errors }, 503);
|
|
@@ -421,8 +403,7 @@ export async function cmdServe() {
|
|
|
421
403
|
return c.json({ success: true, migration: name });
|
|
422
404
|
});
|
|
423
405
|
|
|
424
|
-
// ── Scaffold
|
|
425
|
-
/** POST /api/scaffold/preview — preview generated code without writing */
|
|
406
|
+
// ── Scaffold preview only ─────────────────────────
|
|
426
407
|
app.post("/api/scaffold/preview", async (c) => {
|
|
427
408
|
const { type } = await c.req.json();
|
|
428
409
|
if (!["login", "signup", "middleware", "route-login", "route-signup"].includes(type)) {
|
|
@@ -431,78 +412,6 @@ export async function cmdServe() {
|
|
|
431
412
|
return c.json({ success: true, type, code: previewGenerated(type) });
|
|
432
413
|
});
|
|
433
414
|
|
|
434
|
-
/** POST /api/scaffold/generate — write auth files to a project */
|
|
435
|
-
app.post("/api/scaffold/generate", async (c) => {
|
|
436
|
-
const { targetDir } = await c.req.json();
|
|
437
|
-
if (!targetDir) return c.json({ error: "'targetDir' is required" }, 400);
|
|
438
|
-
try {
|
|
439
|
-
const { files } = await scaffoldAuth(targetDir, { apiRoutes: true });
|
|
440
|
-
c.json({ success: true, files });
|
|
441
|
-
} catch (e) {
|
|
442
|
-
c.json({ success: false, error: e.message }, 500);
|
|
443
|
-
}
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
// ── Init ──────────────────────────────────────────
|
|
447
|
-
/** GET /api/init/scan — autodetect Supabase config in current project */
|
|
448
|
-
app.get("/api/init/scan", (c) => {
|
|
449
|
-
const projectRoot = path.resolve(process.cwd());
|
|
450
|
-
const scan = scanSupabase(projectRoot);
|
|
451
|
-
return c.json(scan);
|
|
452
|
-
});
|
|
453
|
-
|
|
454
|
-
/** POST /api/init/connect — connect with autodetected or manual config */
|
|
455
|
-
app.post("/api/init/connect", async (c) => {
|
|
456
|
-
const body = await c.req.json().catch(() => ({}));
|
|
457
|
-
|
|
458
|
-
// Auto-detect first
|
|
459
|
-
const projectRoot = path.resolve(process.cwd());
|
|
460
|
-
const scan = scanSupabase(projectRoot);
|
|
461
|
-
|
|
462
|
-
// Merge scan results with any manually provided values
|
|
463
|
-
const url = scan.url || body.supabaseUrl || process.env.SUPABASE_URL || "";
|
|
464
|
-
const anonKey = scan.anonKey || body.supabaseAnonKey || process.env.SUPABASE_ANON_KEY || "";
|
|
465
|
-
const serviceKey = scan.serviceKey || body.supabaseServiceKey || process.env.SUPABASE_SERVICE_ROLE_KEY || "";
|
|
466
|
-
|
|
467
|
-
// Set env vars from detected config
|
|
468
|
-
if (url) process.env.SUPABASE_URL = url;
|
|
469
|
-
if (anonKey) process.env.SUPABASE_ANON_KEY = anonKey;
|
|
470
|
-
if (serviceKey) process.env.SUPABASE_SERVICE_ROLE_KEY = serviceKey;
|
|
471
|
-
|
|
472
|
-
const fw = scan.framework || detectFramework();
|
|
473
|
-
if (!fw && !body.supabaseUrl) return c.json({ success: false, error: "No Next.js project detected" }, 400);
|
|
474
|
-
|
|
475
|
-
// Generate .env.local if needed
|
|
476
|
-
if (!fs.existsSync(".env.local")) {
|
|
477
|
-
await generateEnv(".env.local");
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// Write authly.config.json
|
|
481
|
-
const config = {
|
|
482
|
-
framework: fw || "unknown",
|
|
483
|
-
supabase: {
|
|
484
|
-
url,
|
|
485
|
-
projectRef: scan.projectRef || "",
|
|
486
|
-
anonKey: anonKey ? "set" : "",
|
|
487
|
-
serviceKey: serviceKey ? "set" : "",
|
|
488
|
-
autoDetected: scan.detected,
|
|
489
|
-
sources: scan.sources,
|
|
490
|
-
},
|
|
491
|
-
};
|
|
492
|
-
fs.writeFileSync("authly.config.json", JSON.stringify(config, null, 2) + "\n");
|
|
493
|
-
|
|
494
|
-
return c.json({
|
|
495
|
-
success: true,
|
|
496
|
-
framework: fw,
|
|
497
|
-
supabase: {
|
|
498
|
-
url,
|
|
499
|
-
detected: scan.detected,
|
|
500
|
-
canConnect: scan.canConnect,
|
|
501
|
-
sources: scan.sources,
|
|
502
|
-
},
|
|
503
|
-
});
|
|
504
|
-
});
|
|
505
|
-
|
|
506
415
|
// ── MCP (beta) ─────────────────────────────────────
|
|
507
416
|
mountMcp(app);
|
|
508
417
|
|
|
@@ -510,13 +419,11 @@ export async function cmdServe() {
|
|
|
510
419
|
app.post("/api/audit", async (c) => {
|
|
511
420
|
const issues = [];
|
|
512
421
|
|
|
513
|
-
// Env vars
|
|
514
422
|
for (const key of ["SUPABASE_URL", "SUPABASE_ANON_KEY", "AUTHLY_SECRET"]) {
|
|
515
423
|
if (!process.env[key]) issues.push({ check: key, status: "fail", detail: "not set" });
|
|
516
424
|
else issues.push({ check: key, status: "ok" });
|
|
517
425
|
}
|
|
518
426
|
|
|
519
|
-
// Supabase connection
|
|
520
427
|
const { client } = getSupabaseClient();
|
|
521
428
|
if (client) {
|
|
522
429
|
const { error } = await client
|
|
@@ -542,7 +449,7 @@ export async function cmdServe() {
|
|
|
542
449
|
}
|
|
543
450
|
return c.json({
|
|
544
451
|
name: "authly",
|
|
545
|
-
version: "0.
|
|
452
|
+
version: "0.4.0",
|
|
546
453
|
docs: "/api/health",
|
|
547
454
|
});
|
|
548
455
|
});
|