From 3c28276a7f7ab5938a17b1916ffae68215d83afc Mon Sep 17 00:00:00 2001 From: Rachid Flih Date: Thu, 1 Aug 2024 22:04:07 +0100 Subject: [PATCH 1/7] feat: Upgrade API Keys to sk_env_UUID --- packages/api/src/@core/auth/auth.service.ts | 68 +++++++++++-------- .../auth-header-api-key.strategy.ts | 4 +- 2 files changed, 42 insertions(+), 30 deletions(-) diff --git a/packages/api/src/@core/auth/auth.service.ts b/packages/api/src/@core/auth/auth.service.ts index ab18d4f17..156f128e7 100644 --- a/packages/api/src/@core/auth/auth.service.ts +++ b/packages/api/src/@core/auth/auth.service.ts @@ -324,30 +324,36 @@ export class AuthService { keyName: string, ): Promise<{ api_key: string }> { try { + + // Check project & User exist const foundProject = await this.prisma.projects.findUnique({ where: { id_project: projectId }, }); if (!foundProject) { - throw new ReferenceError('project undefined'); + throw new ReferenceError('Project not found'); } const foundUser = await this.prisma.users.findUnique({ where: { id_user: userId }, }); if (!foundUser) { - throw new ReferenceError('user undefined'); + throw new ReferenceError('User Not Found'); } /*if (foundProject.id_organization !== foundUser.id_organization) { throw new ReferenceError('User is not inside the project'); }*/ // Generate a new API key (use a secure method for generation) - const { access_token } = await this.generateApiKey(projectId, userId); + //const { access_token } = await this.generateApiKey(projectId, userId); // Store the API key in the database associated with the user - //const hashed_token = this.hashApiKey(access_token); + //const hashed_token = this.hashApiKey(access_token);" + + const base_key = `sk_${process.env.ENV}_${uuidv4()}`; + const hashed_key = crypto.createHash('sha256').update(base_key).digest('hex'); + const new_api_key = await this.prisma.api_keys.create({ data: { id_api_key: uuidv4(), - api_key_hash: access_token, + api_key_hash: hashed_key, name: keyName, id_project: projectId as string, id_user: userId as string, @@ -357,7 +363,7 @@ export class AuthService { throw new ReferenceError('api key undefined'); } - return { api_key: access_token, ...new_api_key }; + return { api_key: base_key, ...new_api_key }; } catch (error) { throw error; } @@ -379,18 +385,12 @@ export class AuthService { async getProjectIdForApiKey(apiKey: string) { try { - // Decode the JWT to verify if it's valid and get the payload - const decoded = this.jwtService.verify(apiKey, { - secret: process.env.JWT_SECRET, - }); - - //const hashed_api_key = this.hashApiKey(apiKey); + const hashed_key = crypto.createHash('sha256').update(apiKey).digest('hex'); const saved_api_key = await this.prisma.api_keys.findUnique({ where: { - api_key_hash: apiKey, + api_key_hash: hashed_key, }, }); - return saved_api_key.id_project; } catch (error) { throw error; @@ -399,33 +399,43 @@ export class AuthService { async validateApiKey(apiKey: string): Promise { try { + + // TO DO : add Expiration in part 3 + // Decode the JWT to verify if it's valid and get the payload const decoded = this.jwtService.verify(apiKey, { secret: process.env.JWT_SECRET, }); - //const hashed_api_key = this.hashApiKey(apiKey); + + // pseudo-code: + // 1 - SHA256 the API key from the header + const hashed_key = crypto.createHash('sha256').update(apiKey).digest('hex'); + + + // 2- check against DB + // if not found, return false const saved_api_key = await this.prisma.api_keys.findUnique({ where: { - api_key_hash: apiKey, + api_key_hash: hashed_key, }, }); if (!saved_api_key) { - throw new ReferenceError('Api Key undefined'); - } - if (String(decoded.project_id) !== String(saved_api_key.id_project)) { - throw new ReferenceError( - 'Failed to validate API key: projectId mismatch.', - ); - } - - // Validate that the JWT payload matches the provided userId and projectId - if (String(decoded.sub) !== String(saved_api_key.id_user)) { - throw new ReferenceError( - 'Failed to validate API key: userId mismatch.', - ); + throw new ReferenceError('API Key not found.'); } + // if (String(decoded.project_id) !== String(saved_api_key.id_project)) { + // throw new ReferenceError( + // 'Failed to validate API key: projectId mismatch.', + // ); + // } + + // // Validate that the JWT payload matches the provided userId and projectId + // if (String(decoded.sub) !== String(saved_api_key.id_user)) { + // throw new ReferenceError( + // 'Failed to validate API key: userId mismatch.', + // ); + // } return true; } catch (error) { throw error; diff --git a/packages/api/src/@core/auth/strategies/auth-header-api-key.strategy.ts b/packages/api/src/@core/auth/strategies/auth-header-api-key.strategy.ts index 0fbfe5526..97eeda032 100644 --- a/packages/api/src/@core/auth/strategies/auth-header-api-key.strategy.ts +++ b/packages/api/src/@core/auth/strategies/auth-header-api-key.strategy.ts @@ -2,6 +2,7 @@ import { HeaderAPIKeyStrategy } from 'passport-headerapikey'; import { PassportStrategy } from '@nestjs/passport'; import { Injectable, UnauthorizedException } from '@nestjs/common'; import { AuthService } from '../auth.service'; +import * as crypto from 'crypto'; @Injectable() export class ApiKeyStrategy extends PassportStrategy( @@ -18,8 +19,9 @@ export class ApiKeyStrategy extends PassportStrategy( if (!isValid) { return done(new UnauthorizedException('Invalid API Key'), null); } + const hashed_api_key = crypto.createHash('sha256').update(apikey).digest('hex'); const projectId = await this.authService.getProjectIdForApiKey( - apikey, + hashed_api_key, ); //console.log('validating api request... : ' + req.user); // If the API key is valid, attach the user to the request object From 43a98e5ff78be603661434f7b2dcd2e1125320ed Mon Sep 17 00:00:00 2001 From: Rachid Flih Date: Thu, 1 Aug 2024 22:14:03 +0100 Subject: [PATCH 2/7] fix: post api_key route --- docker-compose.dev.yml | 1 + docker-compose.source.yml | 1 + docker-compose.yml | 1 + packages/api/src/@core/auth/auth.controller.ts | 2 +- 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index d61fae1c7..0f6fd1172 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -31,6 +31,7 @@ services: context: ./ dockerfile: ./packages/api/Dockerfile.dev environment: + ENV: ${ENV} DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:5432/${POSTGRES_DB}?ssl=false DISTRIBUTION: ${DISTRIBUTION} WEBHOOK_INGRESS: ${WEBHOOK_INGRESS} diff --git a/docker-compose.source.yml b/docker-compose.source.yml index da0ba4a5a..a3b17be28 100644 --- a/docker-compose.source.yml +++ b/docker-compose.source.yml @@ -31,6 +31,7 @@ services: context: ./ dockerfile: ./packages/api/Dockerfile environment: + ENV: ${ENV} DOPPLER_TOKEN: ${DOPPLER_TOKEN_API} DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:5432/${POSTGRES_DB}?ssl=false DISTRIBUTION: ${DISTRIBUTION} diff --git a/docker-compose.yml b/docker-compose.yml index f82c70f68..4ef6d3bd9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,6 +26,7 @@ services: api: image: panora.docker.scarf.sh/panoradotdev/backend-api:selfhosted environment: + ENV: ${ENV} DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${POSTGRES_HOST}:5432/${POSTGRES_DB}?ssl=false DISTRIBUTION: ${DISTRIBUTION} WEBHOOK_INGRESS: ${WEBHOOK_INGRESS} diff --git a/packages/api/src/@core/auth/auth.controller.ts b/packages/api/src/@core/auth/auth.controller.ts index 01bc42004..e5860837d 100644 --- a/packages/api/src/@core/auth/auth.controller.ts +++ b/packages/api/src/@core/auth/auth.controller.ts @@ -104,7 +104,7 @@ export class AuthController { @ApiBody({ type: ApiKeyDto }) @ApiResponse({ status: 201 }) @UseGuards(JwtAuthGuard) - @Post() + @Post('api_keys') async generateApiKey(@Body() data: ApiKeyDto): Promise<{ api_key: string }> { return this.authService.generateApiKeyForUser( data.userId, From 02d75ba7f04d27c4d78731f322307973d28eb079 Mon Sep 17 00:00:00 2001 From: Rachid Flih Date: Thu, 1 Aug 2024 22:20:38 +0100 Subject: [PATCH 3/7] patch: Moved from Bearer header to x-api-key --- .../src/@core/auth/strategies/auth-header-api-key.strategy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/api/src/@core/auth/strategies/auth-header-api-key.strategy.ts b/packages/api/src/@core/auth/strategies/auth-header-api-key.strategy.ts index 97eeda032..a1d2c10e7 100644 --- a/packages/api/src/@core/auth/strategies/auth-header-api-key.strategy.ts +++ b/packages/api/src/@core/auth/strategies/auth-header-api-key.strategy.ts @@ -11,7 +11,7 @@ export class ApiKeyStrategy extends PassportStrategy( ) { constructor(private authService: AuthService) { super( - { header: 'Authorization', prefix: 'Bearer ' }, + { header: 'x-api-key', prefix: '' }, true, async (apikey: string, done, req) => { try { From d5c8fc2f89c685176e0baf215c2bbf63645cd76d Mon Sep 17 00:00:00 2001 From: Rachid Flih Date: Thu, 1 Aug 2024 22:58:10 +0100 Subject: [PATCH 4/7] Fixes on API keys --- packages/api/src/@core/auth/auth.service.ts | 12 ++++++------ packages/api/src/@core/connections/@utils/index.ts | 4 ++++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/api/src/@core/auth/auth.service.ts b/packages/api/src/@core/auth/auth.service.ts index 156f128e7..11c80912a 100644 --- a/packages/api/src/@core/auth/auth.service.ts +++ b/packages/api/src/@core/auth/auth.service.ts @@ -383,14 +383,14 @@ export class AuthService { - async getProjectIdForApiKey(apiKey: string) { + async getProjectIdForApiKey(hashed_apiKey: string) { try { - const hashed_key = crypto.createHash('sha256').update(apiKey).digest('hex'); const saved_api_key = await this.prisma.api_keys.findUnique({ where: { - api_key_hash: hashed_key, + api_key_hash: hashed_apiKey, }, }); + return saved_api_key.id_project; } catch (error) { throw error; @@ -403,9 +403,9 @@ export class AuthService { // TO DO : add Expiration in part 3 // Decode the JWT to verify if it's valid and get the payload - const decoded = this.jwtService.verify(apiKey, { - secret: process.env.JWT_SECRET, - }); + // const decoded = this.jwtService.verify(apiKey, { + // secret: process.env.JWT_SECRET, + // }); // pseudo-code: diff --git a/packages/api/src/@core/connections/@utils/index.ts b/packages/api/src/@core/connections/@utils/index.ts index e0aaf1075..fa2ec0063 100644 --- a/packages/api/src/@core/connections/@utils/index.ts +++ b/packages/api/src/@core/connections/@utils/index.ts @@ -16,6 +16,10 @@ export class ConnectionUtils { token: string, ): Promise { try { + console.log('**********') + console.log(token); + console.log('**********') + const res = await this.prisma.connections.findFirst({ where: { connection_token: token, From 8d1bf6aed3dfaf10960e033056916e619c68f2f6 Mon Sep 17 00:00:00 2001 From: nael Date: Sat, 3 Aug 2024 18:53:17 +0200 Subject: [PATCH 5/7] :sparkles: Added new api key --- .../src/app/(Dashboard)/api-keys/page.tsx | 40 +++++++++++- .../webapp/src/components/ApiKeys/columns.tsx | 45 ------------- apps/webapp/src/components/ApiKeys/schema.ts | 1 - .../src/hooks/delete/useDeleteApiKey.tsx | 2 +- packages/api/prisma/schema.prisma | 60 ++++++++--------- packages/api/src/@core/auth/auth.service.ts | 65 +++++++++++-------- packages/api/src/main.ts | 14 ++-- packages/api/swagger/swagger-spec.yaml | 28 ++++++-- 8 files changed, 139 insertions(+), 116 deletions(-) diff --git a/apps/webapp/src/app/(Dashboard)/api-keys/page.tsx b/apps/webapp/src/app/(Dashboard)/api-keys/page.tsx index bb71e4f9a..3206e6f17 100644 --- a/apps/webapp/src/app/(Dashboard)/api-keys/page.tsx +++ b/apps/webapp/src/app/(Dashboard)/api-keys/page.tsx @@ -46,26 +46,37 @@ const formSchema = z.object({ interface TSApiKeys { id_api_key: string; name : string; - token : string; } export default function Page() { const [open,setOpen] = useState(false) const [tsApiKeys,setTSApiKeys] = useState([]) + const [isKeyModalOpen, setIsKeyModalOpen] = useState(false); - const queryClient = useQueryClient(); + const queryClient = useQueryClient(); + const [newApiKey, setNewApiKey] = useState<{ key: string; expiration: Date } | null>(null); const {idProject} = useProjectStore(); const {profile} = useProfileStore(); const { createApiKeyPromise } = useCreateApiKey(); const { data: apiKeys, isLoading, error } = useApiKeys(); const columns = useColumns(); + useEffect(() => { + if (newApiKey) { + const timeUntilExpiration = newApiKey.expiration.getTime() - Date.now(); + const timer = setTimeout(() => { + setNewApiKey(null); + }, timeUntilExpiration); + + return () => clearTimeout(timer); + } + }, [newApiKey]); + useEffect(() => { const temp_tsApiKeys = apiKeys?.map((key) => ({ id_api_key: key.id_api_key, name: key.name || "", - token: key.api_key_hash, })) setTSApiKeys(temp_tsApiKeys) },[apiKeys]) @@ -107,6 +118,13 @@ export default function Page() { queryClient.setQueryData(['api-keys'], (oldQueryData = []) => { return [...oldQueryData, data]; }); + // Store the API key and its expiration time in state + setNewApiKey({ + key: data.api_key, + expiration: new Date(Date.now() + 60000), + }); + setIsKeyModalOpen(true); // Open the modal + return (
@@ -225,6 +243,22 @@ export default function Page() { {tsApiKeys && }
+ + + + Your New API Key + + This key will only be shown for the next minute. Please save it now. + + +
+ API Key:

{newApiKey?.key}

+
+ + + +
+
); } \ No newline at end of file diff --git a/apps/webapp/src/components/ApiKeys/columns.tsx b/apps/webapp/src/components/ApiKeys/columns.tsx index 067bd7943..398fc172c 100644 --- a/apps/webapp/src/components/ApiKeys/columns.tsx +++ b/apps/webapp/src/components/ApiKeys/columns.tsx @@ -45,51 +45,6 @@ export function useColumns() { enableSorting: false, enableHiding: false, }, - { - accessorKey: "token", - header: ({ column }) => ( - - ), - cell: ({ row }) => { - const token: string = row.getValue("token"); - const copied = copiedState[token] || false; - - return ( -
-
- -
-
handleCopy(token)} - > - - - - - - -

Copy Key

-
-
-
- -
-
- ); - }, - }, { id: "actions", cell: ({ row }) => , diff --git a/apps/webapp/src/components/ApiKeys/schema.ts b/apps/webapp/src/components/ApiKeys/schema.ts index dfb2acf32..b4db52b4b 100644 --- a/apps/webapp/src/components/ApiKeys/schema.ts +++ b/apps/webapp/src/components/ApiKeys/schema.ts @@ -3,7 +3,6 @@ import { z } from "zod" export const apiKeySchema = z.object({ id_api_key: z.string(), name: z.string(), - token: z.string(), }) export type ApiKey = z.infer \ No newline at end of file diff --git a/apps/webapp/src/hooks/delete/useDeleteApiKey.tsx b/apps/webapp/src/hooks/delete/useDeleteApiKey.tsx index 0784d734c..5efc073e8 100644 --- a/apps/webapp/src/hooks/delete/useDeleteApiKey.tsx +++ b/apps/webapp/src/hooks/delete/useDeleteApiKey.tsx @@ -8,7 +8,7 @@ interface IApiKeyDto { const useDeleteApiKey = () => { const remove = async (apiKeyData: IApiKeyDto) => { - const response = await fetch(`${config.API_URL}/auth/api-keys/${apiKeyData.id_api_key}`, { + const response = await fetch(`${config.API_URL}/auth/api_keys/${apiKeyData.id_api_key}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', diff --git a/packages/api/prisma/schema.prisma b/packages/api/prisma/schema.prisma index 7e2bcb50e..b37a461fe 100644 --- a/packages/api/prisma/schema.prisma +++ b/packages/api/prisma/schema.prisma @@ -1638,28 +1638,6 @@ model acc_vendor_credits { accounting_period String? @db.Uuid } -/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments -model ecom_addresses { - id_ecom_address String @id(map: "pk_ecom_customer_addresses") @db.Uuid - address_type String? - street_1 String? - street_2 String? - city String? - state String? - postal_code String? - country String? - id_ecom_customer String @db.Uuid - modified_at DateTime @db.Timestamptz(6) - created_at DateTime @db.Timestamptz(6) - remote_deleted Boolean - id_ecom_order String @db.Uuid - ecom_customers ecom_customers @relation(fields: [id_ecom_customer], references: [id_ecom_customer], onDelete: NoAction, onUpdate: NoAction, map: "fk_ecom_customer_customeraddress") - ecom_orders ecom_orders @relation(fields: [id_ecom_order], references: [id_ecom_order], onDelete: NoAction, onUpdate: NoAction, map: "fk_ecom_order_address") - - @@index([id_ecom_customer], map: "fk_index_ecom_customer_customeraddress") - @@index([id_ecom_order], map: "fk_index_fk_ecom_order_address") -} - model ecom_customers { id_ecom_customer String @id(map: "pk_ecom_customers") @db.Uuid remote_id String? @@ -1675,10 +1653,6 @@ model ecom_customers { ecom_orders ecom_orders[] } -model ecom_fulfilment_orders { - id_ecom_fulfilment_order String @id(map: "pk_ecom_fulfilment_order") @db.Uuid -} - /// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments model ecom_fulfilments { id_ecom_fulfilment String @id(map: "pk_ecom_fulfilments") @db.Uuid @@ -1697,10 +1671,6 @@ model ecom_fulfilments { @@index([id_ecom_order], map: "fk_index_ecom_order_fulfilment") } -model ecom_order_line_items { - id_ecom_order_line_item String @id(map: "pk_106") @db.Uuid -} - model ecom_orders { id_ecom_order String @id(map: "pk_ecom_orders") @db.Uuid order_status String? @@ -1761,3 +1731,33 @@ model ecom_products { remote_deleted Boolean ecom_product_variants ecom_product_variants[] } + +/// This model or at least one of its fields has comments in the database, and requires an additional setup for migrations: Read more: https://pris.ly/d/database-comments +model ecom_addresses { + id_ecom_address String @id(map: "pk_ecom_customer_addresses") @db.Uuid + address_type String? + street_1 String? + street_2 String? + city String? + state String? + postal_code String? + country String? + id_ecom_customer String @db.Uuid + modified_at DateTime @db.Timestamptz(6) + created_at DateTime @db.Timestamptz(6) + remote_deleted Boolean + id_ecom_order String @db.Uuid + ecom_customers ecom_customers @relation(fields: [id_ecom_customer], references: [id_ecom_customer], onDelete: NoAction, onUpdate: NoAction, map: "fk_ecom_customer_customeraddress") + ecom_orders ecom_orders @relation(fields: [id_ecom_order], references: [id_ecom_order], onDelete: NoAction, onUpdate: NoAction, map: "fk_ecom_order_address") + + @@index([id_ecom_customer], map: "fk_index_ecom_customer_customeraddress") + @@index([id_ecom_order], map: "fk_index_fk_ecom_order_address") +} + +model ecom_fulfilment_orders { + id_ecom_fulfilment_order String @id(map: "pk_ecom_fulfilment_order") @db.Uuid +} + +model ecom_order_line_items { + id_ecom_order_line_item String @id(map: "pk_106") @db.Uuid +} diff --git a/packages/api/src/@core/auth/auth.service.ts b/packages/api/src/@core/auth/auth.service.ts index 11c80912a..de1227e68 100644 --- a/packages/api/src/@core/auth/auth.service.ts +++ b/packages/api/src/@core/auth/auth.service.ts @@ -43,8 +43,11 @@ export class AuthService { throw new BadRequestException('Invalid email or expired request'); } - // Verify the reset token - const isValidToken = await this.verifyResetToken(checkResetRequestIsValid.reset_token, reset_token); + // Verify the reset token + const isValidToken = await this.verifyResetToken( + checkResetRequestIsValid.reset_token, + reset_token, + ); if (!isValidToken) { throw new BadRequestException('Invalid reset token'); @@ -54,7 +57,7 @@ export class AuthService { const hashedPassword = await bcrypt.hash(new_password, 10); // Update the user's password in the database - const updatedPassword =await this.prisma.users.update({ + const updatedPassword = await this.prisma.users.update({ where: { email }, data: { password_hash: hashedPassword }, }); @@ -62,16 +65,18 @@ export class AuthService { return { message: 'Password reset successfully' }; } - private async verifyResetToken(database_token: string, request_token: string): Promise { - const isValidToken = await bcrypt.compare(request_token, database_token); - return isValidToken; + private async verifyResetToken( + database_token: string, + request_token: string, + ): Promise { + const isValidToken = await bcrypt.compare(request_token, database_token); + return isValidToken; } - async requestPasswordReset(requestPasswordResetDto: RequestPasswordResetDto) { const { email } = requestPasswordResetDto; - if (!email){ + if (!email) { throw new BadRequestException('Incorrect API request'); } @@ -105,19 +110,19 @@ export class AuthService { // Create a transporter object using the default SMTP transport const transporter = nodemailer.createTransport({ - host: process.env.SMTP_HOST, - port: Number(process.env.SMTP_PORT), + host: process.env.SMTP_HOST, + port: Number(process.env.SMTP_PORT), //secure: false, auth: { user: process.env.SMTP_USER, pass: process.env.SMTP_PASSWORD, - } + }, }); // Send mail with defined transport object const info = await transporter.sendMail({ from: `${process.env.EMAIL_SENDING_ADDRESS}`, - to: email, + to: email, subject: 'Panora | Password Reset Request', text: `You requested a password reset. Click the following link within one hour from now to reset your password: ${resetLink}`, html: `

You requested a password reset. Click the link to reset your password:

${resetLink}

The link will expire after one hour

`, @@ -126,8 +131,6 @@ export class AuthService { this.logger.log(`Send reset email to ${email} with token ${resetToken}`); } - - async getUsers() { try { return await this.prisma.users.findMany(); @@ -156,11 +159,15 @@ export class AuthService { async getApiKeys(project_id: string) { try { - return await this.prisma.api_keys.findMany({ + const res = await this.prisma.api_keys.findMany({ where: { id_project: project_id, }, }); + return res.map((key) => { + const { api_key_hash, ...rest } = key; + return rest; + }); } catch (error) { throw error; } @@ -173,7 +180,9 @@ export class AuthService { }); if (foundUser) { - throw new ConflictException(`Email already exists. Try resetting your password.`); + throw new ConflictException( + `Email already exists. Try resetting your password.`, + ); } return await this.createUser(user); } catch (error) { @@ -324,7 +333,6 @@ export class AuthService { keyName: string, ): Promise<{ api_key: string }> { try { - // Check project & User exist const foundProject = await this.prisma.projects.findUnique({ where: { id_project: projectId }, @@ -347,8 +355,11 @@ export class AuthService { // Store the API key in the database associated with the user //const hashed_token = this.hashApiKey(access_token);" - const base_key = `sk_${process.env.ENV}_${uuidv4()}`; - const hashed_key = crypto.createHash('sha256').update(base_key).digest('hex'); + const base_key = `sk_${process.env.ENV}_${uuidv4()}`; + const hashed_key = crypto + .createHash('sha256') + .update(base_key) + .digest('hex'); const new_api_key = await this.prisma.api_keys.create({ data: { @@ -362,8 +373,8 @@ export class AuthService { if (!new_api_key) { throw new ReferenceError('api key undefined'); } - - return { api_key: base_key, ...new_api_key }; + const { api_key_hash, ...rest } = new_api_key; + return { api_key: base_key, ...rest }; } catch (error) { throw error; } @@ -381,8 +392,6 @@ export class AuthService { } } - - async getProjectIdForApiKey(hashed_apiKey: string) { try { const saved_api_key = await this.prisma.api_keys.findUnique({ @@ -399,7 +408,6 @@ export class AuthService { async validateApiKey(apiKey: string): Promise { try { - // TO DO : add Expiration in part 3 // Decode the JWT to verify if it's valid and get the payload @@ -407,11 +415,12 @@ export class AuthService { // secret: process.env.JWT_SECRET, // }); - // pseudo-code: // 1 - SHA256 the API key from the header - const hashed_key = crypto.createHash('sha256').update(apiKey).digest('hex'); - + const hashed_key = crypto + .createHash('sha256') + .update(apiKey) + .digest('hex'); // 2- check against DB // if not found, return false @@ -441,4 +450,4 @@ export class AuthService { throw error; } } -} \ No newline at end of file +} diff --git a/packages/api/src/main.ts b/packages/api/src/main.ts index 4f8a30ea3..1960176b5 100644 --- a/packages/api/src/main.ts +++ b/packages/api/src/main.ts @@ -48,12 +48,18 @@ async function bootstrap() { .addServer('https://api.panora.dev', 'Production server') .addServer('https://api-sandbox.panora.dev', 'Sandbox server') .addServer('https://api-dev.panora.dev', 'Development server') - .addSecurity('bearer', { - type: 'http', - scheme: 'bearer', - }) + .addApiKey( + { + type: 'apiKey', + name: 'x-api-key', + in: 'header', + }, + 'api_key', + ) .build(); const document = SwaggerModule.createDocument(app, config); + document.security = [{ api_key: [] }]; + // Dynamically add extended specs const extendedSpecs = { 'x-speakeasy-name-override': [ diff --git a/packages/api/swagger/swagger-spec.yaml b/packages/api/swagger/swagger-spec.yaml index b2f30bb0e..129f8931d 100644 --- a/packages/api/swagger/swagger-spec.yaml +++ b/packages/api/swagger/swagger-spec.yaml @@ -8,6 +8,10 @@ paths: responses: '200': description: '' + content: + application/json: + schema: + type: string /health: get: operationId: health @@ -16,6 +20,10 @@ paths: responses: '200': description: '' + content: + application/json: + schema: + type: number /webhooks: get: operationId: listWebhooks @@ -502,6 +510,8 @@ paths: responses: '200': description: '' + '201': + description: '' tags: *ref_9 x-speakeasy-group: sync /crm/companies: @@ -1877,6 +1887,12 @@ paths: application/json: schema: $ref: '#/components/schemas/PassThroughResponse' + '201': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/PassThroughResponse' tags: - passthrough /hris/bankinfos: @@ -8005,9 +8021,10 @@ servers: description: Development server components: securitySchemes: - bearer: - type: http - scheme: bearer + api_key: + type: apiKey + in: header + name: x-api-key schemas: WebhookResponse: type: object @@ -8017,6 +8034,7 @@ components: description: The unique UUID of the webhook. endpoint_description: type: string + nullable: true description: The description of the webhook. url: type: string @@ -8042,6 +8060,7 @@ components: last_update: format: date-time type: string + nullable: true description: The last update date of the webhook. required: - id_webhook_endpoint @@ -8069,7 +8088,6 @@ components: type: string required: - url - - description - scope EventPayload: type: object @@ -10821,6 +10839,8 @@ components: - file_url - uploader - field_mappings +security: + - api_key: [] x-speakeasy-name-override: - operationId: ^retrieve.* methodNameOverride: retrieve From 7230b49a0a18a480f393d3469d1a3e6c9c05a6f2 Mon Sep 17 00:00:00 2001 From: nael Date: Sat, 3 Aug 2024 19:18:53 +0200 Subject: [PATCH 6/7] :bug: Fix ci --- packages/api/src/@core/auth/auth.service.ts | 55 +++++---------------- 1 file changed, 12 insertions(+), 43 deletions(-) diff --git a/packages/api/src/@core/auth/auth.service.ts b/packages/api/src/@core/auth/auth.service.ts index f9097a49f..2c5caef12 100644 --- a/packages/api/src/@core/auth/auth.service.ts +++ b/packages/api/src/@core/auth/auth.service.ts @@ -1,25 +1,19 @@ import { LoggerService } from '@@core/@core-services/logger/logger.service'; import { ProjectsService } from '@@core/projects/projects.service'; -import { MailerService } from '@nestjs-modules/mailer'; -import { - BadRequestException, - ConflictException, - Injectable, -} from '@nestjs/common'; +import { AuthError } from '@@core/utils/errors'; +import { Injectable, BadRequestException } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import * as bcrypt from 'bcrypt'; import * as crypto from 'crypto'; -import * as nodemailer from 'nodemailer'; import { v4 as uuidv4 } from 'uuid'; import { PrismaService } from '../@core-services/prisma/prisma.service'; import { CreateUserDto } from './dto/create-user.dto'; import { LoginDto } from './dto/login.dto'; -import { RequestPasswordResetDto } from './dto/request-password-reset.dto'; -import { ResetPasswordDto } from './dto/reset-password.dto'; import { VerifyUserDto } from './dto/verify-user.dto'; -import { register } from 'module'; -import { async } from 'rxjs'; -import { string, number, boolean } from 'yargs'; +import { ConflictException } from '@nestjs/common'; +import { ResetPasswordDto } from './dto/reset-password.dto'; +import { RequestPasswordResetDto } from './dto/request-password-reset.dto'; +import * as nodemailer from 'nodemailer'; @Injectable() export class AuthService { @@ -28,7 +22,6 @@ export class AuthService { private projectService: ProjectsService, private jwtService: JwtService, private logger: LoggerService, - private mailerService: MailerService, ) { this.logger.setContext(AuthService.name); } @@ -37,7 +30,7 @@ export class AuthService { const { email, new_password, reset_token } = resetPasswordDto; // verify there is a user with corresponding email and non-expired reset token - /*const checkResetRequestIsValid = await this.prisma.users.findFirst({ + const checkResetRequestIsValid = await this.prisma.users.findFirst({ where: { email: email, reset_token_expires_at: { @@ -50,11 +43,6 @@ export class AuthService { throw new BadRequestException('Invalid email or expired request'); } - // Verify the reset token - const isValidToken = await this.verifyResetToken( - checkResetRequestIsValid.reset_token, - reset_token, - ); // Verify the reset token const isValidToken = await this.verifyResetToken( checkResetRequestIsValid.reset_token, @@ -69,21 +57,14 @@ export class AuthService { const hashedPassword = await bcrypt.hash(new_password, 10); // Update the user's password in the database - const updatedPassword = await this.prisma.users.update({ const updatedPassword = await this.prisma.users.update({ where: { email }, data: { password_hash: hashedPassword }, }); - console.log(updatedPassword);*/ + console.log(updatedPassword); return { message: 'Password reset successfully' }; } - private async verifyResetToken( - database_token: string, - request_token: string, - ): Promise { - const isValidToken = await bcrypt.compare(request_token, database_token); - return isValidToken; private async verifyResetToken( database_token: string, request_token: string, @@ -120,7 +101,7 @@ export class AuthService { }); // Send email with resetToken (implementation depends on your email service) - await this.sendResetEmail(email, resetToken);*/ + await this.sendResetEmail(email, resetToken); return { message: 'Password reset link sent' }; } @@ -129,8 +110,6 @@ export class AuthService { // Create a transporter object using the default SMTP transport const transporter = nodemailer.createTransport({ - host: process.env.SMTP_HOST, - port: Number(process.env.SMTP_PORT), host: process.env.SMTP_HOST, port: Number(process.env.SMTP_PORT), //secure: false, @@ -138,14 +117,12 @@ export class AuthService { user: process.env.SMTP_USER, pass: process.env.SMTP_PASSWORD, }, - }, }); // Send mail with defined transport object const info = await transporter.sendMail({ from: `${process.env.EMAIL_SENDING_ADDRESS}`, to: email, - to: email, subject: 'Panora | Password Reset Request', text: `You requested a password reset. Click the following link within one hour from now to reset your password: ${resetLink}`, html: `

You requested a password reset. Click the link to reset your password:

${resetLink}

The link will expire after one hour

`, @@ -182,15 +159,11 @@ export class AuthService { async getApiKeys(project_id: string) { try { - const res = await this.prisma.api_keys.findMany({ + return await this.prisma.api_keys.findMany({ where: { id_project: project_id, }, }); - return res.map((key) => { - const { api_key_hash, ...rest } = key; - return rest; - }); } catch (error) { throw error; } @@ -206,9 +179,6 @@ export class AuthService { throw new ConflictException( `Email already exists. Try resetting your password.`, ); - throw new ConflictException( - `Email already exists. Try resetting your password.`, - ); } return await this.createUser(user); } catch (error) { @@ -399,8 +369,8 @@ export class AuthService { if (!new_api_key) { throw new ReferenceError('api key undefined'); } - const { api_key_hash, ...rest } = new_api_key; - return { api_key: base_key, ...rest }; + + return { api_key: base_key, ...new_api_key }; } catch (error) { throw error; } @@ -477,4 +447,3 @@ export class AuthService { } } } - From a77e17db7554e2625cf20975cceec38792078e69 Mon Sep 17 00:00:00 2001 From: nael Date: Sat, 3 Aug 2024 19:26:42 +0200 Subject: [PATCH 7/7] :green_heart: Fix --- packages/api/prisma/schema.prisma | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/api/prisma/schema.prisma b/packages/api/prisma/schema.prisma index 93f33ae7a..41e04efb9 100644 --- a/packages/api/prisma/schema.prisma +++ b/packages/api/prisma/schema.prisma @@ -1682,8 +1682,10 @@ model ecom_orders { remote_id String? id_ecom_customer String? @db.Uuid id_connection String @db.Uuid - modifed_at DateTime @db.Timestamptz(6) + modified_at DateTime @db.Timestamptz(6) created_at DateTime @db.Timestamptz(6) + remote_deleted Boolean + ecom_addresses ecom_addresses[] ecom_fulfilments ecom_fulfilments[] ecom_customers ecom_customers? @relation(fields: [id_ecom_customer], references: [id_ecom_customer], onDelete: NoAction, onUpdate: NoAction, map: "fk_ecom_customer_orders")