| import { |
| Body, |
| Controller, |
| Get, |
| Param, |
| Post, |
| Query, |
| Req, |
| Res, |
| StreamableFile, |
| } from '@nestjs/common'; |
| import { ApiTags } from '@nestjs/swagger'; |
| import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service'; |
| import { TrackService } from '@gitroom/nestjs-libraries/track/track.service'; |
| import { RealIP } from 'nestjs-real-ip'; |
| import { UserAgent } from '@gitroom/nestjs-libraries/user/user.agent'; |
| import { TrackEnum } from '@gitroom/nestjs-libraries/user/track.enum'; |
| import { Request, Response } from 'express'; |
| import { makeId } from '@gitroom/nestjs-libraries/services/make.is'; |
| import { getCookieUrlFromDomain } from '@gitroom/helpers/subdomain/subdomain.management'; |
| import { AgentGraphInsertService } from '@gitroom/nestjs-libraries/agent/agent.graph.insert.service'; |
| import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service'; |
| import { AuthService } from '@gitroom/helpers/auth/auth.service'; |
| import { pricing } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/pricing'; |
| import { Readable, pipeline } from 'stream'; |
| import { promisify } from 'util'; |
| import { OnlyURL } from '@gitroom/nestjs-libraries/dtos/webhooks/webhooks.dto'; |
| import { isSafePublicHttpsUrl } from '@gitroom/nestjs-libraries/dtos/webhooks/webhook.url.validator'; |
| import { ssrfSafeDispatcher } from '@gitroom/nestjs-libraries/dtos/webhooks/ssrf.safe.dispatcher'; |
|
|
| const pump = promisify(pipeline); |
|
|
| @ApiTags('Public') |
| @Controller('/public') |
| export class PublicController { |
| constructor( |
| private _trackService: TrackService, |
| private _agentGraphInsertService: AgentGraphInsertService, |
| private _postsService: PostsService, |
| private _subscriptionService: SubscriptionService |
| ) {} |
| @Post('/agent') |
| async createAgent(@Body() body: { text: string; apiKey: string }) { |
| if ( |
| !body.apiKey || |
| !process.env.AGENT_API_KEY || |
| body.apiKey !== process.env.AGENT_API_KEY |
| ) { |
| return; |
| } |
| return this._agentGraphInsertService.newPost(body.text); |
| } |
|
|
| @Get(`/posts/:id`) |
| async getPreview(@Param('id') id: string) { |
| return (await this._postsService.getPostsRecursively(id, true)).map( |
| ({ childrenPost, ...p }) => ({ |
| ...p, |
| ...(p.integration |
| ? { |
| integration: { |
| id: p.integration.id, |
| name: p.integration.name, |
| picture: p.integration.picture, |
| providerIdentifier: p.integration.providerIdentifier, |
| profile: p.integration.profile, |
| }, |
| } |
| : {}), |
| }) |
| ); |
| } |
|
|
| @Get(`/posts/:id/comments`) |
| async getComments(@Param('id') postId: string) { |
| return { comments: await this._postsService.getComments(postId) }; |
| } |
|
|
| @Post('/t') |
| async trackEvent( |
| @Res() res: Response, |
| @Req() req: Request, |
| @RealIP() ip: string, |
| @UserAgent() userAgent: string, |
| @Body() |
| body: { fbclid?: string; tt: TrackEnum; additional: Record<string, any> } |
| ) { |
| const uniqueId = req?.cookies?.track || makeId(10); |
| const fbclid = req?.cookies?.fbclid || body.fbclid; |
| await this._trackService.track( |
| uniqueId, |
| ip, |
| userAgent, |
| body.tt, |
| body.additional, |
| fbclid |
| ); |
| if (!req.cookies.track) { |
| res.cookie('track', uniqueId, { |
| domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!), |
| ...(!process.env.NOT_SECURED |
| ? { |
| secure: true, |
| httpOnly: true, |
| } |
| : {}), |
| sameSite: 'none', |
| expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365), |
| }); |
| } |
|
|
| if (body.fbclid && !req.cookies.fbclid) { |
| res.cookie('fbclid', body.fbclid, { |
| domain: getCookieUrlFromDomain(process.env.FRONTEND_URL!), |
| ...(!process.env.NOT_SECURED |
| ? { |
| secure: true, |
| httpOnly: true, |
| } |
| : {}), |
| sameSite: 'none', |
| expires: new Date(Date.now() + 1000 * 60 * 60 * 24 * 365), |
| }); |
| } |
|
|
| res.status(200).json({ |
| track: uniqueId, |
| }); |
| } |
|
|
| @Post('/modify-subscription') |
| async modifySubscription(@Body('params') params: string) { |
| try { |
| const load = AuthService.verifyJWT(params) as { |
| orgId: string; |
| billing: 'FREE' | 'STANDARD' | 'TEAM' | 'PRO' | 'ULTIMATE'; |
| }; |
|
|
| if (!load || !load.orgId || !load.billing || !pricing[load.billing]) { |
| return { success: false }; |
| } |
|
|
| const totalChannels = pricing[load.billing].channel || 0; |
|
|
| await this._subscriptionService.modifySubscriptionByOrg( |
| load.orgId, |
| totalChannels, |
| load.billing |
| ); |
|
|
| return { success: true }; |
| } catch (err) { |
| return { success: false }; |
| } |
| } |
|
|
|
|
| @Get('/stream') |
| async streamFile( |
| @Query() query: OnlyURL, |
| @Res() res: Response, |
| @Req() req: Request |
| ) { |
| const { url } = query; |
| if (!url.endsWith('mp4')) { |
| return res.status(400).send('Invalid video URL'); |
| } |
|
|
| const ac = new AbortController(); |
| const onClose = () => ac.abort(); |
| req.on('aborted', onClose); |
| res.on('close', onClose); |
|
|
| |
| |
| |
| const MAX_REDIRECTS = 5; |
| let currentUrl = url; |
| let r: globalThis.Response | undefined; |
| for (let hop = 0; hop <= MAX_REDIRECTS; hop++) { |
| if (!(await isSafePublicHttpsUrl(currentUrl))) { |
| return res.status(400).send('Blocked URL'); |
| } |
|
|
| r = await fetch(currentUrl, { |
| signal: ac.signal, |
| redirect: 'manual', |
| |
| dispatcher: ssrfSafeDispatcher, |
| }); |
|
|
| if (r.status >= 300 && r.status < 400) { |
| const location = r.headers.get('location'); |
| if (!location) { |
| return res.status(502).send('Redirect without Location'); |
| } |
| try { |
| currentUrl = new URL(location, currentUrl).toString(); |
| } catch { |
| return res.status(400).send('Invalid redirect target'); |
| } |
| continue; |
| } |
|
|
| break; |
| } |
|
|
| if (!r) { |
| return res.status(502).send('No upstream response'); |
| } |
|
|
| if (r.status >= 300 && r.status < 400) { |
| return res.status(508).send('Too many redirects'); |
| } |
|
|
| if (!r.ok && r.status !== 206) { |
| res.status(r.status); |
| throw new Error(`Upstream error: ${r.statusText}`); |
| } |
|
|
| const type = r.headers.get('content-type') ?? 'application/octet-stream'; |
| res.setHeader('Content-Type', type); |
|
|
| const contentRange = r.headers.get('content-range'); |
| if (contentRange) res.setHeader('Content-Range', contentRange); |
|
|
| const len = r.headers.get('content-length'); |
| if (len) res.setHeader('Content-Length', len); |
|
|
| const acceptRanges = r.headers.get('accept-ranges') ?? 'bytes'; |
| res.setHeader('Accept-Ranges', acceptRanges); |
|
|
| if (r.status === 206) res.status(206); |
|
|
| try { |
| await pump(Readable.fromWeb(r.body as any), res); |
| } catch (err) {} |
| } |
| } |
|
|