- Changed create and update method. Headers+body instead of DTO

- Added token validation for `update` and `remove` operations to ensure it matches the record in the database.
- Ensured `name` and `author` from headers in `update` are validated against the existing record.
- Improved error handling for mismatched or missing tokens, returning appropriate HTTP status codes.
This commit is contained in:
oscargonzalezmoreno@gmail.com 2024-12-27 12:30:50 +01:00
parent c8ce6e7637
commit 121ccadf57
8 changed files with 328 additions and 151 deletions

View file

@ -2,10 +2,11 @@
CREATE TABLE rices ( CREATE TABLE rices (
id UUID NOT NULL, -- Unique identifier id UUID NOT NULL, -- Unique identifier
slug VARCHAR(75) NOT NULL, -- Unique user-friendly identifier
version VARCHAR(10) NOT NULL, -- Data version version VARCHAR(10) NOT NULL, -- Data version
os VARCHAR(30) NOT NULL, -- Operating system os VARCHAR(30) NOT NULL, -- Operating system
slug VARCHAR(75) NOT NULL, -- Unique user-friendly identifier
name VARCHAR(75) NOT NULL, -- Name of the rice name VARCHAR(75) NOT NULL, -- Name of the rice
author VARCHAR(100) NOT NULL, -- Name of the rice
token UUID NOT NULL, -- Unique authorization token token UUID NOT NULL, -- Unique authorization token
visits INTEGER DEFAULT 0 NOT NULL, -- Visit counter, initialized to 0 visits INTEGER DEFAULT 0 NOT NULL, -- Visit counter, initialized to 0
level INTEGER DEFAULT 0 NOT NULL, -- Level: 0 (Public), 1 (Verified) level INTEGER DEFAULT 0 NOT NULL, -- Level: 0 (Public), 1 (Verified)

View file

@ -6,24 +6,14 @@ import {
Delete, Delete,
Param, Param,
Body, Body,
UseInterceptors,
Headers, Headers,
HttpCode, HttpCode,
HttpStatus, HttpStatus,
UnauthorizedException, UnauthorizedException,
} from '@nestjs/common'; } from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import { RicesService } from './rices.service'; import { RicesService } from './rices.service';
import { CreateRiceDto } from './dto/create-rice.dto';
import { UpdateRiceDto } from './dto/update-rice.dto';
import { import { ApiTags, ApiOperation, ApiResponse, ApiHeader } from '@nestjs/swagger';
ApiTags,
ApiOperation,
ApiResponse,
ApiConsumes,
ApiBody,
} from '@nestjs/swagger';
@ApiTags('rices') @ApiTags('rices')
@Controller('rices') @Controller('rices')
@ -32,47 +22,72 @@ export class RicesController {
@ApiOperation({ summary: 'Upload a new Rice' }) @ApiOperation({ summary: 'Upload a new Rice' })
@ApiResponse({ status: 201, description: 'Rice successfully created.' }) @ApiResponse({ status: 201, description: 'Rice successfully created.' })
@ApiConsumes('multipart/form-data') @ApiHeader({
@ApiBody({ name: 'X-Zen-Rice-Name',
description: 'Data required to create a rice', description: 'Name of the rice',
schema: { required: true,
type: 'object', })
properties: { @ApiHeader({
name: { name: 'X-Zen-Rice-Author',
type: 'string', description: 'Author of the rice',
description: 'Name of the rice', required: true,
example: 'My First Rice', })
}, @ApiHeader({
content: { name: 'User-Agent',
type: 'string', description: 'User-Agent in the format ZenBrowser/<version> (<OS>)',
description: 'The JSON content to upload', required: true,
},
},
},
}) })
@Post() @Post()
async createRice(@Body() createRiceDto: CreateRiceDto) { async createRice(
return this.ricesService.create(createRiceDto); @Body() content: string,
@Headers() headers: Record<string, string>,
) {
const contentString =
typeof content === 'string' ? content : JSON.stringify(content);
return this.ricesService.create(contentString, headers);
} }
@ApiOperation({ summary: 'Get information about a Rice' }) @ApiOperation({ summary: 'Get information about a Rice' })
@ApiResponse({ status: 200, description: 'Returns metadata of the Rice.' }) @ApiResponse({ status: 200, description: 'Returns metadata of the Rice.' })
@Get(':slug') @Get(':slug')
/************* ✨ Codeium Command ⭐ *************/
/**
* Retrieve metadata of a rice with the given slug.
* @param slug Slug of the rice.
* @returns Metadata of the rice if found, otherwise throws a NotFoundException.
*/
/****** c6f70808-e78d-4b17-a285-d2fd79527659 *******/
async getRice(@Param('slug') slug: string) { async getRice(@Param('slug') slug: string) {
return this.ricesService.findOne(slug); return this.ricesService.findOne(slug);
} }
@ApiOperation({ summary: 'Update an existing Rice' }) @ApiOperation({ summary: 'Update an existing Rice' })
@ApiResponse({ status: 200, description: 'Rice successfully updated.' }) @ApiResponse({ status: 200, description: 'Rice successfully updated.' })
@ApiConsumes('multipart/form-data') @ApiHeader({
name: 'X-Zen-Rice-Name',
description: 'Name of the rice',
required: true,
})
@ApiHeader({
name: 'X-Zen-Rice-Author',
description: 'Author of the rice',
required: true,
})
@ApiHeader({
name: 'User-Agent',
description: 'User-Agent in the format ZenBrowser/<version> (<OS>)',
required: true,
})
@Put(':slug') @Put(':slug')
@UseInterceptors(FileInterceptor('file'))
async updateRice( async updateRice(
@Param('slug') slug: string, @Param('slug') slug: string,
@Headers('x-rices-token') token: string, @Body() content: string,
@Body() updateRiceDto: UpdateRiceDto, @Headers() headers: Record<string, string>,
@Headers('x-zen-rices-token') token: string,
) { ) {
return this.ricesService.update(slug, token, updateRiceDto); const contentString =
typeof content === 'string' ? content : JSON.stringify(content);
return this.ricesService.update(slug, token, contentString, headers);
} }
@ApiOperation({ summary: 'Delete an existing Rice' }) @ApiOperation({ summary: 'Delete an existing Rice' })
@ -81,15 +96,12 @@ export class RicesController {
@Delete(':slug') @Delete(':slug')
async removeRice( async removeRice(
@Param('slug') slug: string, @Param('slug') slug: string,
@Headers('x-rices-token') token: string, @Headers('x-zen-rices-token') token: string,
) { ) {
await this.ricesService.remove(slug, token); await this.ricesService.remove(slug, token);
return; return;
} }
// =========================================
// NEW ENDPOINT FOR MODERATION DELETION
// =========================================
@ApiOperation({ @ApiOperation({
summary: 'Forcefully delete a Rice (moderation)', summary: 'Forcefully delete a Rice (moderation)',
description: description:
@ -102,12 +114,9 @@ export class RicesController {
@Param('slug') slug: string, @Param('slug') slug: string,
@Headers('x-moderation-secret') moderationSecret: string, @Headers('x-moderation-secret') moderationSecret: string,
) { ) {
// Verify the secret
if (moderationSecret !== process.env.MODERATION_SECRET) { if (moderationSecret !== process.env.MODERATION_SECRET) {
throw new UnauthorizedException('Invalid moderation secret'); throw new UnauthorizedException('Invalid moderation secret');
} }
// Call the service to delete without a token
await this.ricesService.moderateRemove(slug); await this.ricesService.moderateRemove(slug);
return; return;
} }

View file

@ -5,8 +5,6 @@ import {
ConflictException, ConflictException,
BadRequestException, BadRequestException,
} from '@nestjs/common'; } from '@nestjs/common';
import { CreateRiceDto } from './dto/create-rice.dto';
import { UpdateRiceDto } from './dto/update-rice.dto';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
import { generateSlug } from './utils/slug.util'; import { generateSlug } from './utils/slug.util';
import { GitHubService } from '../github/github.service'; import { GitHubService } from '../github/github.service';
@ -19,42 +17,82 @@ export class RicesService {
private readonly supabaseService: SupabaseService, private readonly supabaseService: SupabaseService,
) {} ) {}
async create(createRiceDto: CreateRiceDto) { async create(content: string, headers: Record<string, string>) {
try { try {
// Ensure required fields are present // Validate headers
if (!createRiceDto.name || !createRiceDto.version || !createRiceDto.os) { const name = headers['x-zen-rice-name'];
const author = headers['x-zen-rice-author'];
const userAgent = headers['user-agent'];
if (!name || !author || !userAgent) {
throw new BadRequestException( throw new BadRequestException(
'Missing required fields: name, version, and os are mandatory.', 'Missing required headers: X-Zen-Rice-Name, X-Zen-Rice-Author, and User-Agent are mandatory.',
);
}
// Validate content
if (typeof content !== 'string') {
throw new BadRequestException('The request body must be a string.');
}
// Validate lengths
if (name.length > 75) {
throw new BadRequestException(
`The value of X-Zen-Rice-Name exceeds the maximum allowed length of 75 characters.`,
);
}
if (author.length > 100) {
throw new BadRequestException(
`The value of X-Zen-Rice-Author exceeds the maximum allowed length of 100 characters.`,
);
}
// Parse version and OS from User-Agent
const userAgentRegex = /ZenBrowser\/(\d+\.\d+\.\d+) \((.+)\)/;
const match = userAgent.match(userAgentRegex);
if (!match) {
throw new BadRequestException(
'Invalid User-Agent format. Expected format: ZenBrowser/<version> (<OS>).',
);
}
const [, version, os] = match;
// Validate version and OS lengths
if (version.length > 10) {
throw new BadRequestException(
`The version in User-Agent exceeds the maximum allowed length of 10 characters.`,
);
}
if (os.length > 30) {
throw new BadRequestException(
`The operating system in User-Agent exceeds the maximum allowed length of 30 characters.`,
); );
} }
// Check if a rice with the same name already exists // Check if a rice with the same name already exists
const existingRice = await this.supabaseService.getRiceByName( const existingRice = await this.supabaseService.getRiceByName(name);
createRiceDto.name,
);
if (existingRice) { if (existingRice) {
throw new ConflictException( throw new ConflictException(
`A rice with the name '${createRiceDto.name}' already exists.`, `A rice with the name '${name}' already exists.`,
); );
} }
const slug = createRiceDto.name const slug = `${generateSlug(name)}-${uuidv4()}`;
? `${generateSlug(createRiceDto.name)}-${uuidv4()}`
: uuidv4();
const token = uuidv4(); const token = uuidv4();
const encodedContent = Buffer.from( const encodedContent = Buffer.from(content).toString('base64');
JSON.stringify(createRiceDto.content),
).toString('base64');
const metadata = { const metadata = {
id: uuidv4(), id: uuidv4(),
token, token,
name: createRiceDto.name, name,
version: createRiceDto.version, author,
os: createRiceDto.os, version,
slug: slug, os,
slug,
visits: 0, visits: 0,
level: 0, level: 0,
created_at: new Date().toISOString(), created_at: new Date().toISOString(),
@ -63,32 +101,17 @@ export class RicesService {
// Insert metadata into Supabase // Insert metadata into Supabase
await this.supabaseService.insertRice(metadata); await this.supabaseService.insertRice(metadata);
if (createRiceDto.content) { const uploadedFilePath = `rices/${slug}/data.zenrice`;
const uploadedFilePath = `rices/${slug}/data.zenrice`; await this.gitHubService.createOrUpdateFile(
await this.gitHubService.createOrUpdateFile( uploadedFilePath,
uploadedFilePath, encodedContent,
encodedContent, `Add content to rice ${slug}`,
`Add file createRiceDto.content to rice ${slug}`, );
);
}
return { slug, token }; return { slug, token };
} catch (error) { } catch (error) {
// Log the error for debugging
console.error('Error in create method:', error); console.error('Error in create method:', error);
throw error;
if (
error instanceof ConflictException ||
error instanceof BadRequestException
) {
throw error; // Or create a custom response
} else if (error instanceof Error) {
// Extract a user-friendly message if possible, or log details
throw new Error(`Rice creation failed: ${error.message}`); // More informative error message
} else {
// Catch unexpected errors
throw new Error('Internal Server Error'); // Only for truly unexpected issues
}
} }
} }
@ -114,51 +137,76 @@ export class RicesService {
return content; return content;
} }
async update(slug: string, token: string, updateRiceDto: UpdateRiceDto) { async update(
/************* ✨ Codeium Command ⭐ *************/ slug: string,
/** token: string,
* Updates the metadata and content of a rice entry identified by its slug. content: string,
* headers: Record<string, string>,
* @param slug - The unique identifier for the rice entry. ) {
* @param token - The authorization token to verify the request. try {
* @param updateRiceDto - Data Transfer Object containing fields to update. // Extract fields from headers
* const name = headers['x-zen-rice-name'];
* @returns A confirmation message indicating successful update. const author = headers['x-zen-rice-author'];
* const userAgent = headers['user-agent'];
* @throws NotFoundException - If the rice entry does not exist.
* @throws UnauthorizedException - If the provided token is invalid.
*/
/****** bf5f61f3-c1dc-40a0-85e6-288824144ead *******/ const rice =
await this.supabaseService.getRiceBySlug(slug);
if (!rice) throw new NotFoundException('Rice not found');
if (rice.token !== token) throw new UnauthorizedException('Invalid token');
if (!updateRiceDto.content) {
throw new BadRequestException(
'Missing required fields: content is mandatory.',
);
}
const updatedMetadata = { if (!name || !author || !userAgent) {
...rice, throw new BadRequestException(
updated_at: new Date().toISOString(), 'Missing required headers: X-Zen-Rice-Name, X-Zen-Rice-Author, and User-Agent are mandatory.',
}; );
}
await this.supabaseService.updateRice(slug, updatedMetadata); // Parse version and OS from User-Agent
const userAgentRegex = /ZenBrowser\/(\d+\.\d+\.\d+) \((.+)\)/;
const match = userAgent.match(userAgentRegex);
if (updateRiceDto.content) { if (!match) {
const encodedContent = Buffer.from( throw new BadRequestException(
JSON.stringify(updateRiceDto.content), 'Invalid User-Agent format. Expected format: ZenBrowser/<version> (<OS>).',
).toString('base64'); );
}
const [, version, os] = match;
// Check if the rice exists
const rice = await this.supabaseService.getRiceBySlug(slug);
if (!rice) {
throw new NotFoundException('Rice not found');
}
// Validate token, name, and author match the existing record
if (rice.token !== token) {
throw new UnauthorizedException('Invalid token.');
}
// Validate name and author match the existing record
if (rice.name !== name || rice.author !== author) {
throw new UnauthorizedException(
'Provided name and author do not match the existing record.',
);
}
const updatedMetadata = {
...rice,
version,
os,
updated_at: new Date().toISOString(),
};
await this.supabaseService.updateRice(slug, updatedMetadata);
const encodedContent = Buffer.from(content).toString('base64');
const uploadedFilePath = `rices/${slug}/data.zenrice`; const uploadedFilePath = `rices/${slug}/data.zenrice`;
await this.gitHubService.createOrUpdateFile( await this.gitHubService.createOrUpdateFile(
uploadedFilePath, uploadedFilePath,
encodedContent, encodedContent,
`Update file updateRiceDto.content in rice ${slug}`, `Update content in rice ${slug}`,
); );
}
return { message: `ok` }; return { message: `Rice ${slug} updated successfully.` };
} catch (error) {
console.error('Error in update method:', error);
throw error;
}
} }
async remove(slug: string, token: string): Promise<void> { async remove(slug: string, token: string): Promise<void> {
@ -166,6 +214,11 @@ export class RicesService {
if (!rice) throw new NotFoundException('Rice not found'); if (!rice) throw new NotFoundException('Rice not found');
if (rice.token !== token) throw new UnauthorizedException('Invalid token'); if (rice.token !== token) throw new UnauthorizedException('Invalid token');
// Validate token, name, and author match the existing record
if (rice.token !== token) {
throw new UnauthorizedException('Invalid token.');
}
await this.supabaseService.deleteRice(slug); await this.supabaseService.deleteRice(slug);
const folderPath = `rices/${slug}`; const folderPath = `rices/${slug}`;

View file

@ -87,7 +87,7 @@ describe('Rices API E2E', () => {
const updateResponse = await request(app.getHttpServer()) const updateResponse = await request(app.getHttpServer())
.put(`/rices/${slug}`) .put(`/rices/${slug}`)
.set('x-rices-token', token) .set('x-zen-rices-token', token)
.field('name', 'Updated Rice') .field('name', 'Updated Rice')
.attach('file', path.join(__dirname, 'files', 'example_update.zenrice')) .attach('file', path.join(__dirname, 'files', 'example_update.zenrice'))
.expect(200); .expect(200);
@ -116,7 +116,7 @@ describe('Rices API E2E', () => {
await request(app.getHttpServer()) await request(app.getHttpServer())
.delete(`/rices/${slug}`) .delete(`/rices/${slug}`)
.set('x-rices-token', token) .set('x-zen-rices-token', token)
.expect(204); .expect(204);
const riceInDatabase = await supabaseService.getRiceBySlug(slug); const riceInDatabase = await supabaseService.getRiceBySlug(slug);

View file

@ -1,19 +1,74 @@
@baseUrl = http://localhost:3000 @baseUrl = http://localhost:3000
# {
# "key": "value",
# "description": "Example content"
# }
# {'key':'value','description':'Example content'}
POST {{baseUrl}}/rices POST {{baseUrl}}/rices
Content-Type: application/json Content-Type: application/json
X-Zen-Rice-Name: cool-zenrice-aurora
X-Zen-Rice-Author: jhon@doe.com
User-Agent: ZenBrowser/1.0.0 (EndeavourOS x86_64)
{ {
"name": "cool-zenrice-aurora2", "userChrome": "",
"version": "1.0.0", "userContent": null,
"os": "EndeavourOS x86_64", "enabledMods": [
"content": "{'key':'value','description':'Example content'}" "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
}
]
} }

View file

@ -1,5 +1,5 @@
@baseUrl = http://localhost:3000 @baseUrl = http://localhost:3000
@previous_slug = cool-zenrice-aurora-e99096ae-00da-4d54-9a47-53b20eb57647 @previous_slug = cool-zenrice-aurora-249dd7f2-d669-4d1f-892c-df4caa6fcbfe
GET {{baseUrl}}/rices/{{previous_slug}} GET {{baseUrl}}/rices/{{previous_slug}}

View file

@ -1,19 +1,78 @@
@baseUrl = http://localhost:3000 @baseUrl = http://localhost:3000
@previous_slug = cool-zenrice-aurora-ef732cbc-fdde-4f76-b4e3-cff0ec8b6f39 @previous_slug = cool-zenrice-aurora-249dd7f2-d669-4d1f-892c-df4caa6fcbfe
@previous_token = b406f962-5c51-43a9-8382-40e0983a46e7 @previous_token = 528bb297-274f-4fe1-87f1-956b9b26e0df
# {
# "key": "value",
# "description": "Example updated content"
# }
# {'key':'value','description':'Example updated content'}
PUT {{baseUrl}}/rices/{{previous_slug}} PUT {{baseUrl}}/rices/{{previous_slug}}
Content-Type: application/json Content-Type: application/json
x-rices-token: {{previous_token}} x-zen-rices-token: {{previous_token}}
X-Zen-Rice-Name: cool-zenrice-aurora
X-Zen-Rice-Author: jhon@doe.com
User-Agent: ZenBrowser/1.0.0 (EndeavourOS x86_64)
{ {
"content": "{'key':'value','description':'Example updated content'}" "userChrome": "",
"userContent": null,
"enabledMods": [
"5bb07b6e-c89f-4f4a-a0ed-e483cc535594",
"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
}
]
} }

View file

@ -3,4 +3,4 @@
@previous_token = 03fbfdb4-d3a5-4d64-8740-feac7d32e7a8 @previous_token = 03fbfdb4-d3a5-4d64-8740-feac7d32e7a8
DELETE {{baseUrl}}/rices/{{previous_slug}} DELETE {{baseUrl}}/rices/{{previous_slug}}
x-rices-token: {{previous_token}} x-zen-rices-token: {{previous_token}}