1043 timebox prepare zapier integration (#1967)

* Add create api-key route

* Import module

* Remove required mutation parameter

* Fix Authentication

* Generate random key

* Update Read ApiKeyAbility handler

* Add findMany apiKey route

* Remove useless attribute

* Use signed token for apiKeys

* Authenticate with api keys

* Fix typo

* Add a test for apiKey module

* Revoke token when api key does not exist

* Handler expiresAt parameter

* Fix user passport

* Code review returns: Add API_TOKEN_SECRET

* Code review returns: Rename variable

* Code review returns: Update code style

* Update apiKey schema

* Update create token route

* Update delete token route

* Filter revoked api keys from listApiKeys

* Rename endpoint

* Set default expiry to 2 years

* Code review returns: Update comment

* Generate token after create apiKey

* Code review returns: Update env variable

* Code review returns: Move method to proper service

---------

Co-authored-by: martmull <martmull@hotmail.com>
This commit is contained in:
martmull 2023-10-12 18:07:44 +02:00 committed by GitHub
parent 6b990c8501
commit 8fbad7d3ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 430 additions and 42 deletions

View File

@ -21,6 +21,8 @@ services:
generateValue: true generateValue: true
- key: LOGIN_TOKEN_SECRET - key: LOGIN_TOKEN_SECRET
generateValue: true generateValue: true
- key: API_TOKEN_SECRET
generateValue: true
- key: REFRESH_TOKEN_SECRET - key: REFRESH_TOKEN_SECRET
generateValue: true generateValue: true
- key: PG_DATABASE_URL - key: PG_DATABASE_URL

View File

@ -6,6 +6,7 @@ PG_DATABASE_URL=postgres://twenty:twenty@localhost:5432/default?connection_limit
FRONT_BASE_URL=http://localhost:3001 FRONT_BASE_URL=http://localhost:3001
ACCESS_TOKEN_SECRET=replace_me_with_a_random_string ACCESS_TOKEN_SECRET=replace_me_with_a_random_string
LOGIN_TOKEN_SECRET=replace_me_with_a_random_string LOGIN_TOKEN_SECRET=replace_me_with_a_random_string
API_TOKEN_SECRET=replace_me_with_a_random_string
REFRESH_TOKEN_SECRET=replace_me_with_a_random_string REFRESH_TOKEN_SECRET=replace_me_with_a_random_string
SIGN_IN_PREFILLED=true SIGN_IN_PREFILLED=true
@ -14,6 +15,7 @@ SIGN_IN_PREFILLED=true
# DEBUG_MODE=true # DEBUG_MODE=true
# ACCESS_TOKEN_EXPIRES_IN=30m # ACCESS_TOKEN_EXPIRES_IN=30m
# LOGIN_TOKEN_EXPIRES_IN=15m # LOGIN_TOKEN_EXPIRES_IN=15m
# API_TOKEN_EXPIRES_IN=2y
# REFRESH_TOKEN_EXPIRES_IN=90d # REFRESH_TOKEN_EXPIRES_IN=90d
# FRONT_AUTH_CALLBACK_URL=http://localhost:3001/verify # FRONT_AUTH_CALLBACK_URL=http://localhost:3001/verify
# AUTH_GOOGLE_ENABLED=false # AUTH_GOOGLE_ENABLED=false
@ -25,4 +27,4 @@ SIGN_IN_PREFILLED=true
# LOGGER_DRIVER=console # LOGGER_DRIVER=console
# SENTRY_DSN=https://xxx@xxx.ingest.sentry.io/xxx # SENTRY_DSN=https://xxx@xxx.ingest.sentry.io/xxx
# LOG_LEVEL=error,warn # LOG_LEVEL=error,warn
# FLEXIBLE_BACKEND_ENABLED=false # FLEXIBLE_BACKEND_ENABLED=false

View File

@ -6,9 +6,10 @@ PG_DATABASE_URL=postgres://twenty:twenty@localhost:5432/test?connection_limit=1
# the URL of the front-end app # the URL of the front-end app
FRONT_BASE_URL=http://localhost:3001 FRONT_BASE_URL=http://localhost:3001
# random keys used to generate JWT tokens # random keys used to generate JWT tokens
ACCESS_TOKEN_SECRET=secret_jwt ACCESS_TOKEN_SECRET=secret_jwt
LOGIN_TOKEN_SECRET=secret_login_tokens LOGIN_TOKEN_SECRET=secret_login_tokens
REFRESH_TOKEN_SECRET=secret_refresh_token API_TOKEN_SECRET=secret_api_tokens
REFRESH_TOKEN_SECRET=secret_refresh_token
# ———————— Optional ———————— # ———————— Optional ————————
@ -20,4 +21,4 @@ REFRESH_TOKEN_SECRET=secret_refresh_token
# FRONT_AUTH_CALLBACK_URL=http://localhost:3001/verify # FRONT_AUTH_CALLBACK_URL=http://localhost:3001/verify
# AUTH_GOOGLE_ENABLED=false # AUTH_GOOGLE_ENABLED=false
# STORAGE_TYPE=local # STORAGE_TYPE=local
# STORAGE_LOCAL_PATH=.local-storage # STORAGE_LOCAL_PATH=.local-storage

View File

@ -6,6 +6,7 @@ import {
Activity, Activity,
ActivityTarget, ActivityTarget,
Attachment, Attachment,
ApiKey,
Comment, Comment,
Company, Company,
Favorite, Favorite,
@ -30,6 +31,7 @@ type SubjectsAbility = Subjects<{
Activity: Activity; Activity: Activity;
ActivityTarget: ActivityTarget; ActivityTarget: ActivityTarget;
Attachment: Attachment; Attachment: Attachment;
ApiKey: ApiKey;
Comment: Comment; Comment: Comment;
Company: Company; Company: Company;
Favorite: Favorite; Favorite: Favorite;
@ -55,7 +57,7 @@ export type AppAbility = PureAbility<
@Injectable() @Injectable()
export class AbilityFactory { export class AbilityFactory {
defineAbility(user: User, workspace: Workspace) { defineAbility(workspace: Workspace, user?: User) {
const { can, cannot, build } = new AbilityBuilder<AppAbility>( const { can, cannot, build } = new AbilityBuilder<AppAbility>(
createPrismaAbility, createPrismaAbility,
); );
@ -66,8 +68,18 @@ export class AbilityFactory {
workspaceId: workspace.id, workspaceId: workspace.id,
}, },
}); });
can(AbilityAction.Update, 'User', { id: user.id }); if (user) {
can(AbilityAction.Delete, 'User', { id: user.id }); can(AbilityAction.Update, 'User', { id: user.id });
can(AbilityAction.Delete, 'User', { id: user.id });
} else {
cannot(AbilityAction.Update, 'User');
cannot(AbilityAction.Delete, 'User');
}
// ApiKey
can(AbilityAction.Read, 'ApiKey', { workspaceId: workspace.id });
can(AbilityAction.Create, 'ApiKey');
can(AbilityAction.Update, 'ApiKey', { workspaceId: workspace.id });
// Workspace // Workspace
can(AbilityAction.Read, 'Workspace'); can(AbilityAction.Read, 'Workspace');
@ -76,12 +88,19 @@ export class AbilityFactory {
// Workspace Member // Workspace Member
can(AbilityAction.Read, 'WorkspaceMember', { workspaceId: workspace.id }); can(AbilityAction.Read, 'WorkspaceMember', { workspaceId: workspace.id });
can(AbilityAction.Delete, 'WorkspaceMember', { workspaceId: workspace.id }); if (user) {
cannot(AbilityAction.Delete, 'WorkspaceMember', { userId: user.id }); can(AbilityAction.Delete, 'WorkspaceMember', {
can(AbilityAction.Update, 'WorkspaceMember', { workspaceId: workspace.id,
userId: user.id, });
workspaceId: workspace.id, cannot(AbilityAction.Delete, 'WorkspaceMember', { userId: user.id });
}); can(AbilityAction.Update, 'WorkspaceMember', {
userId: user.id,
workspaceId: workspace.id,
});
} else {
cannot(AbilityAction.Delete, 'WorkspaceMember');
cannot(AbilityAction.Update, 'WorkspaceMember');
}
// Company // Company
can(AbilityAction.Read, 'Company', { workspaceId: workspace.id }); can(AbilityAction.Read, 'Company', { workspaceId: workspace.id });
@ -107,14 +126,19 @@ export class AbilityFactory {
// Comment // Comment
can(AbilityAction.Read, 'Comment', { workspaceId: workspace.id }); can(AbilityAction.Read, 'Comment', { workspaceId: workspace.id });
can(AbilityAction.Create, 'Comment'); can(AbilityAction.Create, 'Comment');
can(AbilityAction.Update, 'Comment', { if (user) {
workspaceId: workspace.id, can(AbilityAction.Update, 'Comment', {
authorId: user.id, workspaceId: workspace.id,
}); authorId: user.id,
can(AbilityAction.Delete, 'Comment', { });
workspaceId: workspace.id, can(AbilityAction.Delete, 'Comment', {
authorId: user.id, workspaceId: workspace.id,
}); authorId: user.id,
});
} else {
cannot(AbilityAction.Update, 'Comment');
cannot(AbilityAction.Delete, 'Comment');
}
// ActivityTarget // ActivityTarget
can(AbilityAction.Read, 'ActivityTarget'); can(AbilityAction.Read, 'ActivityTarget');

View File

@ -122,6 +122,12 @@ import {
ReadViewFilterAbilityHandler, ReadViewFilterAbilityHandler,
UpdateViewFilterAbilityHandler, UpdateViewFilterAbilityHandler,
} from './handlers/view-filter.ability-handler'; } from './handlers/view-filter.ability-handler';
import {
CreateApiKeyAbilityHandler,
UpdateApiKeyAbilityHandler,
ManageApiKeyAbilityHandler,
ReadApiKeyAbilityHandler,
} from './handlers/api-key.ability-handler';
@Global() @Global()
@Module({ @Module({
@ -229,6 +235,11 @@ import {
CreateViewSortAbilityHandler, CreateViewSortAbilityHandler,
UpdateViewSortAbilityHandler, UpdateViewSortAbilityHandler,
DeleteViewSortAbilityHandler, DeleteViewSortAbilityHandler,
// ApiKey
ReadApiKeyAbilityHandler,
ManageApiKeyAbilityHandler,
CreateApiKeyAbilityHandler,
UpdateApiKeyAbilityHandler,
], ],
exports: [ exports: [
AbilityFactory, AbilityFactory,
@ -333,6 +344,11 @@ import {
CreateViewSortAbilityHandler, CreateViewSortAbilityHandler,
UpdateViewSortAbilityHandler, UpdateViewSortAbilityHandler,
DeleteViewSortAbilityHandler, DeleteViewSortAbilityHandler,
// ApiKey
ReadApiKeyAbilityHandler,
ManageApiKeyAbilityHandler,
CreateApiKeyAbilityHandler,
UpdateApiKeyAbilityHandler,
], ],
}) })
export class AbilityModule {} export class AbilityModule {}

View File

@ -0,0 +1,85 @@
import {
ExecutionContext,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { GqlExecutionContext } from '@nestjs/graphql';
import { subject } from '@casl/ability';
import { IAbilityHandler } from 'src/ability/interfaces/ability-handler.interface';
import { AppAbility } from 'src/ability/ability.factory';
import { AbilityAction } from 'src/ability/ability.action';
import { PrismaService } from 'src/database/prisma.service';
import { ApiKeyWhereUniqueInput } from 'src/core/@generated/api-key/api-key-where-unique.input';
import { ApiKeyWhereInput } from 'src/core/@generated/api-key/api-key-where.input';
import { assert } from 'src/utils/assert';
import {
convertToWhereInput,
relationAbilityChecker,
} from 'src/ability/ability.util';
class ApiKeyArgs {
where?: ApiKeyWhereUniqueInput | ApiKeyWhereInput;
[key: string]: any;
}
@Injectable()
export class ManageApiKeyAbilityHandler implements IAbilityHandler {
async handle(ability: AppAbility) {
return ability.can(AbilityAction.Manage, 'ApiKey');
}
}
@Injectable()
export class ReadApiKeyAbilityHandler implements IAbilityHandler {
async handle(ability: AppAbility) {
return ability.can(AbilityAction.Read, 'ApiKey');
}
}
@Injectable()
export class CreateApiKeyAbilityHandler implements IAbilityHandler {
constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs();
const allowed = await relationAbilityChecker(
'ApiKey',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can(AbilityAction.Create, 'ApiKey');
}
}
@Injectable()
export class UpdateApiKeyAbilityHandler implements IAbilityHandler {
constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs<ApiKeyArgs>();
const where = convertToWhereInput(args.where);
const apiKey = await this.prismaService.client.apiKey.findFirst({
where,
});
assert(apiKey, '', NotFoundException);
const allowed = await relationAbilityChecker(
'ApiKey',
ability,
this.prismaService.client,
args,
);
if (!allowed) {
return false;
}
return ability.can(AbilityAction.Update, subject('ApiKey', apiKey));
}
}

View File

@ -74,11 +74,7 @@ import { ExceptionFilter } from './filters/exception.filter';
decoded as JwtPayload, decoded as JwtPayload,
); );
const conditionalSchema = await tenantService.createTenantSchema( return await tenantService.createTenantSchema(workspace.id);
workspace.id,
);
return conditionalSchema;
} catch (error) { } catch (error) {
if (error instanceof JsonWebTokenError) { if (error instanceof JsonWebTokenError) {
//mockedUserJWT //mockedUserJWT

View File

@ -0,0 +1,12 @@
import { Module } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { TokenService } from 'src/core/auth/services/token.service';
import { ApiKeyResolver } from './api-key.resolver';
import { ApiKeyService } from './api-key.service';
@Module({
providers: [ApiKeyResolver, ApiKeyService, TokenService, JwtService],
})
export class ApiKeyModule {}

View File

@ -0,0 +1,28 @@
import { Test, TestingModule } from '@nestjs/testing';
import { JwtService } from '@nestjs/jwt';
import { AbilityFactory } from 'src/ability/ability.factory';
import { TokenService } from 'src/core/auth/services/token.service';
import { ApiKeyResolver } from './api-key.resolver';
import { ApiKeyService } from './api-key.service';
describe('ApiKeyResolver', () => {
let resolver: ApiKeyResolver;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
ApiKeyResolver,
{ provide: ApiKeyService, useValue: {} },
{ provide: TokenService, useValue: {} },
{ provide: JwtService, useValue: {} },
{ provide: AbilityFactory, useValue: {} },
],
}).compile();
resolver = module.get<ApiKeyResolver>(ApiKeyResolver);
});
it('should be defined', () => {
expect(resolver).toBeDefined();
});
});

View File

@ -0,0 +1,82 @@
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { NotFoundException, UseGuards } from '@nestjs/common';
import { accessibleBy } from '@casl/prisma';
import { AbilityGuard } from 'src/guards/ability.guard';
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
import { Workspace } from 'src/core/@generated/workspace/workspace.model';
import { CreateOneApiKeyArgs } from 'src/core/@generated/api-key/create-one-api-key.args';
import { ApiKey } from 'src/core/@generated/api-key/api-key.model';
import { FindManyApiKeyArgs } from 'src/core/@generated/api-key/find-many-api-key.args';
import { DeleteOneApiKeyArgs } from 'src/core/@generated/api-key/delete-one-api-key.args';
import { CheckAbilities } from 'src/decorators/check-abilities.decorator';
import {
CreateApiKeyAbilityHandler,
UpdateApiKeyAbilityHandler,
ReadApiKeyAbilityHandler,
} from 'src/ability/handlers/api-key.ability-handler';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { UserAbility } from 'src/decorators/user-ability.decorator';
import { AppAbility } from 'src/ability/ability.factory';
import { AuthToken } from 'src/core/auth/dto/token.entity';
import { ApiKeyService } from './api-key.service';
@UseGuards(JwtAuthGuard)
@Resolver(() => ApiKey)
export class ApiKeyResolver {
constructor(private readonly apiKeyService: ApiKeyService) {}
@Mutation(() => AuthToken)
@UseGuards(AbilityGuard)
@CheckAbilities(CreateApiKeyAbilityHandler)
async createOneApiKey(
@Args() args: CreateOneApiKeyArgs,
@AuthWorkspace() { id: workspaceId }: Workspace,
): Promise<AuthToken> {
return await this.apiKeyService.generateApiKeyToken(
workspaceId,
args.data.name,
args.data.expiresAt,
);
}
@Mutation(() => ApiKey)
@UseGuards(AbilityGuard)
@CheckAbilities(UpdateApiKeyAbilityHandler)
async revokeOneApiKey(
@Args() args: DeleteOneApiKeyArgs,
): Promise<Partial<ApiKey>> {
const apiKeyToDelete = await this.apiKeyService.findFirst({
where: { ...args.where },
});
if (!apiKeyToDelete) {
throw new NotFoundException();
}
return this.apiKeyService.update({
where: args.where,
data: {
revokedAt: new Date(),
},
});
}
@Query(() => [ApiKey])
@UseGuards(AbilityGuard)
@CheckAbilities(ReadApiKeyAbilityHandler)
async findManyApiKey(
@Args() args: FindManyApiKeyArgs,
@UserAbility() ability: AppAbility,
) {
const filterOptions = [
accessibleBy(ability).WorkspaceMember,
{ revokedAt: null },
];
if (args.where) filterOptions.push(args.where);
return this.apiKeyService.findMany({
...args,
where: { AND: filterOptions },
});
}
}

View File

@ -0,0 +1,63 @@
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { addMilliseconds, addSeconds } from 'date-fns';
import ms from 'ms';
import { PrismaService } from 'src/database/prisma.service';
import { AuthToken } from 'src/core/auth/dto/token.entity';
import { assert } from 'src/utils/assert';
import { EnvironmentService } from 'src/integrations/environment/environment.service';
@Injectable()
export class ApiKeyService {
constructor(
private readonly prismaService: PrismaService,
private readonly environmentService: EnvironmentService,
private readonly jwtService: JwtService,
) {}
findFirst = this.prismaService.client.apiKey.findFirst;
findUniqueOrThrow = this.prismaService.client.apiKey.findUniqueOrThrow;
findMany = this.prismaService.client.apiKey.findMany;
create = this.prismaService.client.apiKey.create;
update = this.prismaService.client.apiKey.update;
delete = this.prismaService.client.apiKey.delete;
async generateApiKeyToken(
workspaceId: string,
name: string,
expiresAt?: Date | string,
): Promise<AuthToken> {
const secret = this.environmentService.getApiTokenSecret();
let expiresIn: string | number;
let expirationDate: Date;
const now = new Date().getTime();
if (expiresAt) {
expiresIn = Math.floor((new Date(expiresAt).getTime() - now) / 1000);
expirationDate = addSeconds(now, expiresIn);
} else {
expiresIn = this.environmentService.getApiTokenExpiresIn();
expirationDate = addMilliseconds(now, ms(expiresIn));
}
assert(expiresIn, '', InternalServerErrorException);
const jwtPayload = {
sub: workspaceId,
};
const { id } = await this.prismaService.client.apiKey.create({
data: {
expiresAt: expiresAt,
name: name,
workspaceId: workspaceId,
},
});
return {
token: this.jwtService.sign(jwtPayload, {
secret,
expiresIn,
jwtid: id,
}),
expiresAt: expirationDate,
};
}
}

View File

@ -1,14 +1,19 @@
import { PassportStrategy } from '@nestjs/passport'; import { PassportStrategy } from '@nestjs/passport';
import { Injectable, UnauthorizedException } from '@nestjs/common'; import {
ForbiddenException,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { Strategy, ExtractJwt } from 'passport-jwt'; import { Strategy, ExtractJwt } from 'passport-jwt';
import { User, Workspace } from '@prisma/client'; import { User, Workspace } from '@prisma/client';
import { PrismaService } from 'src/database/prisma.service'; import { PrismaService } from 'src/database/prisma.service';
import { EnvironmentService } from 'src/integrations/environment/environment.service'; import { EnvironmentService } from 'src/integrations/environment/environment.service';
import { assert } from 'src/utils/assert';
export type JwtPayload = { sub: string; workspaceId: string }; export type JwtPayload = { sub: string; workspaceId: string; jti?: string };
export type PassportUser = { user: User; workspace: Workspace }; export type PassportUser = { user?: User; workspace: Workspace };
@Injectable() @Injectable()
export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') { export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
@ -24,22 +29,25 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
} }
async validate(payload: JwtPayload): Promise<PassportUser> { async validate(payload: JwtPayload): Promise<PassportUser> {
const user = await this.prismaService.client.user.findUniqueOrThrow({ const workspace = await this.prismaService.client.workspace.findUnique({
where: { id: payload.sub }, where: { id: payload.workspaceId ?? payload.sub },
}); });
if (!user) {
throw new UnauthorizedException();
}
const workspace =
await this.prismaService.client.workspace.findUniqueOrThrow({
where: { id: payload.workspaceId },
});
if (!workspace) { if (!workspace) {
throw new UnauthorizedException(); throw new UnauthorizedException();
} }
if (payload.jti) {
// If apiKey has been deleted or revoked, we throw an error
const apiKey = await this.prismaService.client.apiKey.findUniqueOrThrow({
where: { id: payload.jti },
});
assert(!apiKey.revokedAt, 'This API Key is revoked', ForbiddenException);
}
const user = payload.workspaceId
? await this.prismaService.client.user.findUniqueOrThrow({
where: { id: payload.sub },
})
: undefined;
return { user, workspace }; return { user, workspace };
} }

View File

@ -14,6 +14,7 @@ import { AttachmentModule } from './attachment/attachment.module';
import { ActivityModule } from './activity/activity.module'; import { ActivityModule } from './activity/activity.module';
import { ViewModule } from './view/view.module'; import { ViewModule } from './view/view.module';
import { FavoriteModule } from './favorite/favorite.module'; import { FavoriteModule } from './favorite/favorite.module';
import { ApiKeyModule } from './api-key/api-key.module';
@Module({ @Module({
imports: [ imports: [
@ -31,6 +32,7 @@ import { FavoriteModule } from './favorite/favorite.module';
ActivityModule, ActivityModule,
ViewModule, ViewModule,
FavoriteModule, FavoriteModule,
ApiKeyModule,
], ],
exports: [ exports: [
AuthModule, AuthModule,
@ -43,6 +45,7 @@ import { FavoriteModule } from './favorite/favorite.module';
AnalyticsModule, AnalyticsModule,
AttachmentModule, AttachmentModule,
FavoriteModule, FavoriteModule,
ApiKeyModule,
], ],
}) })
export class CoreModule {} export class CoreModule {}

View File

@ -0,0 +1,22 @@
-- AlterEnum
ALTER TYPE "ViewFilterOperand" ADD VALUE 'IsNotNull';
-- CreateTable
CREATE TABLE "api_keys" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"key" TEXT NOT NULL,
"workspaceId" TEXT NOT NULL,
"expiresAt" TIMESTAMP(3),
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"deletedAt" TIMESTAMP(3),
CONSTRAINT "api_keys_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "api_keys_key_key" ON "api_keys"("key");
-- AddForeignKey
ALTER TABLE "api_keys" ADD CONSTRAINT "api_keys_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "workspaces"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@ -0,0 +1,12 @@
/*
Warnings:
- You are about to drop the column `key` on the `api_keys` table. All the data in the column will be lost.
*/
-- DropIndex
DROP INDEX "api_keys_key_key";
-- AlterTable
ALTER TABLE "api_keys" DROP COLUMN "key",
ADD COLUMN "revokedAt" TIMESTAMP(3);

View File

@ -178,6 +178,7 @@ model Workspace {
viewFilters ViewFilter[] viewFilters ViewFilter[]
views View[] views View[]
viewSorts ViewSort[] viewSorts ViewSort[]
apiKeys ApiKey[]
/// @TypeGraphQL.omit(input: true, output: true) /// @TypeGraphQL.omit(input: true, output: true)
deletedAt DateTime? deletedAt DateTime?
@ -886,3 +887,23 @@ model ViewField {
@@id([viewId, key]) @@id([viewId, key])
@@map("viewFields") @@map("viewFields")
} }
model ApiKey {
/// @Validator.IsString()
/// @Validator.IsOptional()
id String @id @default(uuid())
name String
/// @TypeGraphQL.omit(input: true, output: true)
workspace Workspace @relation(fields: [workspaceId], references: [id])
/// @TypeGraphQL.omit(input: true, output: true)
workspaceId String
expiresAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
/// @TypeGraphQL.omit(input: true, output: true)
deletedAt DateTime?
/// @TypeGraphQL.omit(input: true, output: true)
revokedAt DateTime?
@@map("api_keys")
}

View File

@ -35,8 +35,8 @@ export class AbilityGuard implements CanActivate {
assert(passportUser, '', UnauthorizedException); assert(passportUser, '', UnauthorizedException);
const ability = this.abilityFactory.defineAbility( const ability = this.abilityFactory.defineAbility(
passportUser.user,
passportUser.workspace, passportUser.workspace,
passportUser.user,
); );
request.ability = ability; request.ability = ability;

View File

@ -69,10 +69,18 @@ export class EnvironmentService {
return this.configService.get<string>('LOGIN_TOKEN_SECRET')!; return this.configService.get<string>('LOGIN_TOKEN_SECRET')!;
} }
getApiTokenSecret(): string {
return this.configService.get<string>('API_TOKEN_SECRET')!;
}
getLoginTokenExpiresIn(): string { getLoginTokenExpiresIn(): string {
return this.configService.get<string>('LOGIN_TOKEN_EXPIRES_IN') ?? '15m'; return this.configService.get<string>('LOGIN_TOKEN_EXPIRES_IN') ?? '15m';
} }
getApiTokenExpiresIn(): string {
return this.configService.get<string>('API_TOKEN_EXPIRES_IN') ?? '2y';
}
getFrontAuthCallbackUrl(): string { getFrontAuthCallbackUrl(): string {
return ( return (
this.configService.get<string>('FRONT_AUTH_CALLBACK_URL') ?? this.configService.get<string>('FRONT_AUTH_CALLBACK_URL') ??

View File

@ -82,6 +82,8 @@ export class EnvironmentVariables {
@IsString() @IsString()
LOGIN_TOKEN_SECRET: string; LOGIN_TOKEN_SECRET: string;
@IsString()
API_TOKEN_SECRET: string;
@IsDuration() @IsDuration()
@IsOptional() @IsOptional()
LOGIN_TOKEN_EXPIRES_IN: string; LOGIN_TOKEN_EXPIRES_IN: string;

View File

@ -21,4 +21,5 @@ export type ModelSelectMap = {
ViewFilter: Prisma.ViewFilterSelect; ViewFilter: Prisma.ViewFilterSelect;
ViewSort: Prisma.ViewSortSelect; ViewSort: Prisma.ViewSortSelect;
ViewField: Prisma.ViewFieldSelect; ViewField: Prisma.ViewFieldSelect;
ApiKey: Prisma.ApiKeySelect;
}; };