init test

This commit is contained in:
2026-01-24 00:43:25 +08:00
parent 51aed643a0
commit 5657bffd39
22 changed files with 1182 additions and 18 deletions

View File

@@ -9,6 +9,9 @@
*/
import type * as abcv from "../abcv.js";
import type * as actions_jwt from "../actions/jwt.js";
import type * as actions_redis from "../actions/redis.js";
import type * as http from "../http.js";
import type {
ApiFromModules,
@@ -18,6 +21,9 @@ import type {
declare const fullApi: ApiFromModules<{
abcv: typeof abcv;
"actions/jwt": typeof actions_jwt;
"actions/redis": typeof actions_redis;
http: typeof http;
}>;
/**

View File

@@ -1,12 +1,14 @@
import { query, mutation, action } from "./_generated/server";
import { Kevisual } from '@kevisual/ai/browser'
import { v } from "convex/values";
import { } from "convex/server";
export const get = query({
args: {},
handler: async (ctx) => {
const auth = await ctx.auth.getUserIdentity();
console.log("Query abcv.get called", auth);
console.log("Query abcv.get called, auth:", auth);
if (auth) {
console.log("Authenticated user ID:", auth.subject);
}
return await ctx.db.query("abcv").collect();
},
});

83
convex/actions/jwt.ts Normal file
View File

@@ -0,0 +1,83 @@
'use node';
import * as jose from "jose";
import type { GenericId } from "convex/values";
// JWT 验证配置
const JWT_CONFIG = {
// jwksUri: "https://kevisual.cn/root/convex/jwks.json",
jwksUri: "http://convex.kevisual.cn:3211/root/convex/jwks.json",
issuer: "https://kevisual.cn",
audience: "test-convex-app",
algorithms: ["RS256"],
};
// 创建 JWKS 缓存
let jwksCache: jose.JWTVerifyGetKey | null = null;
function getJWKS() {
if (!jwksCache) {
jwksCache = jose.createRemoteJWKSet(new URL(JWT_CONFIG.jwksUri));
}
return jwksCache;
}
export const createJwt = async (payload: jose.JWTPayload): Promise<string> => {
const secret = new TextEncoder().encode("your-256-bit-secret");
const token = await new jose.SignJWT(payload)
.setProtectedHeader({ alg: "HS256", typ: "JWT" })
.setIssuedAt()
.setExpirationTime("2h")
.sign(secret);
return token;
}
// 验证 JWT token 并返回 payload
export async function verifyToken(token: string): Promise<jose.JWTPayload> {
const JWKS = getJWKS();
const { payload } = await jose.jwtVerify(token, JWKS, {
issuer: JWT_CONFIG.issuer,
audience: JWT_CONFIG.audience,
algorithms: JWT_CONFIG.algorithms,
});
return payload;
}
// 从请求 header 中提取 Bearer token
export function extractBearerToken(authHeader: string | null): string | null {
if (!authHeader) return null;
const match = authHeader.match(/^Bearer\s+(.+)$/i);
return match ? match[1] : null;
}
// 获取或创建用户(演示用)
export async function getOrCreateUser(
db: any,
tokenPayload: jose.JWTPayload
): Promise<GenericId<"users">> {
const externalId = tokenPayload.sub as string;
// 查找已存在的用户
const existingUser = await db
.query("users")
.withIndex("id", (q: any) => q.eq("id", externalId))
.first();
if (existingUser) {
// 更新最后登录时间
await db
.table("users")
.doc(existingUser._id)
.patch({ lastLoginAt: new Date().toISOString() });
return existingUser._id;
}
// 创建新用户
return await db
.table("users")
.insert({
id: externalId,
name: (tokenPayload.name as string) || "Unknown",
email: (tokenPayload.email as string) || "",
createdAt: new Date().toISOString(),
});
}

22
convex/actions/redis.ts Normal file
View File

@@ -0,0 +1,22 @@
'use node';
import { query, mutation, action } from "../_generated/server";
import { Kevisual } from '@kevisual/ai/browser'
import { v } from "convex/values";
import { Redis } from "ioredis";
const redisClient = new Redis({
host: process.env.REDIS_HOST,
password: process.env.REDIS_PASSWORD,
});
let time: any = null;
export const isConnected = action({
args: {},
handler: async (ctx) => {
const result = await redisClient.ping();
if (time === null) {
time = new Date();
}
console.log("Redis PING at", new Date().toISOString(), "since", time);
return result === "PONG";
},
});

13
convex/auth.config.ts Normal file
View File

@@ -0,0 +1,13 @@
import { AuthConfig } from "convex/server";
export default {
providers: [
{
type: "customJwt",
applicationID: "convex-app",
issuer: "https://convex.kevisual.cn",
jwks: "https://api-convex.kevisual.cn/root/convex/jwks.json",
algorithm: "RS256",
},
],
};

38
convex/http.ts Normal file
View File

@@ -0,0 +1,38 @@
import { httpRouter } from "convex/server";
import { httpAction } from "./_generated/server";
const http = httpRouter();
http.route({
path: "/auth",
method: "POST",
handler: httpAction(async (ctx, request) => {
// 处理请求并返回响应
return new Response(JSON.stringify({ message: "Hello from custom endpoint!" }), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}),
})
http.route({
path: '/root/convex/jwks.json',
method: 'GET',
handler: httpAction(async (ctx, request) => {
// 返回 JWKS 数据
const jwks = {
"keys": [
{
"kty": "RSA",
"n": "km4cjJJOMFkl2G5qWMuFmWwF7rmeqRYzYdR8SddKeeMW0e9yIf5pv2Mfwv0aMJUpb-_j3j9M7whx_SEGc_2jx1vxCu1AlYURhnnLTWdsR-ZRPr2LK9UstMrgpWV425R2RliqXTDTYlSxUUlD9nPue_tqbfwN2aM9MCarm67xK_ZCcKRlW9o9L2-9UMfzRA7uiy4VQtOemP0PTXp-E9RxNiMPOQXIRls9wTW_EkDT3dGy7JCZhj7_qib3T8k9m84SwU7wI2R_3IH4DcHSMAn1BRRMXZ1_wPhbP39laNtdJgbDjGCqUccGhLUaoo2WGkZ52eb7NPqamp0K1Dh2jwTIJQ",
"e": "AQAB",
"kid": "kid-key-1"
}
]
};
return new Response(JSON.stringify(jwks), {
status: 200,
headers: { "Content-Type": "application/json" },
});
}),
})
export default http;

View File

@@ -1,9 +1,21 @@
import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
import { authTables } from "@convex-dev/auth/server";
// Define a messages table with an index.
export default defineSchema({
abcv: defineTable({
title: v.string(),
})
}),
users: defineTable({
id: v.string(), // 外部系统的用户 ID
name: v.string(),
createdAt: v.string(),
lastLoginAt: v.optional(v.string()),
}).index("id", ["id"]),
sessions: defineTable({
userId: v.id("users"),
createdAt: v.string(),
expiresAt: v.optional(v.string()),
token: v.optional(v.string()),
}).index("token", ["token"]),
});