feat: tiered costs feature + admin tier management + tag selection
Build & Push Docker Image / build (push) Successful in 6m52s
Build & Push Docker Image / build (push) Successful in 6m52s
- costs: nullable notes (fix create without notes), drop renewalDate (schema + API + UI), gate POST/PATCH/DELETE to admin + costs feature - feature middleware: admin-aware hasFeature + getEntitlements union; /auth/me and login return resolved entitlements - users: tier enum (free/premium/enterprise) in create/update/list, admin UI tier select + tier badge - tags: GET /tags/all, TagInput autocomplete in new/edit tool forms, feature suggestions on focus, query invalidation on create/update - openapi: nullable ToolUpdate urls, ToolUpdate tier fields, listAllTags - Dockerfile: push-force to drop renewal_date column
This commit is contained in:
@@ -8,15 +8,19 @@ import { z } from "zod";
|
||||
|
||||
const router: IRouter = Router();
|
||||
|
||||
const Tier = z.enum(["free", "premium", "enterprise"]);
|
||||
|
||||
const UserCreateSchema = z.object({
|
||||
username: z.string().min(2),
|
||||
password: z.string().min(6),
|
||||
email: z.string().optional(),
|
||||
role: z.enum(["admin", "user"]).optional().default("user"),
|
||||
tier: Tier.optional().default("free"),
|
||||
});
|
||||
|
||||
const UserRoleUpdateSchema = z.object({
|
||||
role: z.enum(["admin", "user"]),
|
||||
const UserUpdateSchema = z.object({
|
||||
role: z.enum(["admin", "user"]).optional(),
|
||||
tier: Tier.optional(),
|
||||
});
|
||||
|
||||
router.get("/users", requireAdmin, async (req, res): Promise<void> => {
|
||||
@@ -26,6 +30,7 @@ router.get("/users", requireAdmin, async (req, res): Promise<void> => {
|
||||
username: usersTable.username,
|
||||
email: usersTable.email,
|
||||
role: usersTable.role,
|
||||
tier: usersTable.tier,
|
||||
createdAt: usersTable.createdAt,
|
||||
})
|
||||
.from(usersTable)
|
||||
@@ -59,16 +64,18 @@ router.post("/users", requireAdmin, async (req, res): Promise<void> => {
|
||||
passwordHash,
|
||||
email: parsed.data.email ?? null,
|
||||
role: parsed.data.role ?? "user",
|
||||
tier: parsed.data.tier ?? "free",
|
||||
})
|
||||
.returning({
|
||||
id: usersTable.id,
|
||||
username: usersTable.username,
|
||||
email: usersTable.email,
|
||||
role: usersTable.role,
|
||||
tier: usersTable.tier,
|
||||
createdAt: usersTable.createdAt,
|
||||
});
|
||||
|
||||
await writeAuditLog(req, "user", user.id, "create", { username: user.username, role: user.role });
|
||||
await writeAuditLog(req, "user", user.id, "create", { username: user.username, role: user.role, tier: user.tier });
|
||||
res.status(201).json(user);
|
||||
});
|
||||
|
||||
@@ -79,7 +86,7 @@ router.patch("/users/:id", requireAdmin, async (req, res): Promise<void> => {
|
||||
return;
|
||||
}
|
||||
|
||||
const parsed = UserRoleUpdateSchema.safeParse(req.body);
|
||||
const parsed = UserUpdateSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
res.status(400).json({ error: parsed.error.message });
|
||||
return;
|
||||
@@ -111,19 +118,24 @@ router.patch("/users/:id", requireAdmin, async (req, res): Promise<void> => {
|
||||
}
|
||||
}
|
||||
|
||||
const updateData: Record<string, unknown> = {};
|
||||
if (parsed.data.role !== undefined) updateData.role = parsed.data.role;
|
||||
if (parsed.data.tier !== undefined) updateData.tier = parsed.data.tier;
|
||||
|
||||
const [user] = await db
|
||||
.update(usersTable)
|
||||
.set({ role: parsed.data.role })
|
||||
.set(updateData)
|
||||
.where(eq(usersTable.id, id))
|
||||
.returning({
|
||||
id: usersTable.id,
|
||||
username: usersTable.username,
|
||||
email: usersTable.email,
|
||||
role: usersTable.role,
|
||||
tier: usersTable.tier,
|
||||
createdAt: usersTable.createdAt,
|
||||
});
|
||||
|
||||
await writeAuditLog(req, "user", id, "update", { role: parsed.data.role });
|
||||
await writeAuditLog(req, "user", id, "update", updateData);
|
||||
res.json(user);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user