@rblez/authly 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/README.md +215 -0
- package/bin/authly.js +53 -0
- package/dist/dashboard/app.js +326 -0
- package/dist/dashboard/index.html +238 -0
- package/dist/dashboard/styles.css +742 -0
- package/package.json +48 -0
- package/src/auth/index.js +134 -0
- package/src/commands/audit.js +82 -0
- package/src/commands/init.js +67 -0
- package/src/commands/serve.js +383 -0
- package/src/generators/env.js +37 -0
- package/src/generators/migrations.js +138 -0
- package/src/generators/roles.js +67 -0
- package/src/generators/ui.js +619 -0
- package/src/lib/framework.js +29 -0
- package/src/lib/jwt.js +107 -0
- package/src/lib/oauth.js +301 -0
- package/src/lib/supabase.js +58 -0
- package/src/mcp/server.js +281 -0
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rblez/authly",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local auth dashboard for Next.js + Supabase",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"authly": "./bin/authly.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "node bin/authly.js serve",
|
|
11
|
+
"lint": "prettier --check src/",
|
|
12
|
+
"format": "prettier --write src/"
|
|
13
|
+
},
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18.0.0"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"bin/",
|
|
19
|
+
"src/",
|
|
20
|
+
"dist/"
|
|
21
|
+
],
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@hono/node-server": "^1.19.12",
|
|
24
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
25
|
+
"@supabase/supabase-js": "^2.101.1",
|
|
26
|
+
"bcryptjs": "^2.4.3",
|
|
27
|
+
"chalk": "^5.6.2",
|
|
28
|
+
"hono": "^4.12.10",
|
|
29
|
+
"jose": "^6.2.2",
|
|
30
|
+
"ora": "^9.3.0",
|
|
31
|
+
"zod": "^4.3.6"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"prettier": "^3.8.1"
|
|
35
|
+
},
|
|
36
|
+
"keywords": [
|
|
37
|
+
"auth",
|
|
38
|
+
"supabase",
|
|
39
|
+
"nextjs",
|
|
40
|
+
"dashboard"
|
|
41
|
+
],
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"author": "rblez <rblez@proton.me>",
|
|
44
|
+
"repository": {
|
|
45
|
+
"type": "git",
|
|
46
|
+
"url": "git+https://github.com/rblez/authly.git"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authly SDK — unified auth interface.
|
|
3
|
+
*
|
|
4
|
+
* signIn / signUp / signOut / getProviders / getDashboardConfig
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import bcrypt from "bcryptjs";
|
|
8
|
+
import { getSupabaseClient } from "../lib/supabase.js";
|
|
9
|
+
import { createSessionToken, verifySessionToken } from "../lib/jwt.js";
|
|
10
|
+
import { buildAuthorizeUrl, exchangeTokens, upsertUser, authWithProvider, listProviderStatus } from "../lib/oauth.js";
|
|
11
|
+
|
|
12
|
+
// ── Password auth ────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Register a new user with email + password.
|
|
16
|
+
* Creates user in authly_users, hashes password with bcrypt.
|
|
17
|
+
*/
|
|
18
|
+
export async function signUp({ email, password, name = "" }) {
|
|
19
|
+
const { client, errors } = getSupabaseClient();
|
|
20
|
+
if (!client) return { user: null, error: errors.join(", ") };
|
|
21
|
+
|
|
22
|
+
if (!email || !password || password.length < 8) {
|
|
23
|
+
return { user: null, error: "Email and password (min 8 chars) are required" };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check if user exists
|
|
27
|
+
const { data: existing } = await client
|
|
28
|
+
.from("authly_users")
|
|
29
|
+
.select("id")
|
|
30
|
+
.eq("email", email.toLowerCase())
|
|
31
|
+
.single();
|
|
32
|
+
|
|
33
|
+
if (existing) {
|
|
34
|
+
return { user: null, error: "An account with this email already exists" };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const passwordHash = await bcrypt.hash(password, 12);
|
|
38
|
+
|
|
39
|
+
const { data, error } = await client
|
|
40
|
+
.from("authly_users")
|
|
41
|
+
.insert({ email: email.toLowerCase(), password_hash: passwordHash, name })
|
|
42
|
+
.select("id, email, name, created_at")
|
|
43
|
+
.single();
|
|
44
|
+
|
|
45
|
+
if (error) return { user: null, error: error.message };
|
|
46
|
+
|
|
47
|
+
const token = await createSessionToken({ sub: data.id, role: "user" });
|
|
48
|
+
return { user: data, token, error: null };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Sign in with email + password.
|
|
53
|
+
*/
|
|
54
|
+
export async function signIn({ email, password }) {
|
|
55
|
+
const { client, errors } = getSupabaseClient();
|
|
56
|
+
if (!client) return { user: null, error: errors.join(", ") };
|
|
57
|
+
|
|
58
|
+
const { data, error } = await client
|
|
59
|
+
.from("authly_users")
|
|
60
|
+
.select("id, email, name, password_hash, email_verified")
|
|
61
|
+
.eq("email", email.toLowerCase())
|
|
62
|
+
.single();
|
|
63
|
+
|
|
64
|
+
if (error || !data) {
|
|
65
|
+
return { user: null, error: "Invalid email or password" };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!data.password_hash) {
|
|
69
|
+
return { user: null, error: "This account uses OAuth login. Sign in with your provider." };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const valid = await bcrypt.compare(password, data.password_hash);
|
|
73
|
+
if (!valid) {
|
|
74
|
+
return { user: null, error: "Invalid email or password" };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const token = await createSessionToken({ sub: data.id, role: "user" });
|
|
78
|
+
const { id, email: userEmail, name, email_verified: emailVerified } = data;
|
|
79
|
+
return {
|
|
80
|
+
user: { id, email: userEmail, name, emailVerified },
|
|
81
|
+
token,
|
|
82
|
+
error: null,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Sign out (invalidate session — token-based so no-op server-side).
|
|
88
|
+
*/
|
|
89
|
+
export function signOut() {
|
|
90
|
+
return { success: true };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Verify an existing session token.
|
|
95
|
+
*/
|
|
96
|
+
export async function getSession(token) {
|
|
97
|
+
if (!token) return { session: null, error: "No token provided" };
|
|
98
|
+
const session = await verifySessionToken(token);
|
|
99
|
+
if (!session) return { session: null, error: "Invalid or expired token" };
|
|
100
|
+
return { session, error: null };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Provider management ──────────────────────────────
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get list of all available auth providers and their enabled status.
|
|
107
|
+
*/
|
|
108
|
+
export function getProviders() {
|
|
109
|
+
return listProviderStatus();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Build authorization URL for a provider.
|
|
114
|
+
*/
|
|
115
|
+
export function getProviderUrl({ provider, redirectUri, state, scope }) {
|
|
116
|
+
try {
|
|
117
|
+
return buildAuthorizeUrl({ provider, redirectUri, state, scope });
|
|
118
|
+
} catch (e) {
|
|
119
|
+
return { error: e.message };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Handle OAuth callback — exchange code, upsert user, create session.
|
|
125
|
+
*/
|
|
126
|
+
export async function handleOAuthCallback({ provider, code, redirectUri }) {
|
|
127
|
+
try {
|
|
128
|
+
const user = await authWithProvider({ provider, code, redirectUri });
|
|
129
|
+
const token = await createSessionToken({ sub: user.userId, role: "user" });
|
|
130
|
+
return { user, token, error: null };
|
|
131
|
+
} catch (e) {
|
|
132
|
+
return { user: null, token: null, error: e.message };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import ora from "ora";
|
|
4
|
+
import { detectFramework } from "../lib/framework.js";
|
|
5
|
+
|
|
6
|
+
export async function cmdAudit() {
|
|
7
|
+
console.log(chalk.bold("\n Authly Audit\n"));
|
|
8
|
+
|
|
9
|
+
let issues = [];
|
|
10
|
+
|
|
11
|
+
// Check 1: Project detection
|
|
12
|
+
const fw = detectFramework();
|
|
13
|
+
if (fw) {
|
|
14
|
+
console.log(`${chalk.green("✔")} Project detected: ${chalk.cyan(fw.name)}`);
|
|
15
|
+
} else {
|
|
16
|
+
issues.push("No recognizable project framework found");
|
|
17
|
+
console.log(`${chalk.red("✘")} No project framework detected`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Check 2: .env.local
|
|
21
|
+
if (fs.existsSync(".env.local")) {
|
|
22
|
+
console.log(`${chalk.green("✔")} .env.local exists`);
|
|
23
|
+
const envContent = fs.readFileSync(".env.local", "utf-8");
|
|
24
|
+
const vars = [
|
|
25
|
+
"SUPABASE_URL",
|
|
26
|
+
"SUPABASE_ANON_KEY",
|
|
27
|
+
"SUPABASE_SERVICE_ROLE_KEY",
|
|
28
|
+
"AUTHLY_SECRET",
|
|
29
|
+
];
|
|
30
|
+
for (const v of vars) {
|
|
31
|
+
if (envContent.includes(v)) {
|
|
32
|
+
const val = envContent.split(v)[1]?.split("=")[1]?.trim();
|
|
33
|
+
if (val && val !== '""' && val !== "''") {
|
|
34
|
+
console.log(` ${chalk.green("✔")} ${v} is set`);
|
|
35
|
+
} else {
|
|
36
|
+
issues.push(`${v} is empty`);
|
|
37
|
+
console.log(` ${chalk.red("✘")} ${v} is empty`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
} else {
|
|
42
|
+
issues.push(".env.local not found — run `npx @rblez/authly init`");
|
|
43
|
+
console.log(`${chalk.red("✘")} .env.local not found`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check 3: authly.config.json
|
|
47
|
+
if (fs.existsSync("authly.config.json")) {
|
|
48
|
+
console.log(`${chalk.green("✔")} authly.config.json exists`);
|
|
49
|
+
try {
|
|
50
|
+
JSON.parse(fs.readFileSync("authly.config.json", "utf-8"));
|
|
51
|
+
console.log(`${chalk.green("✔")} authly.config.json is valid JSON`);
|
|
52
|
+
} catch {
|
|
53
|
+
issues.push("authly.config.json contains invalid JSON");
|
|
54
|
+
console.log(`${chalk.red("✘")} authly.config.json is invalid JSON`);
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
issues.push("authly.config.json not found");
|
|
58
|
+
console.log(`${chalk.red("✘")} authly.config.json not found`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Check 4: Middleware
|
|
62
|
+
const middlewarePath = "middleware.ts";
|
|
63
|
+
const middlewarePath2 = "middleware.tsx";
|
|
64
|
+
if (fs.existsSync(middlewarePath) || fs.existsSync(middlewarePath2)) {
|
|
65
|
+
console.log(`${chalk.green("✔")} Next.js middleware exists`);
|
|
66
|
+
} else {
|
|
67
|
+
issues.push("Next.js middleware not found");
|
|
68
|
+
console.log(`${chalk.yellow("•")} Next.js middleware not found (optional)`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Summary
|
|
72
|
+
console.log();
|
|
73
|
+
if (issues.length === 0) {
|
|
74
|
+
console.log(chalk.green.bold(" All checks passed — your auth configuration looks good\n"));
|
|
75
|
+
} else {
|
|
76
|
+
console.log(chalk.red.bold(` ${issues.length} issue(s) found:\n`));
|
|
77
|
+
for (const issue of issues) {
|
|
78
|
+
console.log(` ${chalk.red("•")} ${issue}`);
|
|
79
|
+
}
|
|
80
|
+
console.log();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import ora from "ora";
|
|
5
|
+
import { detectFramework } from "../lib/framework.js";
|
|
6
|
+
import { generateEnv } from "../generators/env.js";
|
|
7
|
+
|
|
8
|
+
export async function cmdInit() {
|
|
9
|
+
console.log(chalk.bold("\n Authly Init\n"));
|
|
10
|
+
|
|
11
|
+
// Detect framework
|
|
12
|
+
const framework = detectFramework();
|
|
13
|
+
if (!framework) {
|
|
14
|
+
console.log(chalk.yellow(" No Next.js project detected. Run authly init from your Next.js project root.\n"));
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
console.log(`${chalk.green("✔")} Detected framework: ${chalk.cyan(framework.name)}`);
|
|
18
|
+
|
|
19
|
+
// Check for existing config
|
|
20
|
+
const hasEnv = fs.existsSync(".env.local");
|
|
21
|
+
const hasAuthlyConfig = fs.existsSync("authly.config.json");
|
|
22
|
+
|
|
23
|
+
if (hasEnv) {
|
|
24
|
+
console.log(`${chalk.yellow("•")} .env.local already exists`);
|
|
25
|
+
}
|
|
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
|
+
|
|
34
|
+
if (!hasEnv) {
|
|
35
|
+
await generateEnv(envPath);
|
|
36
|
+
spinner.succeed("Generated .env.local with authly variables");
|
|
37
|
+
} else {
|
|
38
|
+
spinner.info(".env.local already exists, skipping");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Generate authly config
|
|
42
|
+
const configSpinner = ora("Creating authly.config.json").start();
|
|
43
|
+
if (!hasAuthlyConfig) {
|
|
44
|
+
const config = {
|
|
45
|
+
$schema: "https://raw.githubusercontent.com/rblez/authly/main/schema/config.json",
|
|
46
|
+
framework,
|
|
47
|
+
port: 1284,
|
|
48
|
+
supabase: {
|
|
49
|
+
url: "",
|
|
50
|
+
anonKey: "",
|
|
51
|
+
serviceRoleKey: "",
|
|
52
|
+
},
|
|
53
|
+
providers: {},
|
|
54
|
+
roles: ["admin", "user", "guest"],
|
|
55
|
+
};
|
|
56
|
+
fs.writeFileSync(envPath ? ".env.local" : ".env.local", "", { flag: "a" });
|
|
57
|
+
fs.writeFileSync(
|
|
58
|
+
"authly.config.json",
|
|
59
|
+
JSON.stringify(config, null, 2) + "\n",
|
|
60
|
+
);
|
|
61
|
+
configSpinner.succeed("Created authly.config.json");
|
|
62
|
+
} else {
|
|
63
|
+
configSpinner.info("authly.config.json already exists");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
console.log(chalk.dim("\n Next: run `npx @rblez/authly serve` to start the dashboard\n"));
|
|
67
|
+
}
|