chore: add techbase to apps-archived

Integrated techbase (software comparison platform) into monorepo structure:
- Created NestJS backend with votes and comments modules
- Migrated from external Supabase to own PostgreSQL
- Set up Drizzle ORM schema for votes and comments
- Created API client replacing Supabase in Astro frontend
- Added environment configuration (port 3021)

Archived immediately as it's not yet ready for active development.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Till-JS 2025-12-05 13:47:39 +01:00
parent 17313473aa
commit 34c879929b
161 changed files with 12613 additions and 0 deletions

View file

@ -0,0 +1,57 @@
import { Controller, Post, Get, Patch, Delete, Body, Param, Req } from '@nestjs/common';
import { Request } from 'express';
import { CommentsService } from './comments.service';
import { CreateCommentDto } from './dto/create-comment.dto';
@Controller('comments')
export class CommentsController {
constructor(private readonly commentsService: CommentsService) {}
@Post()
async createComment(@Body() createCommentDto: CreateCommentDto, @Req() req: Request) {
const ipAddress = req.ip || req.socket.remoteAddress || 'unknown';
return this.commentsService.createComment(
createCommentDto.softwareId,
createCommentDto.userName,
createCommentDto.comment,
ipAddress
);
}
@Get(':softwareId')
async getComments(@Param('softwareId') softwareId: string) {
return this.commentsService.getApprovedComments(softwareId);
}
}
@Controller('admin/comments')
export class AdminCommentsController {
constructor(private readonly commentsService: CommentsService) {}
@Get()
async getAllComments() {
return this.commentsService.getAllComments();
}
@Get('pending')
async getPendingComments() {
return this.commentsService.getPendingComments();
}
@Patch(':id/approve')
async approveComment(@Param('id') id: string) {
// TODO: Get actual moderator ID from auth
return this.commentsService.approveComment(id, 'admin');
}
@Patch(':id/reject')
async rejectComment(@Param('id') id: string) {
// TODO: Get actual moderator ID from auth
return this.commentsService.rejectComment(id, 'admin');
}
@Delete(':id')
async deleteComment(@Param('id') id: string) {
return this.commentsService.deleteComment(id);
}
}

View file

@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { CommentsController, AdminCommentsController } from './comments.controller';
import { CommentsService } from './comments.service';
@Module({
controllers: [CommentsController, AdminCommentsController],
providers: [CommentsService],
exports: [CommentsService],
})
export class CommentsModule {}

View file

@ -0,0 +1,91 @@
import { Injectable, Inject } from '@nestjs/common';
import { eq, and, desc } from 'drizzle-orm';
import { DATABASE_CONNECTION } from '../db/database.module';
import { type Database } from '../db/connection';
import { comments, type Comment, type NewComment } from '../db/schema';
import { createHash } from 'crypto';
@Injectable()
export class CommentsService {
constructor(@Inject(DATABASE_CONNECTION) private db: Database) {}
private hashIp(ip: string): string {
return createHash('sha256').update(ip).digest('hex').substring(0, 32);
}
async createComment(
softwareId: string,
userName: string,
commentText: string,
ipAddress: string
): Promise<{ success: boolean; message: string }> {
const ipHash = this.hashIp(ipAddress);
const newComment: NewComment = {
softwareId,
userName,
comment: commentText,
ipHash,
isApproved: false,
isSpam: false,
};
await this.db.insert(comments).values(newComment);
return {
success: true,
message: 'Comment submitted successfully. It will be visible after moderation.',
};
}
async getApprovedComments(softwareId: string): Promise<Comment[]> {
return this.db
.select()
.from(comments)
.where(and(eq(comments.softwareId, softwareId), eq(comments.isApproved, true)))
.orderBy(desc(comments.createdAt));
}
async getAllComments(): Promise<Comment[]> {
return this.db.select().from(comments).orderBy(desc(comments.createdAt));
}
async getPendingComments(): Promise<Comment[]> {
return this.db
.select()
.from(comments)
.where(and(eq(comments.isApproved, false), eq(comments.isSpam, false)))
.orderBy(desc(comments.createdAt));
}
async approveComment(id: string, moderatorId: string): Promise<{ success: boolean }> {
await this.db
.update(comments)
.set({
isApproved: true,
moderatedAt: new Date(),
moderatedBy: moderatorId,
})
.where(eq(comments.id, id));
return { success: true };
}
async rejectComment(id: string, moderatorId: string): Promise<{ success: boolean }> {
await this.db
.update(comments)
.set({
isSpam: true,
moderatedAt: new Date(),
moderatedBy: moderatorId,
})
.where(eq(comments.id, id));
return { success: true };
}
async deleteComment(id: string): Promise<{ success: boolean }> {
await this.db.delete(comments).where(eq(comments.id, id));
return { success: true };
}
}

View file

@ -0,0 +1,19 @@
import { IsString, IsNotEmpty, MaxLength, MinLength } from 'class-validator';
export class CreateCommentDto {
@IsString()
@IsNotEmpty()
softwareId: string;
@IsString()
@IsNotEmpty()
@MinLength(2)
@MaxLength(100)
userName: string;
@IsString()
@IsNotEmpty()
@MinLength(10)
@MaxLength(2000)
comment: string;
}