@bbki.ng/backend 0.0.1 → 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/CHANGELOG.md +2 -0
- package/package.json +4 -2
- package/src/config/app.config.ts +18 -0
- package/src/controllers/authn/db.ts +98 -0
- package/src/controllers/authn/register.controller.ts +53 -0
- package/src/controllers/authn/verify.controller.ts +71 -0
- package/src/controllers/comment/add.controller.ts +25 -0
- package/src/controllers/streaming/list.controller.ts +20 -0
- package/src/index.ts +7 -37
- package/src/routes/authn.routes.ts +10 -0
- package/src/routes/comment.routes.ts +8 -0
- package/src/routes/streaming.routes.ts +8 -0
- package/src/types/index.ts +23 -0
- package/wrangler.jsonc +11 -0
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bbki.ng/backend",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"dependencies": {
|
|
6
|
+
"@simplewebauthn/server": "13.2.2",
|
|
6
7
|
"hono": "^4.10.7"
|
|
7
8
|
},
|
|
8
9
|
"devDependencies": {
|
|
9
10
|
"@cloudflare/workers-types": "4.20251128.0",
|
|
10
|
-
"
|
|
11
|
+
"@types/node": "25.0.3",
|
|
12
|
+
"wrangler": "^4.58.0"
|
|
11
13
|
},
|
|
12
14
|
"scripts": {
|
|
13
15
|
"dev": "wrangler dev",
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { D1Database } from "@cloudflare/workers-types";
|
|
3
|
+
import { cors } from "hono/cors";
|
|
4
|
+
|
|
5
|
+
type Bindings = {
|
|
6
|
+
DB: D1Database;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const app = new Hono<{ Bindings: Bindings }>();
|
|
10
|
+
|
|
11
|
+
app.use(
|
|
12
|
+
"*",
|
|
13
|
+
cors({
|
|
14
|
+
origin: ["https://bbki.ng"],
|
|
15
|
+
}),
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
export default app;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Context } from "hono";
|
|
2
|
+
import { Passkey, User } from "../../types";
|
|
3
|
+
|
|
4
|
+
export const createDatabaseQuery = (c: Context) => {
|
|
5
|
+
const env = c.env;
|
|
6
|
+
return {
|
|
7
|
+
// 用户相关
|
|
8
|
+
async getUserByUsername(username: string): Promise<User | null> {
|
|
9
|
+
// TODO: 替换为 D1 SQL 查询
|
|
10
|
+
const stmt = env.DB.prepare(
|
|
11
|
+
"SELECT id, username, display_name FROM users WHERE username = ?",
|
|
12
|
+
).bind(username);
|
|
13
|
+
const result = await stmt.first();
|
|
14
|
+
return result
|
|
15
|
+
? {
|
|
16
|
+
id: result.id as string,
|
|
17
|
+
username: result.username as string,
|
|
18
|
+
displayName: result.display_name as string,
|
|
19
|
+
}
|
|
20
|
+
: null;
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
async createUser(
|
|
24
|
+
id: string,
|
|
25
|
+
username: string,
|
|
26
|
+
displayName: string,
|
|
27
|
+
): Promise<User> {
|
|
28
|
+
await env.DB.prepare(
|
|
29
|
+
"INSERT INTO users (id, username, display_name) VALUES (?, ?, ?)",
|
|
30
|
+
)
|
|
31
|
+
.bind(id, username, displayName)
|
|
32
|
+
.run();
|
|
33
|
+
return { id, username, displayName };
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
// Passkey 相关
|
|
37
|
+
async getPasskeyById(credentialID: string): Promise<Passkey | null> {
|
|
38
|
+
const stmt = env.DB.prepare("SELECT * FROM passkeys WHERE id = ?").bind(
|
|
39
|
+
credentialID,
|
|
40
|
+
);
|
|
41
|
+
const row = await stmt.first();
|
|
42
|
+
if (!row) return null;
|
|
43
|
+
return {
|
|
44
|
+
id: row.id as string,
|
|
45
|
+
userId: row.user_id as string,
|
|
46
|
+
publicKey: row.public_key as string,
|
|
47
|
+
counter: row.counter as number,
|
|
48
|
+
deviceType: row.device_type as "singleDevice" | "multiDevice",
|
|
49
|
+
backedUp: Boolean(row.backed_up),
|
|
50
|
+
transports: row.transports
|
|
51
|
+
? JSON.parse(row.transports as string)
|
|
52
|
+
: undefined,
|
|
53
|
+
};
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
async savePasskey(passkey: Passkey): Promise<void> {
|
|
57
|
+
const transportsStr = passkey.transports
|
|
58
|
+
? JSON.stringify(passkey.transports)
|
|
59
|
+
: null;
|
|
60
|
+
await env.DB.prepare(
|
|
61
|
+
`
|
|
62
|
+
INSERT INTO passkeys (id, user_id, public_key, counter, device_type, backed_up, transports)
|
|
63
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
64
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
65
|
+
counter = excluded.counter,
|
|
66
|
+
backed_up = excluded.backed_up,
|
|
67
|
+
transports = excluded.transports
|
|
68
|
+
`,
|
|
69
|
+
)
|
|
70
|
+
.bind(
|
|
71
|
+
passkey.id,
|
|
72
|
+
passkey.userId,
|
|
73
|
+
passkey.publicKey,
|
|
74
|
+
passkey.counter,
|
|
75
|
+
passkey.deviceType,
|
|
76
|
+
passkey.backedUp ? 1 : 0,
|
|
77
|
+
transportsStr,
|
|
78
|
+
)
|
|
79
|
+
.run();
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
async listPasskeysByUserId(userId: string): Promise<Passkey[]> {
|
|
83
|
+
const stmt = env.DB.prepare(
|
|
84
|
+
"SELECT * FROM passkeys WHERE user_id = ?",
|
|
85
|
+
).bind(userId);
|
|
86
|
+
const rows = await stmt.all();
|
|
87
|
+
return rows.results.map((row: any) => ({
|
|
88
|
+
id: row.id,
|
|
89
|
+
userId: row.user_id,
|
|
90
|
+
publicKey: row.public_key,
|
|
91
|
+
counter: row.counter,
|
|
92
|
+
deviceType: row.device_type,
|
|
93
|
+
backedUp: Boolean(row.backed_up),
|
|
94
|
+
transports: row.transports ? JSON.parse(row.transports) : undefined,
|
|
95
|
+
}));
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AuthenticatorTransport,
|
|
3
|
+
generateRegistrationOptions,
|
|
4
|
+
} from "@simplewebauthn/server";
|
|
5
|
+
import { Context } from "hono";
|
|
6
|
+
import { Buffer } from "node:buffer";
|
|
7
|
+
import { createDatabaseQuery } from "./db";
|
|
8
|
+
|
|
9
|
+
const rpID = "localhost"; // 主机名,不含协议
|
|
10
|
+
|
|
11
|
+
export const registerOptions = async (c: Context) => {
|
|
12
|
+
const { username, displayName } = await c.req.json();
|
|
13
|
+
const db = createDatabaseQuery(c);
|
|
14
|
+
|
|
15
|
+
if (!username || !displayName) {
|
|
16
|
+
return c.json({ error: "Missing username or displayName" }, 400);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let user = await db.getUserByUsername(username);
|
|
20
|
+
if (!user) {
|
|
21
|
+
// 创建新用户(实际中可能需先验证邮箱等)
|
|
22
|
+
const crypto = c.env.crypto || globalThis.crypto;
|
|
23
|
+
const userId = crypto.randomUUID();
|
|
24
|
+
user = await db.createUser(userId, username, displayName);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const existingPasskeys = await db.listPasskeysByUserId(user.id);
|
|
28
|
+
|
|
29
|
+
const options = await generateRegistrationOptions({
|
|
30
|
+
rpName: "bbki.ng",
|
|
31
|
+
rpID,
|
|
32
|
+
userID: Buffer.from(user.id),
|
|
33
|
+
userName: user.username,
|
|
34
|
+
userDisplayName: user.displayName,
|
|
35
|
+
timeout: 60000,
|
|
36
|
+
attestationType: "none",
|
|
37
|
+
excludeCredentials: existingPasskeys.map((pk) => ({
|
|
38
|
+
id: atob(pk.id),
|
|
39
|
+
transports: pk.transports as AuthenticatorTransport[],
|
|
40
|
+
})),
|
|
41
|
+
authenticatorSelection: {
|
|
42
|
+
residentKey: "discouraged",
|
|
43
|
+
userVerification: "discouraged",
|
|
44
|
+
},
|
|
45
|
+
supportedAlgorithmIDs: [-7, -257], // ES256, RS256
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
c.env.KV.put(username, options.challenge, {
|
|
49
|
+
expirationTtl: 300, // 5 minutes
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return c.json(options);
|
|
53
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {
|
|
2
|
+
RegistrationResponseJSON,
|
|
3
|
+
verifyRegistrationResponse,
|
|
4
|
+
} from "@simplewebauthn/server";
|
|
5
|
+
import { Context } from "hono";
|
|
6
|
+
import { createDatabaseQuery } from "./db";
|
|
7
|
+
|
|
8
|
+
const rpID = "localhost"; // 主机名,不含协议
|
|
9
|
+
const origin = `http://${rpID}:8787`; // 完整的来源 URL,含协议和端口
|
|
10
|
+
|
|
11
|
+
export const verify = async (c: Context) => {
|
|
12
|
+
const {
|
|
13
|
+
username,
|
|
14
|
+
response,
|
|
15
|
+
}: { username: string; response: RegistrationResponseJSON } =
|
|
16
|
+
await c.req.json();
|
|
17
|
+
const db = createDatabaseQuery(c);
|
|
18
|
+
|
|
19
|
+
const user = await db.getUserByUsername(username);
|
|
20
|
+
if (!user) {
|
|
21
|
+
return c.json({ error: "User not found" }, 404);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const challenge = c.env.KV.get(username);
|
|
25
|
+
|
|
26
|
+
let verification;
|
|
27
|
+
try {
|
|
28
|
+
verification = await verifyRegistrationResponse({
|
|
29
|
+
response,
|
|
30
|
+
expectedChallenge: challenge,
|
|
31
|
+
expectedOrigin: origin,
|
|
32
|
+
expectedRPID: rpID,
|
|
33
|
+
requireUserVerification: false,
|
|
34
|
+
});
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error("Registration verification failed:", error);
|
|
37
|
+
return c.json({ error: "Registration failed" }, 400);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const { verified, registrationInfo } = verification;
|
|
41
|
+
|
|
42
|
+
if (!verified || !registrationInfo) {
|
|
43
|
+
return c.json({ error: "Registration not verified" }, 400);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const { credential, credentialDeviceType, credentialBackedUp } =
|
|
47
|
+
registrationInfo;
|
|
48
|
+
|
|
49
|
+
// 将 credentialID 转为 base64url 字符串作为主键
|
|
50
|
+
const credentialIDStr = credential.id
|
|
51
|
+
.replace(/\+/g, "-")
|
|
52
|
+
.replace(/\//g, "_")
|
|
53
|
+
.replace(/=/g, "");
|
|
54
|
+
|
|
55
|
+
const publicKeyStr = btoa(String.fromCharCode(...credential.publicKey))
|
|
56
|
+
.replace(/\+/g, "-")
|
|
57
|
+
.replace(/\//g, "_")
|
|
58
|
+
.replace(/=/g, "");
|
|
59
|
+
|
|
60
|
+
await db.savePasskey({
|
|
61
|
+
id: credentialIDStr,
|
|
62
|
+
userId: user.id,
|
|
63
|
+
publicKey: publicKeyStr,
|
|
64
|
+
counter: credential.counter,
|
|
65
|
+
deviceType: credentialDeviceType,
|
|
66
|
+
backedUp: credentialBackedUp,
|
|
67
|
+
transports: credential.transports,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return c.json({ verified: true });
|
|
71
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Context } from "hono";
|
|
2
|
+
|
|
3
|
+
export const addComment = async (c: Context) => {
|
|
4
|
+
const { articleId, author, content } = await c.req.json();
|
|
5
|
+
|
|
6
|
+
try {
|
|
7
|
+
let { results } = await c.env.DB.prepare(
|
|
8
|
+
"INSERT INTO comment (article_id, author, content, created_at) VALUES (?, ?, ?, ?);",
|
|
9
|
+
)
|
|
10
|
+
.bind(articleId, author, content, new Date().toISOString())
|
|
11
|
+
.run();
|
|
12
|
+
|
|
13
|
+
return c.json({
|
|
14
|
+
status: "success",
|
|
15
|
+
message: "Comment added successfully",
|
|
16
|
+
results,
|
|
17
|
+
});
|
|
18
|
+
} catch (error: any) {
|
|
19
|
+
return c.json({
|
|
20
|
+
status: "error",
|
|
21
|
+
message: "Failed to add comment",
|
|
22
|
+
error: error.message,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Context } from "hono";
|
|
2
|
+
|
|
3
|
+
export const listStreaming = async (c: Context) => {
|
|
4
|
+
try {
|
|
5
|
+
const { results } = await c.env.DB.prepare(
|
|
6
|
+
"SELECT id, author, content, type, created_at as createdAt FROM streaming ORDER BY created_at DESC LIMIT 100"
|
|
7
|
+
).all();
|
|
8
|
+
|
|
9
|
+
return c.json({
|
|
10
|
+
status: "success",
|
|
11
|
+
data: results,
|
|
12
|
+
});
|
|
13
|
+
} catch (error: any) {
|
|
14
|
+
return c.json({
|
|
15
|
+
status: "error",
|
|
16
|
+
message: "Failed to fetch streaming",
|
|
17
|
+
error: error.message,
|
|
18
|
+
}, 500);
|
|
19
|
+
}
|
|
20
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -1,45 +1,15 @@
|
|
|
1
|
-
import
|
|
2
|
-
import type { D1Database } from "@cloudflare/workers-types";
|
|
3
|
-
import { cors } from "hono/cors";
|
|
1
|
+
import app from "./config/app.config";
|
|
4
2
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
};
|
|
3
|
+
import { commentRouter } from "./routes/comment.routes";
|
|
4
|
+
import { authRoutes } from "./routes/authn.routes";
|
|
5
|
+
import { streamingRouter } from "./routes/streaming.routes";
|
|
8
6
|
|
|
9
|
-
|
|
7
|
+
app.route("comment", commentRouter);
|
|
8
|
+
app.route("auth", authRoutes);
|
|
9
|
+
app.route("streaming", streamingRouter);
|
|
10
10
|
|
|
11
11
|
app.get("/", (c) => {
|
|
12
12
|
return c.text("Hello Hono!");
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
-
app.post(
|
|
16
|
-
"/comment/add",
|
|
17
|
-
cors({
|
|
18
|
-
origin: ["https://bbki.ng"],
|
|
19
|
-
}),
|
|
20
|
-
async (c) => {
|
|
21
|
-
const { articleId, author, content } = await c.req.json();
|
|
22
|
-
|
|
23
|
-
try {
|
|
24
|
-
let { results } = await c.env.DB.prepare(
|
|
25
|
-
"INSERT INTO comment (article_id, author, content, created_at) VALUES (?, ?, ?, ?);",
|
|
26
|
-
)
|
|
27
|
-
.bind(articleId, author, content, new Date().toISOString())
|
|
28
|
-
.run();
|
|
29
|
-
|
|
30
|
-
return c.json({
|
|
31
|
-
status: "success",
|
|
32
|
-
message: "Comment added successfully",
|
|
33
|
-
results,
|
|
34
|
-
});
|
|
35
|
-
} catch (error: any) {
|
|
36
|
-
return c.json({
|
|
37
|
-
status: "error",
|
|
38
|
-
message: "Failed to add comment",
|
|
39
|
-
error: error.message,
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
},
|
|
43
|
-
);
|
|
44
|
-
|
|
45
15
|
export default app;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { registerOptions } from "../controllers/authn/register.controller";
|
|
3
|
+
import { verify } from "../controllers/authn/verify.controller";
|
|
4
|
+
|
|
5
|
+
const authRoutes = new Hono();
|
|
6
|
+
|
|
7
|
+
authRoutes.post("/register/options", registerOptions);
|
|
8
|
+
authRoutes.post("/register/verify", verify);
|
|
9
|
+
|
|
10
|
+
export { authRoutes };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type User = {
|
|
2
|
+
id: string;
|
|
3
|
+
username: string;
|
|
4
|
+
displayName: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type Passkey = {
|
|
8
|
+
id: string;
|
|
9
|
+
userId: string;
|
|
10
|
+
publicKey: string; // base64url
|
|
11
|
+
counter: number;
|
|
12
|
+
deviceType: "singleDevice" | "multiDevice";
|
|
13
|
+
backedUp: boolean;
|
|
14
|
+
transports?: string[]; // 存为 JSON 字符串
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type Streaming = {
|
|
18
|
+
id: string;
|
|
19
|
+
author: string;
|
|
20
|
+
content: string;
|
|
21
|
+
type?: string;
|
|
22
|
+
createdAt: string;
|
|
23
|
+
};
|
package/wrangler.jsonc
CHANGED
|
@@ -28,6 +28,17 @@
|
|
|
28
28
|
"database_id": "e938a347-b3be-4d60-9217-52f209ab7bb4",
|
|
29
29
|
},
|
|
30
30
|
],
|
|
31
|
+
// Add this to your wrangler.jsonc
|
|
32
|
+
"kv_namespaces": [
|
|
33
|
+
{
|
|
34
|
+
"binding": "KV",
|
|
35
|
+
"id": "91dd9f607aae46e5829080d1c2aa3baa",
|
|
36
|
+
|
|
37
|
+
// Optional: preview_id used when running `wrangler dev` for local dev
|
|
38
|
+
"preview_id": "BBKING_KV_NAMESPACE_PREVIEW_ID",
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
"compatibility_flags": ["nodejs_compat"],
|
|
31
42
|
// "ai": {
|
|
32
43
|
// "binding": "AI"
|
|
33
44
|
// },
|