feat: tiered costs feature + admin tier management + tag selection
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:
opencode
2026-08-02 01:10:47 +02:00
parent cd4efd16f6
commit 01c70085db
27 changed files with 478 additions and 86 deletions
+19 -4
View File
@@ -105,8 +105,8 @@ export const UpdateToolBody = zod.object({
"name": zod.string().min(1).optional(),
"description": zod.string().optional(),
"category": zod.string().optional(),
"websiteUrl": zod.string().nullable().optional(),
"iconUrl": zod.string().nullable().optional(),
"websiteUrl": zod.string().nullish(),
"iconUrl": zod.string().nullish(),
"features": zod.array(zod.string()).optional(),
"tags": zod.array(zod.string()).optional()
})
@@ -288,6 +288,13 @@ export const ListAllFeaturesResponseItem = zod.string()
export const ListAllFeaturesResponse = zod.array(ListAllFeaturesResponseItem)
/**
* @summary List all distinct tag strings across all tools
*/
export const ListAllTagsResponseItem = zod.string()
export const ListAllTagsResponse = zod.array(ListAllTagsResponseItem)
/**
* @summary Get authentication mode (oidc or local)
*/
@@ -310,6 +317,8 @@ export const LocalLoginResponse = zod.object({
"name": zod.string().nullish(),
"preferredUsername": zod.string().nullish(),
"role": zod.enum(['admin', 'user']).optional(),
"tier": zod.enum(['free', 'premium', 'enterprise']).optional(),
"entitlements": zod.array(zod.string()).optional(),
"isLocal": zod.boolean().optional()
})
@@ -323,6 +332,8 @@ export const GetMeResponse = zod.object({
"name": zod.string().nullish(),
"preferredUsername": zod.string().nullish(),
"role": zod.enum(['admin', 'user']).optional(),
"tier": zod.enum(['free', 'premium', 'enterprise']).optional(),
"entitlements": zod.array(zod.string()).optional(),
"isLocal": zod.boolean().optional()
})
@@ -335,6 +346,7 @@ export const ListUsersResponseItem = zod.object({
"username": zod.string(),
"email": zod.string().nullish(),
"role": zod.enum(['admin', 'user']),
"tier": zod.enum(['free', 'premium', 'enterprise']).optional(),
"createdAt": zod.coerce.date()
})
export const ListUsersResponse = zod.array(ListUsersResponseItem)
@@ -353,7 +365,8 @@ export const CreateUserBody = zod.object({
"username": zod.string().min(createUserBodyUsernameMin),
"password": zod.string().min(createUserBodyPasswordMin),
"email": zod.string().optional(),
"role": zod.enum(['admin', 'user']).optional()
"role": zod.enum(['admin', 'user']).optional(),
"tier": zod.enum(['free', 'premium', 'enterprise']).optional()
})
@@ -365,7 +378,8 @@ export const UpdateUserParams = zod.object({
})
export const UpdateUserBody = zod.object({
"role": zod.enum(['admin', 'user'])
"role": zod.enum(['admin', 'user']).optional(),
"tier": zod.enum(['free', 'premium', 'enterprise']).optional()
})
export const UpdateUserResponse = zod.object({
@@ -373,6 +387,7 @@ export const UpdateUserResponse = zod.object({
"username": zod.string(),
"email": zod.string().nullish(),
"role": zod.enum(['admin', 'user']),
"tier": zod.enum(['free', 'premium', 'enterprise']).optional(),
"createdAt": zod.coerce.date()
})
@@ -6,6 +6,7 @@
* OpenAPI spec version: 0.1.0
*/
import type { AuthUserRole } from './authUserRole';
import type { AuthUserTier } from './authUserTier';
export interface AuthUser {
sub: string;
@@ -16,5 +17,7 @@ export interface AuthUser {
/** @nullable */
preferredUsername?: string | null;
role?: AuthUserRole;
tier?: AuthUserTier;
entitlements?: string[];
isLocal?: boolean;
}
@@ -0,0 +1,16 @@
/**
* Generated by orval v8.9.1 🍺
* Do not edit manually.
* Api
* ToolRate API — Tool listing and rating platform
* OpenAPI spec version: 0.1.0
*/
export type AuthUserTier = typeof AuthUserTier[keyof typeof AuthUserTier];
export const AuthUserTier = {
free: 'free',
premium: 'premium',
enterprise: 'enterprise',
} as const;
+4
View File
@@ -12,6 +12,7 @@ export * from './authMode';
export * from './authModeMode';
export * from './authUser';
export * from './authUserRole';
export * from './authUserTier';
export * from './categoryStats';
export * from './errorResponse';
export * from './getRatingDistributionParams';
@@ -34,6 +35,9 @@ export * from './topToolEntry';
export * from './user';
export * from './userCreateInput';
export * from './userCreateInputRole';
export * from './userCreateInputTier';
export * from './userRole';
export * from './userRoleUpdate';
export * from './userRoleUpdateRole';
export * from './userRoleUpdateTier';
export * from './userTier';
@@ -11,8 +11,10 @@ export interface ToolUpdate {
name?: string;
description?: string;
category?: string;
websiteUrl?: string;
iconUrl?: string;
/** @nullable */
websiteUrl?: string | null;
/** @nullable */
iconUrl?: string | null;
features?: string[];
tags?: string[];
}
+2
View File
@@ -6,6 +6,7 @@
* OpenAPI spec version: 0.1.0
*/
import type { UserRole } from './userRole';
import type { UserTier } from './userTier';
export interface User {
id: number;
@@ -13,5 +14,6 @@ export interface User {
/** @nullable */
email?: string | null;
role: UserRole;
tier?: UserTier;
createdAt: Date;
}
@@ -6,6 +6,7 @@
* OpenAPI spec version: 0.1.0
*/
import type { UserCreateInputRole } from './userCreateInputRole';
import type { UserCreateInputTier } from './userCreateInputTier';
export interface UserCreateInput {
/** @minLength 2 */
@@ -14,4 +15,5 @@ export interface UserCreateInput {
password: string;
email?: string;
role?: UserCreateInputRole;
tier?: UserCreateInputTier;
}
@@ -0,0 +1,16 @@
/**
* Generated by orval v8.9.1 🍺
* Do not edit manually.
* Api
* ToolRate API — Tool listing and rating platform
* OpenAPI spec version: 0.1.0
*/
export type UserCreateInputTier = typeof UserCreateInputTier[keyof typeof UserCreateInputTier];
export const UserCreateInputTier = {
free: 'free',
premium: 'premium',
enterprise: 'enterprise',
} as const;
@@ -6,7 +6,9 @@
* OpenAPI spec version: 0.1.0
*/
import type { UserRoleUpdateRole } from './userRoleUpdateRole';
import type { UserRoleUpdateTier } from './userRoleUpdateTier';
export interface UserRoleUpdate {
role: UserRoleUpdateRole;
role?: UserRoleUpdateRole;
tier?: UserRoleUpdateTier;
}
@@ -0,0 +1,16 @@
/**
* Generated by orval v8.9.1 🍺
* Do not edit manually.
* Api
* ToolRate API — Tool listing and rating platform
* OpenAPI spec version: 0.1.0
*/
export type UserRoleUpdateTier = typeof UserRoleUpdateTier[keyof typeof UserRoleUpdateTier];
export const UserRoleUpdateTier = {
free: 'free',
premium: 'premium',
enterprise: 'enterprise',
} as const;
@@ -0,0 +1,16 @@
/**
* Generated by orval v8.9.1 🍺
* Do not edit manually.
* Api
* ToolRate API — Tool listing and rating platform
* OpenAPI spec version: 0.1.0
*/
export type UserTier = typeof UserTier[keyof typeof UserTier];
export const UserTier = {
free: 'free',
premium: 'premium',
enterprise: 'enterprise',
} as const;