mirror of
https://github.com/zen-browser/rices.git
synced 2025-07-07 08:55:40 +02:00
spaces storage
This commit is contained in:
parent
079cde591e
commit
f05fd437aa
14 changed files with 256 additions and 100 deletions
|
@ -1,22 +1,32 @@
|
||||||
--DROP TABLE IF EXISTS rices;
|
-- Drop tables if they exist
|
||||||
|
DROP TABLE IF EXISTS shared;
|
||||||
|
DROP TABLE IF EXISTS shared_types;
|
||||||
|
|
||||||
CREATE TABLE shared (
|
-- Create table for shared types
|
||||||
id UUID NOT NULL, -- Unique identifier
|
CREATE TABLE shared_types (
|
||||||
slug VARCHAR(75) NOT NULL, -- Unique user-friendly identifier
|
key VARCHAR(50) PRIMARY KEY -- Type key (e.g., 'WORKSPACE', 'RICE')
|
||||||
type INTEGER DEFAULT 0 NOT NULL, -- Type: 1-WORKSPACE 2-RICE
|
|
||||||
version VARCHAR(10) NOT NULL, -- Data version
|
|
||||||
os VARCHAR(30) NOT NULL, -- Operating system
|
|
||||||
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
|
|
||||||
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), -- Composite primary key
|
|
||||||
UNIQUE (slug) -- Ensure slug is unique
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
-- Create table for shared items
|
||||||
|
CREATE TABLE shared (
|
||||||
|
id UUID NOT NULL PRIMARY KEY, -- Unique identifier
|
||||||
|
slug VARCHAR(75) NOT NULL UNIQUE, -- Unique user-friendly identifier
|
||||||
|
type VARCHAR(15) NOT NULL REFERENCES shared_types(key) ON DELETE CASCADE, -- Foreign key to shared_types
|
||||||
|
version VARCHAR(10) NOT NULL, -- Data version
|
||||||
|
os VARCHAR(30) NOT NULL, -- Operating system
|
||||||
|
name VARCHAR(75) NOT NULL, -- Name of the rice
|
||||||
|
author VARCHAR(100) NOT NULL, -- Name of the author
|
||||||
|
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
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Insert default types
|
||||||
|
INSERT INTO shared_types (key) VALUES ('WORKSPACE'), ('RICE');
|
||||||
|
|
||||||
|
-- Create function to increment visit count
|
||||||
CREATE OR REPLACE FUNCTION increment_visits(slug_param TEXT)
|
CREATE OR REPLACE FUNCTION increment_visits(slug_param TEXT)
|
||||||
RETURNS VOID AS $$
|
RETURNS VOID AS $$
|
||||||
BEGIN
|
BEGIN
|
||||||
|
@ -24,4 +34,4 @@ BEGIN
|
||||||
SET visits = visits + 1
|
SET visits = visits + 1
|
||||||
WHERE slug = slug_param;
|
WHERE slug = slug_param;
|
||||||
END;
|
END;
|
||||||
$$ LANGUAGE plpgsql;
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
|
@ -16,11 +16,12 @@ import {
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
import { SharedService } from './shared.service';
|
import { SharedService } from './shared.service';
|
||||||
import { ApiTags, ApiOperation, ApiResponse, ApiHeader } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiResponse, ApiHeader } from '@nestjs/swagger';
|
||||||
|
import { SHARED_TYPES } from './shared.module';
|
||||||
|
|
||||||
@ApiTags('rices')
|
@ApiTags('rices')
|
||||||
@Controller('rices')
|
@Controller('rices')
|
||||||
export class RicesController {
|
export class RicesController {
|
||||||
constructor(private readonly sharedService: SharedService) {}
|
constructor(private readonly sharedService: SharedService) { }
|
||||||
|
|
||||||
@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.' })
|
||||||
|
@ -50,7 +51,7 @@ export class RicesController {
|
||||||
|
|
||||||
this.validateFileSize(contentString); // Validate file size
|
this.validateFileSize(contentString); // Validate file size
|
||||||
|
|
||||||
return this.sharedService.create(contentString, token, headers);
|
return this.sharedService.create(SHARED_TYPES.RICE, contentString, token, headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ summary: 'Get information about a Rice' })
|
@ApiOperation({ summary: 'Get information about a Rice' })
|
||||||
|
@ -60,7 +61,7 @@ export class RicesController {
|
||||||
})
|
})
|
||||||
@Get(':slug')
|
@Get(':slug')
|
||||||
async getRice(@Param('slug') slug: string, @Res() res: Response) {
|
async getRice(@Param('slug') slug: string, @Res() res: Response) {
|
||||||
const riceMetadata = await this.sharedService.getRiceMetadata(slug);
|
const riceMetadata = await this.sharedService.getRiceMetadata(SHARED_TYPES.RICE, slug);
|
||||||
|
|
||||||
const htmlContent = `<!DOCTYPE html>
|
const htmlContent = `<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
@ -120,7 +121,7 @@ export class RicesController {
|
||||||
|
|
||||||
this.validateFileSize(contentString); // Validate file size
|
this.validateFileSize(contentString); // Validate file size
|
||||||
|
|
||||||
return this.sharedService.update(slug, token, contentString, headers);
|
return this.sharedService.update(SHARED_TYPES.RICE, slug, token, contentString, headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ApiOperation({ summary: 'Delete an existing Rice' })
|
@ApiOperation({ summary: 'Delete an existing Rice' })
|
||||||
|
@ -131,7 +132,7 @@ export class RicesController {
|
||||||
@Param('slug') slug: string,
|
@Param('slug') slug: string,
|
||||||
@Headers('x-zen-shared-token') token: string,
|
@Headers('x-zen-shared-token') token: string,
|
||||||
) {
|
) {
|
||||||
await this.sharedService.remove(slug, token);
|
await this.sharedService.remove(SHARED_TYPES.RICE, slug, token);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,7 +151,7 @@ export class RicesController {
|
||||||
if (moderationSecret !== process.env.MODERATION_SECRET) {
|
if (moderationSecret !== process.env.MODERATION_SECRET) {
|
||||||
throw new UnauthorizedException('Invalid moderation secret');
|
throw new UnauthorizedException('Invalid moderation secret');
|
||||||
}
|
}
|
||||||
await this.sharedService.moderateRemove(slug);
|
await this.sharedService.moderateRemove(SHARED_TYPES.RICE, slug);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,12 +4,19 @@ import { Module } from '@nestjs/common';
|
||||||
import { SharedService } from './shared.service';
|
import { SharedService } from './shared.service';
|
||||||
import { GitHubModule } from '../github/github.module';
|
import { GitHubModule } from '../github/github.module';
|
||||||
import { RicesController } from './rices.controller';
|
import { RicesController } from './rices.controller';
|
||||||
|
import { SpacesController } from './spaces.controller';
|
||||||
import { SupabaseService } from '../supabase/supabase.service';
|
import { SupabaseService } from '../supabase/supabase.service';
|
||||||
|
|
||||||
|
export const SHARED_TYPES = {
|
||||||
|
WORKSPACE: "WORKSPACE",
|
||||||
|
RICE: "RICE",
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [GitHubModule],
|
imports: [GitHubModule],
|
||||||
providers: [SharedService, SupabaseService],
|
providers: [SharedService, SupabaseService],
|
||||||
controllers: [RicesController],
|
controllers: [RicesController, SpacesController],
|
||||||
exports: [SharedService],
|
exports: [SharedService],
|
||||||
})
|
})
|
||||||
export class SharedModule {}
|
export class SharedModule { }
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { generateSlug } from './utils/slug.util';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { GitHubService } from '../github/github.service';
|
import { GitHubService } from '../github/github.service';
|
||||||
import { SupabaseService } from '../supabase/supabase.service';
|
import { SupabaseService } from '../supabase/supabase.service';
|
||||||
|
import { SHARED_TYPES } from './shared.module';
|
||||||
|
|
||||||
const userAgentRegex = /ZenBrowser\/(\d+\.\d\w?\.\d) \((.+)\)/;
|
const userAgentRegex = /ZenBrowser\/(\d+\.\d\w?\.\d) \((.+)\)/;
|
||||||
|
|
||||||
|
@ -22,9 +23,10 @@ export class SharedService {
|
||||||
private readonly gitHubService: GitHubService,
|
private readonly gitHubService: GitHubService,
|
||||||
private readonly supabaseService: SupabaseService,
|
private readonly supabaseService: SupabaseService,
|
||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
) {}
|
) { }
|
||||||
|
|
||||||
async create(
|
async create(
|
||||||
|
type: string,
|
||||||
content: string,
|
content: string,
|
||||||
token: string | null,
|
token: string | null,
|
||||||
headers: Record<string, string>,
|
headers: Record<string, string>,
|
||||||
|
@ -36,7 +38,7 @@ export class SharedService {
|
||||||
const userAgent = headers['user-agent'];
|
const userAgent = headers['user-agent'];
|
||||||
|
|
||||||
if (!name || !author || !userAgent) {
|
if (!name || !author || !userAgent) {
|
||||||
throw new BadRequestException('Rice name and author are required!');
|
throw new BadRequestException('shared name and author are required!');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate content
|
// Validate content
|
||||||
|
@ -45,14 +47,19 @@ export class SharedService {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.validateJsonStructure(content);
|
if (type == SHARED_TYPES.RICE) {
|
||||||
|
this.validateRicesJsonStructure(content);
|
||||||
|
}
|
||||||
|
else if (type == SHARED_TYPES.WORKSPACE) {
|
||||||
|
this.validateWorkspaceJsonStructure(content);
|
||||||
|
}
|
||||||
|
|
||||||
content = this.sanitizeJson(content);
|
content = this.sanitizeJson(content);
|
||||||
content = this.minimizeJson(content);
|
content = this.minimizeJson(content);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new BadRequestException('Invalid json request');
|
throw new BadRequestException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate lengths
|
// Validate lengths
|
||||||
|
@ -106,29 +113,36 @@ export class SharedService {
|
||||||
throw new BadRequestException(`Invalid name provided`);
|
throw new BadRequestException(`Invalid name provided`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!type) {
|
||||||
|
throw new BadRequestException(`Invalid type provided`);
|
||||||
|
}
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
token = uuidv4();
|
token = uuidv4();
|
||||||
} else {
|
} else {
|
||||||
const tokenMaxCount = this.configService.get<number>(
|
if (type == SHARED_TYPES.RICE) {
|
||||||
'MAX_RICES_BY_TOKEN',
|
const tokenMaxCount = this.configService.get<number>(
|
||||||
5,
|
'MAX_RICES_BY_TOKEN',
|
||||||
);
|
5,
|
||||||
const tokenCount = await this.supabaseService.countSharedByToken(token);
|
|
||||||
if (tokenCount >= tokenMaxCount) {
|
|
||||||
throw new ConflictException(
|
|
||||||
`The token '${token}' is already associated with 5 or more rices.`,
|
|
||||||
);
|
);
|
||||||
|
const tokenCount = await this.supabaseService.countSharedByToken(token);
|
||||||
|
if (tokenCount >= tokenMaxCount) {
|
||||||
|
throw new ConflictException(
|
||||||
|
`The token '${token}' is already associated with 5 or more rices.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const metadata = {
|
const metadata = {
|
||||||
id: uuidv4(),
|
id: uuidv4(),
|
||||||
token,
|
slug: slug,
|
||||||
name,
|
type: type,
|
||||||
author,
|
version: version,
|
||||||
version,
|
os: os,
|
||||||
os,
|
name: name,
|
||||||
slug,
|
author: author,
|
||||||
|
token: token,
|
||||||
visits: 0,
|
visits: 0,
|
||||||
level: 0,
|
level: 0,
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
|
@ -137,11 +151,11 @@ export class SharedService {
|
||||||
// Insert metadata into Supabase
|
// Insert metadata into Supabase
|
||||||
await this.supabaseService.insertShared(metadata);
|
await this.supabaseService.insertShared(metadata);
|
||||||
|
|
||||||
const uploadedFilePath = `rices/${slug}/data.zenrice`;
|
const uploadedFilePath = this.getSharedFilePath(type, slug);
|
||||||
await this.gitHubService.createOrUpdateFile(
|
await this.gitHubService.createOrUpdateFile(
|
||||||
uploadedFilePath,
|
uploadedFilePath,
|
||||||
content,
|
content,
|
||||||
`Add content to rice ${slug}`,
|
`Add content to shared ${slug}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return { slug, token };
|
return { slug, token };
|
||||||
|
@ -151,17 +165,40 @@ export class SharedService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async findOne(slug: string) {
|
getShortSharedFilePath(type: string, slug: string) {
|
||||||
|
switch (type) {
|
||||||
|
case SHARED_TYPES.WORKSPACE:
|
||||||
|
return `spaces/${slug}`;
|
||||||
|
case SHARED_TYPES.RICE:
|
||||||
|
return `rices/${slug}`;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown shared type: ${type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getSharedFilePath(type: string, slug: string) {
|
||||||
|
switch (type) {
|
||||||
|
case SHARED_TYPES.WORKSPACE:
|
||||||
|
return `spaces/${slug}/data.zenspace`;
|
||||||
|
case SHARED_TYPES.RICE:
|
||||||
|
return `rices/${slug}/data.zenrice`;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown shared type: ${type}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async findOne(type: string, slug: string) {
|
||||||
// Check if the rice exists in the database
|
// Check if the rice exists in the database
|
||||||
const rice = await this.supabaseService.getSharedBySlug(slug);
|
const shared = await this.supabaseService.getSharedBySlug(type, slug);
|
||||||
if (!rice) throw new NotFoundException('Rice not found');
|
if (!shared) throw new NotFoundException('shared not found');
|
||||||
|
|
||||||
// Fetch the file from GitHub
|
// Fetch the file from GitHub
|
||||||
const filePath = `rices/${slug}/data.zenrice`;
|
const filePath = this.getSharedFilePath(type, slug);
|
||||||
const fileContent = await this.gitHubService.getFileContent(filePath);
|
const fileContent = await this.gitHubService.getFileContent(filePath);
|
||||||
|
|
||||||
if (!fileContent) {
|
if (!fileContent) {
|
||||||
throw new NotFoundException('Rice file not found in GitHub');
|
throw new NotFoundException('Shared file not found in GitHub');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove unescaped double quotes at the beginning and end, if present
|
// Remove unescaped double quotes at the beginning and end, if present
|
||||||
|
@ -170,14 +207,15 @@ export class SharedService {
|
||||||
return fileContent;
|
return fileContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getRiceMetadata(slug: string) {
|
async getRiceMetadata(type: string, slug: string) {
|
||||||
const rice = await this.supabaseService.getSharedBySlug(slug);
|
const shared = await this.supabaseService.getSharedBySlug(type, slug);
|
||||||
if (!rice) throw new NotFoundException('Rice not found');
|
if (!shared) throw new NotFoundException('Shared not found');
|
||||||
|
|
||||||
return rice;
|
return shared;
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(
|
async update(
|
||||||
|
type: string,
|
||||||
slug: string,
|
slug: string,
|
||||||
token: string,
|
token: string,
|
||||||
content: string,
|
content: string,
|
||||||
|
@ -203,7 +241,12 @@ export class SharedService {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.validateJsonStructure(content);
|
if (type == SHARED_TYPES.RICE) {
|
||||||
|
this.validateRicesJsonStructure(content);
|
||||||
|
}
|
||||||
|
else if (type == SHARED_TYPES.WORKSPACE) {
|
||||||
|
this.validateWorkspaceJsonStructure(content);
|
||||||
|
}
|
||||||
|
|
||||||
content = this.sanitizeJson(content);
|
content = this.sanitizeJson(content);
|
||||||
content = this.minimizeJson(content);
|
content = this.minimizeJson(content);
|
||||||
|
@ -215,19 +258,19 @@ export class SharedService {
|
||||||
|
|
||||||
const [, version, os] = match;
|
const [, version, os] = match;
|
||||||
|
|
||||||
// Check if the rice exists
|
// Check if the shared exists
|
||||||
const rice = await this.supabaseService.getSharedBySlug(slug);
|
const shared = await this.supabaseService.getSharedBySlug(type, slug);
|
||||||
if (!rice) {
|
if (!shared) {
|
||||||
throw new NotFoundException('Rice not found');
|
throw new NotFoundException('Shared not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate token, name, and author match the existing record
|
// Validate token, name, and author match the existing record
|
||||||
if (rice.token !== token) {
|
if (shared.token !== token) {
|
||||||
throw new UnauthorizedException('Invalid token.');
|
throw new UnauthorizedException('Invalid token.');
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedMetadata = {
|
const updatedMetadata = {
|
||||||
...rice,
|
...shared,
|
||||||
version,
|
version,
|
||||||
os,
|
os,
|
||||||
updated_at: new Date().toISOString(),
|
updated_at: new Date().toISOString(),
|
||||||
|
@ -235,33 +278,33 @@ export class SharedService {
|
||||||
|
|
||||||
await this.supabaseService.updateShared(slug, updatedMetadata);
|
await this.supabaseService.updateShared(slug, updatedMetadata);
|
||||||
|
|
||||||
const uploadedFilePath = `rices/${slug}/data.zenrice`;
|
const uploadedFilePath = this.getSharedFilePath(type, slug);
|
||||||
await this.gitHubService.createOrUpdateFile(
|
await this.gitHubService.createOrUpdateFile(
|
||||||
uploadedFilePath,
|
uploadedFilePath,
|
||||||
content,
|
content,
|
||||||
`Update content in rice ${slug}`,
|
`Update content in shared ${slug}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return { message: `Rice ${slug} updated successfully.` };
|
return { message: `shared ${slug} updated successfully.` };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error in update method:', error);
|
console.error('Error in update method:', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(slug: string, token: string): Promise<void> {
|
async remove(type: string, slug: string, token: string): Promise<void> {
|
||||||
const rice = await this.supabaseService.getSharedBySlug(slug);
|
const shared = await this.supabaseService.getSharedBySlug(type, slug);
|
||||||
if (!rice) throw new NotFoundException('Rice not found');
|
if (!shared) throw new NotFoundException('shared not found');
|
||||||
if (rice.token !== token) throw new UnauthorizedException('Invalid token');
|
if (shared.token !== token) throw new UnauthorizedException('Invalid token');
|
||||||
|
|
||||||
// Validate token, name, and author match the existing record
|
// Validate token, name, and author match the existing record
|
||||||
if (rice.token !== token) {
|
if (shared.token !== token) {
|
||||||
throw new UnauthorizedException('Invalid token.');
|
throw new UnauthorizedException('Invalid token.');
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.supabaseService.deleteShared(slug);
|
await this.supabaseService.deleteShared(slug);
|
||||||
|
|
||||||
const folderPath = `rices/${slug}`;
|
const folderPath = this.getShortSharedFilePath(type, slug);
|
||||||
|
|
||||||
// List all files in the folder
|
// List all files in the folder
|
||||||
const files = await this.gitHubService.listFilesInDirectory(folderPath);
|
const files = await this.gitHubService.listFilesInDirectory(folderPath);
|
||||||
|
@ -271,7 +314,7 @@ export class SharedService {
|
||||||
const filePath = `${folderPath}/${file}`;
|
const filePath = `${folderPath}/${file}`;
|
||||||
await this.gitHubService.deleteFile(
|
await this.gitHubService.deleteFile(
|
||||||
filePath,
|
filePath,
|
||||||
`Remove file ${file} in rice ${slug}`,
|
`Remove file ${file} in shared ${slug}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -283,36 +326,37 @@ export class SharedService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a rice without checking the user's token.
|
* Delete a shared without checking the user's token.
|
||||||
* Exclusive use for moderators with the secret key.
|
* Exclusive use for moderators with the secret key.
|
||||||
*/
|
*/
|
||||||
public async moderateRemove(slug: string): Promise<void> {
|
public async moderateRemove(type: string, slug: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// 1. Check if rice exists in Supabase
|
// 1. Check if shared exists in Supabase
|
||||||
const rice = await this.supabaseService.getSharedBySlug(slug);
|
const shared = await this.supabaseService.getSharedBySlug(type, slug);
|
||||||
if (!rice) {
|
if (!shared) {
|
||||||
throw new NotFoundException('Rice not found');
|
throw new NotFoundException('shared not found');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Delete metadata from Supabase
|
// 2. Delete metadata from Supabase
|
||||||
await this.supabaseService.deleteShared(slug);
|
await this.supabaseService.deleteShared(slug);
|
||||||
|
|
||||||
// 3. Delete data.zenrice from GitHub
|
// 3. Delete data.zenrice from GitHub
|
||||||
const riceJsonPath = `rices/${slug}/data.zenrice`;
|
|
||||||
|
const jsonPath = this.getSharedFilePath(type, slug);
|
||||||
await this.gitHubService.deleteFile(
|
await this.gitHubService.deleteFile(
|
||||||
riceJsonPath,
|
jsonPath,
|
||||||
`[MODERATION] Remove rice ${slug}`,
|
`[MODERATION] Remove shared ${slug}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 4. List and delete uploaded files from GitHub (if any)
|
// 4. List and delete uploaded files from GitHub (if any)
|
||||||
const filesPath = `rices/${slug}`;
|
const filesPath = this.getShortSharedFilePath(type, slug);
|
||||||
const files = await this.gitHubService.listFilesInDirectory(filesPath);
|
const files = await this.gitHubService.listFilesInDirectory(filesPath);
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const filePath = `rices/${slug}/${file}`;
|
const filePath = filesPath + `/${file}`;
|
||||||
await this.gitHubService.deleteFile(
|
await this.gitHubService.deleteFile(
|
||||||
filePath,
|
filePath,
|
||||||
`[MODERATION] Remove file ${file} from rice ${slug}`,
|
`[MODERATION] Remove file ${file} from shared ${slug}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -322,15 +366,19 @@ export class SharedService {
|
||||||
`[MODERATION] Remove folder ${filesPath}`,
|
`[MODERATION] Remove folder ${filesPath}`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error removing rice by moderation:', error);
|
console.error('Error removing shared by moderation:', error);
|
||||||
if (error instanceof NotFoundException) {
|
if (error instanceof NotFoundException) {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
throw new Error('Failed to remove rice by moderation');
|
throw new Error('Failed to remove shared by moderation');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
validateJsonStructure(jsonString: string): boolean {
|
validateWorkspaceJsonStructure(jsonString: string): boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
validateRicesJsonStructure(jsonString: string): boolean {
|
||||||
const requiredKeys: string[] = [
|
const requiredKeys: string[] = [
|
||||||
'userChrome',
|
'userChrome',
|
||||||
'userContent',
|
'userContent',
|
||||||
|
|
59
src/shared/spaces.controller.ts
Normal file
59
src/shared/spaces.controller.ts
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Post,
|
||||||
|
Body,
|
||||||
|
Headers,
|
||||||
|
HttpCode,
|
||||||
|
HttpStatus,
|
||||||
|
BadRequestException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { SharedService } from './shared.service';
|
||||||
|
import { ApiTags, ApiOperation, ApiResponse, ApiHeader } from '@nestjs/swagger';
|
||||||
|
import { SHARED_TYPES } from './shared.module';
|
||||||
|
|
||||||
|
@ApiTags('spaces')
|
||||||
|
@Controller('spaces')
|
||||||
|
export class SpacesController {
|
||||||
|
constructor(private readonly sharedService: SharedService) { }
|
||||||
|
|
||||||
|
@ApiOperation({ summary: 'Shared a Space' })
|
||||||
|
@ApiResponse({ status: 201, description: 'Space successfully shared.' })
|
||||||
|
@ApiHeader({
|
||||||
|
name: 'x-zen-shared-name',
|
||||||
|
description: 'Name of the Space',
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
@ApiHeader({
|
||||||
|
name: 'x-zen-shared-author',
|
||||||
|
description: 'Author of the Space',
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
@ApiHeader({
|
||||||
|
name: 'User-Agent',
|
||||||
|
description: 'User-Agent',
|
||||||
|
required: true,
|
||||||
|
})
|
||||||
|
@Post()
|
||||||
|
async createSpace(
|
||||||
|
@Body() content: string,
|
||||||
|
@Headers() headers: Record<string, string>,
|
||||||
|
@Headers('x-zen-shared-token') token: string,
|
||||||
|
) {
|
||||||
|
const contentString =
|
||||||
|
typeof content === 'string' ? content : JSON.stringify(content);
|
||||||
|
|
||||||
|
this.validateFileSize(contentString); // Validate file size
|
||||||
|
|
||||||
|
return this.sharedService.create(SHARED_TYPES.WORKSPACE, contentString, token, headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
private validateFileSize(content: string) {
|
||||||
|
const sizeInBytes = Buffer.byteLength(content, 'utf-8');
|
||||||
|
const maxSizeInBytes = 1 * 1024 * 512; // 1 MB
|
||||||
|
if (sizeInBytes > maxSizeInBytes) {
|
||||||
|
throw new BadRequestException(
|
||||||
|
`The uploaded content exceeds the size limit of 512 KB.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -44,10 +44,11 @@ export class SupabaseService {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getSharedBySlug(slug: string) {
|
async getSharedBySlug(type: string, slug: string) {
|
||||||
const { data, error } = await this.supabase
|
const { data, error } = await this.supabase
|
||||||
.from('shared')
|
.from('shared')
|
||||||
.select('*')
|
.select('*')
|
||||||
|
.eq('type', type)
|
||||||
.eq('slug', slug)
|
.eq('slug', slug)
|
||||||
.single();
|
.single();
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
@baseUrl = http://localhost:3000
|
|
||||||
@previous_slug = cool-zenrice-test-base-0ae004db-fdca-4df6-8833-1104ac1662f6
|
|
||||||
|
|
||||||
|
|
||||||
GET {{baseUrl}}/rices/{{previous_slug}}
|
|
|
@ -1,6 +0,0 @@
|
||||||
@baseUrl = http://localhost:3000
|
|
||||||
@previous_slug = cool-zenrice-aurora2-b970a742-789c-4349-8a4d-da63c8bbe77d
|
|
||||||
@previous_token = 03fbfdb4-d3a5-4d64-8740-feac7d32e7a8
|
|
||||||
|
|
||||||
DELETE {{baseUrl}}/rices/{{previous_slug}}
|
|
||||||
x-zen-shared-token: {{previous_token}}
|
|
5
test/restclient/rice/02_download_rice.http
Normal file
5
test/restclient/rice/02_download_rice.http
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
@baseUrl = http://localhost:3000
|
||||||
|
@previous_slug = cool-zenrice-test-base-a069a4c2-237d-433f-ab1c-38c6e6ba5244
|
||||||
|
|
||||||
|
|
||||||
|
GET {{baseUrl}}/rices/{{previous_slug}}
|
|
@ -1,6 +1,6 @@
|
||||||
@baseUrl = http://localhost:3000
|
@baseUrl = http://localhost:3000
|
||||||
@previous_slug = cool-zenrice-test-base64211-5f874c8c-71f7-4b45-830a-aa86c9328455
|
@previous_slug = cool-zenrice-test-base-1d576eeb-de28-4df8-a12f-bcfc8f0e9b6e
|
||||||
@previous_token = 84780af0-191e-4f77-8c23-25165c89d27e
|
@previous_token = 00472a9f-8a8c-423d-b4a5-7137c4cc13f6
|
||||||
|
|
||||||
PUT {{baseUrl}}/rices/{{previous_slug}}
|
PUT {{baseUrl}}/rices/{{previous_slug}}
|
||||||
Content-Type: application/json
|
Content-Type: application/json
|
6
test/restclient/rice/04_delete_rice.http
Normal file
6
test/restclient/rice/04_delete_rice.http
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
@baseUrl = http://localhost:3000
|
||||||
|
@previous_slug = cool-zenrice-test-base-1d576eeb-de28-4df8-a12f-bcfc8f0e9b6e
|
||||||
|
@previous_token = 00472a9f-8a8c-423d-b4a5-7137c4cc13f6
|
||||||
|
|
||||||
|
DELETE {{baseUrl}}/rices/{{previous_slug}}
|
||||||
|
x-zen-shared-token: {{previous_token}}
|
30
test/restclient/spaces/01_create_space.http
Normal file
30
test/restclient/spaces/01_create_space.http
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
@baseUrl = http://localhost:3000
|
||||||
|
|
||||||
|
POST {{baseUrl}}/spaces
|
||||||
|
Content-Type: application/json
|
||||||
|
x-zen-shared-name: cool-zenrice-test-base
|
||||||
|
x-zen-shared-author: jhon@doe.com
|
||||||
|
User-Agent: ZenBrowser/1.2b.0 (EndeavourOS x86_64)
|
||||||
|
|
||||||
|
{
|
||||||
|
"space_id": "UNIQUE-IDENTIFIER",
|
||||||
|
"owner_id": "USER_OR_TEAM_ID",
|
||||||
|
"name": "Project X Workspace",
|
||||||
|
"description": "Workspace for Project X collaboration",
|
||||||
|
"tabs": [
|
||||||
|
{
|
||||||
|
"url": "https://example.com",
|
||||||
|
"title": "Example Site",
|
||||||
|
"pinned": true,
|
||||||
|
"archived": false,
|
||||||
|
"metadata": {
|
||||||
|
"last_accessed": "2024-02-20T14:30:00Z",
|
||||||
|
"preview_image": "base64_thumbnail"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"permissions": {
|
||||||
|
"public_sharing": false,
|
||||||
|
"edit_restrictions": "owner_only"
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue