feat(rices): mejorar reglas de publicación de rices

- Permitir publicar un rice con un token existente si no excede el límite.
- Configurar límite de 5 rices por token a través de variable de entorno (env.MAX_RICES_BY_TOKEN).
- Validar duplicidad de nombres al crear un nuevo rice.
This commit is contained in:
oscargonzalezmoreno@gmail.com 2024-12-28 15:56:51 +01:00
parent 612e27a55c
commit 9bf1fe8e0d
7 changed files with 122 additions and 9 deletions

View file

@ -5,4 +5,6 @@ GITHUB_REPO_NAME=rices-store
SUPABASE_URL=https://xxxxxxxxxxxxx.supabase.co
SUPABASE_KEY=XXXXXXXXXXXXXXXXXXX
MAX_RICES_BY_TOKEN=5
MODERATION_SECRET=superSecret123

View file

@ -12,9 +12,8 @@ CREATE TABLE rices (
level INTEGER DEFAULT 0 NOT NULL, -- Level: 0 (Public), 1 (Verified)
created_at TIMESTAMP DEFAULT NOW(), -- Creation date
updated_at TIMESTAMP, -- Last update date
PRIMARY KEY (id, slug), -- Composite primary key
UNIQUE (slug), -- Ensure slug is unique
UNIQUE (name) -- Ensure name is unique
PRIMARY KEY (id), -- Composite primary key
UNIQUE (slug) -- Ensure slug is unique
);
CREATE OR REPLACE FUNCTION increment_visits(slug_param TEXT)

View file

@ -41,10 +41,11 @@ export class RicesController {
async createRice(
@Body() content: string,
@Headers() headers: Record<string, string>,
@Headers('x-zen-rices-token') token: string,
) {
const contentString =
typeof content === 'string' ? content : JSON.stringify(content);
return this.ricesService.create(contentString, headers);
return this.ricesService.create(contentString, token, headers);
}
@ApiOperation({ summary: 'Get information about a Rice' })

View file

@ -7,6 +7,7 @@ import {
} from '@nestjs/common';
import { v4 as uuidv4 } from 'uuid';
import { generateSlug } from './utils/slug.util';
import { ConfigService } from '@nestjs/config';
import { GitHubService } from '../github/github.service';
import { SupabaseService } from '../supabase/supabase.service';
@ -17,9 +18,14 @@ export class RicesService {
constructor(
private readonly gitHubService: GitHubService,
private readonly supabaseService: SupabaseService,
private readonly configService: ConfigService,
) {}
async create(content: string, headers: Record<string, string>) {
async create(
content: string,
token: string | null,
headers: Record<string, string>,
) {
try {
// Validate headers
const name = headers['x-zen-rice-name'];
@ -77,12 +83,12 @@ export class RicesService {
}
// Check if a rice with the same name already exists
const existingRice = await this.supabaseService.getRiceByName(name);
/*const existingRice = await this.supabaseService.getRiceByName(name);
if (existingRice) {
throw new ConflictException(
`A rice with the name '${name}' already exists.`,
);
}
}*/
let slug: string;
try {
@ -93,7 +99,21 @@ export class RicesService {
throw new BadRequestException(`Invalid name provided`);
}
const token = uuidv4();
if (!token) {
token = uuidv4();
} else {
const tokenMaxCount = this.configService.get<number>(
'MAX_RICES_BY_TOKEN',
5,
);
const tokenCount = await this.supabaseService.countRicesByToken(token);
if (tokenCount >= tokenMaxCount) {
throw new ConflictException(
`The token '${token}' is already associated with 5 or more rices.`,
);
}
}
const metadata = {
id: uuidv4(),
token,

View file

@ -132,4 +132,18 @@ export class SupabaseService {
throw new Error(`Failed to update rice level: ${error.message}`);
}
}
async countRicesByToken(token: string): Promise<number> {
const { data, error, count } = await this.supabase
.from('rices') // Nombre de tu tabla en Supabase
.select('*', { count: 'exact' })
.eq('token', token);
if (error) {
console.error('Error counting rices by token:', error);
throw new Error('Failed to count rices by token');
}
return count || 0;
}
}

View file

@ -2,7 +2,7 @@
POST {{baseUrl}}/rices
Content-Type: application/json
X-Zen-Rice-Name: cool-zenrice-test-base64211
X-Zen-Rice-Name: cool-zenrice-test-base
X-Zen-Rice-Author: jhon@doe.com
User-Agent: ZenBrowser/1.2b.0 (EndeavourOS x86_64)

View file

@ -0,0 +1,77 @@
@baseUrl = http://localhost:3000
@previous_token = 32af2ec5-b7f0-4c61-8c9a-e67edb3faad4
POST {{baseUrl}}/rices
Content-Type: application/json
X-Zen-Rice-Name: cool-zenrice-test-base5
X-Zen-Rice-Author: jhon@doe.com
x-zen-rices-token: {{previous_token}}
User-Agent: ZenBrowser/1.2b.0 (EndeavourOS x86_64)
{
"userChrome": "",
"userContent": null,
"enabledMods": [
"5bb07b6e-c89f-4f4a-a0ed-e483cc535594"
],
"preferences": {
"theme.custom_menubutton.default": "Firefox",
"theme.custom_menubutton.custom": "url(chrome://branding/content/icon32.png)",
"zen.view.use-single-toolbar": true,
"zen.view.sidebar-expanded": true,
"zen.tabs.vertical.right-side": false,
"zen.view.experimental-no-window-controls": false,
"zen.view.hide-window-controls": true,
"browser.uiCustomization.state": "{\"placements\":{\"widget-overflow-fixed-list\":[],\"unified-extensions-area\":[\"ublock0_raymondhill_net-browser-action\",\"addon_darkreader_org-browser-action\",\"_7a7a4a92-a2a0-41d1-9fd7-1e92480d612d_-browser-action\",\"cookieautodelete_kennydo_com-browser-action\",\"tab-unloader-we_afnankhan-browser-action\"],\"nav-bar\":[\"back-button\",\"forward-button\",\"stop-reload-button\",\"customizableui-special-spring1\",\"urlbar-container\",\"customizableui-special-spring2\",\"wrapper-sidebar-button\",\"unified-extensions-button\"],\"toolbar-menubar\":[\"menubar-items\"],\"TabsToolbar\":[\"tabbrowser-tabs\"],\"vertical-tabs\":[],\"PersonalToolbar\":[\"personal-bookmarks\"],\"zen-sidebar-top-buttons\":[\"zen-sidepanel-button\"],\"zen-sidebar-icons-wrapper\":[\"zen-profile-button\",\"zen-workspaces-button\",\"downloads-button\"]},\"seen\":[\"_7a7a4a92-a2a0-41d1-9fd7-1e92480d612d_-browser-action\",\"developer-button\",\"cookieautodelete_kennydo_com-browser-action\",\"tab-unloader-we_afnankhan-browser-action\",\"addon_darkreader_org-browser-action\",\"ublock0_raymondhill_net-browser-action\"],\"dirtyAreaCache\":[\"unified-extensions-area\",\"nav-bar\",\"toolbar-menubar\",\"TabsToolbar\",\"vertical-tabs\",\"PersonalToolbar\",\"zen-sidebar-top-buttons\",\"zen-sidebar-icons-wrapper\"],\"currentVersion\":20,\"newElementCount\":2}"
},
"workspaceThemes": [
{
"type": "gradient",
"gradientColors": [
{
"c": [
124,
133,
255
],
"isCustom": false
},
{
"c": [
69,
255,
86
],
"isCustom": false
}
],
"opacity": 0.5,
"rotation": 45,
"texture": 0
},
{
"type": "gradient",
"gradientColors": [
{
"c": [
255,
133,
65
],
"isCustom": false
}
],
"opacity": 0.6,
"rotation": 45,
"texture": null
},
{
"type": "gradient",
"gradientColors": [],
"opacity": 0.5,
"rotation": 45,
"texture": null
}
]
}