Skip to content

Commit

Permalink
chore: Make temporary solution for API key [DEV-3400] (#425)
Browse files Browse the repository at this point in the history
* Refactoring + adding API key logic

* Fix log-in/ log-out button sunctionality

* Format things

* Fix build

* Get rid of magic constant

* Fix review comments
  • Loading branch information
Andrew Nikitin authored Nov 15, 2023
1 parent 56f4675 commit 5e6cdf4
Show file tree
Hide file tree
Showing 34 changed files with 2,226 additions and 803 deletions.
1,172 changes: 964 additions & 208 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@
"@semantic-release/changelog": "^6.0.3",
"@semantic-release/commit-analyzer": "^10.0.4",
"@semantic-release/git": "^10.0.1",
"@semantic-release/github": "^9.2.2",
"@semantic-release/github": "^9.2.3",
"@semantic-release/npm": "^11.0.1",
"@semantic-release/release-notes-generator": "^12.1.0",
"@types/cookie-parser": "^1.4.6",
Expand All @@ -118,7 +118,7 @@
"eslint-config-prettier": "^9.0.0",
"eslint-config-typescript": "^3.0.0",
"jest": "^29.7.0",
"prettier": "^3.0.3",
"prettier": "^3.1.0",
"semantic-release": "^22.0.7",
"swagger-jsdoc": "^6.2.8",
"ts-jest": "^29.1.1",
Expand Down
8 changes: 4 additions & 4 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { AccountController } from './controllers/customer.js';
import { Authentication } from './middleware/authentication.js';
import { Connection } from './database/connection/connection.js';
import { RevocationController } from './controllers/revocation.js';
import { CORS_ALLOWED_ORIGINS, CORS_ERROR_MSG, configLogToExpress } from './types/constants.js';
import { CORS_ALLOWED_ORIGINS, CORS_ERROR_MSG } from './types/constants.js';
import { LogToWebHook } from './middleware/hook.js';
import { Middleware } from './middleware/middleware.js';

Expand All @@ -22,7 +22,6 @@ dotenv.config();

// Define Swagger file
import swaggerDocument from './static/swagger.json' assert { type: 'json' };
import { handleAuthRoutes, withLogto } from '@logto/express';

let swaggerOptions = {};
if (process.env.ENABLE_AUTHENTICATION === 'true') {
Expand Down Expand Up @@ -81,8 +80,8 @@ class App {
);
// Authentication functions/methods
this.express.use(async (_req, _res, next) => await auth.setup(next));
this.express.use(handleAuthRoutes(configLogToExpress));
this.express.use(withLogto(configLogToExpress));
this.express.use(async (_req, _res, next) => await auth.wrapperHandleAuthRoutes(_req, _res, next));
this.express.use(async (_req, _res, next) => await auth.withLogtoWrapper(_req, _res, next));
if (process.env.ENABLE_EXTERNAL_DB === 'true') {
this.express.use(async (req, res, next) => await auth.guard(req, res, next));
}
Expand Down Expand Up @@ -167,6 +166,7 @@ class App {

// Account API
app.get('/account', new AccountController().get);
app.get('/account/idtoken', new AccountController().getIdToken);

// LogTo webhooks
app.post('/account/bootstrap', LogToWebHook.verifyHookSignature, new AccountController().bootstrap);
Expand Down
49 changes: 48 additions & 1 deletion src/controllers/customer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { Request, Response } from 'express';
import { CheqdNetwork, checkBalance } from '@cheqd/sdk';
import { TESTNET_MINIMUM_BALANCE, DEFAULT_DENOM_EXPONENT } from '../types/constants.js';
import { CustomerService } from '../services/customer.js';
import { LogToHelper } from '../middleware/auth/logto.js';
import { LogToHelper } from '../middleware/auth/logto-helper.js';
import { FaucetHelper } from '../helpers/faucet.js';
import { StatusCodes } from 'http-status-codes';
import { LogToWebHook } from '../middleware/hook.js';
Expand Down Expand Up @@ -65,6 +65,53 @@ export class AccountController {
}
}

/**
* @openapi
*
* /account/idtoken:
* get:
* tags: [Account]
* summary: Fetch IdToken.
* description: This endpoint returns IdToken as JWT with list of user roles inside
* responses:
* 200:
* description: The request was successful.
* content:
* application/json:
* idToken:
* type: string
* 400:
* $ref: '#/components/schemas/InvalidRequest'
* 401:
* $ref: '#/components/schemas/UnauthorizedError'
* 500:
* $ref: '#/components/schemas/InternalError'
*/
public async getIdToken(request: Request, response: Response) {
if (!request.user || !request.session.idToken) {
return response.status(StatusCodes.BAD_REQUEST).json({
error: 'Seems like authorisation process was corrupted. Please contact administrator.',
});
}

const identityStrategySetup = new IdentityServiceStrategySetup(response.locals.customer.customerId);
let apiKey = await identityStrategySetup.agent.getAPIKey(response.locals.customer, response.locals.user);
// If there is no API key for the customer - create it
if (!apiKey) {
apiKey = await identityStrategySetup.agent.setAPIKey(
request.session.idToken,
response.locals.customer,
response.locals.user
);
} else if (apiKey.isExpired()) {
// If API key is expired - update it
apiKey = await identityStrategySetup.agent.updateAPIKey(apiKey, request.session.idToken);
}
return response.status(StatusCodes.OK).json({
idToken: apiKey?.apiKey,
});
}

public async setupDefaultRole(request: Request, response: Response) {
if (request.body) {
const { body } = request;
Expand Down
66 changes: 66 additions & 0 deletions src/database/entities/api.key.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { BeforeInsert, BeforeUpdate, Column, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';

import * as dotenv from 'dotenv';
import { CustomerEntity } from './customer.entity.js';
import { UserEntity } from './user.entity.js';
dotenv.config();

@Entity('apiKey')
export class APIKeyEntity {
@PrimaryGeneratedColumn('uuid')
apiKeyId!: string;

@Column({
type: 'text',
nullable: false,
})
apiKey!: string;

@Column({
type: 'timestamptz',
nullable: false,
})
expiresAt!: Date;

@Column({
type: 'timestamptz',
nullable: false,
})
createdAt!: Date;

@Column({
type: 'timestamptz',
nullable: true,
})
updatedAt!: Date;

@ManyToOne(() => CustomerEntity, (customer) => customer.customerId, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'customerId' })
customer!: CustomerEntity;

@ManyToOne(() => UserEntity, (user) => user.logToId, { onDelete: 'CASCADE' })
@JoinColumn({ name: 'userId' })
user!: UserEntity;

@BeforeInsert()
setCreatedAt() {
this.createdAt = new Date();
}

@BeforeUpdate()
setUpdateAt() {
this.updatedAt = new Date();
}

public isExpired(): boolean {
return this.expiresAt < new Date();
}

constructor(apiKeyId: string, apiKey: string, expiresAt: Date, customer: CustomerEntity, user: UserEntity) {
this.apiKeyId = apiKeyId;
this.apiKey = apiKey;
this.expiresAt = expiresAt;
this.customer = customer;
this.user = user;
}
}
50 changes: 50 additions & 0 deletions src/database/migrations/CreateApiKeyMigration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';

export class CreateAPIKeyTable1695740345977 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
const table = new Table({
name: 'apiKey',
columns: [
{
name: 'apiKeyId',
type: 'uuid',
isPrimary: true,
isGenerated: true,
generationStrategy: 'uuid',
},
{ name: 'apiKey', type: 'text', isNullable: false },
{ name: 'customerId', type: 'uuid', isNullable: false },
{ name: 'userId', type: 'text', isNullable: false },
{ name: 'expiresAt', type: 'timestamptz', isNullable: false },
{ name: 'createdAt', type: 'timestamptz', isNullable: false },
{ name: 'updatedAt', type: 'timestamptz', isNullable: true },
],
});

await queryRunner.createTable(table);

await queryRunner.createForeignKey(
table,
new TableForeignKey({
columnNames: ['customerId'],
referencedColumnNames: ['customerId'],
referencedTableName: 'customer',
onDelete: 'CASCADE',
})
);

await queryRunner.createForeignKey(
table,
new TableForeignKey({
columnNames: ['userId'],
referencedColumnNames: ['logToId'],
referencedTableName: 'user',
onDelete: 'CASCADE',
})
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
throw new Error('illegal_operation: cannot roll back initial migration');
}
}
4 changes: 4 additions & 0 deletions src/database/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import { AlterTableKey1695740345977 } from '../migrations/AlterTableKey.js';
import { KeyEntity } from '../entities/key.entity.js';
import { IdentifierEntity } from '../entities/identifier.entity.js';
import { MigrateData1695740345977 } from '../migrations/MigrateData.js';
import { APIKeyEntity } from '../entities/api.key.entity.js';
import { CreateAPIKeyTable1695740345977 } from '../migrations/CreateApiKeyMigration.js';
dotenv.config();

const { EXTERNAL_DB_CONNECTION_URL, EXTERNAL_DB_CERT } = process.env;
Expand Down Expand Up @@ -84,6 +86,7 @@ export class Postgres implements AbstractDatabase {
AlterTableIdentifier1695740345977,
AlterTableKey1695740345977,
MigrateData1695740345977,
CreateAPIKeyTable1695740345977,
],
entities: [
...Entities,
Expand All @@ -96,6 +99,7 @@ export class Postgres implements AbstractDatabase {
ResourceEntity,
KeyEntity,
IdentifierEntity,
APIKeyEntity,
],
logging: ['error', 'info', 'warn'],
});
Expand Down
Loading

0 comments on commit 5e6cdf4

Please sign in to comment.