mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
* refactor: remove root install telemetry * chore: add root telemetry cleanup job * fix: preserve legacy root telemetry deletion * fix: handle token list after account deletion * test: isolate local-auth e2e from production crons
94 lines
2.8 KiB
TypeScript
94 lines
2.8 KiB
TypeScript
import { v } from "convex/values";
|
|
import type { Doc } from "./_generated/dataModel";
|
|
import { internalMutation, internalQuery, mutation, query } from "./functions";
|
|
import { getOptionalActiveAuthUserId, requireUser } from "./lib/access";
|
|
import { generateToken, hashToken } from "./lib/tokens";
|
|
|
|
const TOKEN_TOUCH_MIN_INTERVAL_MS = 15 * 60_000;
|
|
|
|
export const listMine = query({
|
|
args: {},
|
|
handler: async (ctx) => {
|
|
const userId = await getOptionalActiveAuthUserId(ctx);
|
|
if (!userId) return [];
|
|
const tokens = await ctx.db
|
|
.query("apiTokens")
|
|
.withIndex("by_user", (q) => q.eq("userId", userId))
|
|
.order("desc")
|
|
.take(100);
|
|
return tokens.map((token) => ({
|
|
_id: token._id,
|
|
label: token.label,
|
|
prefix: token.prefix,
|
|
createdAt: token.createdAt,
|
|
lastUsedAt: token.lastUsedAt,
|
|
revokedAt: token.revokedAt,
|
|
}));
|
|
},
|
|
});
|
|
|
|
export const create = mutation({
|
|
args: { label: v.string() },
|
|
handler: async (ctx, args) => {
|
|
const { userId } = await requireUser(ctx);
|
|
const label = args.label.trim() || "CLI token";
|
|
const { token, prefix } = generateToken();
|
|
const tokenHash = await hashToken(token);
|
|
|
|
const now = Date.now();
|
|
const tokenId = await ctx.db.insert("apiTokens", {
|
|
userId,
|
|
label,
|
|
prefix,
|
|
tokenHash,
|
|
createdAt: now,
|
|
lastUsedAt: undefined,
|
|
revokedAt: undefined,
|
|
});
|
|
|
|
return { token, tokenId, label, prefix, createdAt: now };
|
|
},
|
|
});
|
|
|
|
export const revoke = mutation({
|
|
args: { tokenId: v.id("apiTokens") },
|
|
handler: async (ctx, args) => {
|
|
const { userId } = await requireUser(ctx);
|
|
const token = await ctx.db.get(args.tokenId);
|
|
if (!token) throw new Error("Token not found");
|
|
if (token.userId !== userId) throw new Error("Forbidden");
|
|
if (token.revokedAt) return;
|
|
await ctx.db.patch(token._id, { revokedAt: Date.now() });
|
|
},
|
|
});
|
|
|
|
export const getByHashInternal = internalQuery({
|
|
args: { tokenHash: v.string() },
|
|
handler: async (ctx, args) => {
|
|
return ctx.db
|
|
.query("apiTokens")
|
|
.withIndex("by_hash", (q) => q.eq("tokenHash", args.tokenHash))
|
|
.unique();
|
|
},
|
|
});
|
|
|
|
export const touchInternal = internalMutation({
|
|
args: { tokenId: v.id("apiTokens") },
|
|
handler: async (ctx, args) => {
|
|
const now = Date.now();
|
|
const token = await ctx.db.get(args.tokenId);
|
|
if (!token || token.revokedAt) return;
|
|
if (token.lastUsedAt && now - token.lastUsedAt < TOKEN_TOUCH_MIN_INTERVAL_MS) return;
|
|
await ctx.db.patch(token._id, { lastUsedAt: now });
|
|
},
|
|
});
|
|
|
|
export const getUserForTokenInternal = internalQuery({
|
|
args: { tokenId: v.id("apiTokens") },
|
|
handler: async (ctx, args): Promise<Doc<"users"> | null> => {
|
|
const token = await ctx.db.get(args.tokenId);
|
|
if (!token || token.revokedAt) return null;
|
|
return ctx.db.get(token.userId);
|
|
},
|
|
});
|