commit
1c877d2af2
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export class UsersController {
|
|||
if (!organization) {
|
||||
throw new HttpForbiddenException();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return {
|
||||
...user,
|
||||
orgId: organization.id,
|
||||
|
|
@ -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 : '',
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export const UserContext = createContext<
|
|||
| (User & {
|
||||
orgId: string;
|
||||
tier: PricingInnerInterface;
|
||||
publicApi: string;
|
||||
role: 'USER' | 'ADMIN' | 'SUPERADMIN';
|
||||
totalChannels: number;
|
||||
isLifetime?: boolean;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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: {
|
||||
|
|
@ -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: {
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -17,7 +17,10 @@ export class OrganizationService {
|
|||
async createOrgAndUser(
|
||||
body: Omit<CreateOrgUserDto, 'providerToken'> & { providerId?: string }
|
||||
) {
|
||||
return this._organizationRepository.createOrgAndUser(body, this._notificationsService.hasEmailProvider());
|
||||
return this._organizationRepository.createOrgAndUser(
|
||||
body,
|
||||
this._notificationsService.hasEmailProvider()
|
||||
);
|
||||
}
|
||||
|
||||
addUserToOrg(
|
||||
|
|
@ -33,6 +36,10 @@ export class OrganizationService {
|
|||
return this._organizationRepository.getOrgById(id);
|
||||
}
|
||||
|
||||
getOrgByApiKey(api: string) {
|
||||
return this._organizationRepository.getOrgByApiKey(api);
|
||||
}
|
||||
|
||||
getUserOrg(id: string) {
|
||||
return this._organizationRepository.getUserOrg(id);
|
||||
}
|
||||
|
|
@ -41,6 +48,10 @@ export class OrganizationService {
|
|||
return this._organizationRepository.getOrgsByUserId(userId);
|
||||
}
|
||||
|
||||
updateApiKey(orgId: string) {
|
||||
return this._organizationRepository.updateApiKey(orgId);
|
||||
}
|
||||
|
||||
getTeam(orgId: string) {
|
||||
return this._organizationRepository.getTeam(orgId);
|
||||
}
|
||||
|
|
@ -82,6 +93,9 @@ export class OrganizationService {
|
|||
}
|
||||
|
||||
disableOrEnableNonSuperAdminUsers(orgId: string, disable: boolean) {
|
||||
return this._organizationRepository.disableOrEnableNonSuperAdminUsers(orgId, disable);
|
||||
return this._organizationRepository.disableOrEnableNonSuperAdminUsers(
|
||||
orgId,
|
||||
disable
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,24 +11,25 @@ datasource db {
|
|||
}
|
||||
|
||||
model Organization {
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
description String?
|
||||
users UserOrganization[]
|
||||
media Media[]
|
||||
paymentId String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
github GitHub[]
|
||||
subscription Subscription?
|
||||
Integration Integration[]
|
||||
post Post[] @relation("organization")
|
||||
submittedPost Post[] @relation("submittedForOrg")
|
||||
Comments Comments[]
|
||||
notifications Notifications[]
|
||||
id String @id @default(uuid())
|
||||
name String
|
||||
description String?
|
||||
apiKey String?
|
||||
users UserOrganization[]
|
||||
media Media[]
|
||||
paymentId String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
github GitHub[]
|
||||
subscription Subscription?
|
||||
Integration Integration[]
|
||||
post Post[] @relation("organization")
|
||||
submittedPost Post[] @relation("submittedForOrg")
|
||||
Comments Comments[]
|
||||
notifications Notifications[]
|
||||
buyerOrganization MessagesGroup[]
|
||||
usedCodes UsedCodes[]
|
||||
credits Credits[]
|
||||
usedCodes UsedCodes[]
|
||||
credits Credits[]
|
||||
plugs Plugs[]
|
||||
customers Customer[]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export interface PricingInnerInterface {
|
|||
import_from_channels: boolean;
|
||||
image_generator?: boolean;
|
||||
image_generation_count: number;
|
||||
public_api: boolean;
|
||||
}
|
||||
export interface PricingInterface {
|
||||
[key: string]: PricingInnerInterface;
|
||||
|
|
@ -29,6 +30,7 @@ export const pricing: PricingInterface = {
|
|||
ai: false,
|
||||
import_from_channels: false,
|
||||
image_generator: false,
|
||||
public_api: false,
|
||||
},
|
||||
STANDARD: {
|
||||
current: 'STANDARD',
|
||||
|
|
@ -43,6 +45,7 @@ export const pricing: PricingInterface = {
|
|||
featured_by_gitroom: false,
|
||||
import_from_channels: true,
|
||||
image_generator: false,
|
||||
public_api: true,
|
||||
},
|
||||
TEAM: {
|
||||
current: 'TEAM',
|
||||
|
|
@ -57,6 +60,7 @@ export const pricing: PricingInterface = {
|
|||
ai: true,
|
||||
import_from_channels: true,
|
||||
image_generator: true,
|
||||
public_api: true,
|
||||
},
|
||||
PRO: {
|
||||
current: 'PRO',
|
||||
|
|
@ -71,6 +75,7 @@ export const pricing: PricingInterface = {
|
|||
ai: true,
|
||||
import_from_channels: true,
|
||||
image_generator: true,
|
||||
public_api: true,
|
||||
},
|
||||
ULTIMATE: {
|
||||
current: 'ULTIMATE',
|
||||
|
|
@ -85,5 +90,6 @@ export const pricing: PricingInterface = {
|
|||
ai: true,
|
||||
import_from_channels: true,
|
||||
image_generator: true,
|
||||
public_api: true,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export class GetPostsDto {
|
|||
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
@Max(52)
|
||||
@Max(12)
|
||||
@Min(1)
|
||||
month: number;
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
import { ThrottlerGuard } from '@nestjs/throttler';
|
||||
import { ExecutionContext, Injectable } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class ThrottlerBehindProxyGuard extends ThrottlerGuard {
|
||||
public override async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
if (context.switchToHttp().getRequest().url.includes('/public/v1')) {
|
||||
return super.canActivate(context);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
protected override async getTracker(req: Record<string, any>): Promise<string> {
|
||||
return req.org.id;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -51,6 +51,7 @@
|
|||
"@nestjs/platform-express": "^10.0.2",
|
||||
"@nestjs/schedule": "^4.0.0",
|
||||
"@nestjs/swagger": "^7.3.0",
|
||||
"@nestjs/throttler": "^6.3.0",
|
||||
"@nx/eslint": "19.7.2",
|
||||
"@nx/eslint-plugin": "19.7.2",
|
||||
"@nx/jest": "19.7.2",
|
||||
|
|
|
|||
Loading…
Reference in New Issue