Skip to content

Commit

Permalink
Merge pull request #156 from juicyllama/feature/support-relations-in-…
Browse files Browse the repository at this point in the history
…get-profile

Feature/support relations in get profile
  • Loading branch information
andyslack authored Dec 20, 2024
2 parents 25bad83 + 9460703 commit f755db1
Show file tree
Hide file tree
Showing 17 changed files with 408 additions and 373 deletions.
66 changes: 53 additions & 13 deletions demo/databases/airtable.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'dotenv/config'
import { Logger } from '../../src/helpers/Logger'
import axios from 'axios'
import axios, { AxiosRequestConfig } from 'axios'

// Data
const Customers = require('./json/Customer.json')
Expand All @@ -13,6 +13,15 @@ const DOMAIN = 'AIRTABLE'
const [apiKey, baseId] = AIRTABLE.split('://')[1].split('@')
const logger = new Logger()

const user = {
userId: 1,
email: '[email protected]',
password: '$2a$10$jm6bM7acpRa18Vdy8FSqIu4yzWAdSgZgRtRrx8zknIeZhSqPJjJU.',
role: 'ADMIN',
firstName: 'Jon',
lastName: 'Doe',
}

const buildUsers = async () => {
const table = 'User'

Expand Down Expand Up @@ -48,19 +57,53 @@ const buildUsers = async () => {
},
}

const recordsRequest = {
method: 'POST',
url: `${ENDPOINT}/${baseId}/${table}`,
data: {
records: [
{
fields: user,
},
],
},
headers: {
Authorization: `Bearer ${apiKey}`,
},
}

return await build(table, tableRequest, recordsRequest)
}

const buildUserApiKey = async userTable => {
const table = 'UserApiKey'

const tableRequest = {
method: 'POST',
url: `${ENDPOINT}/meta/bases/${baseId}/tables`,
data: {
name: table,
fields: [
{ name: 'id', type: 'number', options: { precision: 0 } },
{ name: 'userId', type: 'multipleRecordLinks', options: { linkedTableId: userTable.id } },
{ name: 'apiKey', type: 'singleLineText' },
],
},
headers: {
Authorization: `Bearer ${apiKey}`,
},
}

const recordsRequest = {
method: 'POST',
url: `${ENDPOINT}/${baseId}/${table}`,
data: {
records: [
{
fields: {
userId: 1,
email: '[email protected]',
password: '$2a$10$jm6bM7acpRa18Vdy8FSqIu4yzWAdSgZgRtRrx8zknIeZhSqPJjJU.',
role: 'ADMIN',
firstName: 'Jon',
lastName: 'Doe',
id: 1,
userId: [userTable.records[0].id],
apiKey: 'Ex@mp1eS$Cu7eAp!K3y',
},
},
],
Expand Down Expand Up @@ -724,11 +767,7 @@ const buildSalesOrders = async (shipperTable, customerTable, employeeTable) => {
return await build(table, tableRequest, recordsRequest)
}

const build = async (
table: string,
tableRequest: axios.AxiosRequestConfig<any>,
recordsRequest: axios.AxiosRequestConfig<any>,
) => {
const build = async (table: string, tableRequest: AxiosRequestConfig<any>, recordsRequest: AxiosRequestConfig<any>) => {
let tableResponse

try {
Expand Down Expand Up @@ -814,7 +853,8 @@ const build = async (
const seed = async () => {
logger.log('Seeding Airtable database', DOMAIN)

await buildUsers()
const userTable = await buildUsers()
const userApiKeyTable = await buildUserApiKey(userTable)
const customerTable = await buildCustomers()
const employeeTable = await buildEmployees()
const shipperTable = await buildShippers()
Expand Down
1 change: 0 additions & 1 deletion docker/docker-compose.dev.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
version: '3.8'
name: llana

networks:
Expand Down
152 changes: 152 additions & 0 deletions src/app.controller.auth.test.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import { INestApplication } from '@nestjs/common'
import { Test } from '@nestjs/testing'
import { JwtModule } from '@nestjs/jwt'
import { ConfigModule, ConfigService, ConfigFactory } from '@nestjs/config'
import * as request from 'supertest'

import { AppModule } from './app.module'
import { TIMEOUT } from './testing/testing.const'
import { Logger } from './helpers/Logger'

// Import configs
import auth from './config/auth.config'
import database from './config/database.config'
import hosts from './config/hosts.config'
import jwt from './config/jwt.config'
import roles from './config/roles.config'
import { envValidationSchema } from './config/env.validation'

// Type the config imports
const configs: ConfigFactory[] = [auth, database, hosts, jwt, roles]

describe('App > Controller > Auth', () => {
let app: INestApplication

let access_token: string
let logger = new Logger()

beforeAll(async () => {
const moduleRef = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
load: configs,
validationSchema: envValidationSchema,
isGlobal: true,
}),
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
secret: configService.get('jwt.secret'),
signOptions: configService.get('jwt.signOptions'),
}),
inject: [ConfigService],
}),
AppModule,
],
}).compile()
app = moduleRef.createNestApplication()
await app.init()
}, TIMEOUT)

beforeEach(() => {
logger.debug('===========================================')
logger.log('🧪 ' + expect.getState().currentTestName)
logger.debug('===========================================')
})

describe('Failed Login', () => {
it('Missing username', async function () {
const result = await request(app.getHttpServer())
.post(`/auth/login`)
.send({
password: 'test',
})
.expect(400)
expect(result.body).toBeDefined()
expect(result.body.statusCode).toEqual(400)
expect(result.body.message).toEqual('Username is required')
expect(result.body.error).toEqual('Bad Request')
})

it('Missing password', async () => {
const result = await request(app.getHttpServer())
.post(`/auth/login`)
.send({
username: '[email protected]',
})
.expect(400)
expect(result.body).toBeDefined()
expect(result.body.statusCode).toEqual(400)
expect(result.body.message).toEqual('Password is required')
expect(result.body.error).toEqual('Bad Request')
})

it('Wrong username', async () => {
const result = await request(app.getHttpServer())
.post(`/auth/login`)
.send({
username: '[email protected]',
password: 'test',
})
.expect(401)
expect(result.body).toBeDefined()
expect(result.body.statusCode).toEqual(401)
expect(result.body.message).toEqual('Unauthorized')
})

it('Wrong password', async () => {
const result = await request(app.getHttpServer())
.post(`/auth/login`)
.send({
username: '[email protected]',
password: 'wrong',
})
.expect(401)
expect(result.body).toBeDefined()
expect(result.body.statusCode).toEqual(401)
expect(result.body.message).toEqual('Unauthorized')
})
})

describe('Successful Login', () => {
it('Correct username & password', async () => {
const result = await request(app.getHttpServer())
.post(`/auth/login`)
.send({
username: '[email protected]',
password: 'test',
})
.expect(200)
expect(result.body).toBeDefined()
expect(result.body.access_token).toBeDefined()
access_token = result.body.access_token
})
})

describe('Access Token Works', () => {
it('Get Profile', async () => {
const result = await request(app.getHttpServer())
.get(`/auth/profile`)
.set('Authorization', `Bearer ${access_token}`)
.expect(200)
expect(result.body).toBeDefined()
expect(result.body.email).toBeDefined()
})

it('Get Profile With Relations', async () => {
const result = await request(app.getHttpServer())
.get(`/auth/profile?relations=UserApiKey`)
.set('Authorization', `Bearer ${access_token}`)
.expect(200)
expect(result.body).toBeDefined()
expect(result.body.email).toBeDefined()
expect(result.body.UserApiKey).toBeDefined()
expect(result.body.UserApiKey.length).toBeGreaterThan(0)
expect(result.body.UserApiKey[0].apiKey).toBeDefined()
})
})

afterAll(async () => {
await app.close()
}, TIMEOUT)
})
66 changes: 63 additions & 3 deletions src/app.controller.auth.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import { BadRequestException, Body, Controller, Get, Headers, Post, Req, Res } from '@nestjs/common'
import {
BadRequestException,
Body,
Controller,
Get,
Headers,
ParseArrayPipe,
Post,
Query as QueryParams,
Req,
Res,
} from '@nestjs/common'

import { AuthService } from './app.service.auth'
import { HeaderParams } from './dtos/requests.dto'
import { FindOneResponseObject } from './dtos/response.dto'
import { Authentication } from './helpers/Authentication'
import { Query } from './helpers/Query'
import { Response } from './helpers/Response'
Expand Down Expand Up @@ -42,7 +54,13 @@ export class AuthController {
*/

@Get('/profile')
async profile(@Req() req, @Res() res, @Headers() headers: HeaderParams): Promise<any> {
async profile(
@Req() req,
@Res() res,
@Headers() headers: HeaderParams,
@QueryParams('relations', new ParseArrayPipe({ items: String, separator: ',', optional: true }))
queryRelations?: string[],
): Promise<any> {
if (this.authentication.skipAuth()) {
throw new BadRequestException('Authentication is disabled')
}
Expand All @@ -65,6 +83,31 @@ export class AuthController {
const schema = await this.schema.getSchema({ table, x_request_id })
const identity_column = await this.authentication.getIdentityColumn(x_request_id)

const postQueryRelations = []

try {
if (queryRelations?.length) {
const { valid, message, relations } = await this.schema.validateRelations({
schema,
relation_query: queryRelations,
existing_relations: [],
x_request_id,
})

if (!valid) {
return res.status(400).send(this.response.text(message))
}

for (const relation of relations) {
if (!postQueryRelations.find(r => r.table === relation.table)) {
postQueryRelations.push(relation)
}
}
}
} catch (e) {
return res.status(400).send(this.response.text(e.message))
}

const databaseQuery: DataSourceFindOneOptions = {
schema,
where: [
Expand All @@ -74,9 +117,26 @@ export class AuthController {
value: auth.user_identifier,
},
],
relations: postQueryRelations,
}

let user = (await this.query.perform(
QueryPerform.FIND_ONE,
databaseQuery,
x_request_id,
)) as FindOneResponseObject

if (postQueryRelations?.length) {
user = await this.query.buildRelations(
{
schema,
relations: postQueryRelations,
} as DataSourceFindOneOptions,
user,
x_request_id,
)
}

const user = await this.query.perform(QueryPerform.FIND_ONE, databaseQuery, x_request_id)
return res.status(200).send(user)
}
}
2 changes: 1 addition & 1 deletion src/app.controller.get.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export class GetController {

return res.status(200).send(await this.query.perform(QueryPerform.LIST_TABLES, undefined, x_request_id))
}

@Get('*/schema')
async getSchema(@Req() req, @Res() res, @Headers() headers: HeaderParams): Promise<ListTablesResponseObject> {
const x_request_id = headers['x-request-id']
Expand Down
Loading

0 comments on commit f755db1

Please sign in to comment.