mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-21 15:06:41 +02:00
chore: archive inactive projects to apps-archived/
Move inactive projects out of active workspace: - bauntown (community website) - maerchenzauber (AI story generation) - memoro (voice memo app) - news (news aggregation) - nutriphi (nutrition tracking) - reader (reading app) - uload (URL shortener) - wisekeep (AI wisdom extraction) Update CLAUDE.md documentation: - Add presi to active projects - Document archived projects section - Update workspace configuration Archived apps can be re-activated by moving back to apps/ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
b97149ac12
commit
61d181fbc2
3148 changed files with 437 additions and 46640 deletions
|
|
@ -0,0 +1,95 @@
|
|||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
NotFoundException,
|
||||
ForbiddenException,
|
||||
} from '@nestjs/common';
|
||||
import { AuthGuard, CurrentUser } from '@mana-core/nestjs-integration';
|
||||
import { AnalyticsService } from '../services/analytics.service';
|
||||
import { LinksService } from '../services/links.service';
|
||||
|
||||
@Controller('api/analytics')
|
||||
@UseGuards(AuthGuard)
|
||||
export class AnalyticsController {
|
||||
constructor(
|
||||
private readonly analyticsService: AnalyticsService,
|
||||
private readonly linksService: LinksService
|
||||
) {}
|
||||
|
||||
@Get('links/:linkId')
|
||||
async getLinkAnalytics(
|
||||
@CurrentUser() user: any,
|
||||
@Param('linkId') linkId: string,
|
||||
@Query('from') fromDate?: string,
|
||||
@Query('to') toDate?: string
|
||||
) {
|
||||
const userId = user.sub;
|
||||
|
||||
// Verify user owns the link
|
||||
const link = await this.linksService.getLinkById(linkId, userId);
|
||||
if (!link) {
|
||||
throw new NotFoundException('Link not found');
|
||||
}
|
||||
|
||||
const stats = await this.analyticsService.getStats(
|
||||
linkId,
|
||||
fromDate ? new Date(fromDate) : undefined,
|
||||
toDate ? new Date(toDate) : undefined
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
linkId,
|
||||
shortCode: link.shortCode,
|
||||
stats,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Get('links/:linkId/clicks')
|
||||
async getLinkClicks(
|
||||
@CurrentUser() user: any,
|
||||
@Param('linkId') linkId: string,
|
||||
@Query('limit') limit: number = 100
|
||||
) {
|
||||
const userId = user.sub;
|
||||
|
||||
// Verify user owns the link
|
||||
const link = await this.linksService.getLinkById(linkId, userId);
|
||||
if (!link) {
|
||||
throw new NotFoundException('Link not found');
|
||||
}
|
||||
|
||||
const { clicks, total } = await this.analyticsService.getRecentClicks(linkId, limit);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
linkId,
|
||||
clicks: clicks.map((click) => ({
|
||||
...click,
|
||||
ipHash: undefined, // Don't expose IP hash
|
||||
})),
|
||||
total,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Get('overview')
|
||||
async getOverview(@CurrentUser() user: any) {
|
||||
const userId = user.sub;
|
||||
const totalLinks = await this.linksService.getLinkCount(userId);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
totalLinks,
|
||||
// Add more overview stats as needed
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import { Controller, Get } from '@nestjs/common';
|
||||
import { HealthCheckService, HealthCheck, HealthCheckResult } from '@nestjs/terminus';
|
||||
|
||||
@Controller('health')
|
||||
export class HealthController {
|
||||
constructor(private health: HealthCheckService) {}
|
||||
|
||||
@Get()
|
||||
@HealthCheck()
|
||||
check(): Promise<HealthCheckResult> {
|
||||
return this.health.check([]);
|
||||
}
|
||||
|
||||
@Get('ready')
|
||||
ready() {
|
||||
return {
|
||||
status: 'ready',
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
@Get('live')
|
||||
live() {
|
||||
return {
|
||||
status: 'live',
|
||||
timestamp: new Date().toISOString(),
|
||||
uptime: process.uptime(),
|
||||
environment: process.env.NODE_ENV || 'development',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Patch,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { AuthGuard, CurrentUser } from '@mana-core/nestjs-integration';
|
||||
import { LinksService, type CreateLinkDto, type UpdateLinkDto } from '../services/links.service';
|
||||
|
||||
@Controller('api/links')
|
||||
@UseGuards(AuthGuard)
|
||||
export class LinksController {
|
||||
constructor(private readonly linksService: LinksService) {}
|
||||
|
||||
@Get()
|
||||
async getLinks(
|
||||
@CurrentUser() user: any,
|
||||
@Query('page') page: number = 1,
|
||||
@Query('limit') limit: number = 20,
|
||||
@Query('search') search?: string,
|
||||
@Query('isActive') isActive?: boolean
|
||||
) {
|
||||
const userId = user.sub;
|
||||
const { items, total } = await this.linksService.getLinks(userId, {
|
||||
page,
|
||||
limit,
|
||||
search,
|
||||
isActive,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
links: items.map((link) => ({
|
||||
...link,
|
||||
shortUrl: this.linksService.getShortUrl(link.shortCode),
|
||||
hasPassword: !!link.password,
|
||||
password: undefined, // Never send password to client
|
||||
})),
|
||||
pagination: {
|
||||
page,
|
||||
limit,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limit),
|
||||
hasMore: page * limit < total,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async getLink(@CurrentUser() user: any, @Param('id') id: string) {
|
||||
const userId = user.sub;
|
||||
const link = await this.linksService.getLinkById(id, userId);
|
||||
|
||||
if (!link) {
|
||||
throw new NotFoundException('Link not found');
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
...link,
|
||||
shortUrl: this.linksService.getShortUrl(link.shortCode),
|
||||
hasPassword: !!link.password,
|
||||
password: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Post()
|
||||
async createLink(@CurrentUser() user: any, @Body() dto: CreateLinkDto) {
|
||||
const userId = user.sub;
|
||||
const link = await this.linksService.createLink(userId, dto);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
...link,
|
||||
shortUrl: this.linksService.getShortUrl(link.shortCode),
|
||||
hasPassword: !!link.password,
|
||||
password: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
async updateLink(@CurrentUser() user: any, @Param('id') id: string, @Body() dto: UpdateLinkDto) {
|
||||
const userId = user.sub;
|
||||
const link = await this.linksService.updateLink(id, userId, dto);
|
||||
|
||||
if (!link) {
|
||||
throw new NotFoundException('Link not found');
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
...link,
|
||||
shortUrl: this.linksService.getShortUrl(link.shortCode),
|
||||
hasPassword: !!link.password,
|
||||
password: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
async deleteLink(@CurrentUser() user: any, @Param('id') id: string) {
|
||||
const userId = user.sub;
|
||||
const deleted = await this.linksService.deleteLink(id, userId);
|
||||
|
||||
if (!deleted) {
|
||||
throw new NotFoundException('Link not found');
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Link deleted successfully',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
import { Controller, Get, Post, Param, Body, Req, Res, HttpStatus, Query } from '@nestjs/common';
|
||||
import { Response, Request } from 'express';
|
||||
import { RedirectService } from '../services/redirect.service';
|
||||
import { AnalyticsService } from '../services/analytics.service';
|
||||
|
||||
@Controller()
|
||||
export class RedirectController {
|
||||
constructor(
|
||||
private readonly redirectService: RedirectService,
|
||||
private readonly analyticsService: AnalyticsService
|
||||
) {}
|
||||
|
||||
@Get(':code')
|
||||
async redirect(
|
||||
@Param('code') code: string,
|
||||
@Query('utm_source') utmSource: string,
|
||||
@Query('utm_medium') utmMedium: string,
|
||||
@Query('utm_campaign') utmCampaign: string,
|
||||
@Req() request: Request,
|
||||
@Res() response: Response
|
||||
) {
|
||||
// Skip for API and health routes
|
||||
if (code === 'v1' || code === 'health') {
|
||||
return response.status(HttpStatus.NOT_FOUND).json({
|
||||
success: false,
|
||||
error: 'not_found',
|
||||
});
|
||||
}
|
||||
|
||||
const result = await this.redirectService.getRedirect(code);
|
||||
|
||||
if (!result.success) {
|
||||
switch (result.error) {
|
||||
case 'not_found':
|
||||
return response.status(HttpStatus.NOT_FOUND).json({
|
||||
success: false,
|
||||
error: 'Link not found',
|
||||
});
|
||||
|
||||
case 'expired':
|
||||
return response.status(HttpStatus.GONE).json({
|
||||
success: false,
|
||||
error: 'This link has expired',
|
||||
});
|
||||
|
||||
case 'inactive':
|
||||
return response.status(HttpStatus.GONE).json({
|
||||
success: false,
|
||||
error: 'This link is no longer active',
|
||||
});
|
||||
|
||||
case 'max_clicks':
|
||||
return response.status(HttpStatus.GONE).json({
|
||||
success: false,
|
||||
error: 'This link has reached its maximum clicks',
|
||||
});
|
||||
|
||||
case 'password_required':
|
||||
return response.status(HttpStatus.OK).json({
|
||||
success: false,
|
||||
passwordRequired: true,
|
||||
linkId: result.linkId,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Record click asynchronously (don't wait)
|
||||
this.analyticsService
|
||||
.recordClick(result.linkId!, {
|
||||
userAgent: request.headers['user-agent'] || '',
|
||||
referer: request.headers['referer'] || '',
|
||||
ip: request.ip,
|
||||
utmSource,
|
||||
utmMedium,
|
||||
utmCampaign,
|
||||
})
|
||||
.catch((err) => console.error('Failed to record click:', err));
|
||||
|
||||
// Perform redirect
|
||||
return response.redirect(302, result.targetUrl!);
|
||||
}
|
||||
|
||||
@Post(':code/unlock')
|
||||
async unlockLink(
|
||||
@Param('code') code: string,
|
||||
@Body('password') password: string,
|
||||
@Res() response: Response
|
||||
) {
|
||||
const result = await this.redirectService.verifyPassword(code, password);
|
||||
|
||||
if (!result.success) {
|
||||
return response.status(HttpStatus.UNAUTHORIZED).json({
|
||||
success: false,
|
||||
error: 'Invalid password',
|
||||
});
|
||||
}
|
||||
|
||||
return response.json({
|
||||
success: true,
|
||||
targetUrl: result.targetUrl,
|
||||
});
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue