mirror of
https://github.com/zen-browser/rices.git
synced 2025-07-07 17:05:40 +02:00
supabase persistence and multiple api fixes
This commit is contained in:
parent
ff50da2afd
commit
37586abe97
17 changed files with 438 additions and 505 deletions
|
@ -2,4 +2,7 @@ GITHUB_TOKEN=ghp_XXXXXXXXXXXXXXXXXXXXXXXXXXXXX
|
|||
GITHUB_REPO_OWNER=zen-browser
|
||||
GITHUB_REPO_NAME=rices-store
|
||||
|
||||
SUPABASE_URL=https://xxxxxxxxxxxxx.supabase.co
|
||||
SUPABASE_KEY=XXXXXXXXXXXXXXXXXXX
|
||||
|
||||
MODERATION_SECRET=superSecret123
|
||||
|
|
|
@ -20,8 +20,6 @@
|
|||
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^10.0.0",
|
||||
"@nestjs/core": "^10.0.0",
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/swagger": "^8.1.0",
|
||||
"@octokit/rest": "^18.12.0",
|
||||
|
@ -37,10 +35,13 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@nestjs/cli": "^10.0.0",
|
||||
"@nestjs/common": "^10.4.15",
|
||||
"@nestjs/config": "^3.3.0",
|
||||
"@nestjs/core": "^10.4.15",
|
||||
"@nestjs/schematics": "^10.0.0",
|
||||
"@nestjs/testing": "^10.0.0",
|
||||
"@nestjs/throttler": "^6.3.0",
|
||||
"@supabase/supabase-js": "^2.47.10",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^20.3.1",
|
||||
|
|
25
sql/ddl_1.0.0.sql
Normal file
25
sql/ddl_1.0.0.sql
Normal file
|
@ -0,0 +1,25 @@
|
|||
-- DROP TABLE IF EXISTS rices;
|
||||
|
||||
CREATE TABLE rices (
|
||||
id UUID NOT NULL, -- Unique identifier
|
||||
slug VARCHAR(75) NOT NULL, -- Unique user-friendly identifier
|
||||
name VARCHAR(75) NOT NULL, -- Name of the rice
|
||||
token UUID NOT NULL, -- Unique authorization token
|
||||
visits INTEGER DEFAULT 0 NOT NULL, -- Visit counter, initialized to 0
|
||||
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
|
||||
);
|
||||
|
||||
|
||||
CREATE OR REPLACE FUNCTION increment_visits(slug_param TEXT)
|
||||
RETURNS VOID AS $$
|
||||
BEGIN
|
||||
UPDATE rices
|
||||
SET visits = visits + 1
|
||||
WHERE slug = slug_param;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
|
@ -146,9 +146,7 @@ export class GitHubService implements OnModuleInit {
|
|||
branch: this.defaultBranch,
|
||||
});
|
||||
|
||||
this.logger.log(
|
||||
`File ${filePath} created/updated successfully.`,
|
||||
);
|
||||
this.logger.log(`File ${filePath} created/updated successfully.`);
|
||||
return;
|
||||
} catch (error: any) {
|
||||
if (error.status === 409 && attempt < retries) {
|
||||
|
@ -241,7 +239,9 @@ export class GitHubService implements OnModuleInit {
|
|||
}
|
||||
|
||||
if (isOctokitResponseError(error) && error.status === 404) {
|
||||
this.logger.warn(`The file ${filePath} does not exist in the repository.`);
|
||||
this.logger.warn(
|
||||
`The file ${filePath} does not exist in the repository.`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -330,9 +330,7 @@ export class GitHubService implements OnModuleInit {
|
|||
`Error listing files in ${directoryPath}: ${error.message} (Status: ${error.status})`,
|
||||
);
|
||||
} else {
|
||||
this.logger.error(
|
||||
`Error listing files in ${directoryPath}: ${error}`,
|
||||
);
|
||||
this.logger.error(`Error listing files in ${directoryPath}: ${error}`);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
@ -434,9 +432,7 @@ export class GitHubService implements OnModuleInit {
|
|||
*/
|
||||
private async lockDirectory(directoryPath: string): Promise<void> {
|
||||
while (this.directoryLocks.get(directoryPath)) {
|
||||
this.logger.warn(
|
||||
`Directory ${directoryPath} is locked. Waiting...`,
|
||||
);
|
||||
this.logger.warn(`Directory ${directoryPath} is locked. Waiting...`);
|
||||
await this.delay(100); // Wait 100ms before retrying
|
||||
}
|
||||
this.directoryLocks.set(directoryPath, true);
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { IsOptional, IsString } from 'class-validator';
|
||||
import { IsString } from 'class-validator';
|
||||
|
||||
export class CreateRiceDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
name!: string;
|
||||
|
||||
@IsString()
|
||||
content!: string;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import { IsOptional, IsString } from 'class-validator';
|
||||
import { IsString } from 'class-validator';
|
||||
|
||||
export class UpdateRiceDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
content!: string;
|
||||
}
|
||||
|
|
|
@ -6,7 +6,6 @@ import {
|
|||
Delete,
|
||||
Param,
|
||||
Body,
|
||||
UploadedFile,
|
||||
UseInterceptors,
|
||||
Headers,
|
||||
HttpCode,
|
||||
|
@ -23,8 +22,8 @@ import {
|
|||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiConsumes,
|
||||
ApiBody,
|
||||
} from '@nestjs/swagger';
|
||||
import { Throttle } from '@nestjs/throttler';
|
||||
|
||||
@ApiTags('rices')
|
||||
@Controller('rices')
|
||||
|
@ -34,45 +33,57 @@ export class RicesController {
|
|||
@ApiOperation({ summary: 'Upload a new Rice' })
|
||||
@ApiResponse({ status: 201, description: 'Rice successfully created.' })
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@ApiBody({
|
||||
description: 'Data required to create a rice',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: {
|
||||
type: 'string',
|
||||
description: 'Name of the rice',
|
||||
example: 'My First Rice',
|
||||
},
|
||||
content: {
|
||||
type: 'string',
|
||||
description: 'The JSON content to upload',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
@Post()
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async createRice(
|
||||
@Body() createRiceDto: CreateRiceDto,
|
||||
@UploadedFile() file: Express.Multer.File,
|
||||
) {
|
||||
return this.ricesService.create(createRiceDto, file);
|
||||
async createRice(@Body() createRiceDto: CreateRiceDto) {
|
||||
return this.ricesService.create(createRiceDto);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: 'Get information about a Rice' })
|
||||
@ApiResponse({ status: 200, description: 'Returns metadata of the Rice.' })
|
||||
@Get(':identifier')
|
||||
async getRice(@Param('identifier') identifier: string) {
|
||||
return this.ricesService.findOne(identifier);
|
||||
@Get(':slug')
|
||||
async getRice(@Param('slug') slug: string) {
|
||||
return this.ricesService.findOne(slug);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: 'Update an existing Rice' })
|
||||
@ApiResponse({ status: 200, description: 'Rice successfully updated.' })
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@Put(':identifier')
|
||||
@Put(':slug')
|
||||
@UseInterceptors(FileInterceptor('file'))
|
||||
async updateRice(
|
||||
@Param('identifier') identifier: string,
|
||||
@Param('slug') slug: string,
|
||||
@Headers('x-rices-token') token: string,
|
||||
@Body() updateRiceDto: UpdateRiceDto,
|
||||
@UploadedFile() file?: Express.Multer.File,
|
||||
) {
|
||||
return this.ricesService.update(identifier, token, updateRiceDto, file);
|
||||
return this.ricesService.update(slug, token, updateRiceDto);
|
||||
}
|
||||
|
||||
@ApiOperation({ summary: 'Delete an existing Rice' })
|
||||
@ApiResponse({ status: 204, description: 'Rice successfully deleted.' })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@Delete(':identifier')
|
||||
@Delete(':slug')
|
||||
async removeRice(
|
||||
@Param('identifier') identifier: string,
|
||||
@Param('slug') slug: string,
|
||||
@Headers('x-rices-token') token: string,
|
||||
) {
|
||||
await this.ricesService.remove(identifier, token);
|
||||
await this.ricesService.remove(slug, token);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -86,9 +97,9 @@ export class RicesController {
|
|||
})
|
||||
@ApiResponse({ status: 204, description: 'Rice deleted by moderation.' })
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
@Delete('moderate/delete/:identifier')
|
||||
@Delete('moderate/delete/:slug')
|
||||
async removeRiceByModerator(
|
||||
@Param('identifier') identifier: string,
|
||||
@Param('slug') slug: string,
|
||||
@Headers('x-moderation-secret') moderationSecret: string,
|
||||
) {
|
||||
// Verify the secret
|
||||
|
@ -97,7 +108,7 @@ export class RicesController {
|
|||
}
|
||||
|
||||
// Call the service to delete without a token
|
||||
await this.ricesService.moderateRemove(identifier);
|
||||
await this.ricesService.moderateRemove(slug);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,10 +4,11 @@ import { Module } from '@nestjs/common';
|
|||
import { RicesService } from './rices.service';
|
||||
import { GitHubModule } from '../github/github.module';
|
||||
import { RicesController } from './rices.controller';
|
||||
import { SupabaseService } from '../supabase/supabase.service';
|
||||
|
||||
@Module({
|
||||
imports: [GitHubModule],
|
||||
providers: [RicesService],
|
||||
providers: [RicesService, SupabaseService],
|
||||
controllers: [RicesController],
|
||||
exports: [RicesService],
|
||||
})
|
||||
|
|
|
@ -1,296 +1,172 @@
|
|||
// src/rices/rices.service.ts
|
||||
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
UnauthorizedException,
|
||||
ConflictException,
|
||||
} from '@nestjs/common';
|
||||
import { CreateRiceDto } from './dto/create-rice.dto';
|
||||
import { UpdateRiceDto } from './dto/update-rice.dto';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { generateSlug } from './utils/slug.util';
|
||||
import { GitHubService } from '../github/github.service';
|
||||
|
||||
/**
|
||||
* Checks if the provided error has a 'status' property of type 'number'
|
||||
* and a 'message' property of type 'string'.
|
||||
*/
|
||||
function isOctokitResponseError(
|
||||
error: unknown,
|
||||
): error is { status: number; message: string } {
|
||||
return (
|
||||
error !== null &&
|
||||
typeof error === 'object' &&
|
||||
'status' in error &&
|
||||
typeof (error as any).status === 'number' &&
|
||||
'message' in error &&
|
||||
typeof (error as any).message === 'string'
|
||||
);
|
||||
}
|
||||
import { SupabaseService } from '../supabase/supabase.service';
|
||||
|
||||
@Injectable()
|
||||
export class RicesService {
|
||||
constructor(private readonly gitHubService: GitHubService) {}
|
||||
constructor(
|
||||
private readonly gitHubService: GitHubService,
|
||||
private readonly supabaseService: SupabaseService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create a new rice
|
||||
*/
|
||||
async create(createRiceDto: CreateRiceDto, file?: Express.Multer.File) {
|
||||
try {
|
||||
// 1. Generate identifier (slug + UUID or just UUID)
|
||||
let identifier: string;
|
||||
if (createRiceDto.name) {
|
||||
// Generate slug from the name
|
||||
const slug = generateSlug(createRiceDto.name);
|
||||
identifier = `${slug}-${uuidv4()}`;
|
||||
} else {
|
||||
identifier = uuidv4();
|
||||
async create(createRiceDto: CreateRiceDto) {
|
||||
// Check if a rice with the same name already exists
|
||||
const existingRice = await this.supabaseService.getRiceByName(
|
||||
createRiceDto.name,
|
||||
);
|
||||
if (existingRice) {
|
||||
throw new ConflictException(
|
||||
`A rice with the name '${createRiceDto.name}' already exists.`,
|
||||
);
|
||||
}
|
||||
|
||||
// 2. Generate token and save metadata
|
||||
const slug = createRiceDto.name
|
||||
? `${generateSlug(createRiceDto.name)}-${uuidv4()}`
|
||||
: uuidv4();
|
||||
|
||||
const token = uuidv4();
|
||||
const metadata = {
|
||||
id: identifier,
|
||||
id: uuidv4(),
|
||||
token,
|
||||
name: createRiceDto.name || null,
|
||||
createdAt: new Date().toISOString(),
|
||||
slug: slug,
|
||||
visits: 0,
|
||||
level: 0,
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
const metadataContent = JSON.stringify(metadata, null, 2);
|
||||
const riceJsonPath = `rices/${identifier}/rice.json`;
|
||||
|
||||
// 3. Create or update rice.json in GitHub
|
||||
await this.supabaseService.insertRice(metadata);
|
||||
|
||||
const metadataContent = JSON.stringify(metadata, null, 2);
|
||||
const riceJsonPath = `rices/${slug}/rice.json`;
|
||||
await this.gitHubService.createOrUpdateFile(
|
||||
riceJsonPath,
|
||||
metadataContent,
|
||||
`Add rice ${identifier}`,
|
||||
`Add rice ${slug}`,
|
||||
);
|
||||
|
||||
// 4. If there's a file, upload it to GitHub
|
||||
if (file && file.originalname && file.buffer) {
|
||||
const fileContent = file.buffer.toString('utf-8');
|
||||
const uploadedFilePath = `rices/${identifier}/data.zenrice`;
|
||||
if (createRiceDto.content) {
|
||||
const uploadedFilePath = `rices/${slug}/data.zenrice`;
|
||||
await this.gitHubService.createOrUpdateFile(
|
||||
uploadedFilePath,
|
||||
fileContent,
|
||||
`Add file ${file.originalname} to rice ${identifier}/data.zenrice`,
|
||||
createRiceDto.content,
|
||||
`Add file createRiceDto.content to rice ${slug}`,
|
||||
);
|
||||
}
|
||||
|
||||
// 5. Return identifier and token
|
||||
return {
|
||||
identifier,
|
||||
token,
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error creating the rice:', error);
|
||||
throw new Error('Failed to create rice');
|
||||
}
|
||||
return { slug, token };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get rice information by its identifier
|
||||
*/
|
||||
async findOne(identifier: string) {
|
||||
try {
|
||||
const riceJsonPath = `rices/${identifier}/data.zenrice`;
|
||||
const fileContent = await this.gitHubService.getFileContent(riceJsonPath);
|
||||
async findOne(slug: string) {
|
||||
// Check if the rice exists in the database
|
||||
const rice = await this.supabaseService.getRiceBySlug(slug);
|
||||
if (!rice) throw new NotFoundException('Rice not found');
|
||||
|
||||
// Fetch the file from GitHub
|
||||
const filePath = `rices/${slug}/data.zenrice`;
|
||||
const fileContent = await this.gitHubService.getFileContent(filePath);
|
||||
|
||||
if (!fileContent) {
|
||||
throw new NotFoundException('Rice not found');
|
||||
throw new NotFoundException('Rice file not found in GitHub');
|
||||
}
|
||||
|
||||
return fileContent;
|
||||
} catch (error) {
|
||||
if (isOctokitResponseError(error) && error.status === 404) {
|
||||
throw new NotFoundException('Rice not found');
|
||||
}
|
||||
console.error('Error getting the rice:', error);
|
||||
throw new Error('Failed to get rice');
|
||||
}
|
||||
return { slug, fileContent };
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing rice
|
||||
*/
|
||||
async update(
|
||||
identifier: string,
|
||||
token: string,
|
||||
updateRiceDto: UpdateRiceDto,
|
||||
file?: Express.Multer.File,
|
||||
) {
|
||||
try {
|
||||
// 1. Retrieve and validate metadata
|
||||
const riceJsonPath = `rices/${identifier}/rice.json`;
|
||||
const metadataContent =
|
||||
await this.gitHubService.getFileContent(riceJsonPath);
|
||||
async update(slug: string, token: string, updateRiceDto: UpdateRiceDto) {
|
||||
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 (!metadataContent) {
|
||||
throw new NotFoundException('Rice not found');
|
||||
}
|
||||
const updatedMetadata = {
|
||||
...rice,
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
const metadata = JSON.parse(metadataContent);
|
||||
await this.supabaseService.updateRice(slug, updatedMetadata);
|
||||
|
||||
if (metadata.token !== token) {
|
||||
throw new UnauthorizedException('Invalid token');
|
||||
}
|
||||
|
||||
// 2. Update metadata
|
||||
if (updateRiceDto.name) {
|
||||
metadata.name = updateRiceDto.name;
|
||||
}
|
||||
metadata.updatedAt = new Date().toISOString();
|
||||
const updatedMetadataContent = JSON.stringify(metadata, null, 2);
|
||||
|
||||
// 3. Update rice.json in GitHub
|
||||
const metadataContent = JSON.stringify(updatedMetadata, null, 2);
|
||||
const riceJsonPath = `rices/${slug}/rice.json`;
|
||||
await this.gitHubService.createOrUpdateFile(
|
||||
riceJsonPath,
|
||||
updatedMetadataContent,
|
||||
`Update rice ${identifier}`,
|
||||
metadataContent,
|
||||
`Update rice ${slug}`,
|
||||
);
|
||||
|
||||
// 4. If there's a file, update it in GitHub
|
||||
if (file && file.originalname && file.buffer) {
|
||||
const fileContent = file.buffer.toString('utf-8');
|
||||
const uploadedFilePath = `rices/${identifier}/data.zenrice`;
|
||||
if (updateRiceDto.content) {
|
||||
const uploadedFilePath = `rices/${slug}/data.zenrice`;
|
||||
await this.gitHubService.createOrUpdateFile(
|
||||
uploadedFilePath,
|
||||
fileContent,
|
||||
`Update file ${file.originalname} in rice ${identifier}/data.zenrice`,
|
||||
updateRiceDto.content,
|
||||
`Update file updateRiceDto.content in rice ${slug}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
message: `Rice ${identifier} updated`,
|
||||
};
|
||||
} catch (error) {
|
||||
if (isOctokitResponseError(error)) {
|
||||
if (error.status === 404) {
|
||||
throw new NotFoundException('Rice not found');
|
||||
}
|
||||
if (error.status === 401 || error.status === 403) {
|
||||
throw new UnauthorizedException('Invalid token');
|
||||
}
|
||||
}
|
||||
console.error('Error updating the rice:', error);
|
||||
throw new Error('Failed to update rice');
|
||||
}
|
||||
return { message: `ok` };
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an existing rice
|
||||
*/
|
||||
async remove(identifier: string, token: string): Promise<void> {
|
||||
try {
|
||||
// 1. Retrieve and validate metadata
|
||||
const riceJsonPath = `rices/${identifier}/rice.json`;
|
||||
const metadataContent =
|
||||
await this.gitHubService.getFileContent(riceJsonPath);
|
||||
async remove(slug: string, token: string): Promise<void> {
|
||||
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 (!metadataContent) {
|
||||
throw new NotFoundException('Rice not found');
|
||||
}
|
||||
await this.supabaseService.deleteRice(slug);
|
||||
|
||||
const metadata = JSON.parse(metadataContent);
|
||||
|
||||
if (metadata.token !== token) {
|
||||
throw new UnauthorizedException('Invalid token');
|
||||
}
|
||||
|
||||
// 2. Delete rice.json from GitHub
|
||||
await this.gitHubService.deleteFile(
|
||||
riceJsonPath,
|
||||
`Remove rice ${identifier}`,
|
||||
);
|
||||
|
||||
// 3. List and delete uploaded files (if any)
|
||||
const uploadedFilesPath = `rices/${identifier}`;
|
||||
const files =
|
||||
await this.gitHubService.listFilesInDirectory(uploadedFilesPath);
|
||||
|
||||
for (const file of files) {
|
||||
if (file !== 'rice.json') {
|
||||
const filePath = `rices/${identifier}/${file}`;
|
||||
await this.gitHubService.deleteFile(
|
||||
filePath,
|
||||
`Remove file ${file} from rice ${identifier}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (isOctokitResponseError(error)) {
|
||||
if (error.status === 404) {
|
||||
throw new NotFoundException('Rice not found');
|
||||
}
|
||||
if (error.status === 401 || error.status === 403) {
|
||||
throw new UnauthorizedException('Invalid token');
|
||||
}
|
||||
}
|
||||
console.error('Error deleting the rice:', error);
|
||||
throw new Error('Failed to remove rice');
|
||||
}
|
||||
const riceJsonPath = `rices/${slug}/rice.json`;
|
||||
await this.gitHubService.deleteFile(riceJsonPath, `Remove rice ${slug}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a rice without checking the user's token.
|
||||
* Exclusive use for moderators with the secret key.
|
||||
*/
|
||||
public async moderateRemove(identifier: string): Promise<void> {
|
||||
public async moderateRemove(slug: string): Promise<void> {
|
||||
try {
|
||||
// 1. Check if rice.json exists
|
||||
const riceJsonPath = `rices/${identifier}/rice.json`;
|
||||
const metadataContent =
|
||||
await this.gitHubService.getFileContent(riceJsonPath);
|
||||
|
||||
if (!metadataContent) {
|
||||
// 1. Check if rice exists in Supabase
|
||||
const rice = await this.supabaseService.getRiceBySlug(slug);
|
||||
if (!rice) {
|
||||
throw new NotFoundException('Rice not found');
|
||||
}
|
||||
|
||||
// 2. Delete rice.json from GitHub
|
||||
// 2. Delete metadata from Supabase
|
||||
await this.supabaseService.deleteRice(slug);
|
||||
|
||||
// 3. Delete rice.json from GitHub
|
||||
const riceJsonPath = `rices/${slug}/rice.json`;
|
||||
await this.gitHubService.deleteFile(
|
||||
riceJsonPath,
|
||||
`[MODERATION] Remove rice ${identifier}`,
|
||||
`[MODERATION] Remove rice ${slug}`,
|
||||
);
|
||||
|
||||
// 3. List and delete uploaded files (if any)
|
||||
const uploadedFilesPath = `rices/${identifier}`;
|
||||
// 4. List and delete uploaded files from GitHub (if any)
|
||||
const uploadedFilesPath = `rices/${slug}`;
|
||||
const files =
|
||||
await this.gitHubService.listFilesInDirectory(uploadedFilesPath);
|
||||
|
||||
for (const file of files) {
|
||||
if (file !== 'rice.json') {
|
||||
const filePath = `rices/${identifier}/${file}`;
|
||||
const filePath = `rices/${slug}/${file}`;
|
||||
await this.gitHubService.deleteFile(
|
||||
filePath,
|
||||
`[MODERATION] Remove file ${file} from rice ${identifier}`,
|
||||
`[MODERATION] Remove file ${file} from rice ${slug}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (isOctokitResponseError(error)) {
|
||||
if (error.status === 404) {
|
||||
throw new NotFoundException('Rice not found');
|
||||
}
|
||||
if (error.status === 401 || error.status === 403) {
|
||||
throw new UnauthorizedException('Invalid token');
|
||||
}
|
||||
}
|
||||
console.error('Error removing rice by moderation:', error);
|
||||
if (error instanceof NotFoundException) {
|
||||
throw error;
|
||||
}
|
||||
throw new Error('Failed to remove rice by moderation');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List files in a specific directory in GitHub
|
||||
*/
|
||||
private async listFilesInDirectory(pathInRepo: string): Promise<string[]> {
|
||||
try {
|
||||
return await this.gitHubService.listFilesInDirectory(pathInRepo);
|
||||
} catch (error) {
|
||||
if (isOctokitResponseError(error) && error.status === 404) {
|
||||
return [];
|
||||
}
|
||||
console.error(`Error listing files in ${pathInRepo}:`, error);
|
||||
throw new Error('Failed to list files in directory');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,25 @@
|
|||
export function generateSlug(name: string): string {
|
||||
return name
|
||||
// Ensure the input is a string and trim whitespace
|
||||
if (typeof name !== 'string') {
|
||||
throw new Error('Input must be a string');
|
||||
}
|
||||
const sanitizedInput = name.trim();
|
||||
|
||||
// Replace accented characters with their unaccented counterparts
|
||||
const normalized = sanitizedInput
|
||||
.normalize('NFD')
|
||||
.replace(/\p{Diacritic}/gu, '');
|
||||
|
||||
// Replace any non-alphanumeric characters (excluding '-') with a hyphen
|
||||
const slug = normalized
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '');
|
||||
.replace(/[^a-z0-9]+/g, '-') // Replace invalid characters
|
||||
.replace(/^-+|-+$/g, ''); // Trim leading and trailing hyphens
|
||||
|
||||
// Ensure the slug is not empty
|
||||
if (!slug) {
|
||||
throw new Error('Generated slug is empty');
|
||||
}
|
||||
|
||||
return slug;
|
||||
}
|
||||
|
|
135
src/supabase/supabase.service.ts
Normal file
135
src/supabase/supabase.service.ts
Normal file
|
@ -0,0 +1,135 @@
|
|||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { createClient, SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
@Injectable()
|
||||
export class SupabaseService {
|
||||
private supabase: SupabaseClient;
|
||||
private supabase_url: string;
|
||||
private supabase_key: string;
|
||||
private readonly logger = new Logger(SupabaseService.name);
|
||||
|
||||
constructor(private configService: ConfigService) {
|
||||
// Initialize properties in the constructor
|
||||
this.supabase_url = this.configService.get<string>('SUPABASE_URL') || '';
|
||||
this.supabase_key = this.configService.get<string>('SUPABASE_KEY') || '';
|
||||
|
||||
this.supabase = createClient(this.supabase_url, this.supabase_key);
|
||||
}
|
||||
|
||||
async insertRice(metadata: any) {
|
||||
const { error } = await this.supabase.from('rices').insert(metadata);
|
||||
if (error) {
|
||||
this.logger.error(
|
||||
`Failed to insert rice: ${error.message}`,
|
||||
error.details,
|
||||
);
|
||||
throw new Error(`Failed to insert rice: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async getRiceById(id: string) {
|
||||
const { data, error } = await this.supabase
|
||||
.from('rices')
|
||||
.select('*')
|
||||
.eq('id', id)
|
||||
.single();
|
||||
if (error) {
|
||||
this.logger.error(
|
||||
`Failed to fetch rice with ID ${id}: ${error.message}`,
|
||||
error.details,
|
||||
);
|
||||
throw new Error(`Failed to fetch rice: ${error.message}`);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
async getRiceBySlug(slug: string) {
|
||||
const { data, error } = await this.supabase
|
||||
.from('rices')
|
||||
.select('*')
|
||||
.eq('slug', slug)
|
||||
.single();
|
||||
if (error) {
|
||||
this.logger.error(
|
||||
`Failed to fetch rice with slug ${slug}: ${error.message}`,
|
||||
error.details,
|
||||
);
|
||||
throw new Error(`Failed to fetch rice: ${error.message}`);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
async getRiceByName(name: string) {
|
||||
const { data, error } = await this.supabase
|
||||
.from('rices')
|
||||
.select('*')
|
||||
.eq('name', name)
|
||||
.single();
|
||||
if (error && error.code !== 'PGRST116') {
|
||||
// Handle "no rows found" separately
|
||||
this.logger.error(
|
||||
`Failed to fetch rice with name ${name}: ${error.message}`,
|
||||
error.details,
|
||||
);
|
||||
throw new Error(`Failed to fetch rice: ${error.message}`);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
async updateRice(slug: string, metadata: any) {
|
||||
const { error } = await this.supabase
|
||||
.from('rices')
|
||||
.update(metadata)
|
||||
.eq('slug', slug);
|
||||
if (error) {
|
||||
this.logger.error(
|
||||
`Failed to update rice with slug ${slug}: ${error.message}`,
|
||||
error.details,
|
||||
);
|
||||
throw new Error(`Failed to update rice: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async deleteRice(slug: string) {
|
||||
const { error } = await this.supabase
|
||||
.from('rices')
|
||||
.delete()
|
||||
.eq('slug', slug);
|
||||
if (error) {
|
||||
this.logger.error(
|
||||
`Failed to delete rice with slug ${slug}: ${error.message}`,
|
||||
error.details,
|
||||
);
|
||||
throw new Error(`Failed to delete rice: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async incrementVisits(slug: string) {
|
||||
const { error } = await this.supabase.rpc('increment_visits', {
|
||||
slug_param: slug,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
this.logger.error(
|
||||
`Failed to increment visits for rice with slug ${slug}: ${error.message}`,
|
||||
error.details,
|
||||
);
|
||||
throw new Error(`Failed to increment visits: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async updateLevel(slug: string, level: number) {
|
||||
const { error } = await this.supabase
|
||||
.from('rices')
|
||||
.update({ level })
|
||||
.eq('slug', slug);
|
||||
if (error) {
|
||||
this.logger.error(
|
||||
`Failed to update level for rice with slug ${slug}: ${error.message}`,
|
||||
error.details,
|
||||
);
|
||||
throw new Error(`Failed to update rice level: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,10 +5,12 @@ import { AppModule } from './../src/app.module';
|
|||
import * as path from 'path';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { GitHubService } from '../src/github/github.service';
|
||||
import { SupabaseService } from '../src/supabase/supabase.service';
|
||||
|
||||
describe('Rices API E2E', () => {
|
||||
let app: INestApplication;
|
||||
let gitHubService: GitHubService;
|
||||
let supabaseService: SupabaseService;
|
||||
const moderationSecret = 'testSecret999';
|
||||
|
||||
beforeAll(async () => {
|
||||
|
@ -19,6 +21,7 @@ describe('Rices API E2E', () => {
|
|||
}).compile();
|
||||
|
||||
gitHubService = moduleFixture.get<GitHubService>(GitHubService);
|
||||
supabaseService = moduleFixture.get<SupabaseService>(SupabaseService);
|
||||
|
||||
app = moduleFixture.createNestApplication();
|
||||
app.useGlobalPipes(new ValidationPipe({ transform: true }));
|
||||
|
@ -31,181 +34,138 @@ describe('Rices API E2E', () => {
|
|||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
// await gitHubService.clearRepository();
|
||||
// Limpiar repositorio y base de datos antes de cada test si es necesario
|
||||
});
|
||||
|
||||
it('POST /rices - Create new zenrice', async () => {
|
||||
it('POST /rices - Create a new rice entry', async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.post('/rices')
|
||||
.field('name', 'My first zenrice')
|
||||
.field('name', 'Test Rice')
|
||||
.attach('file', path.join(__dirname, 'files', 'example.zenrice'))
|
||||
.expect(201);
|
||||
|
||||
expect(response.body).toHaveProperty('identifier');
|
||||
expect(response.body).toHaveProperty('token');
|
||||
const { slug, token } = response.body;
|
||||
expect(slug).toBeDefined();
|
||||
expect(token).toBeDefined();
|
||||
|
||||
const { identifier, token } = response.body;
|
||||
const riceInDatabase = await supabaseService.getRiceBySlug(slug);
|
||||
expect(riceInDatabase).not.toBeNull();
|
||||
expect(riceInDatabase.name).toBe('Test Rice');
|
||||
|
||||
const uploadedFileContent = await gitHubService.getFileContent(
|
||||
`rices/${identifier}/data.zenrice`,
|
||||
const fileInGitHub = await gitHubService.getFileContent(
|
||||
`rices/${slug}/data.zenrice`,
|
||||
);
|
||||
expect(uploadedFileContent).not.toBeNull();
|
||||
expect(uploadedFileContent).toContain('This is an example zenrice file.');
|
||||
expect(fileInGitHub).toContain('This is an example zenrice file.');
|
||||
});
|
||||
|
||||
it('GET /rices/:identifier - Download zenrice', async () => {
|
||||
it('GET /rices/:slug - Retrieve a rice entry and increment visits', async () => {
|
||||
const createResponse = await request(app.getHttpServer())
|
||||
.post('/rices')
|
||||
.field('name', 'My first zenrice')
|
||||
.field('name', 'Test Rice')
|
||||
.attach('file', path.join(__dirname, 'files', 'example.zenrice'))
|
||||
.expect(201);
|
||||
|
||||
const { identifier, token } = createResponse.body;
|
||||
const { slug } = createResponse.body;
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(`/rices/${identifier}`)
|
||||
.expect(200);
|
||||
const initialData = await supabaseService.getRiceBySlug(slug);
|
||||
expect(initialData.visits).toBe(0);
|
||||
|
||||
await request(app.getHttpServer()).get(`/rices/${slug}`).expect(200);
|
||||
|
||||
const updatedData = await supabaseService.getRiceBySlug(slug);
|
||||
expect(updatedData.visits).toBe(1);
|
||||
});
|
||||
|
||||
it('PUT /rices/:identifier - Update zenrice', async () => {
|
||||
it('PUT /rices/:slug - Update a rice entry', async () => {
|
||||
const createResponse = await request(app.getHttpServer())
|
||||
.post('/rices')
|
||||
.field('name', 'My first zenrice')
|
||||
.field('name', 'Original Rice')
|
||||
.attach('file', path.join(__dirname, 'files', 'example.zenrice'))
|
||||
.expect(201);
|
||||
|
||||
const { identifier, token } = createResponse.body;
|
||||
const { slug, token } = createResponse.body;
|
||||
|
||||
const updateResponse = await request(app.getHttpServer())
|
||||
.put(`/rices/${identifier}`)
|
||||
.put(`/rices/${slug}`)
|
||||
.set('x-rices-token', token)
|
||||
.field('name', 'Mi rice renombrado')
|
||||
.field('name', 'Updated Rice')
|
||||
.attach('file', path.join(__dirname, 'files', 'example_update.zenrice'))
|
||||
.expect(200);
|
||||
|
||||
expect(updateResponse.body).toHaveProperty(
|
||||
'message',
|
||||
`Rice ${identifier} updated`,
|
||||
);
|
||||
expect(updateResponse.body.message).toBe(`ok`);
|
||||
|
||||
const uploadedFileContent = await gitHubService.getFileContent(
|
||||
`rices/${identifier}/data.zenrice`,
|
||||
const updatedData = await supabaseService.getRiceBySlug(slug);
|
||||
expect(updatedData.name).toBe('Updated Rice');
|
||||
|
||||
const updatedFile = await gitHubService.getFileContent(
|
||||
`rices/${slug}/data.zenrice`,
|
||||
);
|
||||
expect(uploadedFileContent).not.toBeNull();
|
||||
expect(uploadedFileContent).toContain(
|
||||
expect(updatedFile).toContain(
|
||||
'This is an example zenrice file (modified).',
|
||||
);
|
||||
});
|
||||
|
||||
it('DELETE /rices/:identifier - Delete zenrice with previous token', async () => {
|
||||
it('DELETE /rices/:slug - Delete a rice entry', async () => {
|
||||
const createResponse = await request(app.getHttpServer())
|
||||
.post('/rices')
|
||||
.field('name', 'My first zenrice')
|
||||
.field('name', 'Rice to Delete')
|
||||
.attach('file', path.join(__dirname, 'files', 'example.zenrice'))
|
||||
.expect(201);
|
||||
|
||||
const { identifier, token } = createResponse.body;
|
||||
const { slug, token } = createResponse.body;
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.delete(`/rices/${identifier}`)
|
||||
.delete(`/rices/${slug}`)
|
||||
.set('x-rices-token', token)
|
||||
.expect(204);
|
||||
|
||||
const riceJsonContent = await gitHubService.getFileContent(
|
||||
`rices/${identifier}/rice.json`,
|
||||
);
|
||||
expect(riceJsonContent).toBeNull();
|
||||
const riceInDatabase = await supabaseService.getRiceBySlug(slug);
|
||||
expect(riceInDatabase).toBeNull();
|
||||
|
||||
const uploadedFileContent = await gitHubService.getFileContent(
|
||||
`rices/${identifier}/data.zenrice`,
|
||||
const fileInGitHub = await gitHubService.getFileContent(
|
||||
`rices/${slug}/data.zenrice`,
|
||||
);
|
||||
expect(uploadedFileContent).toBeNull();
|
||||
expect(fileInGitHub).toBeNull();
|
||||
});
|
||||
|
||||
it('GET /rices/:identifier - Trying to download deleted zenrice', async () => {
|
||||
it('DELETE /rices/moderate/delete/:slug - Moderation delete with correct secret', async () => {
|
||||
const createResponse = await request(app.getHttpServer())
|
||||
.post('/rices')
|
||||
.field('name', 'My first zenrice')
|
||||
.field('name', 'Moderation Test Rice')
|
||||
.attach('file', path.join(__dirname, 'files', 'example.zenrice'))
|
||||
.expect(201);
|
||||
|
||||
const { identifier, token } = createResponse.body;
|
||||
const { slug } = createResponse.body;
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.delete(`/rices/${identifier}`)
|
||||
.set('x-rices-token', token)
|
||||
.expect(204);
|
||||
|
||||
await request(app.getHttpServer()).get(`/rices/${identifier}`).expect(404);
|
||||
});
|
||||
|
||||
it('POST /rices - New zenrice for moderation test', async () => {
|
||||
const response = await request(app.getHttpServer())
|
||||
.post('/rices')
|
||||
.field('name', 'Rice for moderation')
|
||||
.attach('file', path.join(__dirname, 'files', 'example.zenrice'))
|
||||
.expect(201);
|
||||
|
||||
expect(response.body).toHaveProperty('identifier');
|
||||
expect(response.body).toHaveProperty('token');
|
||||
|
||||
const { identifier, token } = response.body;
|
||||
|
||||
const riceJsonContent = await gitHubService.getFileContent(
|
||||
`rices/${identifier}/rice.json`,
|
||||
);
|
||||
expect(riceJsonContent).not.toBeNull();
|
||||
|
||||
const riceData = JSON.parse(riceJsonContent!);
|
||||
expect(riceData).toMatchObject({
|
||||
id: identifier,
|
||||
token,
|
||||
name: 'Rice for moderation',
|
||||
});
|
||||
|
||||
const uploadedFileContent = await gitHubService.getFileContent(
|
||||
`rices/${identifier}/data.zenrice`,
|
||||
);
|
||||
expect(uploadedFileContent).not.toBeNull();
|
||||
expect(uploadedFileContent).toContain('This is an example zenrice file.');
|
||||
});
|
||||
|
||||
it('DELETE /rices/moderate/delete/:identifier - Delete zenrice for moderation using a correct secret', async () => {
|
||||
const createResponse = await request(app.getHttpServer())
|
||||
.post('/rices')
|
||||
.field('name', 'Rice for moderation')
|
||||
.attach('file', path.join(__dirname, 'files', 'example.zenrice'))
|
||||
.expect(201);
|
||||
|
||||
const { identifier, token } = createResponse.body;
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.delete(`/rices/moderate/delete/${identifier}`)
|
||||
.delete(`/rices/moderate/delete/${slug}`)
|
||||
.set('x-moderation-secret', moderationSecret)
|
||||
.expect(204);
|
||||
|
||||
const riceJsonContent = await gitHubService.getFileContent(
|
||||
`rices/${identifier}/rice.json`,
|
||||
);
|
||||
expect(riceJsonContent).toBeNull();
|
||||
const riceInDatabase = await supabaseService.getRiceBySlug(slug);
|
||||
expect(riceInDatabase).toBeNull();
|
||||
|
||||
const uploadedFileContent = await gitHubService.getFileContent(
|
||||
`rices/${identifier}/data.zenrice`,
|
||||
const fileInGitHub = await gitHubService.getFileContent(
|
||||
`rices/${slug}/data.zenrice`,
|
||||
);
|
||||
expect(uploadedFileContent).toBeNull();
|
||||
expect(fileInGitHub).toBeNull();
|
||||
});
|
||||
|
||||
it('DELETE /rices/moderate/delete/:identifier - Delete zenrice for moderation using an incorrect secret', async () => {
|
||||
it('DELETE /rices/moderate/delete/:slug - Moderation delete with incorrect secret', async () => {
|
||||
const createResponse = await request(app.getHttpServer())
|
||||
.post('/rices')
|
||||
.field('name', 'Moderation Failure Test')
|
||||
.attach('file', path.join(__dirname, 'files', 'example.zenrice'))
|
||||
.expect(201);
|
||||
|
||||
const { slug } = createResponse.body;
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.delete(`/rices/moderate/delete/${uuidv4()}`)
|
||||
.set('x-moderation-secret', 'claveIncorrecta')
|
||||
.delete(`/rices/moderate/delete/${slug}`)
|
||||
.set('x-moderation-secret', 'wrongSecret')
|
||||
.expect(401);
|
||||
});
|
||||
|
||||
it('DELETE /rices/moderate/delete/:identifier - Delete non existent zenrice for moderation', async () => {
|
||||
await request(app.getHttpServer())
|
||||
.delete(`/rices/moderate/delete/${uuidv4()}`)
|
||||
.set('x-moderation-secret', moderationSecret)
|
||||
.expect(404);
|
||||
const riceInDatabase = await supabaseService.getRiceBySlug(slug);
|
||||
expect(riceInDatabase).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
@baseUrl = http://localhost:3000
|
||||
@moderationSecret = superSecret123
|
||||
@random_identifier = 123e4567-e89b-12d3-a456-426614174000
|
||||
|
||||
# base64 of following json: ewogICAgImtleSI6ICJ2YWx1ZSIsCiAgICAiZGVzY3JpcHRpb24iOiAiRXhhbXBsZSBjb250ZW50Igp9
|
||||
|
||||
# {
|
||||
# "key": "value",
|
||||
# "description": "Example content"
|
||||
# }
|
||||
|
||||
|
||||
POST {{baseUrl}}/rices
|
||||
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||
Content-Type: application/json
|
||||
|
||||
------WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||
Content-Disposition: form-data; name="name"
|
||||
{
|
||||
"name": "cool-zenrice-aurora2",
|
||||
"content": "ewogICAgImtleSI6ICJ2YWx1ZSIsCiAgICAiZGVzY3JpcHRpb24iOiAiRXhhbXBsZSBjb250ZW50Igp9"
|
||||
}
|
||||
|
||||
Mi primer zenrice
|
||||
------WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||
Content-Disposition: form-data; name="file"; filename="example.zenrice"
|
||||
Content-Type: text/plain
|
||||
|
||||
This is an example zenrice file.
|
||||
------WebKitFormBoundary7MA4YWxkTrZu0gW--
|
|
@ -1,5 +1,4 @@
|
|||
@baseUrl = http://localhost:3000
|
||||
@moderationSecret = superSecret123
|
||||
@previous_identifier = my-first-zenrice-feffbf61-b815-4357-ba0c-2cbadf94fcfe
|
||||
@previous_slug = cool-zenrice-aurora-84154784-8dc4-4153-8bd5-fd9f55f03b93
|
||||
|
||||
GET {{baseUrl}}/rices/{{previous_identifier}}
|
||||
GET {{baseUrl}}/rices/{{previous_slug}}
|
|
@ -1,19 +1,20 @@
|
|||
@baseUrl = http://localhost:3000
|
||||
@moderationSecret = superSecret123
|
||||
@previous_identifier = my-first-zenrice-feffbf61-b815-4357-ba0c-2cbadf94fcfe
|
||||
@previous_token = 806cd360-6c14-44de-92db-f46f328dab5a
|
||||
@previous_slug = cool-zenrice-aurora2-1d1e74b3-8d6d-40ea-bd4f-a6f4ad893f88
|
||||
@previous_token = be4545f4-d92b-416c-8b3b-50cc9a49dee9
|
||||
|
||||
PUT {{baseUrl}}/rices/{{previous_identifier}}
|
||||
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||
# base64 of following json: ewogICAgImtleSI6ICJuZXdfdmFsdWUiLAogICAgImRlc2NyaXB0aW9uIjogIkV4YW1wbGUgY29udGVudCIKfQ
|
||||
|
||||
# {
|
||||
# "key": "new_value",
|
||||
# "description": "Example content"
|
||||
# }
|
||||
|
||||
PUT {{baseUrl}}/rices/{{previous_slug}}
|
||||
Content-Type: application/json
|
||||
x-rices-token: {{previous_token}}
|
||||
|
||||
------WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||
Content-Disposition: form-data; name="name"
|
||||
{
|
||||
|
||||
Mi rice renombrado
|
||||
------WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||
Content-Disposition: form-data; name="file"; filename="newfile.txt"
|
||||
Content-Type: text/plain
|
||||
"content": "ewogICAgImtleSI6ICJuZXdfdmFsdWUiLAogICAgImRlc2NyaXB0aW9uIjogIkV4YW1wbGUgY29udGVudCIKfQ"
|
||||
}
|
||||
|
||||
This is an example zenrice file (modified).
|
||||
------WebKitFormBoundary7MA4YWxkTrZu0gW--
|
|
@ -1,7 +1,6 @@
|
|||
@baseUrl = http://localhost:3000
|
||||
@moderationSecret = superSecret123
|
||||
@previous_identifier = my-first-zenrice-b7b94d24-ecb6-4495-93de-ba85be2e3052
|
||||
@previous_slug = my-first-zenrice-b7b94d24-ecb6-4495-93de-ba85be2e3052
|
||||
@previous_token = 6181664b-00e8-4eef-8e23-1f7fa0c64021
|
||||
|
||||
DELETE {{baseUrl}}/rices/{{previous_identifier}}
|
||||
DELETE {{baseUrl}}/rices/{{previous_slug}}
|
||||
x-rices-token: {{previous_token}}
|
||||
|
|
|
@ -1,94 +0,0 @@
|
|||
### Variables Iniciales
|
||||
@baseUrl = http://localhost:3000
|
||||
@moderationSecret = superSecret123
|
||||
@random_identifier = 123e4567-e89b-12d3-a456-426614174000 # Give a valid UUID
|
||||
|
||||
@identifier = YOUR_IDENTIFIER_HERE
|
||||
@token = YOUR_TOKEN_HERE
|
||||
|
||||
###
|
||||
|
||||
### 1. Crear un Nuevo Pack
|
||||
POST {{baseUrl}}/rices
|
||||
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||
|
||||
------WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||
Content-Disposition: form-data; name="name"
|
||||
|
||||
Mi primer rice
|
||||
------WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||
Content-Disposition: form-data; name="file"; filename="example.txt"
|
||||
Content-Type: text/plain
|
||||
|
||||
Contenido de mi archivo.
|
||||
------WebKitFormBoundary7MA4YWxkTrZu0gW--
|
||||
|
||||
### **Instrucciones Después de Ejecutar:**
|
||||
1. Ejecuta esta solicitud.
|
||||
2. En la respuesta, copia los valores de `identifier` y `token`.
|
||||
3. Reemplaza `YOUR_IDENTIFIER_HERE` y `YOUR_TOKEN_HERE` en las variables al inicio del archivo con los valores copiados.
|
||||
|
||||
###
|
||||
|
||||
GET {{baseUrl}}/rices/{{identifier}}
|
||||
|
||||
###
|
||||
|
||||
PUT {{baseUrl}}/rices/{{identifier}}
|
||||
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||
x-rices-token: {{token}}
|
||||
|
||||
------WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||
Content-Disposition: form-data; name="name"
|
||||
|
||||
Mi rice renombrado
|
||||
------WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||
Content-Disposition: form-data; name="file"; filename="newfile.txt"
|
||||
Content-Type: text/plain
|
||||
|
||||
Contenido de mi nuevo archivo.
|
||||
------WebKitFormBoundary7MA4YWxkTrZu0gW--
|
||||
|
||||
###
|
||||
|
||||
### 4. Eliminar el Pack Usando el Token
|
||||
DELETE {{baseUrl}}/rices/{{identifier}}
|
||||
x-rices-token: {{token}}
|
||||
|
||||
###
|
||||
|
||||
### 5. Intentar Obtener el Pack Eliminado (Debe Retornar 404)
|
||||
GET {{baseUrl}}/rices/{{identifier}}
|
||||
|
||||
###
|
||||
|
||||
### 6. Crear un Nuevo Pack para Moderación
|
||||
POST {{baseUrl}}/rices
|
||||
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||
|
||||
------WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||
Content-Disposition: form-data; name="name"
|
||||
|
||||
Pack para moderación
|
||||
------WebKitFormBoundary7MA4YWxkTrZu0gW
|
||||
Content-Disposition: form-data; name="file"; filename="example.txt"
|
||||
Content-Type: text/plain
|
||||
|
||||
Contenido de mi archivo.
|
||||
------WebKitFormBoundary7MA4YWxkTrZu0gW--
|
||||
|
||||
###
|
||||
|
||||
### 7. Eliminar el Pack por Moderación con el Secreto Correcto
|
||||
DELETE {{baseUrl}}/rices/moderate/delete/{{identifier}}
|
||||
x-moderation-secret: {{moderationSecret}}
|
||||
|
||||
###
|
||||
|
||||
DELETE {{baseUrl}}/rices/moderate/delete/{{random_identifier}}
|
||||
x-moderation-secret: claveIncorrecta
|
||||
|
||||
###
|
||||
|
||||
DELETE {{baseUrl}}/rices/moderate/delete/{{random_identifier}}
|
||||
x-moderation-secret: {{moderationSecret}}
|
Loading…
Add table
Add a link
Reference in a new issue