Skip to content

Commit

Permalink
Merge pull request #490 from gitroomhq/feat/public-api
Browse files Browse the repository at this point in the history
Public API
  • Loading branch information
nevo-david authored Dec 14, 2024
2 parents 87892ed + 92fa8f9 commit 1c877d2
Show file tree
Hide file tree
Showing 18 changed files with 550 additions and 525 deletions.
1 change: 0 additions & 1 deletion apps/backend/src/api/routes/media.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import handleR2Upload from '@gitroom/nestjs-libraries/upload/r2.uploader';
import { FileInterceptor } from '@nestjs/platform-express';
import { CustomFileValidationPipe } from '@gitroom/nestjs-libraries/upload/custom.upload.validation';
import { SubscriptionService } from '@gitroom/nestjs-libraries/database/prisma/subscriptions/subscription.service';
import { basename } from 'path';
import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';

@ApiTags('Media')
Expand Down
4 changes: 3 additions & 1 deletion apps/backend/src/api/routes/users.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export class UsersController {
if (!organization) {
throw new HttpForbiddenException();
}

// @ts-ignore
return {
...user,
orgId: organization.id,
Expand All @@ -61,6 +61,8 @@ export class UsersController {
isLifetime: !!organization?.subscription?.isLifetime,
admin: !!user.isSuperAdmin,
impersonate: !!req.cookies.impersonate,
// @ts-ignore
publicApi: (organization?.users[0]?.role === 'SUPERADMIN' || organization?.users[0]?.role === 'ADMIN') ? organization?.apiKey : '',
};
}

Expand Down
21 changes: 20 additions & 1 deletion apps/backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,31 @@ import { APP_GUARD } from '@nestjs/core';
import { PoliciesGuard } from '@gitroom/backend/services/auth/permissions/permissions.guard';
import { BullMqModule } from '@gitroom/nestjs-libraries/bull-mq-transport-new/bull.mq.module';
import { PluginModule } from '@gitroom/plugins/plugin.module';
import { PublicApiModule } from '@gitroom/backend/public-api/public.api.module';
import { ThrottlerBehindProxyGuard } from '@gitroom/nestjs-libraries/throttler/throttler.provider';
import { ThrottlerModule } from '@nestjs/throttler';

@Global()
@Module({
imports: [BullMqModule, DatabaseModule, ApiModule, PluginModule],
imports: [
BullMqModule,
DatabaseModule,
ApiModule,
PluginModule,
PublicApiModule,
ThrottlerModule.forRoot([
{
ttl: 3600000,
limit: 30,
},
]),
],
controllers: [],
providers: [
{
provide: APP_GUARD,
useClass: ThrottlerBehindProxyGuard,
},
{
provide: APP_GUARD,
useClass: PoliciesGuard,
Expand Down
42 changes: 42 additions & 0 deletions apps/backend/src/public-api/public.api.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { AuthService } from '@gitroom/backend/services/auth/auth.service';
import { StripeService } from '@gitroom/nestjs-libraries/services/stripe.service';
import { PoliciesGuard } from '@gitroom/backend/services/auth/permissions/permissions.guard';
import { PermissionsService } from '@gitroom/backend/services/auth/permissions/permissions.service';
import { IntegrationManager } from '@gitroom/nestjs-libraries/integrations/integration.manager';
import { UploadModule } from '@gitroom/nestjs-libraries/upload/upload.module';
import { OpenaiService } from '@gitroom/nestjs-libraries/openai/openai.service';
import { ExtractContentService } from '@gitroom/nestjs-libraries/openai/extract.content.service';
import { CodesService } from '@gitroom/nestjs-libraries/services/codes.service';
import { PublicIntegrationsController } from '@gitroom/backend/public-api/routes/v1/public.integrations.controller';
import { PublicAuthMiddleware } from '@gitroom/backend/services/auth/public.auth.middleware';

const authenticatedController = [
PublicIntegrationsController
];
@Module({
imports: [
UploadModule,
],
controllers: [
...authenticatedController,
],
providers: [
AuthService,
StripeService,
OpenaiService,
ExtractContentService,
PoliciesGuard,
PermissionsService,
CodesService,
IntegrationManager,
],
get exports() {
return [...this.imports, ...this.providers];
},
})
export class PublicApiModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(PublicAuthMiddleware).forRoutes(...authenticatedController);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import {
Body,
Controller,
Get,
HttpException,
Post,
Query,
UploadedFile,
UseInterceptors,
} from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { GetOrgFromRequest } from '@gitroom/nestjs-libraries/user/org.from.request';
import { Organization } from '@prisma/client';
import { IntegrationService } from '@gitroom/nestjs-libraries/database/prisma/integrations/integration.service';
import { CheckPolicies } from '@gitroom/backend/services/auth/permissions/permissions.ability';
import {
AuthorizationActions,
Sections,
} from '@gitroom/backend/services/auth/permissions/permissions.service';
import { CreatePostDto } from '@gitroom/nestjs-libraries/dtos/posts/create.post.dto';
import { PostsService } from '@gitroom/nestjs-libraries/database/prisma/posts/posts.service';
import { FileInterceptor } from '@nestjs/platform-express';
import { UploadFactory } from '@gitroom/nestjs-libraries/upload/upload.factory';
import { MediaService } from '@gitroom/nestjs-libraries/database/prisma/media/media.service';
import { GetPostsDto } from '@gitroom/nestjs-libraries/dtos/posts/get.posts.dto';

@ApiTags('Public API')
@Controller('/public/v1')
export class PublicIntegrationsController {
private storage = UploadFactory.createStorage();

constructor(
private _integrationService: IntegrationService,
private _postsService: PostsService,
private _mediaService: MediaService
) {}

@Post('/upload')
@UseInterceptors(FileInterceptor('file'))
async uploadSimple(
@GetOrgFromRequest() org: Organization,
@UploadedFile('file') file: Express.Multer.File
) {
if (!file) {
throw new HttpException({ msg: 'No file provided' }, 400);
}

const getFile = await this.storage.uploadFile(file);
return this._mediaService.saveFile(
org.id,
getFile.originalname,
getFile.path
);
}

@Get('/posts')
async getPosts(
@GetOrgFromRequest() org: Organization,
@Query() query: GetPostsDto
) {
const posts = await this._postsService.getPosts(org.id, query);

return {
posts,
// comments,
};
}

@Post('/posts')
@CheckPolicies([AuthorizationActions.Create, Sections.POSTS_PER_MONTH])
createPost(
@GetOrgFromRequest() org: Organization,
@Body() body: CreatePostDto
) {
console.log(JSON.stringify(body, null, 2));
return this._postsService.createPost(org.id, body);
}

@Get('/integrations')
async listIntegration(@GetOrgFromRequest() org: Organization) {
return (await this._integrationService.getIntegrationsList(org.id)).map(
(org) => ({
id: org.id,
name: org.name,
identifier: org.providerIdentifier,
picture: org.picture,
disabled: org.disabled,
profile: org.profile,
customer: org.customer
? {
id: org.customer.id,
name: org.customer.name,
}
: undefined,
})
);
}
}
4 changes: 4 additions & 0 deletions apps/backend/src/services/auth/auth.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ export class AuthMiddleware implements NestMiddleware {
throw new HttpForbiddenException();
}

if (!setOrg.apiKey) {
await this._organizationService.updateApiKey(setOrg.id);
}

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
req.user = user;
Expand Down
35 changes: 35 additions & 0 deletions apps/backend/src/services/auth/public.auth.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { HttpStatus, Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';
import { OrganizationService } from '@gitroom/nestjs-libraries/database/prisma/organizations/organization.service';
import { HttpForbiddenException } from '@gitroom/nestjs-libraries/services/exception.filter';

@Injectable()
export class PublicAuthMiddleware implements NestMiddleware {
constructor(private _organizationService: OrganizationService) {}
async use(req: Request, res: Response, next: NextFunction) {
const auth = (req.headers.authorization ||
req.headers.Authorization) as string;
if (!auth) {
res.status(HttpStatus.UNAUTHORIZED).json({ msg: 'No API Key found' });
return;
}
try {
const org = await this._organizationService.getOrgByApiKey(auth);
if (!org) {
res.status(HttpStatus.UNAUTHORIZED).json({ msg: 'Invalid API key' });
return ;
}

if (!!process.env.STRIPE_SECRET_KEY && !org.subscription) {
res.status(HttpStatus.UNAUTHORIZED).json({ msg: 'No subscription found' });
return ;
}

// @ts-ignore
req.org = {...org, users: [{users: {role: 'SUPERADMIN'}}]};
} catch (err) {
throw new HttpForbiddenException();
}
next();
}
}
2 changes: 2 additions & 0 deletions apps/frontend/src/components/layout/settings.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { useUser } from '@gitroom/frontend/components/layout/user.context';
import { LogoutComponent } from '@gitroom/frontend/components/layout/logout.component';
import { useSearchParams } from 'next/navigation';
import { useVariables } from '@gitroom/react/helpers/variable.context';
import { PublicComponent } from '@gitroom/frontend/components/public-api/public.component';

export const SettingsPopup: FC<{ getRef?: Ref<any> }> = (props) => {
const {isGeneral} = useVariables();
Expand Down Expand Up @@ -195,6 +196,7 @@ export const SettingsPopup: FC<{ getRef?: Ref<any> }> = (props) => {
</div>
)}
{!!user?.tier?.team_members && isGeneral && <TeamsComponent />}
{!!user?.tier?.public_api && isGeneral && <PublicComponent />}
{showLogout && <LogoutComponent />}
</div>
</form>
Expand Down
2 changes: 2 additions & 0 deletions apps/frontend/src/components/layout/user.context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const UserContext = createContext<
| (User & {
orgId: string;
tier: PricingInnerInterface;
publicApi: string;
role: 'USER' | 'ADMIN' | 'SUPERADMIN';
totalChannels: number;
isLifetime?: boolean;
Expand All @@ -24,6 +25,7 @@ export const ContextWrapper: FC<{
orgId: string;
tier: 'FREE' | 'STANDARD' | 'PRO' | 'ULTIMATE' | 'TEAM';
role: 'USER' | 'ADMIN' | 'SUPERADMIN';
publicApi: string;
totalChannels: number;
};
children: ReactNode;
Expand Down
50 changes: 50 additions & 0 deletions apps/frontend/src/components/public-api/public.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use client';

import { useState, useCallback } from 'react';
import { useUser } from '../layout/user.context';
import { Button } from '@gitroom/react/form/button';
import copy from 'copy-to-clipboard';
import { useToaster } from '@gitroom/react/toaster/toaster';

export const PublicComponent = () => {
const user = useUser();
const toaster = useToaster();
const [reveal, setReveal] = useState(false);

const copyToClipboard = useCallback(() => {
toaster.show('API Key copied to clipboard', 'success');
copy(user?.publicApi!);
}, [user]);

if (!user || !user.publicApi) {
return null;
}

return (
<div className="flex flex-col">
<h2 className="text-[24px]">Public API</h2>
<div className="text-customColor18 mt-[4px]">
Use Postiz API to integrate with your tools.
</div>
<div className="my-[16px] mt-[16px] bg-sixth border-fifth items-center border rounded-[4px] p-[24px] flex gap-[24px]">
<div className="flex items-center">
{reveal ? (
user.publicApi
) : (
<>
<div className="blur-sm">{user.publicApi.slice(0, -5)}</div>
<div>{user.publicApi.slice(-5)}</div>
</>
)}
</div>
<div>
{!reveal ? (
<Button onClick={() => setReveal(true)}>Reveal</Button>
) : (
<Button onClick={copyToClipboard}>Copy Key</Button>
)}
</div>
</div>
</div>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Role, SubscriptionTier } from '@prisma/client';
import { Injectable } from '@nestjs/common';
import { AuthService } from '@gitroom/helpers/auth/auth.service';
import { CreateOrgUserDto } from '@gitroom/nestjs-libraries/dtos/auth/create.org.user.dto';
import { makeId } from '@gitroom/nestjs-libraries/services/make.is';

@Injectable()
export class OrganizationRepository {
Expand All @@ -12,6 +13,23 @@ export class OrganizationRepository {
private _user: PrismaRepository<'user'>
) {}

getOrgByApiKey(api: string) {
return this._organization.model.organization.findFirst({
where: {
apiKey: api,
},
include: {
subscription: {
select: {
subscriptionTier: true,
totalChannels: true,
isLifetime: true,
},
},
},
});
}

getUserOrg(id: string) {
return this._userOrg.model.userOrganization.findFirst({
where: {
Expand Down Expand Up @@ -83,6 +101,17 @@ export class OrganizationRepository {
});
}

updateApiKey(orgId: string) {
return this._organization.model.organization.update({
where: {
id: orgId,
},
data: {
apiKey: AuthService.fixedEncryption(makeId(20)),
},
});
}

async getOrgsByUserId(userId: string) {
return this._organization.model.organization.findMany({
where: {
Expand Down Expand Up @@ -183,6 +212,7 @@ export class OrganizationRepository {
return this._organization.model.organization.create({
data: {
name: body.company,
apiKey: AuthService.fixedEncryption(makeId(20)),
users: {
create: {
role: Role.SUPERADMIN,
Expand Down
Loading

0 comments on commit 1c877d2

Please sign in to comment.