Skip to content

Commit

Permalink
feat(api): add base setup for external integrations
Browse files Browse the repository at this point in the history
  • Loading branch information
Jozwiaczek committed Aug 4, 2021
1 parent cd09b40 commit 06eb566
Show file tree
Hide file tree
Showing 12 changed files with 164 additions and 2 deletions.
2 changes: 2 additions & 0 deletions packages/api/src/modules/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { AuthModule } from './auth/auth.module';
import { Config } from './config/config';
import { ConfigModule } from './config/config.module';
import { DatabaseModule } from './database/database.module';
import { ExternalAutomationsModule } from './external-automations/external-automations.module';
import { InvitationsModule } from './invitations/invitations.module';
import { MailerModule } from './mailer/mailer.module';
import { PasswordResetModule } from './password-reset/password-reset.module';
Expand Down Expand Up @@ -37,6 +38,7 @@ import { WebsocketModule } from './websocket/websocket.module';
WebsocketModule,
TicketModule,
PushNotificationsModule,
ExternalAutomationsModule,
],
})
export class AppModule {}
7 changes: 7 additions & 0 deletions packages/api/src/modules/database/entities/user.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,13 @@ export class UserEntity extends BaseEntity {
})
public roles: Array<Role>;

@Column({
type: 'varchar',
unique: true,
nullable: true,
})
public externalAutomationToken: string;

@OneToMany(() => RefreshTokenEntity, (token) => token.user, { onDelete: 'CASCADE' })
public refreshTokens: Promise<[RefreshTokenEntity]>;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class ExternalAutomationToken1627855502271 implements MigrationInterface {
name = 'ExternalAutomationToken1627855502271';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "users" ADD "externalAutomationToken" character varying`);
await queryRunner.query(
`ALTER TABLE "users" ADD CONSTRAINT "UQ_bb0586787a0de49b8a4edd82f3c" UNIQUE ("externalAutomationToken")`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "users" DROP CONSTRAINT "UQ_bb0586787a0de49b8a4edd82f3c"`);
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "externalAutomationToken"`);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Controller, Get, Headers } from '@nestjs/common';

import { ExternalAutomationsService } from './external-automations.service';

interface ToggleGateHeaders {
from: string;
authorization: string;
}

@Controller('external-automations')
export class ExternalAutomationsController {
constructor(private readonly externalAutomationsService: ExternalAutomationsService) {}

@Get('toggle-gate')
toggleGate(@Headers() { authorization, from }: ToggleGateHeaders) {
return this.externalAutomationsService.toggleGate(authorization, from);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';

import { UsersModule } from '../users/users.module';
import { WebsocketModule } from '../websocket/websocket.module';
import { ExternalAutomationsController } from './external-automations.controller';
import { ExternalAutomationsService } from './external-automations.service';

@Module({
controllers: [ExternalAutomationsController],
providers: [ExternalAutomationsService],
imports: [WebsocketModule, UsersModule],
})
export class ExternalAutomationsModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Injectable, Logger, UnauthorizedException } from '@nestjs/common';
import { v4 as uuidV4 } from 'uuid';

import { UsersService } from '../users/users.service';
import { Websocket } from '../websocket/websocket.gateway';

@Injectable()
export class ExternalAutomationsService {
constructor(
private readonly websocketGateway: Websocket,
private readonly usersService: UsersService,
) {}

private logger: Logger = new Logger(ExternalAutomationsService.name);

async generateToken(email: string): Promise<string> {
const token = uuidV4();

const { externalAutomationToken } = await this.usersService.updateExternalAutomationToken(
email,
token,
);
this.logger.log(`New external automation token generated for ${email}`);

return externalAutomationToken;
}

private async validateToken(token: string, email: string): Promise<true> {
const foundUser = await this.usersService.findOneByEmail(email);

if (!foundUser || token !== foundUser.externalAutomationToken) {
throw new UnauthorizedException();
}

return true;
}

async toggleGate(token: string, email: string): Promise<string> {
await this.validateToken(token, email);
// this.websocketGateway.toggleGate();
return 'Successfully open';
}
}
19 changes: 19 additions & 0 deletions packages/api/src/modules/users/users.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,25 @@ export class UsersService {
});
}

async findOneByEmail(email: string): Promise<UserEntity> {
return this.userRepository.findOneByEmailOrFail(email);
}

async updateExternalAutomationToken(email: string, newToken: string): Promise<UserEntity> {
try {
const foundUser = await this.findOneByEmail(email);

if (newToken !== undefined) {
foundUser.externalAutomationToken = newToken;
}

await this.userRepository.update(foundUser.id, foundUser);
return foundUser;
} catch (error) {
throw new NotFoundException(`User with email: ${email} not found`);
}
}

async update(id: string, updateUserDto: UpdateUserDto): Promise<UserEntity | undefined> {
const foundUser = await this.findOne(id);

Expand Down
2 changes: 1 addition & 1 deletion packages/client/src/elements/Link/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const Link = ({ children, to, asOuterLink, colorVariant = 'default', ...rest }:
$colorVariant={colorVariant}
href={to}
to=""
rel="noreferrer"
rel="noreferrer noopener"
target="_blank"
{...rest}
>
Expand Down
5 changes: 5 additions & 0 deletions packages/client/src/i18n/resources/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ const en = {
'Are you sure you want to delete your account? This will permanently erase your account.',
accountDeleted: 'Account deleted successfully',
},
automations: {
title: 'Automations',
generateApiKey: 'Generate API key',
regenerate: 'Regenerate',
},
},
privileges: {
title: 'Privileges',
Expand Down
5 changes: 5 additions & 0 deletions packages/client/src/i18n/resources/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,11 @@ const pl: TranslationStructure = {
'Czy na pewno chcesz usunąć swoje konto? Spowoduje to trwałe usunięcie Twojego konta.',
accountDeleted: 'Konto zostało usunięte',
},
automations: {
title: 'Automatyzacje',
generateApiKey: 'Wygeneruj klucz API',
regenerate: 'Wygeneruj ponownie',
},
},
privileges: {
title: 'Uprawnienia',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { UserActionsIcon, UserIcon } from '../../../../../icons';
import SettingsSection from '../SettingsSection';
import { StyledCard, TabPanelWrapper, TabsWrapper } from './Account.styled';
import ActionsTab from './tabs/ActionsTab';
import AutomationsTab from './tabs/AutomationsTab';
import BasicsTab from './tabs/BasicsTab';

const tabs: Array<TabProps> = [
Expand All @@ -17,9 +18,13 @@ const tabs: Array<TabProps> = [
label: 'routes.settings.account.actions.title',
icon: <UserActionsIcon />,
},
{
label: 'routes.settings.account.automations.title',
icon: <UserActionsIcon />,
},
];

const tabsPanels = [<BasicsTab />, <ActionsTab />];
const tabsPanels = [<BasicsTab />, <ActionsTab />, <AutomationsTab />];

const Account = () => {
const [activeTab, setActiveTab] = useState(0);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from 'react';
import { useTranslation } from 'react-i18next';

import Link from '../../../../../../elements/Link';
import { KeyIcon } from '../../../../../../icons';
import { StyledButton } from '../Account.styled';

const AutomationsTab = () => {
const { t } = useTranslation();

return (
<div>
<Link to="https://www.icloud.com/shortcuts/5f754527e564467d8b56322d2fab010b" asOuterLink>
Apple shortcut EN
</Link>
<br />
<Link to="https://www.icloud.com/shortcuts/2f72b75a096549bc8352b317f7d7c9fc" asOuterLink>
Apple shortcut PL
</Link>
<StyledButton type="submit" fullWidth>
{t('routes.settings.account.automations.generateApiKey')}
<KeyIcon />
</StyledButton>
</div>
);
};

export default AutomationsTab;

0 comments on commit 06eb566

Please sign in to comment.