From 0c18bd71c41845604d45f556f0bf3f194c4cb8be Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Mon, 5 Jun 2023 00:44:10 +0100 Subject: [PATCH 01/19] Implement preliminary pentest remediations --- backend/src/controllers/v1/authController.ts | 2 +- .../controllers/v1/membershipOrgController.ts | 18 + .../src/controllers/v1/passwordController.ts | 4 +- .../src/controllers/v1/signupController.ts | 4 + .../src/controllers/v1/workspaceController.ts | 14 + backend/src/controllers/v2/authController.ts | 8 +- backend/src/data/disposable_emails.txt | 3519 +++++++++++++++++ backend/src/helpers/nodemailer.ts | 3 +- backend/src/helpers/rateLimiter.ts | 13 +- backend/src/index.ts | 3 - backend/src/models/user.ts | 3 +- backend/src/routes/v2/auth.ts | 2 +- backend/src/validation/index.ts | 1 + backend/src/validation/user.ts | 20 +- 14 files changed, 3592 insertions(+), 22 deletions(-) create mode 100644 backend/src/data/disposable_emails.txt diff --git a/backend/src/controllers/v1/authController.ts b/backend/src/controllers/v1/authController.ts index e60002e9d4..6f6a147bb1 100644 --- a/backend/src/controllers/v1/authController.ts +++ b/backend/src/controllers/v1/authController.ts @@ -89,7 +89,7 @@ export const login2 = async (req: Request, res: Response) => { const { email, clientProof } = req.body; const user = await User.findOne({ email - }).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag'); + }).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag +devices'); if (!user) throw new Error('Failed to find user'); diff --git a/backend/src/controllers/v1/membershipOrgController.ts b/backend/src/controllers/v1/membershipOrgController.ts index 60cdc96911..51f89b8ebb 100644 --- a/backend/src/controllers/v1/membershipOrgController.ts +++ b/backend/src/controllers/v1/membershipOrgController.ts @@ -7,8 +7,10 @@ import { createToken } from '../../helpers/auth'; import { updateSubscriptionOrgQuantity } from '../../helpers/organization'; import { sendMail } from '../../helpers/nodemailer'; import { TokenService } from '../../services'; +import { EELicenseService } from '../../ee/services'; import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED, TOKEN_EMAIL_ORG_INVITATION } from '../../variables'; import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config'; +import { validateUserEmail } from '../../validation'; /** * Delete organization membership with id [membershipOrgId] from organization @@ -115,6 +117,19 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => { if (!membershipOrg) { throw new Error('Failed to validate organization membership'); } + + const plan = await EELicenseService.getOrganizationPlan(organizationId); + + if (plan.memberLimit !== null) { + // case: limit imposed on number of members allowed + + if (plan.membersUsed >= plan.memberLimit) { + // case: number of members used exceeds the number of members allowed + return res.status(400).send({ + message: 'Failed to invite member due to member limit reached. Upgrade plan to invite more members.' + }); + } + } invitee = await User.findOne({ email: inviteeEmail @@ -153,6 +168,9 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => { if (!inviteeMembershipOrg) { // case: invitee has never been invited before + + // validate that email is not disposable + validateUserEmail(inviteeEmail); await new MembershipOrg({ inviteEmail: inviteeEmail, diff --git a/backend/src/controllers/v1/passwordController.ts b/backend/src/controllers/v1/passwordController.ts index 85c2445053..f0e5ee3db2 100644 --- a/backend/src/controllers/v1/passwordController.ts +++ b/backend/src/controllers/v1/passwordController.ts @@ -28,7 +28,7 @@ export const emailPasswordReset = async (req: Request, res: Response) => { // case: user has already completed account return res.status(403).send({ - error: 'Failed to send email verification for password reset' + message: "If an account exists with this email, a password reset link has been sent" }); } @@ -56,7 +56,7 @@ export const emailPasswordReset = async (req: Request, res: Response) => { } return res.status(200).send({ - message: `Sent an email for account recovery to ${email}` + message:"If an account exists with this email, a password reset link has been sent" }); } diff --git a/backend/src/controllers/v1/signupController.ts b/backend/src/controllers/v1/signupController.ts index 681b654599..17e0125f44 100644 --- a/backend/src/controllers/v1/signupController.ts +++ b/backend/src/controllers/v1/signupController.ts @@ -8,6 +8,7 @@ import { import { createToken } from '../../helpers/auth'; import { BadRequestError } from '../../utils/errors'; import { getInviteOnlySignup, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config'; +import { validateUserEmail } from '../../validation'; /** * Signup step 1: Initialize account for user under email [email] and send a verification code @@ -20,6 +21,9 @@ export const beginEmailSignup = async (req: Request, res: Response) => { let email: string; try { email = req.body.email; + + // validate that email is not disposable + validateUserEmail(email); const user = await User.findOne({ email }).select('+publicKey'); if (user && user?.publicKey) { diff --git a/backend/src/controllers/v1/workspaceController.ts b/backend/src/controllers/v1/workspaceController.ts index 2b0a89f431..a914fa9c26 100644 --- a/backend/src/controllers/v1/workspaceController.ts +++ b/backend/src/controllers/v1/workspaceController.ts @@ -14,6 +14,7 @@ import { createWorkspace as create, deleteWorkspace as deleteWork, } from "../../helpers/workspace"; +import { EELicenseService } from '../../ee/services'; import { addMemberships } from "../../helpers/membership"; import { ADMIN } from "../../variables"; @@ -141,6 +142,7 @@ export const getWorkspace = async (req: Request, res: Response) => { */ export const createWorkspace = async (req: Request, res: Response) => { let workspace; + try { const { workspaceName, organizationId } = req.body; @@ -154,6 +156,18 @@ export const createWorkspace = async (req: Request, res: Response) => { throw new Error("Failed to validate organization membership"); } + const plan = await EELicenseService.getOrganizationPlan(organizationId); + + if (plan.workspaceLimit !== null) { + // case: limit imposed on number of workspaces allowed + if (plan.workspacesUsed >= plan.workspaceLimit) { + // case: number of workspaces used exceeds the number of workspaces allowed + return res.status(400).send({ + message: 'Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces.' + }); + } + } + if (workspaceName.length < 1) { throw new Error("Workspace names must be at least 1-character long"); } diff --git a/backend/src/controllers/v2/authController.ts b/backend/src/controllers/v2/authController.ts index 15350f6e58..f468e6c20c 100644 --- a/backend/src/controllers/v2/authController.ts +++ b/backend/src/controllers/v2/authController.ts @@ -22,6 +22,10 @@ import { getHttpsEnabled } from '../../config'; +// note: move this out +import path from 'path'; +import fs from 'fs'; + declare module 'jsonwebtoken' { export interface UserIDJwtPayload extends jwt.JwtPayload { userId: string; @@ -93,7 +97,7 @@ export const login2 = async (req: Request, res: Response) => { const { email, clientProof } = req.body; const user = await User.findOne({ email - }).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag'); + }).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices'); if (!user) throw new Error('Failed to find user'); @@ -231,6 +235,8 @@ export const login2 = async (req: Request, res: Response) => { } }; +import { validateUserEmail } from '../../validation'; + /** * Send MFA token to email [email] * @param req diff --git a/backend/src/data/disposable_emails.txt b/backend/src/data/disposable_emails.txt new file mode 100644 index 0000000000..70e24a460d --- /dev/null +++ b/backend/src/data/disposable_emails.txt @@ -0,0 +1,3519 @@ +0-mail.com +027168.com +0815.ru +0815.ry +0815.su +0845.ru +0box.eu +0clickemail.com +0n0ff.net +0nelce.com +0v.ro +0w.ro +0wnd.net +0wnd.org +0x207.info +1-8.biz +1-tm.com +10-minute-mail.com +1000rebates.stream +100likers.com +105kg.ru +10dk.email +10mail.com +10mail.org +10minut.com.pl +10minut.xyz +10minutemail.be +10minutemail.cf +10minutemail.co.uk +10minutemail.co.za +10minutemail.com +10minutemail.de +10minutemail.ga +10minutemail.gq +10minutemail.ml +10minutemail.net +10minutemail.nl +10minutemail.pro +10minutemail.us +10minutemailbox.com +10minutemails.in +10minutenemail.de +10minutesmail.com +10minutesmail.fr +10minutmail.pl +10x9.com +11163.com +123-m.com +12hosting.net +12houremail.com +12minutemail.com +12minutemail.net +12storage.com +140unichars.com +147.cl +14n.co.uk +15qm.com +1blackmoon.com +1ce.us +1chuan.com +1clck2.com +1fsdfdsfsdf.tk +1mail.ml +1pad.de +1s.fr +1secmail.com +1secmail.net +1secmail.org +1st-forms.com +1to1mail.org +1usemail.com +1webmail.info +1zhuan.com +2012-2016.ru +20email.eu +20email.it +20mail.eu +20mail.in +20mail.it +20minutemail.com +20minutemail.it +20mm.eu +2120001.net +21cn.com +247web.net +24hinbox.com +24hourmail.com +24hourmail.net +2anom.com +2chmail.net +2ether.net +2fdgdfgdfgdf.tk +2odem.com +2prong.com +2wc.info +300book.info +30mail.ir +30minutemail.com +30wave.com +3202.com +36ru.com +3d-painting.com +3l6.com +3mail.ga +3trtretgfrfe.tk +4-n.us +4057.com +418.dk +42o.org +4gfdsgfdgfd.tk +4k5.net +4mail.cf +4mail.ga +4nextmail.com +4nmv.ru +4tb.host +4warding.com +4warding.net +4warding.org +50set.ru +55hosting.net +5ghgfhfghfgh.tk +5gramos.com +5july.org +5mail.cf +5mail.ga +5minutemail.net +5oz.ru +5tb.in +5x25.com +5ymail.com +60minutemail.com +672643.net +675hosting.com +675hosting.net +675hosting.org +6hjgjhgkilkj.tk +6ip.us +6mail.cf +6mail.ga +6mail.ml +6paq.com +6somok.ru +6url.com +75hosting.com +75hosting.net +75hosting.org +7days-printing.com +7mail.ga +7mail.ml +7tags.com +80665.com +8127ep.com +8mail.cf +8mail.ga +8mail.ml +99.com +99cows.com +99experts.com +9mail.cf +9me.site +9mot.ru +9ox.net +9q.ro +a-bc.net +a45.in +a7996.com +aa5zy64.com +abacuswe.us +abakiss.com +abcmail.email +abilitywe.us +abovewe.us +absolutewe.us +abundantwe.us +abusemail.de +abuser.eu +abyssmail.com +ac20mail.in +academiccommunity.com +academywe.us +acceleratewe.us +accentwe.us +acceptwe.us +acclaimwe.us +accordwe.us +accreditedwe.us +acentri.com +achievementwe.us +achievewe.us +acornwe.us +acrossgracealley.com +acrylicwe.us +activatewe.us +activitywe.us +acucre.com +acuitywe.us +acumenwe.us +adaptivewe.us +adaptwe.us +add3000.pp.ua +addictingtrailers.com +adeptwe.us +adfskj.com +adios.email +adiq.eu +aditus.info +admiralwe.us +ado888.biz +adobeccepdm.com +adoniswe.us +adpugh.org +adroh.com +adsd.org +adubiz.info +advantagewe.us +advantimo.com +adventurewe.us +adventwe.us +advisorwe.us +advocatewe.us +adwaterandstir.com +aegde.com +aegia.net +aegiscorp.net +aegiswe.us +aelo.es +aeonpsi.com +afarek.com +affiliate-nebenjob.info +affiliatedwe.us +affilikingz.de +affinitywe.us +affluentwe.us +affordablewe.us +afia.pro +afrobacon.com +afterhourswe.us +agedmail.com +agendawe.us +agger.ro +agilewe.us +agorawe.us +agtx.net +aheadwe.us +ahem.email +ahk.jp +ahmedkhlef.com +air2token.com +airmailbox.website +airsi.de +ajaxapp.net +akapost.com +akerd.com +akgq701.com +akmail.in +al-qaeda.us +albionwe.us +alchemywe.us +alfaceti.com +aliaswe.us +alienware13.com +aligamel.com +alisongamel.com +alivance.com +alivewe.us +all-cats.ru +allaccesswe.us +allamericanwe.us +allaroundwe.us +alldirectbuy.com +allegiancewe.us +allegrowe.us +allemojikeyboard.com +allgoodwe.us +alliancewe.us +allinonewe.us +allofthem.net +alloutwe.us +allowed.org +alloywe.us +allprowe.us +allseasonswe.us +allstarwe.us +allthegoodnamesaretaken.org +allurewe.us +almondwe.us +alph.wtf +alpha-web.net +alphaomegawe.us +alpinewe.us +altairwe.us +altitudewe.us +altuswe.us +ama-trade.de +ama-trans.de +amadeuswe.us +amail.club +amail.com +amail1.com +amail4.me +amazon-aws.org +amberwe.us +ambiancewe.us +ambitiouswe.us +amelabs.com +americanawe.us +americasbestwe.us +americaswe.us +amicuswe.us +amilegit.com +amiri.net +amiriindustries.com +amplewe.us +amplifiedwe.us +amplifywe.us +ampsylike.com +analogwe.us +analysiswe.us +analyticalwe.us +analyticswe.us +analyticwe.us +anappfor.com +anappthat.com +andreihusanu.ro +andthen.us +animesos.com +anit.ro +ano-mail.net +anon-mail.de +anonbox.net +anonmail.top +anonmails.de +anonymail.dk +anonymbox.com +anonymized.org +anonymousness.com +anotherdomaincyka.tk +ansibleemail.com +anthony-junkmail.com +antireg.com +antireg.ru +antispam.de +antispam24.de +antispammail.de +anyalias.com +aoeuhtns.com +apfelkorps.de +aphlog.com +apkmd.com +appc.se +appinventor.nl +appixie.com +apps.dj +appzily.com +arduino.hk +ariaz.jetzt +armyspy.com +aron.us +arroisijewellery.com +art-en-ligne.pro +artman-conception.com +arur01.tk +arurgitu.gq +arvato-community.de +aschenbrandt.net +asdasd.nl +asdasd.ru +ashleyandrew.com +ask-mail.com +asorent.com +ass.pp.ua +astonut.tk +astroempires.info +asu.mx +asu.su +at.hm +at0mik.org +atnextmail.com +attnetwork.com +augmentationtechnology.com +ausgefallen.info +auti.st +autorobotica.com +autosouvenir39.ru +autotwollow.com +autowb.com +aver.com +averdov.com +avia-tonic.fr +avls.pt +awatum.de +awdrt.org +awiki.org +awsoo.com +axiz.org +axon7zte.com +axsup.net +ayakamail.cf +azazazatashkent.tk +azcomputerworks.com +azmeil.tk +b1of96u.com +b2bx.net +b2cmail.de +badgerland.eu +badoop.com +badpotato.tk +balaket.com +banit.club +banit.me +bank-opros1.ru +bareed.ws +barryogorman.com +bartdevos.be +basscode.org +bauwerke-online.com +bazaaboom.com +bbbbyyzz.info +bbhost.us +bbitf.com +bbitj.com +bbitq.com +bcaoo.com +bcast.ws +bcb.ro +bccto.me +bdmuzic.pw +beaconmessenger.com +bearsarefuzzy.com +beddly.com +beefmilk.com +belamail.org +belljonestax.com +beluckygame.com +benipaula.org +bepureme.com +beribase.ru +beribaza.ru +berirabotay.ru +best-john-boats.com +bestchoiceusedcar.com +bestlistbase.com +bestoption25.club +bestparadize.com +bestsoundeffects.com +besttempmail.com +betr.co +bgtmail.com +bgx.ro +bheps.com +bidourlnks.com +big1.us +bigprofessor.so +bigstring.com +bigwhoop.co.za +bij.pl +binka.me +binkmail.com +binnary.com +bio-muesli.info +bio-muesli.net +bione.co +bitwhites.top +bitymails.us +blackgoldagency.ru +blackmarket.to +bladesmail.net +blip.ch +blnkt.net +block521.com +blogmyway.org +blogos.net +blogspam.ro +blondemorkin.com +bluedumpling.info +bluewerks.com +bnote.com +boatmail.us +bobmail.info +bobmurchison.com +bofthew.com +bonobo.email +boofx.com +bookthemmore.com +bootybay.de +borged.com +borged.net +borged.org +bot.nu +boun.cr +bouncr.com +boxformail.in +boximail.com +boxmail.lol +boxomail.live +boxtemp.com.br +bptfp.net +brand-app.biz +brandallday.net +brasx.org +breakthru.com +brefmail.com +brennendesreich.de +briggsmarcus.com +broadbandninja.com +bsnow.net +bspamfree.org +bspooky.com +bst-72.com +btb-notes.com +btc.email +btcmail.pw +btcmod.com +btizet.pl +buccalmassage.ru +budaya-tionghoa.com +budayationghoa.com +buffemail.com +bugfoo.com +bugmenever.com +bugmenot.com +bukhariansiddur.com +bulrushpress.com +bum.net +bumpymail.com +bunchofidiots.com +bund.us +bundes-li.ga +bunsenhoneydew.com +burnthespam.info +burstmail.info +businessbackend.com +businesssuccessislifesuccess.com +buspad.org +bussitussi.com +buymoreplays.com +buyordie.info +buyusdomain.com +buyusedlibrarybooks.org +buzzcluby.com +byebyemail.com +byespm.com +byom.de +c51vsgq.com +cachedot.net +californiafitnessdeals.com +cam4you.cc +camping-grill.info +candymail.de +cane.pw +capitalistdilemma.com +car101.pro +carbtc.net +cars2.club +carsencyclopedia.com +cartelera.org +caseedu.tk +cashflow35.com +casualdx.com +cavi.mx +cbair.com +cbes.net +cc.liamria +ccmail.uk +cdfaq.com +cdpa.cc +ceed.se +cek.pm +cellurl.com +centermail.com +centermail.net +cetpass.com +cfo2go.ro +chacuo.net +chaichuang.com +chalupaurybnicku.cz +chammy.info +chasefreedomactivate.com +chatich.com +cheaphub.net +cheatmail.de +chenbot.email +chibakenma.ml +chickenkiller.com +chielo.com +childsavetrust.org +chilkat.com +chinamkm.com +chithinh.com +chitthi.in +choco.la +chogmail.com +choicemail1.com +chong-mail.com +chong-mail.net +chong-mail.org +chumpstakingdumps.com +cigar-auctions.com +civikli.com +civx.org +ckaazaza.tk +ckiso.com +cl-cl.org +cl0ne.net +claimab.com +clandest.in +classesmail.com +clearwatermail.info +click-email.com +clickdeal.co +clipmail.eu +clixser.com +clonemoi.tk +cloud-mail.top +cloudns.cx +clout.wiki +clrmail.com +cmail.club +cmail.com +cmail.net +cmail.org +cnamed.com +cndps.com +cnew.ir +cnmsg.net +cnsds.de +co.cc +cobarekyo1.ml +cocoro.uk +cocovpn.com +codeandscotch.com +codivide.com +coffeetimer24.com +coieo.com +coin-host.net +coinlink.club +coldemail.info +compareshippingrates.org +completegolfswing.com +comwest.de +conf.work +consumerriot.com +contbay.com +cooh-2.site +coolandwacky.us +coolimpool.org +coreclip.com +cosmorph.com +courrieltemporaire.com +coza.ro +crankhole.com +crapmail.org +crastination.de +crazespaces.pw +crazymailing.com +cream.pink +crepeau12.com +cringemonster.com +cross-law.ga +cross-law.gq +crossmailjet.com +crossroadsmail.com +crunchcompass.com +crusthost.com +cs.email +csh.ro +cszbl.com +ctmailing.us +ctos.ch +cu.cc +cubiclink.com +cuendita.com +cuirushi.org +cuoly.com +cupbest.com +curlhph.tk +curryworld.de +cust.in +cutout.club +cutradition.com +cuvox.de +cyber-innovation.club +cyber-phone.eu +cylab.org +d1yun.com +d3p.dk +daabox.com +dab.ro +dacoolest.com +daemsteam.com +daibond.info +daily-email.com +daintly.com +damai.webcam +dammexe.net +damnthespam.com +dandikmail.com +darkharvestfilms.com +daryxfox.net +dasdasdascyka.tk +dash-pads.com +dataarca.com +datarca.com +datazo.ca +datenschutz.ru +datum2.com +davidkoh.net +davidlcreative.com +dawin.com +daymail.life +daymailonline.com +dayrep.com +dbunker.com +dcctb.com +dcemail.com +ddcrew.com +de-a.org +dea-21olympic.com +deadaddress.com +deadchildren.org +deadfake.cf +deadfake.ga +deadfake.ml +deadfake.tk +deadspam.com +deagot.com +dealja.com +dealrek.com +deekayen.us +defomail.com +degradedfun.net +deinbox.com +delayload.com +delayload.net +delikkt.de +delivrmail.com +demen.ml +dengekibunko.ga +dengekibunko.gq +dengekibunko.ml +der-kombi.de +derkombi.de +derluxuswagen.de +desoz.com +despam.it +despammed.com +dev-null.cf +dev-null.ga +dev-null.gq +dev-null.ml +developermail.com +devnullmail.com +deyom.com +dharmatel.net +dhm.ro +dhy.cc +dialogus.com +diapaulpainting.com +dicopto.com +digdig.org +digital-message.com +digitalesbusiness.info +digitalmail.info +digitalmariachis.com +digitalsanctuary.com +dildosfromspace.com +dim-coin.com +dingbone.com +diolang.com +directmail24.net +disaq.com +disbox.net +disbox.org +discard.cf +discard.email +discard.ga +discard.gq +discard.ml +discard.tk +discardmail.com +discardmail.de +discos4.com +disign-concept.eu +disign-revelation.com +dispo.in +dispomail.eu +disposable-e.ml +disposable-email.ml +disposable.cf +disposable.ga +disposable.ml +disposable.site +disposableaddress.com +disposableemailaddresses.com +disposableinbox.com +disposablemails.com +dispose.it +disposeamail.com +disposemail.com +disposemymail.com +dispostable.com +divad.ga +divermail.com +divismail.ru +diwaq.com +dlemail.ru +dmarc.ro +dndent.com +dnses.ro +doanart.com +dob.jp +dodgeit.com +dodgemail.de +dodgit.com +dodgit.org +dodsi.com +doiea.com +dolphinnet.net +domforfb1.tk +domforfb18.tk +domforfb19.tk +domforfb2.tk +domforfb23.tk +domforfb27.tk +domforfb29.tk +domforfb3.tk +domforfb4.tk +domforfb5.tk +domforfb6.tk +domforfb7.tk +domforfb8.tk +domforfb9.tk +domozmail.com +donemail.ru +dongqing365.com +dontreg.com +dontsendmespam.de +doojazz.com +doquier.tk +dotman.de +dotmsg.com +dotslashrage.com +doublemail.de +douchelounge.com +dozvon-spb.ru +dp76.com +dr69.site +drdrb.com +drdrb.net +dred.ru +drevo.si +drivetagdev.com +drmail.in +droolingfanboy.de +dropcake.de +dropjar.com +droplar.com +dropmail.me +dropsin.net +dsgvo.ru +dsiay.com +dspwebservices.com +duam.net +duck2.club +dudmail.com +duk33.com +dukedish.com +dump-email.info +dumpandjunk.com +dumpmail.de +dumpyemail.com +durandinterstellar.com +duskmail.com +dwse.edu.pl +dyceroprojects.com +dz17.net +e-mail.com +e-mail.org +e-marketstore.ru +e-tomarigi.com +e3z.de +e4ward.com +eanok.com +easy-trash-mail.com +easynetwork.info +easytrashmail.com +eatmea2z.club +eay.jp +ebbob.com +ebeschlussbuch.de +ecallheandi.com +ecolo-online.fr +edgex.ru +edinburgh-airporthotels.com +edv.to +ee1.pl +ee2.pl +eeedv.de +eelmail.com +efxs.ca +egzones.com +einmalmail.de +einrot.com +einrot.de +eintagsmail.de +elearningjournal.org +electro.mn +elitevipatlantamodels.com +elki-mkzn.ru +email-fake.cf +email-fake.com +email-fake.ga +email-fake.gq +email-fake.ml +email-fake.tk +email-jetable.fr +email-lab.com +email-temp.com +email.edu.pl +email.net +email1.pro +email60.com +emailage.cf +emailage.ga +emailage.gq +emailage.ml +emailage.tk +emailate.com +emailcu.icu +emaildienst.de +emaildrop.io +emailfake.com +emailfake.ml +emailfreedom.ml +emailgenerator.de +emailgo.de +emailias.com +emailigo.de +emailinfive.com +emailisvalid.com +emaillime.com +emailmiser.com +emailna.co +emailnax.com +emailo.pro +emailondeck.com +emailportal.info +emailproxsy.com +emailresort.com +emails.ga +emailsecurer.com +emailsensei.com +emailsingularity.net +emailspam.cf +emailspam.ga +emailspam.gq +emailspam.ml +emailspam.tk +emailsy.info +emailtech.info +emailtemporanea.com +emailtemporanea.net +emailtemporar.ro +emailtemporario.com.br +emailthe.net +emailtmp.com +emailto.de +emailure.net +emailwarden.com +emailxfer.com +emailz.cf +emailz.ga +emailz.gq +emailz.ml +emeil.in +emeil.ir +emeraldwebmail.com +emil.com +emkei.cf +emkei.ga +emkei.gq +emkei.ml +emkei.tk +eml.pp.ua +emlhub.com +emlpro.com +emltmp.com +empireanime.ga +emstjzh.com +emz.net +enayu.com +enterto.com +envy17.com +eoffice.top +eoopy.com +epb.ro +ephemail.net +ephemeral.email +eposta.buzz +eposta.work +eqiluxspam.ga +ereplyzy.com +ericjohnson.ml +ero-tube.org +esadverse.com +esbano-ru.ru +esc.la +escapehatchapp.com +esemay.com +esgeneri.com +esiix.com +esprity.com +estate-invest.fr +eth2btc.info +ether123.net +ethereum1.top +ethersports.org +ethersportz.info +etotvibor.ru +etranquil.com +etranquil.net +etranquil.org +euaqa.com +evanfox.info +eveav.com +evilcomputer.com +evopo.com +evyush.com +exdonuts.com +existiert.net +exitstageleft.net +explodemail.com +express.net.ua +extracurricularsociety.com +extremail.ru +eyepaste.com +ez.lv +ezehe.com +ezfill.com +ezstest.com +f4k.es +f5.si +facebook-email.cf +facebook-email.ga +facebook-email.ml +facebookmail.gq +facebookmail.ml +fackme.gq +fadingemail.com +faecesmail.me +fag.wf +failbone.com +faithkills.com +fake-box.com +fake-email.pp.ua +fake-mail.cf +fake-mail.ga +fake-mail.ml +fakedemail.com +fakeinbox.cf +fakeinbox.com +fakeinbox.ga +fakeinbox.info +fakeinbox.ml +fakeinbox.tk +fakeinformation.com +fakemail.fr +fakemail.io +fakemailgenerator.com +fakemailz.com +fallinhay.com +fammix.com +fanclub.pm +fangoh.com +fansworldwide.de +fantasymail.de +farrse.co.uk +fast-email.info +fast-mail.fr +fastacura.com +fastchevy.com +fastchrysler.com +fasternet.biz +fastkawasaki.com +fastmazda.com +fastmitsubishi.com +fastnissan.com +fastsubaru.com +fastsuzuki.com +fasttoyota.com +fastyamaha.com +fatflap.com +fbma.tk +fddns.ml +fdfdsfds.com +femailtor.com +fer-gabon.org +fermaxxi.ru +fettometern.com +fexbox.org +fexbox.ru +fexpost.com +fextemp.com +ficken.de +fictionsite.com +fightallspam.com +figjs.com +figshot.com +figurescoin.com +fiifke.de +filbert4u.com +filberts4u.com +film-blog.biz +filzmail.com +findemail.info +findu.pl +finews.biz +fir.hk +firemailbox.club +fitnesrezink.ru +fivemail.de +fixmail.tk +fizmail.com +fleckens.hu +flemail.ru +flowu.com +flu.cc +fluidsoft.us +flurred.com +fly-ts.de +flyinggeek.net +flyspam.com +foobarbot.net +footard.com +foreastate.com +forecastertests.com +foreskin.cf +foreskin.ga +foreskin.gq +foreskin.ml +foreskin.tk +forgetmail.com +fornow.eu +forspam.net +forward.cat +fosil.pro +foxja.com +foxtrotter.info +fr.cr +fr.nf +fr33mail.info +fragolina2.tk +frapmail.com +frappina.tk +free-email.cf +free-email.ga +free-temp.net +freebabysittercam.com +freeblackbootytube.com +freecat.net +freedom4you.info +freedompop.us +freefattymovies.com +freehotmail.net +freeinbox.email +freelance-france.eu +freeletter.me +freemail.ms +freemails.cf +freemails.ga +freemails.ml +freemeil.ga +freemeil.gq +freemeil.ml +freeml.net +freeplumpervideos.com +freerubli.ru +freeschoolgirlvids.com +freesistercam.com +freeteenbums.com +freundin.ru +friendlymail.co.uk +front14.org +frwdmail.com +ftp.sh +ftpinc.ca +fuckedupload.com +fuckingduh.com +fuckme69.club +fucknloveme.top +fuckxxme.top +fudgerub.com +fuirio.com +fukaru.com +fukurou.ch +fullangle.org +fulvie.com +fun64.com +funnycodesnippets.com +funnymail.de +furzauflunge.de +futuramind.com +fuwamofu.com +fuwari.be +fux0ringduh.com +fxnxs.com +fyii.de +g14l71lb.com +g1xmail.top +g2xmail.top +g3xmail.top +g4hdrop.us +gafy.net +gage.ga +galaxy.tv +gally.jp +gamail.top +gamegregious.com +gamgling.com +garasikita.pw +garbagecollector.org +garbagemail.org +gardenscape.ca +garizo.com +garliclife.com +garrymccooey.com +gav0.com +gawab.com +gbcmail.win +gbmail.top +gcmail.top +gdmail.top +gedmail.win +geekforex.com +geew.ru +gehensiemirnichtaufdensack.de +geldwaschmaschine.de +gelitik.in +genderfuck.net +geronra.com +geschent.biz +get-mail.cf +get-mail.ga +get-mail.ml +get-mail.tk +get.pp.ua +get1mail.com +get2mail.fr +getairmail.cf +getairmail.com +getairmail.ga +getairmail.gq +getairmail.ml +getairmail.tk +geteit.com +getfun.men +getmails.eu +getnada.com +getnowtoday.cf +getonemail.com +getonemail.net +getover.de +getsimpleemail.com +gett.icu +gexik.com +ggmal.ml +ghosttexter.de +giacmosuaviet.info +giaiphapmuasam.com +giantmail.de +gifto12.com +ginzi.be +ginzi.co.uk +ginzi.es +ginzi.net +ginzy.co.uk +ginzy.eu +girlmail.win +girlsindetention.com +girlsundertheinfluence.com +gishpuppy.com +giveh2o.info +givememail.club +givmail.com +glitch.sx +globaltouron.com +glubex.com +glucosegrin.com +gmal.com +gmatch.org +gmial.com +gmx1mail.top +gmxmail.top +gmxmail.win +gnctr-calgary.com +go2usa.info +go2vpn.net +goemailgo.com +golemico.com +gomail.in +goonby.com +goplaygame.ru +gorillaswithdirtyarmpits.com +goround.info +gosuslugi-spravka.ru +gothere.biz +gotmail.com +gotmail.net +gotmail.org +gowikibooks.com +gowikicampus.com +gowikicars.com +gowikifilms.com +gowikigames.com +gowikimusic.com +gowikinetwork.com +gowikitravel.com +gowikitv.com +grandmamail.com +grandmasmail.com +great-host.in +greencafe24.com +greendike.com +greenhousemail.com +greensloth.com +greggamel.com +greggamel.net +gregorsky.zone +gregorygamel.com +gregorygamel.net +grish.de +griuc.schule +grn.cc +groupbuff.com +grr.la +grugrug.ru +gruz-m.ru +gs-arc.org +gsredcross.org +gsrv.co.uk +gsxstring.ga +gudanglowongan.com +guerillamail.biz +guerillamail.com +guerillamail.de +guerillamail.info +guerillamail.net +guerillamail.org +guerillamailblock.com +guerrillamail.biz +guerrillamail.com +guerrillamail.de +guerrillamail.info +guerrillamail.net +guerrillamail.org +guerrillamailblock.com +gufum.com +gustr.com +gxemail.men +gynzi.co.uk +gynzi.es +gynzy.at +gynzy.es +gynzy.eu +gynzy.gr +gynzy.info +gynzy.lt +gynzy.mobi +gynzy.pl +gynzy.ro +gynzy.sk +gzb.ro +h8s.org +habitue.net +hacccc.com +hackersquad.tk +hackthatbit.ch +hahawrong.com +haida-edu.cn +hairs24.ru +haltospam.com +hamham.uk +hangxomcuatoilatotoro.ml +happydomik.ru +harakirimail.com +haribu.com +hartbot.de +hasanmail.ml +hat-geld.de +hatespam.org +hawrong.com +haydoo.com +hazelnut4u.com +hazelnuts4u.com +hazmatshipping.org +hccmail.win +headstrong.de +heathenhammer.com +heathenhero.com +hecat.es +heisei.be +hellodream.mobi +helloricky.com +helpinghandtaxcenter.org +helpjobs.ru +heros3.com +herp.in +herpderp.nl +hezll.com +hi5.si +hiddentragedy.com +hidebox.org +hidebusiness.xyz +hidemail.de +hidemail.pro +hidemail.us +hidzz.com +highbros.org +hiltonvr.com +himail.online +hmail.us +hmamail.com +hmh.ro +hoanggiaanh.com +hoanglong.tech +hochsitze.com +hola.org +holl.ga +honeys.be +honor-8.com +hopemail.biz +hornyalwary.top +host1s.com +hostcalls.com +hostguru.top +hostingmail.me +hostlaba.com +hot-mail.cf +hot-mail.ga +hot-mail.gq +hot-mail.ml +hot-mail.tk +hotmai.com +hotmailproduct.com +hotmial.com +hotpop.com +hotprice.co +hotsoup.be +housat.com +hpc.tw +hs.vc +ht.cx +huangniu8.com +hukkmu.tk +hulapla.de +humaility.com +hungpackage.com +hushmail.cf +huskion.net +hvastudiesucces.nl +hwsye.net +i2pmail.org +i6.cloudns.cc +iaoss.com +ibnuh.bz +icantbelieveineedtoexplainthisshit.com +icemail.club +ichigo.me +icx.in +icx.ro +idx4.com +idxue.com +ieatspam.eu +ieatspam.info +ieh-mail.de +iencm.com +iffymedia.com +ige.es +igg.biz +ignoremail.com +ihateyoualot.info +ihazspam.ca +iheartspam.org +ikbenspamvrij.nl +illistnoise.com +ilovespam.com +imail1.net +imails.info +imailt.com +imgof.com +imgv.de +immo-gerance.info +imstations.com +imul.info +in-ulm.de +in2reach.com +inactivemachine.com +inbax.tk +inbound.plus +inbox.si +inbox2.info +inboxalias.com +inboxbear.com +inboxclean.com +inboxclean.org +inboxdesign.me +inboxed.im +inboxed.pw +inboxkitten.com +inboxproxy.com +inboxstore.me +inclusiveprogress.com +incognitomail.com +incognitomail.net +incognitomail.org +incq.com +ind.st +indieclad.com +indirect.ws +indomaed.pw +indomina.cf +indoserver.stream +indosukses.press +ineec.net +infocom.zp.ua +inggo.org +inkomail.com +inmynetwork.tk +inoutmail.de +inoutmail.eu +inoutmail.info +inoutmail.net +inpwa.com +insanumingeniumhomebrew.com +insorg-mail.info +instaddr.ch +instance-email.com +instant-mail.de +instantblingmail.info +instantemailaddress.com +instantmail.fr +internet-v-stavropole.ru +internetoftags.com +interstats.org +intersteller.com +intopwa.com +intopwa.net +intopwa.org +investore.co +iozak.com +ip4.pp.ua +ip6.li +ip6.pp.ua +ipoo.org +ippandansei.tk +ipsur.org +irabops.com +irc.so +irish2me.com +irishspringrealty.com +iroid.com +ironiebehindert.de +irssi.tv +is.af +isdaq.com +ishop2k.com +isosq.com +istii.ro +isukrainestillacountry.com +it7.ovh +italy-mail.com +itcompu.com +itfast.net +itunesgiftcodegenerator.com +iubridge.com +iuemail.men +iwi.net +ixaks.com +ixx.io +j-p.us +jafps.com +jajxz.com +janproz.com +jaqis.com +jdmadventures.com +jdz.ro +je-recycle.info +jellow.ml +jellyrolls.com +jeoce.com +jet-renovation.fr +jetable.com +jetable.net +jetable.org +jetable.pp.ua +jiooq.com +jmail.ovh +jmail.ro +jnxjn.com +jobbikszimpatizans.hu +jobbrett.com +jobposts.net +jobs-to-be-done.net +joelpet.com +joetestalot.com +jopho.com +joseihorumon.info +josse.ltd +jourrapide.com +jpco.org +jsrsolutions.com +jumonji.tk +jungkamushukum.com +junk.to +junk1e.com +junkmail.ga +junkmail.gq +just-email.com +justemail.ml +juyouxi.com +jwork.ru +kademen.com +kadokawa.cf +kadokawa.ga +kadokawa.gq +kadokawa.ml +kadokawa.tk +kaengu.ru +kagi.be +kakadua.net +kalapi.org +kamen-market.ru +kamsg.com +kaovo.com +kappala.info +kara-turk.net +karatraman.ml +kariplan.com +karta-kykyruza.ru +kartvelo.com +kasmail.com +kaspop.com +katztube.com +kazelink.ml +kbox.li +kcrw.de +keepmymail.com +keinhirn.de +keipino.de +kekita.com +kellychibale-researchgroup-uct.com +kemptvillebaseball.com +kennedy808.com +kiani.com +killmail.com +killmail.net +kimsdisk.com +kingsq.ga +kino-100.ru +kiois.com +kismail.ru +kisstwink.com +kitnastar.com +kjkszpjcompany.com +kkmail.be +kksm.be +klassmaster.com +klassmaster.net +klick-tipp.us +klipschx12.com +kloap.com +kludgemush.com +klzlk.com +kmail.li +kmhow.com +knol-power.nl +kobrandly.com +kommunity.biz +kon42.com +konultant-jurist.ru +kook.ml +kopagas.com +kopaka.net +korona-nedvizhimosti.ru +koshu.ru +kosmetik-obatkuat.com +kostenlosemailadresse.de +koszmail.pl +kpay.be +kpooa.com +kpost.be +krd.ag +krsw.tk +kruay.com +krypton.tk +ksmtrck.tk +kuhrap.com +kulmeo.com +kulturbetrieb.info +kurzepost.de +kutakbisajauhjauh.gq +kvhrr.com +kvhrs.com +kvhrw.com +kwift.net +kwilco.net +kyal.pl +kyois.com +kzccv.com +l-c-a.us +l33r.eu +l6factors.com +labetteraverouge.at +labworld.org +lacedmail.com +lackmail.net +lackmail.ru +lacto.info +lags.us +lain.ch +lak.pp.ua +lakelivingstonrealestate.com +lakqs.com +lamasticots.com +landmail.co +laoeq.com +larisia.com +larland.com +last-chance.pro +lastmail.co +lastmail.com +lawlita.com +lazyinbox.com +lazyinbox.us +ldaho.biz +ldop.com +ldtp.com +le-tim.ru +lee.mx +leeching.net +leetmail.co +legalrc.loan +lellno.gq +lenovog4.com +lerbhe.com +letmeinonthis.com +letthemeatspam.com +lez.se +lgxscreen.com +lhsdv.com +liamcyrus.com +lifebyfood.com +lifetimefriends.info +lifetotech.com +ligsb.com +lillemap.net +lilo.me +lindenbaumjapan.com +link2mail.net +linkedintuts2016.pw +linshiyouxiang.net +linuxmail.so +litedrop.com +liveradio.tk +lkgn.se +llogin.ru +loadby.us +loan101.pro +loaoa.com +loapq.com +locanto1.club +locantofuck.top +locantowsite.club +locomodev.net +login-email.cf +login-email.ga +login-email.ml +login-email.tk +logular.com +loh.pp.ua +loin.in +lolfreak.net +lolmail.biz +lookugly.com +lordsofts.com +lortemail.dk +losemymail.com +lovemeet.faith +lovemeleaveme.com +lpfmgmtltd.com +lr7.us +lr78.com +lroid.com +lru.me +ls-server.ru +lsyx24.com +luckymail.org +lukecarriere.com +lukemail.info +lukop.dk +luv2.us +lyfestylecreditsolutions.com +lyft.live +lyricspad.net +lzoaq.com +m21.cc +m4ilweb.info +maboard.com +mac-24.com +macr2.com +macromaid.com +macromice.info +magamail.com +maggotymeat.ga +magicbox.ro +magim.be +magspam.net +maidlow.info +mail-card.net +mail-easy.fr +mail-filter.com +mail-help.net +mail-hosting.co +mail-hub.info +mail-now.top +mail-owl.com +mail-share.com +mail-temporaire.com +mail-temporaire.fr +mail-tester.com +mail.by +mail.wtf +mail0.ga +mail1.top +mail114.net +mail1a.de +mail1web.org +mail21.cc +mail22.club +mail2rss.org +mail333.com +mail4trash.com +mail666.ru +mail7.io +mail707.com +mail72.com +mailapp.top +mailback.com +mailbidon.com +mailbiz.biz +mailblocks.com +mailbox.in.ua +mailbox52.ga +mailbox80.biz +mailbox82.biz +mailbox87.de +mailbox92.biz +mailboxy.fun +mailbucket.org +mailcat.biz +mailcatch.com +mailchop.com +mailcker.com +maildax.me +mailde.de +mailde.info +maildrop.cc +maildrop.cf +maildrop.ga +maildrop.gq +maildrop.ml +maildu.de +maildx.com +maileater.com +mailed.in +mailed.ro +maileimer.de +maileme101.com +mailexpire.com +mailf5.com +mailfa.tk +mailfall.com +mailfirst.icu +mailforspam.com +mailfree.ga +mailfree.gq +mailfree.ml +mailfreeonline.com +mailfs.com +mailguard.me +mailgutter.com +mailhazard.com +mailhazard.us +mailhex.com +mailhub.pro +mailhz.me +mailimate.com +mailin8r.com +mailinatar.com +mailinater.com +mailinator.co.uk +mailinator.com +mailinator.gq +mailinator.info +mailinator.net +mailinator.org +mailinator.us +mailinator0.com +mailinator1.com +mailinator2.com +mailinator2.net +mailinator3.com +mailinator4.com +mailinator5.com +mailinator6.com +mailinator7.com +mailinator8.com +mailinator9.com +mailincubator.com +mailismagic.com +mailita.tk +mailjunk.cf +mailjunk.ga +mailjunk.gq +mailjunk.ml +mailjunk.tk +mailmate.com +mailme.gq +mailme.ir +mailme.lv +mailme24.com +mailmetrash.com +mailmoat.com +mailmoth.com +mailms.com +mailna.biz +mailna.co +mailna.in +mailna.me +mailnator.com +mailnesia.com +mailnull.com +mailonaut.com +mailorc.com +mailorg.org +mailosaur.net +mailox.fun +mailpick.biz +mailpluss.com +mailpooch.com +mailpoof.com +mailpress.gq +mailproxsy.com +mailquack.com +mailrock.biz +mailsac.com +mailscrap.com +mailseal.de +mailshell.com +mailshiv.com +mailsiphon.com +mailslapping.com +mailslite.com +mailsucker.net +mailt.net +mailt.top +mailtechx.com +mailtemp.info +mailtemporaire.com +mailtemporaire.fr +mailto.plus +mailtome.de +mailtothis.com +mailtraps.com +mailtrash.net +mailtrix.net +mailtv.net +mailtv.tv +mailuniverse.co.uk +mailzi.ru +mailzilla.com +mailzilla.org +mainerfolg.info +makemenaughty.club +makemetheking.com +malahov.de +malayalamdtp.com +mama3.org +mamulenok.ru +mandraghen.cf +manifestgenerator.com +mannawo.com +mansiondev.com +manybrain.com +mark-compressoren.ru +marketlink.info +markmurfin.com +mask03.ru +masonline.info +maswae.world +matamuasu.ga +matchpol.net +matra.site +max-mail.org +mbox.re +mbx.cc +mcache.net +mciek.com +mdhc.tk +meantinc.com +mebelnu.info +mechanicalresumes.com +medkabinet-uzi.ru +meepsheep.eu +meidecn.com +meinspamschutz.de +meltedbrownies.com +meltmail.com +memsg.site +mentonit.net +mepost.pw +merry.pink +messagebeamer.de +messwiththebestdielikethe.rest +metadownload.org +metaintern.net +metalunits.com +mezimages.net +mfsa.info +mfsa.ru +mhzayt.online +miaferrari.com +miauj.com +midcoastcustoms.com +midcoastcustoms.net +midcoastsolutions.com +midcoastsolutions.net +midiharmonica.com +midlertidig.com +midlertidig.net +midlertidig.org +mierdamail.com +migmail.net +migmail.pl +migumail.com +mihep.com +mijnhva.nl +ministry-of-silly-walks.de +minsmail.com +mintemail.com +mirai.re +misterpinball.de +miucce.com +mji.ro +mjj.edu.ge +mjukglass.nu +mkpfilm.com +ml8.ca +mm.my +mm5.se +mnode.me +moakt.cc +moakt.co +moakt.com +moakt.ws +mobileninja.co.uk +mobilevpn.top +moburl.com +mockmyid.com +moeri.org +mofu.be +mohmal.com +mohmal.im +mohmal.in +mohmal.tech +moimoi.re +molms.com +momentics.ru +monachat.tk +monadi.ml +moneypipe.net +monumentmail.com +moonwake.com +moot.es +moreawesomethanyou.com +moreorcs.com +morriesworld.ml +morsin.com +moruzza.com +motique.de +mountainregionallibrary.net +mox.pp.ua +moy-elektrik.ru +moza.pl +mozej.com +mp-j.ga +mr24.co +mrvpm.net +mrvpt.com +msgos.com +mspeciosa.com +msrc.ml +mswork.ru +msxd.com +mt2009.com +mt2014.com +mt2015.com +mtmdev.com +muathegame.com +muchomail.com +mucincanon.com +muehlacker.tk +muell.icu +muell.monster +muell.xyz +muellemail.com +muellmail.com +munoubengoshi.gq +musiccode.me +mutant.me +mvrht.com +mvrht.net +mwarner.org +mxclip.com +mxfuel.com +my-pomsies.ru +my-teddyy.ru +my10minutemail.com +mybitti.de +mycleaninbox.net +mycorneroftheinter.net +myde.ml +mydefipet.live +mydemo.equipment +myecho.es +myemailboxy.com +mygeoweb.info +myindohome.services +myinterserver.ml +mykickassideas.com +mymail-in.net +mymail90.com +mymailoasis.com +mynetstore.de +myopang.com +mypacks.net +mypartyclip.de +myphantomemail.com +mysamp.de +myspaceinc.com +myspaceinc.net +myspaceinc.org +myspacepimpedup.com +myspamless.com +mystvpn.com +mysugartime.ru +mytemp.email +mytempemail.com +mytempmail.com +mytrashmail.com +mywarnernet.net +mywrld.site +mywrld.top +myzx.com +mzico.com +n1nja.org +na-cat.com +nabuma.com +nada.email +nada.ltd +nagi.be +nakedtruth.biz +nanonym.ch +naslazhdai.ru +nationalgardeningclub.com +nawmin.info +nbzmr.com +negated.com +neko2.net +nekochan.fr +neomailbox.com +neotlozhniy-zaim.ru +nepwk.com +nervmich.net +nervtmich.net +net1mail.com +netcom.ws +netmails.com +netmails.net +netricity.nl +netris.net +netviewer-france.com +netzidiot.de +nevermail.de +newbpotato.tk +newfilm24.ru +newideasfornewpeople.info +newmail.top +next.ovh +nextmail.info +nextstopvalhalla.com +nezdiro.org +nezid.com +nezumi.be +nezzart.com +nfast.net +nguyenusedcars.com +nh3.ro +nice-4u.com +nicknassar.com +nincsmail.com +nincsmail.hu +niseko.be +niwl.net +nm7.cc +nmail.cf +nnh.com +nnot.net +nnoway.ru +no-spam.ws +no-ux.com +noblepioneer.com +nobugmail.com +nobulk.com +nobuma.com +noclickemail.com +nodezine.com +nogmailspam.info +noicd.com +nokiamail.com +nolemail.ga +nomail.cf +nomail.ga +nomail.pw +nomail2me.com +nomorespamemails.com +nonspam.eu +nonspammer.de +nonze.ro +noref.in +norseforce.com +norwegischlernen.info +nospam4.us +nospamfor.us +nospamthanks.info +nothingtoseehere.ca +notif.me +notmailinator.com +notrnailinator.com +notsharingmy.info +now.im +nowhere.org +nowmymail.com +nowmymail.net +nproxi.com +nthrl.com +ntlhelp.net +nubescontrol.com +nullbox.info +nurfuerspam.de +nut.cc +nutpa.net +nuts2trade.com +nvhrw.com +nwldx.com +nwytg.com +nwytg.net +ny7.me +nypato.com +nyrmusic.com +o2stk.org +o7i.net +oalsp.com +obfusko.com +objectmail.com +obobbo.com +oborudovanieizturcii.ru +obxpestcontrol.com +octovie.com +odaymail.com +odem.com +odnorazovoe.ru +oepia.com +oerpub.org +offshore-proxies.net +ohaaa.de +ohi.tw +oida.icu +oing.cf +okclprojects.com +okinawa.li +okrent.us +okzk.com +olimp-case.ru +olypmall.ru +omail.pro +omnievents.org +omtecha.com +one-mail.top +one-time.email +one2mail.info +onekisspresave.com +onemail.host +oneoffemail.com +oneoffmail.com +onetm.jp +onewaymail.com +onlatedotcom.info +online.ms +onlineidea.info +onqin.com +ontyne.biz +oohioo.com +oolus.com +oonies-shoprus.ru +oopi.org +oosln.com +opayq.com +openavz.com +opendns.ro +opentrash.com +opmmedia.ga +opp24.com +optimaweb.me +opwebw.com +oranek.com +ordinaryamerican.net +oreidresume.com +orgmbx.cc +oroki.de +oshietechan.link +otherinbox.com +ourklips.com +ourpreviewdomain.com +outlawspam.com +outmail.win +ovomail.co +ovpn.to +owleyes.ch +owlpic.com +ownsyou.de +oxopoha.com +ozyl.de +p-banlis.ru +p33.org +p71ce1m.com +pa9e.com +pachilly.com +packiu.com +pagamenti.tk +paharpurmim.ga +pakadebu.ga +pamaweb.com +pancakemail.com +papierkorb.me +paplease.com +para2019.ru +parlimentpetitioner.tk +pastebitch.com +patonce.com +pavilionx2.com +payperex2.com +payspun.com +pe.hu +pecinan.com +pecinan.net +pecinan.org +penisgoes.in +penoto.tk +pepbot.com +peterdethier.com +petloca.com +petrzilka.net +pewpewpewpew.pw +pfui.ru +phone-elkey.ru +photo-impact.eu +photomark.net +pi.vu +piaa.me +pig.pp.ua +pii.at +piki.si +pimpedupmyspace.com +pinehill-seattle.org +pingir.com +pipemail.space +pisls.com +pitaniezdorovie.ru +pivo-bar.ru +pixiil.com +pjjkp.com +placebomail10.com +pleasenoham.org +plexfirm.com +plexolan.de +plhk.ru +ploae.com +plw.me +poehali-otdihat.ru +pojok.ml +pokemail.net +pokiemobile.com +polarkingxx.ml +politikerclub.de +polyfaust.net +pooae.com +poofy.org +pookmail.com +poopiebutt.club +popcornfarm7.com +popcornfly.com +popesodomy.com +popgx.com +porjoton.com +porsh.net +posdz.com +posta.store +postacin.com +postonline.me +poutineyourface.com +powered.name +powerencry.com +powlearn.com +pp7rvv.com +ppetw.com +pptrvv.com +pqoia.com +pratikmail.com +pratikmail.net +pratikmail.org +prazdnik-37.ru +predatorrat.cf +predatorrat.ga +predatorrat.gq +predatorrat.ml +predatorrat.tk +premium-mail.fr +primabananen.net +prin.be +privacy.net +privatdemail.net +privy-mail.com +privy-mail.de +privymail.de +pro-tag.org +pro5g.com +procrackers.com +profast.top +projectcl.com +promailt.com +proprietativalcea.ro +propscore.com +protempmail.com +proxymail.eu +proxyparking.com +prtnx.com +prtshr.com +prtz.eu +psh.me +psles.com +psnator.com +psoxs.com +puglieisi.com +puji.pro +punkass.com +puppetmail.de +purcell.email +purelogistics.org +put2.net +puttanamaiala.tk +putthisinyourspamdatabase.com +pwrby.com +qasti.com +qbfree.us +qc.to +qibl.at +qiott.com +qipmail.net +qiq.us +qisdo.com +qisoa.com +qmrbe.com +qoika.com +qopow.com +qq.my +qsl.ro +qtum-ico.com +quadrafit.com +quick-mail.cc +quickemail.info +quickinbox.com +quickmail.nl +quicksend.ch +ququb.com +qvy.me +qwickmail.com +r4nd0m.de +ra3.us +rabin.ca +rabiot.reisen +rackabzar.com +raetp9.com +rainbowly.ml +raketenmann.de +rancidhome.net +randomail.io +randomail.net +rapt.be +raqid.com +rax.la +raxtest.com +razemail.com +razuz.com +rbb.org +rcasd.com +rcpt.at +rdklcrv.xyz +re-gister.com +reality-concept.club +reallymymail.com +realtyalerts.ca +rebates.stream +receiveee.com +recipeforfailure.com +recode.me +reconmail.com +recyclemail.dk +redfeathercrow.com +reftoken.net +regbypass.com +regspaces.tk +reimondo.com +rejectmail.com +rejo.technology +reliable-mail.com +remail.cf +remail.ga +remarkable.rocks +remote.li +reptilegenetics.com +resgedvgfed.tk +revolvingdoorhoax.org +rfc822.org +rhyta.com +richfinances.pw +riddermark.de +rifkian.ga +rippb.com +risingsuntouch.com +riski.cf +rklips.com +rkomo.com +rm2rf.com +rma.ec +rmqkr.net +rnailinator.com +ro.lt +robertspcrepair.com +robot-mail.com +rollindo.agency +ronnierage.net +rootfest.net +rosebearmylove.ru +rotaniliam.com +rover.info +rowe-solutions.com +royal.net +royaldoodles.org +royalmarket.life +royandk.com +rppkn.com +rsvhr.com +rtrtr.com +rtskiya.xyz +rudymail.ml +rumgel.com +runi.ca +rupayamail.com +ruru.be +rustydoor.com +rvb.ro +ryteto.me +s0ny.net +s33db0x.com +sabrestlouis.com +sackboii.com +saeoil.com +safaat.cf +safermail.info +safersignup.de +safetymail.info +safetypost.de +saharanightstempe.com +salmeow.tk +samsclass.info +sandcars.net +sandelf.de +sandwhichvideo.com +sanfinder.com +sanim.net +sanstr.com +sast.ro +satisfyme.club +satukosong.com +sausen.com +saynotospams.com +scatmail.com +scay.net +schachrol.com +schafmail.de +schmeissweg.tk +schrott-email.de +scrsot.com +sd3.in +sdvft.com +sdvgeft.com +sdvrecft.com +secmail.pw +secretemail.de +secure-mail.biz +secure-mail.cc +secured-link.net +securehost.com.es +seekapps.com +seekjobs4u.com +sejaa.lv +selfdestructingmail.com +selfdestructingmail.org +send22u.info +sendfree.org +sendingspecialflyers.com +sendnow.win +sendspamhere.com +senseless-entertainment.com +server.ms +services391.com +sexforswingers.com +sexical.com +sexyalwasmi.top +shadap.org +shalar.net +sharedmailbox.org +sharklasers.com +sheryli.com +shhmail.com +shhuut.org +shieldedmail.com +shieldemail.com +shiftmail.com +shipfromto.com +shiphazmat.org +shipping-regulations.com +shippingterms.org +shitaway.tk +shitmail.de +shitmail.me +shitmail.org +shmeriously.com +shopxda.com +shortmail.net +shotmail.ru +showslow.de +shrib.com +shut.name +shut.ws +siberpay.com +sidelka-mytischi.ru +siftportal.ru +sify.com +sika3.com +sikux.com +siliwangi.ga +silvercoin.life +sim-simka.ru +simaenaga.com +simpleitsecurity.info +sin.cl +sinaite.net +sinema.ml +sinfiltro.cl +singlespride.com +sinnlos-mail.de +sino.tw +siteposter.net +sizzlemctwizzle.com +sjuaq.com +skeefmail.com +skrx.tk +sky-inbox.com +sky-ts.de +skyrt.de +slapsfromlastnight.com +slaskpost.se +slave-auctions.net +slippery.email +slipry.net +slopsbox.com +slothmail.net +slushmail.com +sluteen.com +sly.io +smallker.tk +smapfree24.com +smapfree24.de +smapfree24.eu +smapfree24.info +smapfree24.org +smartnator.com +smarttalent.pw +smashmail.de +smellfear.com +smellrear.com +smellypotato.tk +smtp99.com +smwg.info +snakemail.com +snapwet.com +sneakmail.de +snece.com +social-mailer.tk +socialfurry.org +sofia.re +sofimail.com +sofort-mail.de +sofortmail.de +sofrge.com +softkey-office.ru +softpls.asia +sogetthis.com +sohai.ml +sohus.cn +soioa.com +soisz.com +solar-impact.pro +solvemail.info +solventtrap.wiki +songsign.com +sonshi.cf +soodmail.com +soodomail.com +soodonims.com +soombo.com +soon.it +spacebazzar.ru +spam-be-gone.com +spam.care +spam.la +spam.org.es +spam.su +spam4.me +spamail.de +spamarrest.com +spamavert.com +spambob.com +spambob.net +spambob.org +spambog.com +spambog.de +spambog.net +spambog.ru +spambooger.com +spambox.info +spambox.me +spambox.org +spambox.us +spamcero.com +spamcon.org +spamcorptastic.com +spamcowboy.com +spamcowboy.net +spamcowboy.org +spamday.com +spamdecoy.net +spamex.com +spamfighter.cf +spamfighter.ga +spamfighter.gq +spamfighter.ml +spamfighter.tk +spamfree.eu +spamfree24.com +spamfree24.de +spamfree24.eu +spamfree24.info +spamfree24.net +spamfree24.org +spamgoes.in +spamherelots.com +spamhereplease.com +spamhole.com +spamify.com +spaminator.de +spamkill.info +spaml.com +spaml.de +spamlot.net +spammer.fail +spammotel.com +spammy.host +spamobox.com +spamoff.de +spamsalad.in +spamslicer.com +spamsphere.com +spamspot.com +spamstack.net +spamthis.co.uk +spamthis.network +spamthisplease.com +spamtrail.com +spamtrap.ro +spamtroll.net +spamwc.cf +spamwc.ga +spamwc.gq +spamwc.ml +speedgaus.net +sperma.cf +spikio.com +spindl-e.com +spoofmail.de +spr.io +spritzzone.de +spruzme.com +spybox.de +spymail.com +squizzy.de +squizzy.net +sroff.com +sry.li +ssoia.com +stanfordujjain.com +starlight-breaker.net +starpower.space +startfu.com +startkeys.com +statdvr.com +stathost.net +statiix.com +stayhome.li +steam-area.ru +steambot.net +stexsy.com +stinkefinger.net +stop-my-spam.cf +stop-my-spam.com +stop-my-spam.ga +stop-my-spam.ml +stop-my-spam.pp.ua +stop-my-spam.tk +stopspam.app +storiqax.top +storj99.com +storj99.top +streetwisemail.com +stromox.com +stuckmail.com +stuffmail.de +stumpfwerk.com +stylist-volos.ru +submic.com +suburbanthug.com +suckmyd.com +sueshaw.com +suexamplesb.com +suioe.com +super-auswahl.de +supergreatmail.com +supermailer.jp +superplatyna.com +superrito.com +supersave.net +superstachel.de +superyp.com +suremail.info +sute.jp +svip520.cn +svk.jp +svxr.org +sweetpotato.ml +sweetxxx.de +swift-mail.net +swift10minutemail.com +syinxun.com +sylvannet.com +symphonyresume.com +syosetu.gq +syujob.accountants +szerz.com +tafmail.com +tafoi.gr +taglead.com +tagmymedia.com +tagyourself.com +talkinator.com +tanukis.org +tapchicuoihoi.com +taphear.com +tapi.re +tarzanmail.cf +tastrg.com +taukah.com +tb-on-line.net +tcwlm.com +tcwlx.com +tdtda.com +tech69.com +techblast.ch +techemail.com +techgroup.me +technoproxy.ru +teerest.com +teewars.org +tefl.ro +telecomix.pl +teleg.eu +teleworm.com +teleworm.us +tellos.xyz +teml.net +temp-link.net +temp-mail.com +temp-mail.de +temp-mail.org +temp-mail.pp.ua +temp-mail.ru +temp-mails.com +tempail.com +tempalias.com +tempe-mail.com +tempemail.biz +tempemail.co.za +tempemail.com +tempemail.net +tempinbox.co.uk +tempinbox.com +tempmail.cn +tempmail.co +tempmail.de +tempmail.eu +tempmail.it +tempmail.pp.ua +tempmail.us +tempmail.ws +tempmail2.com +tempmaildemo.com +tempmailer.com +tempmailer.de +tempmailer.net +tempmailo.com +tempomail.fr +tempomail.org +temporarily.de +temporarioemail.com.br +temporary-mail.net +temporaryemail.net +temporaryemail.us +temporaryforwarding.com +temporaryinbox.com +temporarymailaddress.com +tempr.email +tempsky.com +tempthe.net +tempymail.com +tensi.org +ternaklele.ga +testore.co +testudine.com +thanksnospam.info +thankyou2010.com +thatim.info +thc.st +theaviors.com +thebearshark.com +thecarinformation.com +thechildrensfocus.com +thecity.biz +thecloudindex.com +thediamants.org +thedirhq.info +theeyeoftruth.com +thejoker5.com +thelightningmail.net +thelimestones.com +thembones.com.au +themegreview.com +themostemail.com +thereddoors.online +theroyalweb.club +thescrappermovie.com +theteastory.info +thex.ro +thichanthit.com +thietbivanphong.asia +thisisnotmyrealemail.com +thismail.net +thisurl.website +thnikka.com +thoas.ru +thraml.com +thrma.com +throam.com +thrott.com +throwam.com +throwawayemailaddress.com +throwawaymail.com +throwawaymail.pp.ua +throya.com +thrubay.com +thunderbolt.science +thunkinator.org +thxmate.com +tiapz.com +tic.ec +tilien.com +timgiarevn.com +timkassouf.com +tinoza.org +tinyurl24.com +tipsb.com +tittbit.in +tiv.cc +tizi.com +tkitc.de +tlpn.org +tmail.com +tmail.ws +tmailinator.com +tmails.net +tmmbt.net +tmpbox.net +tmpemails.com +tmpeml.com +tmpeml.info +tmpjr.me +tmpmail.net +tmpmail.org +tmpx.sa.com +toddsbighug.com +tofeat.com +toiea.com +tokem.co +tokenmail.de +tonaeto.com +tonne.to +tonymanso.com +toomail.biz +toon.ml +top-shop-tovar.ru +top101.de +top1mail.ru +top1post.ru +topinrock.cf +topmail2.com +topmail2.net +topofertasdehoy.com +topranklist.de +toprumours.com +tormail.org +toss.pw +tosunkaya.com +totallynotfake.net +totalvista.com +totesmail.com +totoan.info +tourcc.com +tp-qa-mail.com +tpwlb.com +tqoai.com +tqosi.com +tradermail.info +tranceversal.com +trash-amil.com +trash-mail.at +trash-mail.cf +trash-mail.com +trash-mail.de +trash-mail.ga +trash-mail.gq +trash-mail.ml +trash-mail.tk +trash-me.com +trash2009.com +trash2010.com +trash2011.com +trashcanmail.com +trashdevil.com +trashdevil.de +trashemail.de +trashemails.de +trashinbox.com +trashmail.at +trashmail.com +trashmail.de +trashmail.gq +trashmail.io +trashmail.me +trashmail.net +trashmail.org +trashmail.ws +trashmailer.com +trashmailgenerator.de +trashmails.com +trashymail.com +trashymail.net +trasz.com +trayna.com +trbvm.com +trbvn.com +trbvo.com +trend-maker.ru +trgfu.com +trgovinanaveliko.info +trialmail.de +trickmail.net +trillianpro.com +triots.com +trixtrux1.ru +trollproject.com +tropicalbass.info +trungtamtoeic.com +truthfinderlogin.com +tryalert.com +tryninja.io +tryzoe.com +ttirv.org +ttszuo.xyz +tualias.com +tuofs.com +turoid.com +turual.com +turuma.com +tutuapp.bid +tvchd.com +tverya.com +twinmail.de +twkly.ml +twocowmail.net +twoweirdtricks.com +twzhhq.online +txen.de +txtadvertise.com +tyhe.ro +tyldd.com +tympe.net +uacro.com +uber-mail.com +ubismail.net +ubm.md +ucche.us +ucupdong.ml +uemail99.com +ufacturing.com +uggsrock.com +uguuchantele.com +uhe2.com +uhhu.ru +uiu.us +ujijima1129.gq +uk.to +ultra.fyi +ultrada.ru +uma3.be +umail.net +undo.it +unicodeworld.com +unids.com +unimark.org +unit7lahaina.com +unmail.ru +uooos.com +upliftnow.com +uplipht.com +uploadnolimit.com +upozowac.info +urfunktion.se +urhen.com +uroid.com +us.af +us.to +usa.cc +usako.net +usbc.be +used-product.fr +ushijima1129.cf +ushijima1129.ga +ushijima1129.gq +ushijima1129.ml +ushijima1129.tk +utiket.us +uu.gl +uu2.ovh +uuf.me +uwork4.us +uyhip.com +vaasfc4.tk +vaati.org +valemail.net +valhalladev.com +vankin.de +vctel.com +vda.ro +vddaz.com +vdig.com +veanlo.com +vemomail.win +venompen.com +veo.kr +ver0.cf +ver0.ga +ver0.gq +ver0.ml +ver0.tk +vercelli.cf +vercelli.ga +vercelli.gq +vercelli.ml +verdejo.com +vermutlich.net +veryday.ch +veryday.eu +veryday.info +veryrealemail.com +vesa.pw +vevs.de +vfemail.net +via.tokyo.jp +vickaentb.tk +victime.ninja +victoriantwins.com +vidchart.com +viditag.com +viewcastmedia.com +viewcastmedia.net +viewcastmedia.org +vikingsonly.com +vinernet.com +vintomaper.com +vipepe.com +vipmail.name +vipmail.pw +vipxm.net +viralplays.com +virtualemail.info +visal007.tk +visal168.cf +visal168.ga +visal168.gq +visal168.ml +visal168.tk +vixletdev.com +vixtricks.com +vkcode.ru +vmailing.info +vmani.com +vmpanda.com +vnedu.me +voidbay.com +volaj.com +voltaer.com +vomoto.com +vorga.org +votiputox.org +voxelcore.com +vpn.st +vps30.com +vps911.net +vradportal.com +vremonte24-store.ru +vrmtr.com +vsimcard.com +vssms.com +vtxmail.us +vubby.com +vuiy.pw +vusra.com +vztc.com +w-asertun.ru +w3internet.co.uk +wakingupesther.com +walala.org +walkmail.net +walkmail.ru +wallm.com +wanko.be +watch-harry-potter.com +watchever.biz +watchfull.net +watchironman3onlinefreefullmovie.com +wazabi.club +wbdev.tech +wbml.net +web-contact.info +web-ideal.fr +web-inc.net +web-mail.pp.ua +web2mailco.com +webcontact-france.eu +webemail.me +webhook.site +webm4il.info +webmail24.top +webtrip.ch +webuser.in +wee.my +wef.gr +weg-werf-email.de +wegwerf-email-addressen.de +wegwerf-email-adressen.de +wegwerf-email.at +wegwerf-email.de +wegwerf-email.net +wegwerf-emails.de +wegwerfadresse.de +wegwerfemail.com +wegwerfemail.de +wegwerfemail.info +wegwerfemail.net +wegwerfemail.org +wegwerfemailadresse.com +wegwerfmail.de +wegwerfmail.info +wegwerfmail.net +wegwerfmail.org +wegwerpmailadres.nl +wegwrfmail.de +wegwrfmail.net +wegwrfmail.org +wekawa.com +welikecookies.com +wellsfargocomcardholders.com +wemel.top +wetrainbayarea.com +wetrainbayarea.org +wfgdfhj.tk +wg0.com +wh4f.org +whatiaas.com +whatifanalytics.com +whatpaas.com +whatsaas.com +whiffles.org +whopy.com +whyspam.me +wibblesmith.com +wickmail.net +widaryanto.info +widget.gg +wierie.tk +wifimaple.com +wifioak.com +wikidocuslava.ru +wilemail.com +willhackforfood.biz +willselfdestruct.com +wimsg.com +winemaven.info +wins.com.br +wlist.ro +wmail.cf +wmail.club +wokcy.com +wolfmail.ml +wolfsmail.tk +wollan.info +worldspace.link +wpdork.com +wpg.im +wralawfirm.com +writeme.us +wronghead.com +ws.gy +wsym.de +wudet.men +wuespdj.xyz +wupics.com +wuuvo.com +wuzup.net +wuzupmail.net +wwjmp.com +wwwnew.eu +wxnw.net +x24.com +xagloo.co +xagloo.com +xbaby69.top +xcode.ro +xcodes.net +xcompress.com +xcoxc.com +xcpy.com +xemaps.com +xemne.com +xents.com +xjoi.com +xkx.me +xl.cx +xmail.com +xmailer.be +xmaily.com +xn--9kq967o.com +xn--d-bga.net +xojxe.com +xost.us +xoxox.cc +xperiae5.com +xrap.de +xrho.com +xvx.us +xwaretech.com +xwaretech.info +xwaretech.net +xww.ro +xxhamsterxx.ga +xxi2.com +xxlocanto.us +xxolocanto.us +xxqx3802.com +xy9ce.tk +xyzfree.net +xzsok.com +yabai-oppai.tk +yahmail.top +yahooproduct.net +yamail.win +yanet.me +yannmail.win +yapped.net +yaqp.com +yarnpedia.ga +ycare.de +ycn.ro +ye.vc +yedi.org +yeezus.ru +yep.it +yermail.net +yhg.biz +ynmrealty.com +yodx.ro +yogamaven.com +yoggm.com +yomail.info +yoo.ro +yopmail.com +yopmail.fr +yopmail.gq +yopmail.net +yopmail.pp.ua +yordanmail.cf +you-spam.com +yougotgoated.com +youmail.ga +youmailr.com +youneedmore.info +youpymail.com +yourdomain.com +youremail.cf +yourewronghereswhy.com +yourlms.biz +yourspamgoesto.space +yourtube.ml +yroid.com +yspend.com +ytpayy.com +yugasandrika.com +yui.it +yuoia.com +yuurok.com +yxzx.net +yyolf.net +z-o-e-v-a.ru +z0d.eu +z1p.biz +z86.ru +zain.site +zainmax.net +zaktouni.fr +zarabotokdoma11.ru +zasod.com +zaym-zaym.ru +zcrcd.com +zdenka.net +ze.tc +zebins.com +zebins.eu +zehnminuten.de +zehnminutenmail.de +zepp.dk +zetmail.com +zfymail.com +zhaoqian.ninja +zhaoyuanedu.cn +zhcne.com +zhewei88.com +zhorachu.com +zik.dj +zipcad.com +zipo1.gq +zippymail.info +zipsendtest.com +zoaxe.com +zoemail.com +zoemail.net +zoemail.org +zoetropes.org +zombie-hive.com +zomg.info +zsero.com +zumpul.com +zv68.com +zxcv.com +zxcvbnm.com +zymuying.com +zzi.us +zzrgg.com +zzz.com \ No newline at end of file diff --git a/backend/src/helpers/nodemailer.ts b/backend/src/helpers/nodemailer.ts index 386db2c392..78e8a12a03 100644 --- a/backend/src/helpers/nodemailer.ts +++ b/backend/src/helpers/nodemailer.ts @@ -34,12 +34,13 @@ const sendMail = async ({ const temp = handlebars.compile(html); const htmlToSend = temp(substitutions); - await smtpTransporter.sendMail({ + const x = await smtpTransporter.sendMail({ from: `"${await getSmtpFromName()}" <${await getSmtpFromAddress()}>`, to: recipients.join(', '), subject: subjectLine, html: htmlToSend }); + } catch (err) { Sentry.setUser(null); Sentry.captureException(err); diff --git a/backend/src/helpers/rateLimiter.ts b/backend/src/helpers/rateLimiter.ts index 073ba11bc5..b082bceb3c 100644 --- a/backend/src/helpers/rateLimiter.ts +++ b/backend/src/helpers/rateLimiter.ts @@ -8,9 +8,6 @@ const apiLimiter = rateLimit({ legacyHeaders: false, skip: (request) => { return request.path === '/healthcheck' || request.path === '/api/status' - }, - keyGenerator: (req, res) => { - return req.clientIp } }); @@ -19,10 +16,7 @@ const authLimit = rateLimit({ windowMs: 60 * 1000, max: 10, standardHeaders: true, - legacyHeaders: false, - keyGenerator: (req, res) => { - return req.clientIp - } + legacyHeaders: false }); // 10 requests per hour @@ -30,10 +24,7 @@ const passwordLimiter = rateLimit({ windowMs: 60 * 60 * 1000, max: 10, standardHeaders: true, - legacyHeaders: false, - keyGenerator: (req, res) => { - return req.clientIp - } + legacyHeaders: false }); const authLimiter = (req: any, res: any, next: any) => { diff --git a/backend/src/index.ts b/backend/src/index.ts index 2e230a66ca..8bd4dd96eb 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -13,7 +13,6 @@ import swaggerUi = require('swagger-ui-express'); // eslint-disable-next-line @typescript-eslint/no-var-requires const swaggerFile = require('../spec.json'); // eslint-disable-next-line @typescript-eslint/no-var-requires -const requestIp = require('request-ip'); import { apiLimiter } from './helpers/rateLimiter'; import { workspace as eeWorkspaceRouter, @@ -88,8 +87,6 @@ const main = async () => { }) ); - app.use(requestIp.mw()); - if ((await getNodeEnv()) === 'production') { // enable app-wide rate-limiting + helmet security // in production diff --git a/backend/src/models/user.ts b/backend/src/models/user.ts index 545c5256d5..4059ce8e0c 100644 --- a/backend/src/models/user.ts +++ b/backend/src/models/user.ts @@ -94,7 +94,8 @@ const userSchema = new Schema( ip: String, userAgent: String }], - default: [] + default: [], + select: false } }, { diff --git a/backend/src/routes/v2/auth.ts b/backend/src/routes/v2/auth.ts index 128a08faee..288004e657 100644 --- a/backend/src/routes/v2/auth.ts +++ b/backend/src/routes/v2/auth.ts @@ -26,7 +26,7 @@ router.post( router.post( '/mfa/send', authLimiter, - body('email').isString().trim().notEmpty(), + body('email').isString().trim().notEmpty().isEmail(), validateRequest, authController.sendMfaToken ); diff --git a/backend/src/validation/index.ts b/backend/src/validation/index.ts index 84f25bb742..177c716ea3 100644 --- a/backend/src/validation/index.ts +++ b/backend/src/validation/index.ts @@ -1,3 +1,4 @@ +export * from './user'; export * from './workspace'; export * from './bot'; export * from './integration'; diff --git a/backend/src/validation/user.ts b/backend/src/validation/user.ts index 1329adfc4b..4c412e52f3 100644 --- a/backend/src/validation/user.ts +++ b/backend/src/validation/user.ts @@ -1,3 +1,5 @@ +import fs from 'fs'; +import path from 'path'; import { Types } from 'mongoose'; import { IUser, @@ -8,7 +10,7 @@ import { } from '../models'; import { validateMembership } from '../helpers/membership'; import _ from 'lodash'; -import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; +import { BadRequestError, UnauthorizedRequestError, ValidationError } from '../utils/errors'; import { validateMembershipOrg } from '../helpers/membershipOrg'; @@ -17,6 +19,22 @@ import { PERMISSION_WRITE_SECRETS } from '../variables'; +/** + * Validate that email [email] is not disposable + * @param email - email to validate + */ +export const validateUserEmail = (email: string) => { + const emailDomain = email.split('@')[1]; + const disposableEmails = fs.readFileSync( + path.resolve(__dirname, '../data/' + 'disposable_emails.txt'), + 'utf8' + ).split('\n'); + + if (disposableEmails.includes(emailDomain)) throw ValidationError({ + message: 'Failed to validate email as non-disposable' + }); +} + /** * Validate that user (client) can access workspace * with id [workspaceId] and its environment [environment] with required permissions From bfee0a6d304c34cb28af4cd62e3215df07bfb6db Mon Sep 17 00:00:00 2001 From: Spelchure Date: Fri, 2 Jun 2023 20:51:09 +0300 Subject: [PATCH 02/19] feat: remove try-catch blocks for handling errors in middleware --- backend/src/controllers/v1/authController.ts | 298 +-- backend/src/controllers/v1/botController.ts | 116 +- .../v1/integrationAuthController.ts | 196 +- .../controllers/v1/integrationController.ts | 181 +- backend/src/controllers/v1/keyController.ts | 72 +- .../controllers/v1/membershipController.ts | 261 +- .../controllers/v1/membershipOrgController.ts | 344 ++- .../controllers/v1/organizationController.ts | 314 +-- .../src/controllers/v1/passwordController.ts | 476 ++-- .../src/controllers/v1/secretController.ts | 278 +- .../src/controllers/v1/signupController.ts | 112 +- .../src/controllers/v1/stripeController.ts | 30 +- .../controllers/v1/userActionController.ts | 53 +- .../src/controllers/v1/workspaceController.ts | 250 +- .../controllers/v2/apiKeyDataController.ts | 88 +- backend/src/controllers/v2/authController.ts | 341 ++- .../controllers/v2/environmentController.ts | 252 +- .../controllers/v2/organizationsController.ts | 70 +- .../v2/serviceTokenDataController.ts | 3 +- .../src/controllers/v2/signupController.ts | 403 ++- backend/src/controllers/v2/tagController.ts | 3 +- backend/src/controllers/v2/usersController.ts | 72 +- .../src/controllers/v2/workspaceController.ts | 352 +-- .../src/ee/controllers/v1/actionController.ts | 3 +- .../controllers/v1/cloudProductsController.ts | 20 +- .../src/ee/controllers/v1/secretController.ts | 173 +- .../v1/secretSnapshotController.ts | 22 +- .../src/ee/controllers/v1/stripeController.ts | 30 +- .../ee/controllers/v1/workspaceController.ts | 683 +++-- backend/src/ee/services/EELicenseService.ts | 47 +- backend/src/helpers/integration.ts | 393 ++- backend/src/helpers/membership.ts | 93 +- backend/src/helpers/nodemailer.ts | 30 +- backend/src/helpers/signup.ts | 51 +- backend/src/helpers/workspace.ts | 74 +- backend/src/integrations/sync.ts | 2250 ++++++++--------- 36 files changed, 3681 insertions(+), 4753 deletions(-) diff --git a/backend/src/controllers/v1/authController.ts b/backend/src/controllers/v1/authController.ts index c1725456cf..eef1c9ad3f 100644 --- a/backend/src/controllers/v1/authController.ts +++ b/backend/src/controllers/v1/authController.ts @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/node'; import { Request, Response } from 'express'; import jwt from 'jsonwebtoken'; import * as bigintConversion from 'bigint-conversion'; @@ -34,47 +33,39 @@ declare module 'jsonwebtoken' { * @returns */ export const login1 = async (req: Request, res: Response) => { - try { - const { - email, - clientPublicKey - }: { email: string; clientPublicKey: string } = req.body; + const { + email, + clientPublicKey + }: { email: string; clientPublicKey: string } = req.body; - const user = await User.findOne({ - email - }).select('+salt +verifier'); + const user = await User.findOne({ + email + }).select('+salt +verifier'); - if (!user) throw new Error('Failed to find user'); + if (!user) throw new Error('Failed to find user'); - const server = new jsrp.server(); - server.init( - { - salt: user.salt, - verifier: user.verifier - }, - async () => { - // generate server-side public key - const serverPublicKey = server.getPublicKey(); + const server = new jsrp.server(); + server.init( + { + salt: user.salt, + verifier: user.verifier + }, + async () => { + // generate server-side public key + const serverPublicKey = server.getPublicKey(); - await LoginSRPDetail.findOneAndReplace({ email: email }, { - email: email, - clientPublicKey: clientPublicKey, - serverBInt: bigintConversion.bigintToBuf(server.bInt), - }, { upsert: true, returnNewDocument: false }) + await LoginSRPDetail.findOneAndReplace({ email: email }, { + email: email, + clientPublicKey: clientPublicKey, + serverBInt: bigintConversion.bigintToBuf(server.bInt), + }, { upsert: true, returnNewDocument: false }) - return res.status(200).send({ - serverPublicKey, - salt: user.salt - }); - } - ); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to start authentication process' - }); - } + return res.status(200).send({ + serverPublicKey, + salt: user.salt + }); + } + ); }; /** @@ -85,84 +76,76 @@ export const login1 = async (req: Request, res: Response) => { * @returns */ export const login2 = async (req: Request, res: Response) => { - try { - const { email, clientProof } = req.body; - const user = await User.findOne({ - email - }).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag'); + const { email, clientProof } = req.body; + const user = await User.findOne({ + email + }).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag'); - if (!user) throw new Error('Failed to find user'); + if (!user) throw new Error('Failed to find user'); - const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: email }) + const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: email }) - if (!loginSRPDetailFromDB) { - return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again")) - } + if (!loginSRPDetailFromDB) { + return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again")) + } - const server = new jsrp.server(); - server.init( - { - salt: user.salt, - verifier: user.verifier, - b: loginSRPDetailFromDB.serverBInt - }, - async () => { - server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey); + const server = new jsrp.server(); + server.init( + { + salt: user.salt, + verifier: user.verifier, + b: loginSRPDetailFromDB.serverBInt + }, + async () => { + server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey); - // compare server and client shared keys - if (server.checkClientProof(clientProof)) { - // issue tokens + // compare server and client shared keys + if (server.checkClientProof(clientProof)) { + // issue tokens - await checkUserDevice({ - user, - ip: req.ip, - userAgent: req.headers['user-agent'] ?? '' - }); + await checkUserDevice({ + user, + ip: req.ip, + userAgent: req.headers['user-agent'] ?? '' + }); - const tokens = await issueAuthTokens({ userId: user._id.toString() }); + const tokens = await issueAuthTokens({ userId: user._id.toString() }); - // store (refresh) token in httpOnly cookie - res.cookie('jid', tokens.refreshToken, { - httpOnly: true, - path: '/', - sameSite: 'strict', - secure: await getHttpsEnabled() - }); + // store (refresh) token in httpOnly cookie + res.cookie('jid', tokens.refreshToken, { + httpOnly: true, + path: '/', + sameSite: 'strict', + secure: await getHttpsEnabled() + }); - const loginAction = await EELogService.createAction({ - name: ACTION_LOGIN, - userId: user._id - }); + const loginAction = await EELogService.createAction({ + name: ACTION_LOGIN, + userId: user._id + }); - loginAction && await EELogService.createLog({ - userId: user._id, - actions: [loginAction], - channel: getChannelFromUserAgent(req.headers['user-agent']), - ipAddress: req.ip - }); + loginAction && await EELogService.createLog({ + userId: user._id, + actions: [loginAction], + channel: getChannelFromUserAgent(req.headers['user-agent']), + ipAddress: req.ip + }); - // return (access) token in response - return res.status(200).send({ - token: tokens.token, - publicKey: user.publicKey, - encryptedPrivateKey: user.encryptedPrivateKey, - iv: user.iv, - tag: user.tag - }); - } - - return res.status(400).send({ - message: 'Failed to authenticate. Try again?' + // return (access) token in response + return res.status(200).send({ + token: tokens.token, + publicKey: user.publicKey, + encryptedPrivateKey: user.encryptedPrivateKey, + iv: user.iv, + tag: user.tag }); } - ); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to authenticate. Try again?' - }); - } + + return res.status(400).send({ + message: 'Failed to authenticate. Try again?' + }); + } + ); }; /** @@ -172,38 +155,29 @@ export const login2 = async (req: Request, res: Response) => { * @returns */ export const logout = async (req: Request, res: Response) => { - try { - await clearTokens({ - userId: req.user._id.toString() - }); + await clearTokens({ + userId: req.user._id.toString() + }); - // clear httpOnly cookie - res.cookie('jid', '', { - httpOnly: true, - path: '/', - sameSite: 'strict', - secure: (await getHttpsEnabled()) as boolean - }); + // clear httpOnly cookie + res.cookie('jid', '', { + httpOnly: true, + path: '/', + sameSite: 'strict', + secure: (await getHttpsEnabled()) as boolean + }); - const logoutAction = await EELogService.createAction({ - name: ACTION_LOGOUT, - userId: req.user._id - }); + const logoutAction = await EELogService.createAction({ + name: ACTION_LOGOUT, + userId: req.user._id + }); - logoutAction && await EELogService.createLog({ - userId: req.user._id, - actions: [logoutAction], - channel: getChannelFromUserAgent(req.headers['user-agent']), - ipAddress: req.ip - }); - - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to logout' - }); - } + logoutAction && await EELogService.createLog({ + userId: req.user._id, + actions: [logoutAction], + channel: getChannelFromUserAgent(req.headers['user-agent']), + ipAddress: req.ip + }); return res.status(200).send({ message: 'Successfully logged out.' @@ -229,43 +203,35 @@ export const checkAuth = async (req: Request, res: Response) => { * @returns */ export const getNewToken = async (req: Request, res: Response) => { - try { - const refreshToken = req.cookies.jid; + const refreshToken = req.cookies.jid; - if (!refreshToken) { - throw new Error('Failed to find token in request cookies'); - } - - const decodedToken = ( - jwt.verify(refreshToken, await getJwtRefreshSecret()) - ); - - const user = await User.findOne({ - _id: decodedToken.userId - }).select('+publicKey'); - - if (!user) throw new Error('Failed to authenticate unfound user'); - if (!user?.publicKey) - throw new Error('Failed to authenticate not fully set up account'); - - const token = createToken({ - payload: { - userId: decodedToken.userId - }, - expiresIn: await getJwtAuthLifetime(), - secret: await getJwtAuthSecret() - }); - - return res.status(200).send({ - token - }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Invalid request' - }); + if (!refreshToken) { + throw new Error('Failed to find token in request cookies'); } + + const decodedToken = ( + jwt.verify(refreshToken, await getJwtRefreshSecret()) + ); + + const user = await User.findOne({ + _id: decodedToken.userId + }).select('+publicKey'); + + if (!user) throw new Error('Failed to authenticate unfound user'); + if (!user?.publicKey) + throw new Error('Failed to authenticate not fully set up account'); + + const token = createToken({ + payload: { + userId: decodedToken.userId + }, + expiresIn: await getJwtAuthLifetime(), + secret: await getJwtAuthSecret() + }); + + return res.status(200).send({ + token + }); }; export const handleAuthProviderCallback = (req: Request, res: Response) => { diff --git a/backend/src/controllers/v1/botController.ts b/backend/src/controllers/v1/botController.ts index 0fdce2cd9f..f155f58a5e 100644 --- a/backend/src/controllers/v1/botController.ts +++ b/backend/src/controllers/v1/botController.ts @@ -1,6 +1,5 @@ import { Request, Response } from 'express'; import { Types } from 'mongoose'; -import * as Sentry from '@sentry/node'; import { Bot, BotKey } from '../../models'; import { createBot } from '../../helpers/bot'; @@ -17,33 +16,24 @@ interface BotKey { * @returns */ export const getBotByWorkspaceId = async (req: Request, res: Response) => { - let bot; - try { - const { workspaceId } = req.params; + const { workspaceId } = req.params; - bot = await Bot.findOne({ - workspace: workspaceId - }); - - if (!bot) { - // case: bot doesn't exist for workspace with id [workspaceId] - // -> create a new bot and return it - bot = await createBot({ - name: 'Infisical Bot', - workspaceId: new Types.ObjectId(workspaceId) - }); - } - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get bot for workspace' - }); - } + let bot = await Bot.findOne({ + workspace: workspaceId + }); + + if (!bot) { + // case: bot doesn't exist for workspace with id [workspaceId] + // -> create a new bot and return it + bot = await createBot({ + name: 'Infisical Bot', + workspaceId: new Types.ObjectId(workspaceId) + }); + } - return res.status(200).send({ - bot - }); + return res.status(200).send({ + bot + }); }; /** @@ -53,54 +43,44 @@ export const getBotByWorkspaceId = async (req: Request, res: Response) => { * @returns */ export const setBotActiveState = async (req: Request, res: Response) => { - let bot; - try { - const { isActive, botKey }: { isActive: boolean, botKey: BotKey } = req.body; - - if (isActive) { - // bot state set to active -> share workspace key with bot - if (!botKey?.encryptedKey || !botKey?.nonce) { - return res.status(400).send({ - message: 'Failed to set bot state to active - missing bot key' - }); - } - - await BotKey.findOneAndUpdate({ - workspace: req.bot.workspace - }, { - encryptedKey: botKey.encryptedKey, - nonce: botKey.nonce, - sender: req.user._id, - bot: req.bot._id, - workspace: req.bot.workspace - }, { - upsert: true, - new: true - }); - } else { - // case: bot state set to inactive -> delete bot's workspace key - await BotKey.deleteOne({ - bot: req.bot._id + const { isActive, botKey }: { isActive: boolean, botKey: BotKey } = req.body; + + if (isActive) { + // bot state set to active -> share workspace key with bot + if (!botKey?.encryptedKey || !botKey?.nonce) { + return res.status(400).send({ + message: 'Failed to set bot state to active - missing bot key' }); } - - bot = await Bot.findOneAndUpdate({ - _id: req.bot._id + + await BotKey.findOneAndUpdate({ + workspace: req.bot.workspace }, { - isActive + encryptedKey: botKey.encryptedKey, + nonce: botKey.nonce, + sender: req.user._id, + bot: req.bot._id, + workspace: req.bot.workspace }, { + upsert: true, new: true }); - - if (!bot) throw new Error('Failed to update bot active state'); - - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to update bot active state' - }); - } + } else { + // case: bot state set to inactive -> delete bot's workspace key + await BotKey.deleteOne({ + bot: req.bot._id + }); + } + + let bot = await Bot.findOneAndUpdate({ + _id: req.bot._id + }, { + isActive + }, { + new: true + }); + + if (!bot) throw new Error('Failed to update bot active state'); return res.status(200).send({ bot diff --git a/backend/src/controllers/v1/integrationAuthController.ts b/backend/src/controllers/v1/integrationAuthController.ts index 97032aaa4d..a002187c82 100644 --- a/backend/src/controllers/v1/integrationAuthController.ts +++ b/backend/src/controllers/v1/integrationAuthController.ts @@ -1,6 +1,5 @@ import { Request, Response } from 'express'; import { Types } from 'mongoose'; -import * as Sentry from '@sentry/node'; import { IntegrationAuth, Bot @@ -22,22 +21,13 @@ import { standardRequest } from '../../config/request'; * Return integration authorization with id [integrationAuthId] */ export const getIntegrationAuth = async (req: Request, res: Response) => { - let integrationAuth; - try { - const { integrationAuthId } = req.params; - integrationAuth = await IntegrationAuth.findById(integrationAuthId); - - if (!integrationAuth) return res.status(400).send({ - message: 'Failed to find integration authorization' - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get integration authorization' - }); - } - + const { integrationAuthId } = req.params; + const integrationAuth = await IntegrationAuth.findById(integrationAuthId); + + if (!integrationAuth) return res.status(400).send({ + message: 'Failed to find integration authorization' + }); + return res.status(200).send({ integrationAuth }); @@ -61,33 +51,25 @@ export const oAuthExchange = async ( req: Request, res: Response ) => { - try { - const { workspaceId, code, integration } = req.body; - if (!INTEGRATION_SET.has(integration)) - throw new Error('Failed to validate integration'); - - const environments = req.membership.workspace?.environments || []; - if(environments.length === 0){ - throw new Error("Failed to get environments") - } - - const integrationAuth = await IntegrationService.handleOAuthExchange({ - workspaceId, - integration, - code, - environment: environments[0].slug, - }); - - return res.status(200).send({ - integrationAuth - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get OAuth2 code-token exchange' - }); - } + const { workspaceId, code, integration } = req.body; + if (!INTEGRATION_SET.has(integration)) + throw new Error('Failed to validate integration'); + + const environments = req.membership.workspace?.environments || []; + if(environments.length === 0){ + throw new Error("Failed to get environments") + } + + const integrationAuth = await IntegrationService.handleOAuthExchange({ + workspaceId, + integration, + code, + environment: environments[0].slug, + }); + + return res.status(200).send({ + integrationAuth + }); }; /** @@ -104,55 +86,47 @@ export const saveIntegrationAccessToken = async ( // TODO: check if access token is valid for each integration let integrationAuth; - try { - const { - workspaceId, - accessId, - accessToken, - integration - }: { - workspaceId: string; - accessId: string | null; - accessToken: string; - integration: string; - } = req.body; + const { + workspaceId, + accessId, + accessToken, + integration + }: { + workspaceId: string; + accessId: string | null; + accessToken: string; + integration: string; + } = req.body; - const bot = await Bot.findOne({ - workspace: new Types.ObjectId(workspaceId), - isActive: true - }); - - if (!bot) throw new Error('Bot must be enabled to save integration access token'); + const bot = await Bot.findOne({ + workspace: new Types.ObjectId(workspaceId), + isActive: true + }); + + if (!bot) throw new Error('Bot must be enabled to save integration access token'); - integrationAuth = await IntegrationAuth.findOneAndUpdate({ - workspace: new Types.ObjectId(workspaceId), - integration - }, { - workspace: new Types.ObjectId(workspaceId), - integration, - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 - }, { - new: true, - upsert: true - }); - - // encrypt and save integration access details - integrationAuth = await IntegrationService.setIntegrationAuthAccess({ - integrationAuthId: integrationAuth._id.toString(), - accessId, - accessToken, - accessExpiresAt: undefined - }); - - if (!integrationAuth) throw new Error('Failed to save integration access token'); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to save access token for integration' - }); - } + integrationAuth = await IntegrationAuth.findOneAndUpdate({ + workspace: new Types.ObjectId(workspaceId), + integration + }, { + workspace: new Types.ObjectId(workspaceId), + integration, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8 + }, { + new: true, + upsert: true + }); + + // encrypt and save integration access details + integrationAuth = await IntegrationService.setIntegrationAuthAccess({ + integrationAuthId: integrationAuth._id.toString(), + accessId, + accessToken, + accessExpiresAt: undefined + }); + + if (!integrationAuth) throw new Error('Failed to save integration access token'); return res.status(200).send({ integrationAuth @@ -166,22 +140,13 @@ export const saveIntegrationAccessToken = async ( * @returns */ export const getIntegrationAuthApps = async (req: Request, res: Response) => { - let apps; - try { - const teamId = req.query.teamId as string; - - apps = await getApps({ - integrationAuth: req.integrationAuth, - accessToken: req.accessToken, - ...teamId && { teamId } - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: "Failed to get integration authorization applications", - }); - } + const teamId = req.query.teamId as string; + + const apps = await getApps({ + integrationAuth: req.integrationAuth, + accessToken: req.accessToken, + ...teamId && { teamId } + }); return res.status(200).send({ apps @@ -402,19 +367,10 @@ export const getIntegrationAuthRailwayServices = async (req: Request, res: Respo * @returns */ export const deleteIntegrationAuth = async (req: Request, res: Response) => { - let integrationAuth; - try { - integrationAuth = await revokeAccess({ - integrationAuth: req.integrationAuth, - accessToken: req.accessToken, - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: "Failed to delete integration authorization", - }); - } + const integrationAuth = await revokeAccess({ + integrationAuth: req.integrationAuth, + accessToken: req.accessToken, + }); return res.status(200).send({ integrationAuth, diff --git a/backend/src/controllers/v1/integrationController.ts b/backend/src/controllers/v1/integrationController.ts index 060f89b339..83f7c784a4 100644 --- a/backend/src/controllers/v1/integrationController.ts +++ b/backend/src/controllers/v1/integrationController.ts @@ -1,6 +1,5 @@ import { Request, Response } from 'express'; import { Types } from 'mongoose'; -import * as Sentry from '@sentry/node'; import { Integration } from '../../models'; @@ -14,62 +13,50 @@ import { eventPushSecrets } from '../../events'; * @returns */ export const createIntegration = async (req: Request, res: Response) => { - let integration; - - try { - const { - integrationAuthId, - app, - appId, - isActive, - sourceEnvironment, - targetEnvironment, - targetEnvironmentId, - targetService, - targetServiceId, - owner, - path, - region - } = req.body; - - // TODO: validate [sourceEnvironment] and [targetEnvironment] - - // initialize new integration after saving integration access token - integration = await new Integration({ - workspace: req.integrationAuth.workspace._id, - environment: sourceEnvironment, - isActive, - app, - appId, - targetEnvironment, - targetEnvironmentId, - targetService, - targetServiceId, - owner, - path, - region, - integration: req.integrationAuth.integration, - integrationAuth: new Types.ObjectId(integrationAuthId) - }).save(); - - if (integration) { - // trigger event - push secrets - EventService.handleEvent({ - event: eventPushSecrets({ - workspaceId: integration.workspace, - environment: sourceEnvironment - }) - }); - } - - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to create integration' - }); - } + const { + integrationAuthId, + app, + appId, + isActive, + sourceEnvironment, + targetEnvironment, + targetEnvironmentId, + targetService, + targetServiceId, + owner, + path, + region + } = req.body; + + // TODO: validate [sourceEnvironment] and [targetEnvironment] + // initialize new integration after saving integration access token + const integration = await new Integration({ + workspace: req.integrationAuth.workspace._id, + environment: sourceEnvironment, + isActive, + app, + appId, + targetEnvironment, + targetEnvironmentId, + targetService, + targetServiceId, + owner, + path, + region, + integration: req.integrationAuth.integration, + integrationAuth: new Types.ObjectId(integrationAuthId) + }).save(); + + if (integration) { + // trigger event - push secrets + EventService.handleEvent({ + event: eventPushSecrets({ + workspaceId: integration.workspace, + environment: sourceEnvironment + }) + }); + } return res.status(200).send({ integration, }); @@ -82,52 +69,43 @@ export const createIntegration = async (req: Request, res: Response) => { * @returns */ export const updateIntegration = async (req: Request, res: Response) => { - let integration; // TODO: add integration-specific validation to ensure that each // integration has the correct fields populated in [Integration] - try { - const { + const { + environment, + isActive, + app, + appId, + targetEnvironment, + owner, // github-specific integration param + } = req.body; + + const integration = await Integration.findOneAndUpdate( + { + _id: req.integration._id, + }, + { environment, isActive, app, appId, targetEnvironment, - owner, // github-specific integration param - } = req.body; - - integration = await Integration.findOneAndUpdate( - { - _id: req.integration._id, - }, - { - environment, - isActive, - app, - appId, - targetEnvironment, - owner, - }, - { - new: true, - } - ); - - if (integration) { - // trigger event - push secrets - EventService.handleEvent({ - event: eventPushSecrets({ - workspaceId: integration.workspace, - environment - }), - }); + owner, + }, + { + new: true, } - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: "Failed to update integration", + ); + + if (integration) { + // trigger event - push secrets + EventService.handleEvent({ + event: eventPushSecrets({ + workspaceId: integration.workspace, + environment + }), }); } @@ -144,22 +122,13 @@ export const updateIntegration = async (req: Request, res: Response) => { * @returns */ export const deleteIntegration = async (req: Request, res: Response) => { - let integration; - try { - const { integrationId } = req.params; + const { integrationId } = req.params; - integration = await Integration.findOneAndDelete({ - _id: integrationId, - }); + const integration = await Integration.findOneAndDelete({ + _id: integrationId, + }); - if (!integration) throw new Error("Failed to find integration"); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: "Failed to delete integration", - }); - } + if (!integration) throw new Error("Failed to find integration"); return res.status(200).send({ integration, diff --git a/backend/src/controllers/v1/keyController.ts b/backend/src/controllers/v1/keyController.ts index ffcb16d9cc..beb4d0d073 100644 --- a/backend/src/controllers/v1/keyController.ts +++ b/backend/src/controllers/v1/keyController.ts @@ -1,5 +1,4 @@ import { Request, Response } from 'express'; -import * as Sentry from '@sentry/node'; import { Key } from '../../models'; import { findMembership } from '../../helpers/membership'; @@ -11,34 +10,26 @@ import { findMembership } from '../../helpers/membership'; * @returns */ export const uploadKey = async (req: Request, res: Response) => { - try { - const { workspaceId } = req.params; - const { key } = req.body; + const { workspaceId } = req.params; + const { key } = req.body; - // validate membership of receiver - const receiverMembership = await findMembership({ - user: key.userId, - workspace: workspaceId - }); + // validate membership of receiver + const receiverMembership = await findMembership({ + user: key.userId, + workspace: workspaceId + }); - if (!receiverMembership) { - throw new Error('Failed receiver membership validation for workspace'); - } + if (!receiverMembership) { + throw new Error('Failed receiver membership validation for workspace'); + } - await new Key({ - encryptedKey: key.encryptedKey, - nonce: key.nonce, - sender: req.user._id, - receiver: key.userId, - workspace: workspaceId - }).save(); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to upload key to workspace' - }); - } + await new Key({ + encryptedKey: key.encryptedKey, + nonce: key.nonce, + sender: req.user._id, + receiver: key.userId, + workspace: workspaceId + }).save(); return res.status(200).send({ message: 'Successfully uploaded key to workspace' @@ -52,25 +43,16 @@ export const uploadKey = async (req: Request, res: Response) => { * @returns */ export const getLatestKey = async (req: Request, res: Response) => { - let latestKey; - try { - const { workspaceId } = req.params; + const { workspaceId } = req.params; - // get latest key - latestKey = await Key.find({ - workspace: workspaceId, - receiver: req.user._id - }) - .sort({ createdAt: -1 }) - .limit(1) - .populate('sender', '+publicKey'); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get latest key' - }); - } + // get latest key + const latestKey = await Key.find({ + workspace: workspaceId, + receiver: req.user._id + }) + .sort({ createdAt: -1 }) + .limit(1) + .populate('sender', '+publicKey'); const resObj: any = {}; @@ -79,4 +61,4 @@ export const getLatestKey = async (req: Request, res: Response) => { } return res.status(200).send(resObj); -}; \ No newline at end of file +}; diff --git a/backend/src/controllers/v1/membershipController.ts b/backend/src/controllers/v1/membershipController.ts index 67dd41a0df..d7e512adbd 100644 --- a/backend/src/controllers/v1/membershipController.ts +++ b/backend/src/controllers/v1/membershipController.ts @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/node'; import { Request, Response } from 'express'; import { Membership, MembershipOrg, User, Key } from '../../models'; import { @@ -16,25 +15,16 @@ import { getSiteURL } from '../../config'; * @returns */ export const validateMembership = async (req: Request, res: Response) => { - try { - const { workspaceId } = req.params; + const { workspaceId } = req.params; + // validate membership + const membership = await findMembership({ + user: req.user._id, + workspace: workspaceId + }); - // validate membership - const membership = await findMembership({ - user: req.user._id, - workspace: workspaceId - }); - - if (!membership) { - throw new Error('Failed to validate membership'); - } - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed workspace connection check' - }); - } + if (!membership) { + throw new Error('Failed to validate membership'); + } return res.status(200).send({ message: 'Workspace membership confirmed' @@ -48,48 +38,39 @@ export const validateMembership = async (req: Request, res: Response) => { * @returns */ export const deleteMembership = async (req: Request, res: Response) => { - let deletedMembership; - try { - const { membershipId } = req.params; + const { membershipId } = req.params; - // check if membership to delete exists - const membershipToDelete = await Membership.findOne({ - _id: membershipId - }).populate('user'); + // check if membership to delete exists + const membershipToDelete = await Membership.findOne({ + _id: membershipId + }).populate('user'); - if (!membershipToDelete) { - throw new Error( - "Failed to delete workspace membership that doesn't exist" - ); - } + if (!membershipToDelete) { + throw new Error( + "Failed to delete workspace membership that doesn't exist" + ); + } - // check if user is a member and admin of the workspace - // whose membership we wish to delete - const membership = await Membership.findOne({ - user: req.user._id, - workspace: membershipToDelete.workspace - }); + // check if user is a member and admin of the workspace + // whose membership we wish to delete + const membership = await Membership.findOne({ + user: req.user._id, + workspace: membershipToDelete.workspace + }); - if (!membership) { - throw new Error('Failed to validate workspace membership'); - } + if (!membership) { + throw new Error('Failed to validate workspace membership'); + } - if (membership.role !== ADMIN) { - // user is not an admin member of the workspace - throw new Error('Insufficient role for deleting workspace membership'); - } + if (membership.role !== ADMIN) { + // user is not an admin member of the workspace + throw new Error('Insufficient role for deleting workspace membership'); + } - // delete workspace membership - deletedMembership = await deleteMember({ - membershipId: membershipToDelete._id.toString() - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to delete membership' - }); - } + // delete workspace membership + const deletedMembership = await deleteMember({ + membershipId: membershipToDelete._id.toString() + }); return res.status(200).send({ deletedMembership @@ -103,49 +84,40 @@ export const deleteMembership = async (req: Request, res: Response) => { * @returns */ export const changeMembershipRole = async (req: Request, res: Response) => { - let membershipToChangeRole; - try { - const { membershipId } = req.params; - const { role } = req.body; + const { membershipId } = req.params; + const { role } = req.body; - if (![ADMIN, MEMBER].includes(role)) { - throw new Error('Failed to validate role'); - } + if (![ADMIN, MEMBER].includes(role)) { + throw new Error('Failed to validate role'); + } - // validate target membership - membershipToChangeRole = await findMembership({ - _id: membershipId - }); + // validate target membership + const membershipToChangeRole = await findMembership({ + _id: membershipId + }); - if (!membershipToChangeRole) { - throw new Error('Failed to find membership to change role'); - } + if (!membershipToChangeRole) { + throw new Error('Failed to find membership to change role'); + } - // check if user is a member and admin of target membership's - // workspace - const membership = await findMembership({ - user: req.user._id, - workspace: membershipToChangeRole.workspace - }); + // check if user is a member and admin of target membership's + // workspace + const membership = await findMembership({ + user: req.user._id, + workspace: membershipToChangeRole.workspace + }); - if (!membership) { - throw new Error('Failed to validate membership'); - } + if (!membership) { + throw new Error('Failed to validate membership'); + } - if (membership.role !== ADMIN) { - // user is not an admin member of the workspace - throw new Error('Insufficient role for changing member roles'); - } + if (membership.role !== ADMIN) { + // user is not an admin member of the workspace + throw new Error('Insufficient role for changing member roles'); + } - membershipToChangeRole.role = role; - await membershipToChangeRole.save(); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to change membership role' - }); - } + membershipToChangeRole.role = role; + await membershipToChangeRole.save(); return res.status(200).send({ membership: membershipToChangeRole @@ -159,75 +131,66 @@ export const changeMembershipRole = async (req: Request, res: Response) => { * @returns */ export const inviteUserToWorkspace = async (req: Request, res: Response) => { - let invitee, latestKey; - try { - const { workspaceId } = req.params; - const { email }: { email: string } = req.body; + const { workspaceId } = req.params; + const { email }: { email: string } = req.body; - invitee = await User.findOne({ - email - }).select('+publicKey'); + const invitee = await User.findOne({ + email + }).select('+publicKey'); - if (!invitee || !invitee?.publicKey) - throw new Error('Failed to validate invitee'); + if (!invitee || !invitee?.publicKey) + throw new Error('Failed to validate invitee'); - // validate invitee's workspace membership - ensure member isn't - // already a member of the workspace - const inviteeMembership = await Membership.findOne({ - user: invitee._id, - workspace: workspaceId - }); + // validate invitee's workspace membership - ensure member isn't + // already a member of the workspace + const inviteeMembership = await Membership.findOne({ + user: invitee._id, + workspace: workspaceId + }); - if (inviteeMembership) - throw new Error('Failed to add existing member of workspace'); + if (inviteeMembership) + throw new Error('Failed to add existing member of workspace'); - // validate invitee's organization membership - ensure that only - // (accepted) organization members can be added to the workspace - const membershipOrg = await MembershipOrg.findOne({ - user: invitee._id, - organization: req.membership.workspace.organization, - status: ACCEPTED - }); + // validate invitee's organization membership - ensure that only + // (accepted) organization members can be added to the workspace + const membershipOrg = await MembershipOrg.findOne({ + user: invitee._id, + organization: req.membership.workspace.organization, + status: ACCEPTED + }); - if (!membershipOrg) - throw new Error("Failed to validate invitee's organization membership"); + if (!membershipOrg) + throw new Error("Failed to validate invitee's organization membership"); - // get latest key - latestKey = await Key.findOne({ - workspace: workspaceId, - receiver: req.user._id - }) - .sort({ createdAt: -1 }) - .populate('sender', '+publicKey'); + // get latest key + const latestKey = await Key.findOne({ + workspace: workspaceId, + receiver: req.user._id + }) + .sort({ createdAt: -1 }) + .populate('sender', '+publicKey'); - // create new workspace membership - const m = await new Membership({ - user: invitee._id, - workspace: workspaceId, - role: MEMBER - }).save(); + // create new workspace membership + const m = await new Membership({ + user: invitee._id, + workspace: workspaceId, + role: MEMBER + }).save(); - await sendMail({ - template: 'workspaceInvitation.handlebars', - subjectLine: 'Infisical workspace invitation', - recipients: [invitee.email], - substitutions: { - inviterFirstName: req.user.firstName, - inviterEmail: req.user.email, - workspaceName: req.membership.workspace.name, - callback_url: (await getSiteURL()) + '/login' - } - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to invite user to workspace' - }); - } + await sendMail({ + template: 'workspaceInvitation.handlebars', + subjectLine: 'Infisical workspace invitation', + recipients: [invitee.email], + substitutions: { + inviterFirstName: req.user.firstName, + inviterEmail: req.user.email, + workspaceName: req.membership.workspace.name, + callback_url: (await getSiteURL()) + '/login' + } + }); return res.status(200).send({ invitee, latestKey }); -}; \ No newline at end of file +}; diff --git a/backend/src/controllers/v1/membershipOrgController.ts b/backend/src/controllers/v1/membershipOrgController.ts index 60cdc96911..f6b0ee3ccb 100644 --- a/backend/src/controllers/v1/membershipOrgController.ts +++ b/backend/src/controllers/v1/membershipOrgController.ts @@ -1,6 +1,5 @@ import { Types } from 'mongoose'; import { Request, Response } from 'express'; -import * as Sentry from '@sentry/node'; import { MembershipOrg, Organization, User } from '../../models'; import { deleteMembershipOrg as deleteMemberFromOrg } from '../../helpers/membershipOrg'; import { createToken } from '../../helpers/auth'; @@ -17,52 +16,43 @@ import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured * @returns */ export const deleteMembershipOrg = async (req: Request, res: Response) => { - let membershipOrgToDelete; - try { - const { membershipOrgId } = req.params; + const { membershipOrgId } = req.params; - // check if organization membership to delete exists - membershipOrgToDelete = await MembershipOrg.findOne({ - _id: membershipOrgId - }).populate('user'); + // check if organization membership to delete exists + const membershipOrgToDelete = await MembershipOrg.findOne({ + _id: membershipOrgId + }).populate('user'); - if (!membershipOrgToDelete) { - throw new Error( - "Failed to delete organization membership that doesn't exist" - ); - } + if (!membershipOrgToDelete) { + throw new Error( + "Failed to delete organization membership that doesn't exist" + ); + } - // check if user is a member and admin of the organization - // whose membership we wish to delete - const membershipOrg = await MembershipOrg.findOne({ - user: req.user._id, - organization: membershipOrgToDelete.organization - }); + // check if user is a member and admin of the organization + // whose membership we wish to delete + const membershipOrg = await MembershipOrg.findOne({ + user: req.user._id, + organization: membershipOrgToDelete.organization + }); - if (!membershipOrg) { - throw new Error('Failed to validate organization membership'); - } + if (!membershipOrg) { + throw new Error('Failed to validate organization membership'); + } - if (membershipOrg.role !== OWNER && membershipOrg.role !== ADMIN) { - // user is not an admin member of the organization - throw new Error('Insufficient role for deleting organization membership'); - } + if (membershipOrg.role !== OWNER && membershipOrg.role !== ADMIN) { + // user is not an admin member of the organization + throw new Error('Insufficient role for deleting organization membership'); + } - // delete organization membership - const deletedMembershipOrg = await deleteMemberFromOrg({ - membershipOrgId: membershipOrgToDelete._id.toString() - }); + // delete organization membership + const deletedMembershipOrg = await deleteMemberFromOrg({ + membershipOrgId: membershipOrgToDelete._id.toString() + }); - await updateSubscriptionOrgQuantity({ - organizationId: membershipOrg.organization.toString() - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to delete organization membership' - }); - } + await updateSubscriptionOrgQuantity({ + organizationId: membershipOrg.organization.toString() + }); return membershipOrgToDelete; }; @@ -78,14 +68,6 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => { // [membershipOrgId] let membershipToChangeRole; - // try { - // } catch (err) { - // Sentry.setUser({ email: req.user.email }); - // Sentry.captureException(err); - // return res.status(400).send({ - // message: 'Failed to change organization membership role' - // }); - // } return res.status(200).send({ membershipOrg: membershipToChangeRole @@ -101,106 +83,98 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => { */ export const inviteUserToOrganization = async (req: Request, res: Response) => { let invitee, inviteeMembershipOrg, completeInviteLink; - try { - const { organizationId, inviteeEmail } = req.body; - const host = req.headers.host; - const siteUrl = `${req.protocol}://${host}`; + const { organizationId, inviteeEmail } = req.body; + const host = req.headers.host; + const siteUrl = `${req.protocol}://${host}`; - // validate membership - const membershipOrg = await MembershipOrg.findOne({ - user: req.user._id, - organization: organizationId - }); + // validate membership + const membershipOrg = await MembershipOrg.findOne({ + user: req.user._id, + organization: organizationId + }); - if (!membershipOrg) { - throw new Error('Failed to validate organization membership'); - } + if (!membershipOrg) { + throw new Error('Failed to validate organization membership'); + } - invitee = await User.findOne({ - email: inviteeEmail - }).select('+publicKey'); + invitee = await User.findOne({ + email: inviteeEmail + }).select('+publicKey'); - if (invitee) { - // case: invitee is an existing user + if (invitee) { + // case: invitee is an existing user - inviteeMembershipOrg = await MembershipOrg.findOne({ - user: invitee._id, - organization: organizationId - }); + inviteeMembershipOrg = await MembershipOrg.findOne({ + user: invitee._id, + organization: organizationId + }); - if (inviteeMembershipOrg && inviteeMembershipOrg.status === ACCEPTED) { - throw new Error( - 'Failed to invite an existing member of the organization' - ); - } + if (inviteeMembershipOrg && inviteeMembershipOrg.status === ACCEPTED) { + throw new Error( + 'Failed to invite an existing member of the organization' + ); + } - if (!inviteeMembershipOrg) { - - await new MembershipOrg({ - user: invitee, - inviteEmail: inviteeEmail, - organization: organizationId, - role: MEMBER, - status: INVITED - }).save(); - } - } else { - // check if invitee has been invited before - inviteeMembershipOrg = await MembershipOrg.findOne({ - inviteEmail: inviteeEmail, - organization: organizationId - }); + if (!inviteeMembershipOrg) { + + await new MembershipOrg({ + user: invitee, + inviteEmail: inviteeEmail, + organization: organizationId, + role: MEMBER, + status: INVITED + }).save(); + } + } else { + // check if invitee has been invited before + inviteeMembershipOrg = await MembershipOrg.findOne({ + inviteEmail: inviteeEmail, + organization: organizationId + }); - if (!inviteeMembershipOrg) { - // case: invitee has never been invited before + if (!inviteeMembershipOrg) { + // case: invitee has never been invited before - await new MembershipOrg({ - inviteEmail: inviteeEmail, - organization: organizationId, - role: MEMBER, - status: INVITED - }).save(); - } - } + await new MembershipOrg({ + inviteEmail: inviteeEmail, + organization: organizationId, + role: MEMBER, + status: INVITED + }).save(); + } + } - const organization = await Organization.findOne({ _id: organizationId }); + const organization = await Organization.findOne({ _id: organizationId }); - if (organization) { + if (organization) { - const token = await TokenService.createToken({ - type: TOKEN_EMAIL_ORG_INVITATION, - email: inviteeEmail, - organizationId: organization._id - }); + const token = await TokenService.createToken({ + type: TOKEN_EMAIL_ORG_INVITATION, + email: inviteeEmail, + organizationId: organization._id + }); - await sendMail({ - template: 'organizationInvitation.handlebars', - subjectLine: 'Infisical organization invitation', - recipients: [inviteeEmail], - substitutions: { - inviterFirstName: req.user.firstName, - inviterEmail: req.user.email, - organizationName: organization.name, - email: inviteeEmail, - organizationId: organization._id.toString(), - token, - callback_url: (await getSiteURL()) + '/signupinvite' - } - }); + await sendMail({ + template: 'organizationInvitation.handlebars', + subjectLine: 'Infisical organization invitation', + recipients: [inviteeEmail], + substitutions: { + inviterFirstName: req.user.firstName, + inviterEmail: req.user.email, + organizationName: organization.name, + email: inviteeEmail, + organizationId: organization._id.toString(), + token, + callback_url: (await getSiteURL()) + '/signupinvite' + } + }); - if (!(await getSmtpConfigured())) { - completeInviteLink = `${siteUrl + '/signupinvite'}?token=${token}&to=${inviteeEmail}&organization_id=${organization._id}` - } - } + if (!(await getSmtpConfigured())) { + completeInviteLink = `${siteUrl + '/signupinvite'}?token=${token}&to=${inviteeEmail}&organization_id=${organization._id}` + } + } - await updateSubscriptionOrgQuantity({ organizationId }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to send organization invite' - }); - } + await updateSubscriptionOrgQuantity({ organizationId }); return res.status(200).send({ message: `Sent an invite link to ${req.body.inviteeEmail}`, @@ -216,70 +190,62 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => { * @returns */ export const verifyUserToOrganization = async (req: Request, res: Response) => { - let user, token; - try { - const { - email, - organizationId, - code - } = req.body; + let user; + const { + email, + organizationId, + code + } = req.body; - user = await User.findOne({ email }).select('+publicKey'); + user = await User.findOne({ email }).select('+publicKey'); - const membershipOrg = await MembershipOrg.findOne({ - inviteEmail: email, - status: INVITED, - organization: new Types.ObjectId(organizationId) - }); + const membershipOrg = await MembershipOrg.findOne({ + inviteEmail: email, + status: INVITED, + organization: new Types.ObjectId(organizationId) + }); - if (!membershipOrg) - throw new Error('Failed to find any invitations for email'); + if (!membershipOrg) + throw new Error('Failed to find any invitations for email'); - await TokenService.validateToken({ - type: TOKEN_EMAIL_ORG_INVITATION, - email, - organizationId: membershipOrg.organization, - token: code - }); + await TokenService.validateToken({ + type: TOKEN_EMAIL_ORG_INVITATION, + email, + organizationId: membershipOrg.organization, + token: code + }); - if (user && user?.publicKey) { - // case: user has already completed account - // membership can be approved and redirected to login/dashboard - membershipOrg.status = ACCEPTED; - await membershipOrg.save(); - - await updateSubscriptionOrgQuantity({ - organizationId - }); + if (user && user?.publicKey) { + // case: user has already completed account + // membership can be approved and redirected to login/dashboard + membershipOrg.status = ACCEPTED; + await membershipOrg.save(); + + await updateSubscriptionOrgQuantity({ + organizationId + }); - return res.status(200).send({ - message: 'Successfully verified email', - user, - }); - } + return res.status(200).send({ + message: 'Successfully verified email', + user, + }); + } - if (!user) { - // initialize user account - user = await new User({ - email - }).save(); - } + if (!user) { + // initialize user account + user = await new User({ + email + }).save(); + } - // generate temporary signup token - token = createToken({ - payload: { - userId: user._id.toString() - }, - expiresIn: await getJwtSignupLifetime(), - secret: await getJwtSignupSecret() - }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(400).send({ - error: 'Failed email magic link verification for organization invitation' - }); - } + // generate temporary signup token + const token = createToken({ + payload: { + userId: user._id.toString() + }, + expiresIn: await getJwtSignupLifetime(), + secret: await getJwtSignupSecret() + }); return res.status(200).send({ message: 'Successfully verified email', diff --git a/backend/src/controllers/v1/organizationController.ts b/backend/src/controllers/v1/organizationController.ts index b9fbc9f4b8..f0c73d75c6 100644 --- a/backend/src/controllers/v1/organizationController.ts +++ b/backend/src/controllers/v1/organizationController.ts @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/node'; import { Request, Response } from 'express'; import Stripe from 'stripe'; import { @@ -15,21 +14,12 @@ import _ from 'lodash'; import { getStripeSecretKey, getSiteURL } from '../../config'; export const getOrganizations = async (req: Request, res: Response) => { - let organizations; - try { - organizations = ( - await MembershipOrg.find({ - user: req.user._id, - status: ACCEPTED - }).populate('organization') - ).map((m) => m.organization); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get organizations' - }); - } + const organizations = ( + await MembershipOrg.find({ + user: req.user._id, + status: ACCEPTED + }).populate('organization') + ).map((m) => m.organization); return res.status(200).send({ organizations @@ -44,33 +34,24 @@ export const getOrganizations = async (req: Request, res: Response) => { * @returns */ export const createOrganization = async (req: Request, res: Response) => { - let organization; - try { - const { organizationName } = req.body; + const { organizationName } = req.body; - if (organizationName.length < 1) { - throw new Error('Organization names must be at least 1-character long'); - } + if (organizationName.length < 1) { + throw new Error('Organization names must be at least 1-character long'); + } - // create organization and add user as member - organization = await create({ - email: req.user.email, - name: organizationName - }); + // create organization and add user as member + const organization = await create({ + email: req.user.email, + name: organizationName + }); - await addMembershipsOrg({ - userIds: [req.user._id.toString()], - organizationId: organization._id.toString(), - roles: [OWNER], - statuses: [ACCEPTED] - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to create organization' - }); - } + await addMembershipsOrg({ + userIds: [req.user._id.toString()], + organizationId: organization._id.toString(), + roles: [OWNER], + statuses: [ACCEPTED] + }); return res.status(200).send({ organization @@ -84,17 +65,7 @@ export const createOrganization = async (req: Request, res: Response) => { * @returns */ export const getOrganization = async (req: Request, res: Response) => { - let organization; - try { - organization = req.organization - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to find organization' - }); - } - + const organization = req.organization return res.status(200).send({ organization }); @@ -107,20 +78,11 @@ export const getOrganization = async (req: Request, res: Response) => { * @returns */ export const getOrganizationMembers = async (req: Request, res: Response) => { - let users; - try { - const { organizationId } = req.params; + const { organizationId } = req.params; - users = await MembershipOrg.find({ - organization: organizationId - }).populate('user', '+publicKey'); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get organization members' - }); - } + const users = await MembershipOrg.find({ + organization: organizationId + }).populate('user', '+publicKey'); return res.status(200).send({ users @@ -137,35 +99,26 @@ export const getOrganizationWorkspaces = async ( req: Request, res: Response ) => { - let workspaces; - try { - const { organizationId } = req.params; + const { organizationId } = req.params; - const workspacesSet = new Set( - ( - await Workspace.find( - { - organization: organizationId - }, - '_id' - ) - ).map((w) => w._id.toString()) - ); + const workspacesSet = new Set( + ( + await Workspace.find( + { + organization: organizationId + }, + '_id' + ) + ).map((w) => w._id.toString()) + ); - workspaces = ( - await Membership.find({ - user: req.user._id - }).populate('workspace') - ) - .filter((m) => workspacesSet.has(m.workspace._id.toString())) - .map((m) => m.workspace); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get my workspaces' - }); - } + const workspaces = ( + await Membership.find({ + user: req.user._id + }).populate('workspace') + ) + .filter((m) => workspacesSet.has(m.workspace._id.toString())) + .map((m) => m.workspace); return res.status(200).send({ workspaces @@ -179,29 +132,20 @@ export const getOrganizationWorkspaces = async ( * @returns */ export const changeOrganizationName = async (req: Request, res: Response) => { - let organization; - try { - const { organizationId } = req.params; - const { name } = req.body; + const { organizationId } = req.params; + const { name } = req.body; - organization = await Organization.findOneAndUpdate( - { - _id: organizationId - }, - { - name - }, - { - new: true - } - ); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to change organization name' - }); - } + const organization = await Organization.findOneAndUpdate( + { + _id: organizationId + }, + { + name + }, + { + new: true + } + ); return res.status(200).send({ message: 'Successfully changed organization name', @@ -219,20 +163,11 @@ export const getOrganizationIncidentContacts = async ( req: Request, res: Response ) => { - let incidentContactsOrg; - try { - const { organizationId } = req.params; + const { organizationId } = req.params; - incidentContactsOrg = await IncidentContactOrg.find({ - organization: organizationId - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get organization incident contacts' - }); - } + const incidentContactsOrg = await IncidentContactOrg.find({ + organization: organizationId + }); return res.status(200).send({ incidentContactsOrg @@ -249,23 +184,14 @@ export const addOrganizationIncidentContact = async ( req: Request, res: Response ) => { - let incidentContactOrg; - try { - const { organizationId } = req.params; - const { email } = req.body; + const { organizationId } = req.params; + const { email } = req.body; - incidentContactOrg = await IncidentContactOrg.findOneAndUpdate( - { email, organization: organizationId }, - { email, organization: organizationId }, - { upsert: true, new: true } - ); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to add incident contact for organization' - }); - } + const incidentContactOrg = await IncidentContactOrg.findOneAndUpdate( + { email, organization: organizationId }, + { email, organization: organizationId }, + { upsert: true, new: true } + ); return res.status(200).send({ incidentContactOrg @@ -282,22 +208,13 @@ export const deleteOrganizationIncidentContact = async ( req: Request, res: Response ) => { - let incidentContactOrg; - try { - const { organizationId } = req.params; - const { email } = req.body; + const { organizationId } = req.params; + const { email } = req.body; - incidentContactOrg = await IncidentContactOrg.findOneAndDelete({ - email, - organization: organizationId - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to delete organization incident contact' - }); - } + const incidentContactOrg = await IncidentContactOrg.findOneAndDelete({ + email, + organization: organizationId + }); return res.status(200).send({ message: 'Successfully deleted organization incident contact', @@ -317,41 +234,33 @@ export const createOrganizationPortalSession = async ( res: Response ) => { let session; - try { - const stripe = new Stripe(await getStripeSecretKey(), { - apiVersion: '2022-08-01' - }); + const stripe = new Stripe(await getStripeSecretKey(), { + apiVersion: '2022-08-01' + }); - // check if there is a payment method on file - const paymentMethods = await stripe.paymentMethods.list({ - customer: req.organization.customerId, - type: 'card' - }); - - if (paymentMethods.data.length < 1) { - // case: no payment method on file - session = await stripe.checkout.sessions.create({ - customer: req.organization.customerId, - mode: 'setup', - payment_method_types: ['card'], - success_url: (await getSiteURL()) + '/dashboard', - cancel_url: (await getSiteURL()) + '/dashboard' - }); - } else { - session = await stripe.billingPortal.sessions.create({ - customer: req.organization.customerId, - return_url: (await getSiteURL()) + '/dashboard' - }); - } + // check if there is a payment method on file + const paymentMethods = await stripe.paymentMethods.list({ + customer: req.organization.customerId, + type: 'card' + }); + + if (paymentMethods.data.length < 1) { + // case: no payment method on file + session = await stripe.checkout.sessions.create({ + customer: req.organization.customerId, + mode: 'setup', + payment_method_types: ['card'], + success_url: (await getSiteURL()) + '/dashboard', + cancel_url: (await getSiteURL()) + '/dashboard' + }); + } else { + session = await stripe.billingPortal.sessions.create({ + customer: req.organization.customerId, + return_url: (await getSiteURL()) + '/dashboard' + }); + } - return res.status(200).send({ url: session.url }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to redirect to organization billing portal' - }); - } + return res.status(200).send({ url: session.url }); }; /** @@ -364,22 +273,13 @@ export const getOrganizationSubscriptions = async ( req: Request, res: Response ) => { - let subscriptions; - try { - const stripe = new Stripe(await getStripeSecretKey(), { - apiVersion: '2022-08-01' - }); - - subscriptions = await stripe.subscriptions.list({ - customer: req.organization.customerId - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get organization subscriptions' - }); - } + const stripe = new Stripe(await getStripeSecretKey(), { + apiVersion: '2022-08-01' + }); + + const subscriptions = await stripe.subscriptions.list({ + customer: req.organization.customerId + }); return res.status(200).send({ subscriptions @@ -425,4 +325,4 @@ export const getOrganizationMembersAndTheirWorkspaces = async ( }); return res.json(userToWorkspaceIds); -}; \ No newline at end of file +}; diff --git a/backend/src/controllers/v1/passwordController.ts b/backend/src/controllers/v1/passwordController.ts index 85c2445053..5ce092038d 100644 --- a/backend/src/controllers/v1/passwordController.ts +++ b/backend/src/controllers/v1/passwordController.ts @@ -1,5 +1,4 @@ import { Request, Response } from 'express'; -import * as Sentry from '@sentry/node'; // eslint-disable-next-line @typescript-eslint/no-var-requires const jsrp = require('jsrp'); import * as bigintConversion from 'bigint-conversion'; @@ -19,41 +18,32 @@ import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret } from '../../conf * @returns */ export const emailPasswordReset = async (req: Request, res: Response) => { - let email: string; - try { - email = req.body.email; + const email = req.body.email; - const user = await User.findOne({ email }).select('+publicKey'); - if (!user || !user?.publicKey) { - // case: user has already completed account + const user = await User.findOne({ email }).select('+publicKey'); + if (!user || !user?.publicKey) { + // case: user has already completed account - return res.status(403).send({ - error: 'Failed to send email verification for password reset' - }); - } - - const token = await TokenService.createToken({ - type: TOKEN_EMAIL_PASSWORD_RESET, - email - }); - - await sendMail({ - template: 'passwordReset.handlebars', - subjectLine: 'Infisical password reset', - recipients: [email], - substitutions: { - email, - token, - callback_url: (await getSiteURL()) + '/password-reset' - } - }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to send email for account recovery' - }); - } + return res.status(403).send({ + error: 'Failed to send email verification for password reset' + }); + } + + const token = await TokenService.createToken({ + type: TOKEN_EMAIL_PASSWORD_RESET, + email + }); + + await sendMail({ + template: 'passwordReset.handlebars', + subjectLine: 'Infisical password reset', + recipients: [email], + substitutions: { + email, + token, + callback_url: (await getSiteURL()) + '/password-reset' + } + }); return res.status(200).send({ message: `Sent an email for account recovery to ${email}` @@ -67,40 +57,31 @@ export const emailPasswordReset = async (req: Request, res: Response) => { * @returns */ export const emailPasswordResetVerify = async (req: Request, res: Response) => { - let user, token; - try { - const { email, code } = req.body; + const { email, code } = req.body; - user = await User.findOne({ email }).select('+publicKey'); - if (!user || !user?.publicKey) { - // case: user doesn't exist with email [email] or - // hasn't even completed their account - return res.status(403).send({ - error: 'Failed email verification for password reset' - }); - } - - await TokenService.validateToken({ - type: TOKEN_EMAIL_PASSWORD_RESET, - email, - token: code - }); + const user = await User.findOne({ email }).select('+publicKey'); + if (!user || !user?.publicKey) { + // case: user doesn't exist with email [email] or + // hasn't even completed their account + return res.status(403).send({ + error: 'Failed email verification for password reset' + }); + } + + await TokenService.validateToken({ + type: TOKEN_EMAIL_PASSWORD_RESET, + email, + token: code + }); - // generate temporary password-reset token - token = createToken({ - payload: { - userId: user._id.toString() - }, - expiresIn: await getJwtSignupLifetime(), - secret: await getJwtSignupSecret() - }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed email verification for password reset' - }); - } + // generate temporary password-reset token + const token = createToken({ + payload: { + userId: user._id.toString() + }, + expiresIn: await getJwtSignupLifetime(), + secret: await getJwtSignupSecret() + }); return res.status(200).send({ message: 'Successfully verified email', @@ -117,43 +98,35 @@ export const emailPasswordResetVerify = async (req: Request, res: Response) => { */ export const srp1 = async (req: Request, res: Response) => { // return salt, serverPublicKey as part of first step of SRP protocol - try { - const { clientPublicKey } = req.body; - const user = await User.findOne({ - email: req.user.email - }).select('+salt +verifier'); + const { clientPublicKey } = req.body; + const user = await User.findOne({ + email: req.user.email + }).select('+salt +verifier'); - if (!user) throw new Error('Failed to find user'); + if (!user) throw new Error('Failed to find user'); - const server = new jsrp.server(); - server.init( - { - salt: user.salt, - verifier: user.verifier - }, - async () => { - // generate server-side public key - const serverPublicKey = server.getPublicKey(); + const server = new jsrp.server(); + server.init( + { + salt: user.salt, + verifier: user.verifier + }, + async () => { + // generate server-side public key + const serverPublicKey = server.getPublicKey(); - await LoginSRPDetail.findOneAndReplace({ email: req.user.email }, { - email: req.user.email, - clientPublicKey: clientPublicKey, - serverBInt: bigintConversion.bigintToBuf(server.bInt), - }, { upsert: true, returnNewDocument: false }) + await LoginSRPDetail.findOneAndReplace({ email: req.user.email }, { + email: req.user.email, + clientPublicKey: clientPublicKey, + serverBInt: bigintConversion.bigintToBuf(server.bInt), + }, { upsert: true, returnNewDocument: false }) - return res.status(200).send({ - serverPublicKey, - salt: user.salt - }); - } - ); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - error: 'Failed to start change password process' - }); - } + return res.status(200).send({ + serverPublicKey, + salt: user.salt + }); + } + ); }; /** @@ -165,80 +138,72 @@ export const srp1 = async (req: Request, res: Response) => { * @returns */ export const changePassword = async (req: Request, res: Response) => { - try { - const { - clientProof, - protectedKey, - protectedKeyIV, - protectedKeyTag, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt, - verifier - } = req.body; + const { + clientProof, + protectedKey, + protectedKeyIV, + protectedKeyTag, + encryptedPrivateKey, + encryptedPrivateKeyIV, + encryptedPrivateKeyTag, + salt, + verifier + } = req.body; - const user = await User.findOne({ - email: req.user.email - }).select('+salt +verifier'); + const user = await User.findOne({ + email: req.user.email + }).select('+salt +verifier'); - if (!user) throw new Error('Failed to find user'); + if (!user) throw new Error('Failed to find user'); - const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email }) + const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email }) - if (!loginSRPDetailFromDB) { - return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again")) - } + if (!loginSRPDetailFromDB) { + return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again")) + } - const server = new jsrp.server(); - server.init( - { - salt: user.salt, - verifier: user.verifier, - b: loginSRPDetailFromDB.serverBInt - }, - async () => { - server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey); + const server = new jsrp.server(); + server.init( + { + salt: user.salt, + verifier: user.verifier, + b: loginSRPDetailFromDB.serverBInt + }, + async () => { + server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey); - // compare server and client shared keys - if (server.checkClientProof(clientProof)) { - // change password + // compare server and client shared keys + if (server.checkClientProof(clientProof)) { + // change password - await User.findByIdAndUpdate( - req.user._id.toString(), - { - encryptionVersion: 2, - protectedKey, - protectedKeyIV, - protectedKeyTag, - encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag, - salt, - verifier - }, - { - new: true - } - ); + await User.findByIdAndUpdate( + req.user._id.toString(), + { + encryptionVersion: 2, + protectedKey, + protectedKeyIV, + protectedKeyTag, + encryptedPrivateKey, + iv: encryptedPrivateKeyIV, + tag: encryptedPrivateKeyTag, + salt, + verifier + }, + { + new: true + } + ); - return res.status(200).send({ - message: 'Successfully changed password' - }); - } + return res.status(200).send({ + message: 'Successfully changed password' + }); + } - return res.status(400).send({ - error: 'Failed to change password. Try again?' - }); - } - ); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - error: 'Failed to change password. Try again?' - }); - } + return res.status(400).send({ + error: 'Failed to change password. Try again?' + }); + } + ); }; /** @@ -252,69 +217,61 @@ export const createBackupPrivateKey = async (req: Request, res: Response) => { // requires verifying [clientProof] as part of second step of SRP protocol // as initiated in /srp1 - try { - const { clientProof, encryptedPrivateKey, iv, tag, salt, verifier } = - req.body; - const user = await User.findOne({ - email: req.user.email - }).select('+salt +verifier'); + const { clientProof, encryptedPrivateKey, iv, tag, salt, verifier } = + req.body; + const user = await User.findOne({ + email: req.user.email + }).select('+salt +verifier'); - if (!user) throw new Error('Failed to find user'); + if (!user) throw new Error('Failed to find user'); - const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email }) + const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email }) - if (!loginSRPDetailFromDB) { - return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again")) - } + if (!loginSRPDetailFromDB) { + return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again")) + } - const server = new jsrp.server(); - server.init( - { - salt: user.salt, - verifier: user.verifier, - b: loginSRPDetailFromDB.serverBInt - }, - async () => { - server.setClientPublicKey( - loginSRPDetailFromDB.clientPublicKey - ); + const server = new jsrp.server(); + server.init( + { + salt: user.salt, + verifier: user.verifier, + b: loginSRPDetailFromDB.serverBInt + }, + async () => { + server.setClientPublicKey( + loginSRPDetailFromDB.clientPublicKey + ); - // compare server and client shared keys - if (server.checkClientProof(clientProof)) { - // create new or replace backup private key + // compare server and client shared keys + if (server.checkClientProof(clientProof)) { + // create new or replace backup private key - const backupPrivateKey = await BackupPrivateKey.findOneAndUpdate( - { user: req.user._id }, - { - user: req.user._id, - encryptedPrivateKey, - iv, - tag, - salt, - verifier - }, - { upsert: true, new: true } - ).select('+user, encryptedPrivateKey'); + const backupPrivateKey = await BackupPrivateKey.findOneAndUpdate( + { user: req.user._id }, + { + user: req.user._id, + encryptedPrivateKey, + iv, + tag, + salt, + verifier + }, + { upsert: true, new: true } + ).select('+user, encryptedPrivateKey'); - // issue tokens - return res.status(200).send({ - message: 'Successfully updated backup private key', - backupPrivateKey - }); - } + // issue tokens + return res.status(200).send({ + message: 'Successfully updated backup private key', + backupPrivateKey + }); + } - return res.status(400).send({ - message: 'Failed to update backup private key' - }); - } - ); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to update backup private key' - }); - } + return res.status(400).send({ + message: 'Failed to update backup private key' + }); + } + ); }; /** @@ -324,20 +281,11 @@ export const createBackupPrivateKey = async (req: Request, res: Response) => { * @returns */ export const getBackupPrivateKey = async (req: Request, res: Response) => { - let backupPrivateKey; - try { - backupPrivateKey = await BackupPrivateKey.findOne({ - user: req.user._id - }).select('+encryptedPrivateKey +iv +tag'); + const backupPrivateKey = await BackupPrivateKey.findOne({ + user: req.user._id + }).select('+encryptedPrivateKey +iv +tag'); - if (!backupPrivateKey) throw new Error('Failed to find backup private key'); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get backup private key' - }); - } + if (!backupPrivateKey) throw new Error('Failed to find backup private key'); return res.status(200).send({ backupPrivateKey @@ -345,44 +293,36 @@ export const getBackupPrivateKey = async (req: Request, res: Response) => { } export const resetPassword = async (req: Request, res: Response) => { - try { - const { - protectedKey, - protectedKeyIV, - protectedKeyTag, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt, - verifier, - } = req.body; + const { + protectedKey, + protectedKeyIV, + protectedKeyTag, + encryptedPrivateKey, + encryptedPrivateKeyIV, + encryptedPrivateKeyTag, + salt, + verifier, + } = req.body; - await User.findByIdAndUpdate( - req.user._id.toString(), - { - encryptionVersion: 2, - protectedKey, - protectedKeyIV, - protectedKeyTag, - encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag, - salt, - verifier - }, - { - new: true - } - ); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get backup private key' - }); - } + await User.findByIdAndUpdate( + req.user._id.toString(), + { + encryptionVersion: 2, + protectedKey, + protectedKeyIV, + protectedKeyTag, + encryptedPrivateKey, + iv: encryptedPrivateKeyIV, + tag: encryptedPrivateKeyTag, + salt, + verifier + }, + { + new: true + } + ); return res.status(200).send({ message: 'Successfully reset password' }); -} \ No newline at end of file +} diff --git a/backend/src/controllers/v1/secretController.ts b/backend/src/controllers/v1/secretController.ts index 316f6cc3e5..e3d6818ae0 100644 --- a/backend/src/controllers/v1/secretController.ts +++ b/backend/src/controllers/v1/secretController.ts @@ -1,5 +1,4 @@ import { Request, Response } from 'express'; -import * as Sentry from '@sentry/node'; import { Types } from 'mongoose'; import { Key, Secret } from '../../models'; import { @@ -37,66 +36,56 @@ interface PushSecret { */ export const pushSecrets = async (req: Request, res: Response) => { // upload (encrypted) secrets to workspace with id [workspaceId] + const postHogClient = await TelemetryService.getPostHogClient(); + let { secrets }: { secrets: PushSecret[] } = req.body; + const { keys, environment, channel } = req.body; + const { workspaceId } = req.params; - try { - const postHogClient = await TelemetryService.getPostHogClient(); - let { secrets }: { secrets: PushSecret[] } = req.body; - const { keys, environment, channel } = req.body; - const { workspaceId } = req.params; + // validate environment + const workspaceEnvs = req.membership.workspace.environments; + if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { + throw new Error('Failed to validate environment'); + } - // validate environment - const workspaceEnvs = req.membership.workspace.environments; - if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { - throw new Error('Failed to validate environment'); - } + // sanitize secrets + secrets = secrets.filter( + (s: PushSecret) => s.ciphertextKey !== '' && s.ciphertextValue !== '' + ); - // sanitize secrets - secrets = secrets.filter( - (s: PushSecret) => s.ciphertextKey !== '' && s.ciphertextValue !== '' - ); + await push({ + userId: req.user._id, + workspaceId, + environment, + secrets + }); - await push({ - userId: req.user._id, - workspaceId, - environment, - secrets - }); + await pushKeys({ + userId: req.user._id, + workspaceId, + keys + }); + + + if (postHogClient) { + postHogClient.capture({ + event: 'secrets pushed', + distinctId: req.user.email, + properties: { + numberOfSecrets: secrets.length, + environment, + workspaceId, + channel: channel ? channel : 'cli' + } + }); + } - await pushKeys({ - userId: req.user._id, - workspaceId, - keys - }); - - - if (postHogClient) { - postHogClient.capture({ - event: 'secrets pushed', - distinctId: req.user.email, - properties: { - numberOfSecrets: secrets.length, - environment, - workspaceId, - channel: channel ? channel : 'cli' - } - }); - } - - // trigger event - push secrets - EventService.handleEvent({ - event: eventPushSecrets({ - workspaceId: new Types.ObjectId(workspaceId), - environment - }) - }); - - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to upload workspace secrets' - }); - } + // trigger event - push secrets + EventService.handleEvent({ + event: eventPushSecrets({ + workspaceId: new Types.ObjectId(workspaceId), + environment + }) + }); return res.status(200).send({ message: 'Successfully uploaded workspace secrets' @@ -111,59 +100,50 @@ export const pushSecrets = async (req: Request, res: Response) => { * @returns */ export const pullSecrets = async (req: Request, res: Response) => { - let secrets; - let key; - try { - const postHogClient = await TelemetryService.getPostHogClient(); - const environment: string = req.query.environment as string; - const channel: string = req.query.channel as string; - const { workspaceId } = req.params; + const postHogClient = await TelemetryService.getPostHogClient(); + const environment: string = req.query.environment as string; + const channel: string = req.query.channel as string; + const { workspaceId } = req.params; - // validate environment - const workspaceEnvs = req.membership.workspace.environments; - if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { - throw new Error('Failed to validate environment'); - } + // validate environment + const workspaceEnvs = req.membership.workspace.environments; + if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { + throw new Error('Failed to validate environment'); + } - secrets = await pull({ - userId: req.user._id.toString(), - workspaceId, - environment, - channel: channel ? channel : 'cli', - ipAddress: req.ip - }); + let secrets = await pull({ + userId: req.user._id.toString(), + workspaceId, + environment, + channel: channel ? channel : 'cli', + ipAddress: req.ip + }); - key = await Key.findOne({ - workspace: workspaceId, - receiver: req.user._id - }) - .sort({ createdAt: -1 }) - .populate('sender', '+publicKey'); - - if (channel !== 'cli') { - secrets = reformatPullSecrets({ secrets }); - } + const key = await Key.findOne({ + workspace: workspaceId, + receiver: req.user._id + }) + .sort({ createdAt: -1 }) + .populate('sender', '+publicKey'); + + if (channel !== 'cli') { + // FIX: Fix this any + secrets = reformatPullSecrets({ secrets }) as any; + } - if (postHogClient) { - // capture secrets pushed event in production - postHogClient.capture({ - distinctId: req.user.email, - event: 'secrets pulled', - properties: { - numberOfSecrets: secrets.length, - environment, - workspaceId, - channel: channel ? channel : 'cli' - } - }); - } - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to pull workspace secrets' - }); - } + if (postHogClient) { + // capture secrets pushed event in production + postHogClient.capture({ + distinctId: req.user.email, + event: 'secrets pulled', + properties: { + numberOfSecrets: secrets.length, + environment, + workspaceId, + channel: channel ? channel : 'cli' + } + }); + } return res.status(200).send({ secrets, @@ -180,61 +160,51 @@ export const pullSecrets = async (req: Request, res: Response) => { * @returns */ export const pullSecretsServiceToken = async (req: Request, res: Response) => { - let secrets; - let key; - try { - const postHogClient = await TelemetryService.getPostHogClient(); - const environment: string = req.query.environment as string; - const channel: string = req.query.channel as string; - const { workspaceId } = req.params; + const postHogClient = await TelemetryService.getPostHogClient(); + const environment: string = req.query.environment as string; + const channel: string = req.query.channel as string; + const { workspaceId } = req.params; - // validate environment - const workspaceEnvs = req.membership.workspace.environments; - if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { - throw new Error('Failed to validate environment'); - } + // validate environment + const workspaceEnvs = req.membership.workspace.environments; + if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { + throw new Error('Failed to validate environment'); + } - secrets = await pull({ - userId: req.serviceToken.user._id.toString(), - workspaceId, - environment, - channel: 'cli', - ipAddress: req.ip - }); + const secrets = await pull({ + userId: req.serviceToken.user._id.toString(), + workspaceId, + environment, + channel: 'cli', + ipAddress: req.ip + }); - key = { - encryptedKey: req.serviceToken.encryptedKey, - nonce: req.serviceToken.nonce, - sender: { - publicKey: req.serviceToken.publicKey - }, - receiver: req.serviceToken.user, - workspace: req.serviceToken.workspace - }; + const key = { + encryptedKey: req.serviceToken.encryptedKey, + nonce: req.serviceToken.nonce, + sender: { + publicKey: req.serviceToken.publicKey + }, + receiver: req.serviceToken.user, + workspace: req.serviceToken.workspace + }; - if (postHogClient) { - // capture secrets pulled event in production - postHogClient.capture({ - distinctId: req.serviceToken.user.email, - event: 'secrets pulled', - properties: { - numberOfSecrets: secrets.length, - environment, - workspaceId, - channel: channel ? channel : 'cli' - } - }); - } - } catch (err) { - Sentry.setUser({ email: req.serviceToken.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to pull workspace secrets' - }); - } + if (postHogClient) { + // capture secrets pulled event in production + postHogClient.capture({ + distinctId: req.serviceToken.user.email, + event: 'secrets pulled', + properties: { + numberOfSecrets: secrets.length, + environment, + workspaceId, + channel: channel ? channel : 'cli' + } + }); + } return res.status(200).send({ secrets: reformatPullSecrets({ secrets }), key }); -}; \ No newline at end of file +}; diff --git a/backend/src/controllers/v1/signupController.ts b/backend/src/controllers/v1/signupController.ts index 681b654599..9fede9b1a4 100644 --- a/backend/src/controllers/v1/signupController.ts +++ b/backend/src/controllers/v1/signupController.ts @@ -1,5 +1,4 @@ import { Request, Response } from 'express'; -import * as Sentry from '@sentry/node'; import { User } from '../../models'; import { sendEmailVerification, @@ -17,29 +16,20 @@ import { getInviteOnlySignup, getJwtSignupLifetime, getJwtSignupSecret, getSmtpC * @returns */ export const beginEmailSignup = async (req: Request, res: Response) => { - let email: string; - try { - email = req.body.email; + const email = req.body.email; - const user = await User.findOne({ email }).select('+publicKey'); - if (user && user?.publicKey) { - // case: user has already completed account + const user = await User.findOne({ email }).select('+publicKey'); + if (user && user?.publicKey) { + // case: user has already completed account - return res.status(403).send({ - error: 'Failed to send email verification code for complete account' - }); - } + return res.status(403).send({ + error: 'Failed to send email verification code for complete account' + }); + } + + // send send verification email + await sendEmailVerification({ email }); - // send send verification email - await sendEmailVerification({ email }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(400).send({ - error: 'Failed to send email verification code' - }); - } - return res.status(200).send({ message: `Sent an email verification code to ${email}` }); @@ -54,55 +44,47 @@ export const beginEmailSignup = async (req: Request, res: Response) => { */ export const verifyEmailSignup = async (req: Request, res: Response) => { let user, token; - try { - const { email, code } = req.body; + const { email, code } = req.body; - // initialize user account - user = await User.findOne({ email }).select('+publicKey'); - if (user && user?.publicKey) { - // case: user has already completed account - return res.status(403).send({ - error: 'Failed email verification for complete user' - }); - } + // initialize user account + user = await User.findOne({ email }).select('+publicKey'); + if (user && user?.publicKey) { + // case: user has already completed account + return res.status(403).send({ + error: 'Failed email verification for complete user' + }); + } - if (await getInviteOnlySignup()) { - // Only one user can create an account without being invited. The rest need to be invited in order to make an account - const userCount = await User.countDocuments({}) - if (userCount != 0) { - throw BadRequestError({ message: "New user sign ups are not allowed at this time. You must be invited to sign up." }) - } - } + if (await getInviteOnlySignup()) { + // Only one user can create an account without being invited. The rest need to be invited in order to make an account + const userCount = await User.countDocuments({}) + if (userCount != 0) { + throw BadRequestError({ message: "New user sign ups are not allowed at this time. You must be invited to sign up." }) + } + } - // verify email - if (await getSmtpConfigured()) { - await checkEmailVerification({ - email, - code - }); - } + // verify email + if (await getSmtpConfigured()) { + await checkEmailVerification({ + email, + code + }); + } - if (!user) { - user = await new User({ - email - }).save(); - } + if (!user) { + user = await new User({ + email + }).save(); + } - // generate temporary signup token - token = createToken({ - payload: { - userId: user._id.toString() - }, - expiresIn: await getJwtSignupLifetime(), - secret: await getJwtSignupSecret() - }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(400).send({ - error: 'Failed email verification' - }); - } + // generate temporary signup token + token = createToken({ + payload: { + userId: user._id.toString() + }, + expiresIn: await getJwtSignupLifetime(), + secret: await getJwtSignupSecret() + }); return res.status(200).send({ message: 'Successfuly verified email', diff --git a/backend/src/controllers/v1/stripeController.ts b/backend/src/controllers/v1/stripeController.ts index 809107ad9c..4aa7adce98 100644 --- a/backend/src/controllers/v1/stripeController.ts +++ b/backend/src/controllers/v1/stripeController.ts @@ -1,5 +1,4 @@ import { Request, Response } from 'express'; -import * as Sentry from '@sentry/node'; import Stripe from 'stripe'; import { getStripeSecretKey, getStripeWebhookSecret } from '../../config'; @@ -10,26 +9,17 @@ import { getStripeSecretKey, getStripeWebhookSecret } from '../../config'; * @returns */ export const handleWebhook = async (req: Request, res: Response) => { - let event; - try { - // check request for valid stripe signature - const stripe = new Stripe(await getStripeSecretKey(), { - apiVersion: '2022-08-01' - }); + // check request for valid stripe signature + const stripe = new Stripe(await getStripeSecretKey(), { + apiVersion: '2022-08-01' + }); - const sig = req.headers['stripe-signature'] as string; - event = stripe.webhooks.constructEvent( - req.body, - sig, - await getStripeWebhookSecret() - ); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - error: 'Failed to process webhook' - }); - } + const sig = req.headers['stripe-signature'] as string; + const event = stripe.webhooks.constructEvent( + req.body, + sig, + await getStripeWebhookSecret() + ); switch (event.type) { case '': diff --git a/backend/src/controllers/v1/userActionController.ts b/backend/src/controllers/v1/userActionController.ts index c7a3c2337d..cba78c7b83 100644 --- a/backend/src/controllers/v1/userActionController.ts +++ b/backend/src/controllers/v1/userActionController.ts @@ -1,5 +1,4 @@ import { Request, Response } from 'express'; -import * as Sentry from '@sentry/node'; import { UserAction } from '../../models'; /** @@ -11,28 +10,19 @@ import { UserAction } from '../../models'; export const addUserAction = async (req: Request, res: Response) => { // add/record new action [action] for user with id [req.user._id] - let userAction; - try { - const { action } = req.body; + const { action } = req.body; - userAction = await UserAction.findOneAndUpdate( - { - user: req.user._id, - action - }, - { user: req.user._id, action }, - { - new: true, - upsert: true - } - ); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to record user action' - }); - } + const userAction = await UserAction.findOneAndUpdate( + { + user: req.user._id, + action + }, + { user: req.user._id, action }, + { + new: true, + upsert: true + } + ); return res.status(200).send({ message: 'Successfully recorded user action', @@ -48,21 +38,12 @@ export const addUserAction = async (req: Request, res: Response) => { */ export const getUserAction = async (req: Request, res: Response) => { // get user action [action] for user with id [req.user._id] - let userAction; - try { - const action: string = req.query.action as string; + const action: string = req.query.action as string; - userAction = await UserAction.findOne({ - user: req.user._id, - action - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get user action' - }); - } + const userAction = await UserAction.findOne({ + user: req.user._id, + action + }); return res.status(200).send({ userAction diff --git a/backend/src/controllers/v1/workspaceController.ts b/backend/src/controllers/v1/workspaceController.ts index 2b0a89f431..92ae52b11f 100644 --- a/backend/src/controllers/v1/workspaceController.ts +++ b/backend/src/controllers/v1/workspaceController.ts @@ -1,5 +1,4 @@ import { Request, Response } from "express"; -import * as Sentry from "@sentry/node"; import { Workspace, Membership, @@ -24,27 +23,18 @@ import { ADMIN } from "../../variables"; * @returns */ export const getWorkspacePublicKeys = async (req: Request, res: Response) => { - let publicKeys; - try { - const { workspaceId } = req.params; + const { workspaceId } = req.params; - publicKeys = ( - await Membership.find({ - workspace: workspaceId, - }).populate<{ user: IUser }>("user", "publicKey") - ).map((member) => { - return { - publicKey: member.user.publicKey, - userId: member.user._id, - }; - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: "Failed to get workspace member public keys", - }); - } + const publicKeys = ( + await Membership.find({ + workspace: workspaceId, + }).populate<{ user: IUser }>("user", "publicKey") + ).map((member) => { + return { + publicKey: member.user.publicKey, + userId: member.user._id, + }; + }); return res.status(200).send({ publicKeys, @@ -58,20 +48,11 @@ export const getWorkspacePublicKeys = async (req: Request, res: Response) => { * @returns */ export const getWorkspaceMemberships = async (req: Request, res: Response) => { - let users; - try { - const { workspaceId } = req.params; + const { workspaceId } = req.params; - users = await Membership.find({ - workspace: workspaceId, - }).populate("user", "+publicKey"); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: "Failed to get workspace members", - }); - } + const users = await Membership.find({ + workspace: workspaceId, + }).populate("user", "+publicKey"); return res.status(200).send({ users, @@ -85,20 +66,11 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => { * @returns */ export const getWorkspaces = async (req: Request, res: Response) => { - let workspaces; - try { - workspaces = ( - await Membership.find({ - user: req.user._id, - }).populate("workspace") - ).map((m) => m.workspace); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: "Failed to get workspaces", - }); - } + const workspaces = ( + await Membership.find({ + user: req.user._id, + }).populate("workspace") + ).map((m) => m.workspace); return res.status(200).send({ workspaces, @@ -112,20 +84,11 @@ export const getWorkspaces = async (req: Request, res: Response) => { * @returns */ export const getWorkspace = async (req: Request, res: Response) => { - let workspace; - try { - const { workspaceId } = req.params; + const { workspaceId } = req.params; - workspace = await Workspace.findOne({ - _id: workspaceId, - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: "Failed to get workspace", - }); - } + const workspace = await Workspace.findOne({ + _id: workspaceId, + }); return res.status(200).send({ workspace, @@ -140,43 +103,34 @@ export const getWorkspace = async (req: Request, res: Response) => { * @returns */ export const createWorkspace = async (req: Request, res: Response) => { - let workspace; - try { - const { workspaceName, organizationId } = req.body; + const { workspaceName, organizationId } = req.body; - // validate organization membership - const membershipOrg = await MembershipOrg.findOne({ - user: req.user._id, - organization: organizationId, - }); + // validate organization membership + const membershipOrg = await MembershipOrg.findOne({ + user: req.user._id, + organization: organizationId, + }); - if (!membershipOrg) { - throw new Error("Failed to validate organization membership"); - } - - if (workspaceName.length < 1) { - throw new Error("Workspace names must be at least 1-character long"); - } - - // create workspace and add user as member - workspace = await create({ - name: workspaceName, - organizationId, - }); - - await addMemberships({ - userIds: [req.user._id], - workspaceId: workspace._id.toString(), - roles: [ADMIN], - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: "Failed to create workspace", - }); + if (!membershipOrg) { + throw new Error("Failed to validate organization membership"); } + if (workspaceName.length < 1) { + throw new Error("Workspace names must be at least 1-character long"); + } + + // create workspace and add user as member + const workspace = await create({ + name: workspaceName, + organizationId, + }); + + await addMemberships({ + userIds: [req.user._id], + workspaceId: workspace._id.toString(), + roles: [ADMIN], + }); + return res.status(200).send({ workspace, }); @@ -189,20 +143,12 @@ export const createWorkspace = async (req: Request, res: Response) => { * @returns */ export const deleteWorkspace = async (req: Request, res: Response) => { - try { - const { workspaceId } = req.params; + const { workspaceId } = req.params; - // delete workspace - await deleteWork({ - id: workspaceId, - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: "Failed to delete workspace", - }); - } + // delete workspace + await deleteWork({ + id: workspaceId, + }); return res.status(200).send({ message: "Successfully deleted workspace", @@ -216,29 +162,20 @@ export const deleteWorkspace = async (req: Request, res: Response) => { * @returns */ export const changeWorkspaceName = async (req: Request, res: Response) => { - let workspace; - try { - const { workspaceId } = req.params; - const { name } = req.body; + const { workspaceId } = req.params; + const { name } = req.body; - workspace = await Workspace.findOneAndUpdate( - { - _id: workspaceId, - }, - { - name, - }, - { - new: true, - } - ); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: "Failed to change workspace name", - }); - } + const workspace = await Workspace.findOneAndUpdate( + { + _id: workspaceId, + }, + { + name, + }, + { + new: true, + } + ); return res.status(200).send({ message: "Successfully changed workspace name", @@ -253,20 +190,11 @@ export const changeWorkspaceName = async (req: Request, res: Response) => { * @returns */ export const getWorkspaceIntegrations = async (req: Request, res: Response) => { - let integrations; - try { - const { workspaceId } = req.params; + const { workspaceId } = req.params; - integrations = await Integration.find({ - workspace: workspaceId, - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: "Failed to get workspace integrations", - }); - } + const integrations = await Integration.find({ + workspace: workspaceId, + }); return res.status(200).send({ integrations, @@ -283,20 +211,11 @@ export const getWorkspaceIntegrationAuthorizations = async ( req: Request, res: Response ) => { - let authorizations; - try { - const { workspaceId } = req.params; + const { workspaceId } = req.params; - authorizations = await IntegrationAuth.find({ - workspace: workspaceId, - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: "Failed to get workspace integration authorizations", - }); - } + const authorizations = await IntegrationAuth.find({ + workspace: workspaceId, + }); return res.status(200).send({ authorizations, @@ -313,21 +232,12 @@ export const getWorkspaceServiceTokens = async ( req: Request, res: Response ) => { - let serviceTokens; - try { - const { workspaceId } = req.params; - // ?? FIX. - serviceTokens = await ServiceToken.find({ - user: req.user._id, - workspace: workspaceId, - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: "Failed to get workspace service tokens", - }); - } + const { workspaceId } = req.params; + // ?? FIX. + const serviceTokens = await ServiceToken.find({ + user: req.user._id, + workspace: workspaceId, + }); return res.status(200).send({ serviceTokens, diff --git a/backend/src/controllers/v2/apiKeyDataController.ts b/backend/src/controllers/v2/apiKeyDataController.ts index 86533c733a..73fd1afbff 100644 --- a/backend/src/controllers/v2/apiKeyDataController.ts +++ b/backend/src/controllers/v2/apiKeyDataController.ts @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/node'; import { Request, Response } from 'express'; import crypto from 'crypto'; import bcrypt from 'bcrypt'; @@ -14,18 +13,9 @@ import { getSaltRounds } from '../../config'; * @returns */ export const getAPIKeyData = async (req: Request, res: Response) => { - let apiKeyData; - try { - apiKeyData = await APIKeyData.find({ - user: req.user._id - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get API key data' - }); - } + const apiKeyData = await APIKeyData.find({ + user: req.user._id + }); return res.status(200).send({ apiKeyData @@ -38,39 +28,30 @@ export const getAPIKeyData = async (req: Request, res: Response) => { * @param res */ export const createAPIKeyData = async (req: Request, res: Response) => { - let apiKey, apiKeyData; - try { - const { name, expiresIn } = req.body; - - const secret = crypto.randomBytes(16).toString('hex'); - const secretHash = await bcrypt.hash(secret, await getSaltRounds()); - - const expiresAt = new Date(); - expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); - - apiKeyData = await new APIKeyData({ - name, - lastUsed: new Date(), - expiresAt, - user: req.user._id, - secretHash - }).save(); - - // return api key data without sensitive data - apiKeyData = await APIKeyData.findById(apiKeyData._id); - - if (!apiKeyData) throw new Error('Failed to find API key data'); - - apiKey = `ak.${apiKeyData._id.toString()}.${secret}`; - - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to API key data' - }); - } + const { name, expiresIn } = req.body; + const secret = crypto.randomBytes(16).toString('hex'); + const secretHash = await bcrypt.hash(secret, await getSaltRounds()); + + const expiresAt = new Date(); + expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); + + let apiKeyData = await new APIKeyData({ + name, + lastUsed: new Date(), + expiresAt, + user: req.user._id, + secretHash + }).save(); + + // return api key data without sensitive data + // FIX: fix this any + apiKeyData = await APIKeyData.findById(apiKeyData._id) as any + + if (!apiKeyData) throw new Error('Failed to find API key data'); + + const apiKey = `ak.${apiKeyData._id.toString()}.${secret}`; + return res.status(200).send({ apiKey, apiKeyData @@ -84,21 +65,10 @@ export const createAPIKeyData = async (req: Request, res: Response) => { * @returns */ export const deleteAPIKeyData = async (req: Request, res: Response) => { - let apiKeyData; - try { - const { apiKeyDataId } = req.params; - - apiKeyData = await APIKeyData.findByIdAndDelete(apiKeyDataId); - - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to delete API key data' - }); - } + const { apiKeyDataId } = req.params; + const apiKeyData = await APIKeyData.findByIdAndDelete(apiKeyDataId); return res.status(200).send({ apiKeyData }); -} \ No newline at end of file +} diff --git a/backend/src/controllers/v2/authController.ts b/backend/src/controllers/v2/authController.ts index fbc4ecb401..62735d05ca 100644 --- a/backend/src/controllers/v2/authController.ts +++ b/backend/src/controllers/v2/authController.ts @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/no-var-requires */ import { Request, Response } from 'express'; import jwt from 'jsonwebtoken'; -import * as Sentry from '@sentry/node'; import * as bigintConversion from 'bigint-conversion'; const jsrp = require('jsrp'); import { User, LoginSRPDetail } from '../../models'; @@ -35,47 +34,40 @@ declare module 'jsonwebtoken' { * @returns */ export const login1 = async (req: Request, res: Response) => { - try { - const { - email, - clientPublicKey - }: { email: string; clientPublicKey: string } = req.body; + const { + email, + clientPublicKey + }: { email: string; clientPublicKey: string } = req.body; - const user = await User.findOne({ - email - }).select('+salt +verifier'); + const user = await User.findOne({ + email + }).select('+salt +verifier'); - if (!user) throw new Error('Failed to find user'); + if (!user) throw new Error('Failed to find user'); - const server = new jsrp.server(); - server.init( - { - salt: user.salt, - verifier: user.verifier - }, - async () => { - // generate server-side public key - const serverPublicKey = server.getPublicKey(); + const server = new jsrp.server(); + server.init( + { + salt: user.salt, + verifier: user.verifier + }, + async () => { + // generate server-side public key + const serverPublicKey = server.getPublicKey(); - await LoginSRPDetail.findOneAndReplace({ email: email }, { - email: email, - clientPublicKey: clientPublicKey, - serverBInt: bigintConversion.bigintToBuf(server.bInt), - }, { upsert: true, returnNewDocument: false }); + await LoginSRPDetail.findOneAndReplace({ email: email }, { + email: email, + clientPublicKey: clientPublicKey, + serverBInt: bigintConversion.bigintToBuf(server.bInt), + }, { upsert: true, returnNewDocument: false }); - return res.status(200).send({ - serverPublicKey, - salt: user.salt - }); - } - ); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to start authentication process' - }); - } + return res.status(200).send({ + serverPublicKey, + salt: user.salt + }); + } + ); + }; /** @@ -86,149 +78,140 @@ export const login1 = async (req: Request, res: Response) => { * @returns */ export const login2 = async (req: Request, res: Response) => { - try { + if (!req.headers['user-agent']) throw InternalServerError({ message: 'User-Agent header is required' }); - if (!req.headers['user-agent']) throw InternalServerError({ message: 'User-Agent header is required' }); + const { email, clientProof } = req.body; + const user = await User.findOne({ + email + }).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag'); - const { email, clientProof } = req.body; - const user = await User.findOne({ - email - }).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag'); + if (!user) throw new Error('Failed to find user'); - if (!user) throw new Error('Failed to find user'); + const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email }) - const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email }) + if (!loginSRPDetail) { + return BadRequestError(Error("Failed to find login details for SRP")) + } - if (!loginSRPDetail) { - return BadRequestError(Error("Failed to find login details for SRP")) - } + const server = new jsrp.server(); + server.init( + { + salt: user.salt, + verifier: user.verifier, + b: loginSRPDetail.serverBInt + }, + async () => { + server.setClientPublicKey(loginSRPDetail.clientPublicKey); - const server = new jsrp.server(); - server.init( - { - salt: user.salt, - verifier: user.verifier, - b: loginSRPDetail.serverBInt - }, - async () => { - server.setClientPublicKey(loginSRPDetail.clientPublicKey); + // compare server and client shared keys + if (server.checkClientProof(clientProof)) { - // compare server and client shared keys - if (server.checkClientProof(clientProof)) { + if (user.isMfaEnabled) { + // case: user has MFA enabled - if (user.isMfaEnabled) { - // case: user has MFA enabled - - // generate temporary MFA token - const token = createToken({ - payload: { - userId: user._id.toString() - }, - expiresIn: await getJwtMfaLifetime(), - secret: await getJwtMfaSecret() - }); - - const code = await TokenService.createToken({ - type: TOKEN_EMAIL_MFA, - email - }); - - // send MFA code [code] to [email] - await sendMail({ - template: 'emailMfa.handlebars', - subjectLine: 'Infisical MFA code', - recipients: [email], - substitutions: { - code - } - }); - - return res.status(200).send({ - mfaEnabled: true, - token - }); - } - - await checkUserDevice({ - user, - ip: req.ip, - userAgent: req.headers['user-agent'] ?? '' + // generate temporary MFA token + const token = createToken({ + payload: { + userId: user._id.toString() + }, + expiresIn: await getJwtMfaLifetime(), + secret: await getJwtMfaSecret() }); - // issue tokens - const tokens = await issueAuthTokens({ userId: user._id.toString() }); - - // store (refresh) token in httpOnly cookie - res.cookie('jid', tokens.refreshToken, { - httpOnly: true, - path: '/', - sameSite: 'strict', - secure: await getHttpsEnabled() + const code = await TokenService.createToken({ + type: TOKEN_EMAIL_MFA, + email }); - // case: user does not have MFA enablgged - // return (access) token in response - - interface ResponseData { - mfaEnabled: boolean; - encryptionVersion: any; - protectedKey?: string; - protectedKeyIV?: string; - protectedKeyTag?: string; - token: string; - publicKey?: string; - encryptedPrivateKey?: string; - iv?: string; - tag?: string; - } - - const response: ResponseData = { - mfaEnabled: false, - encryptionVersion: user.encryptionVersion, - token: tokens.token, - publicKey: user.publicKey, - encryptedPrivateKey: user.encryptedPrivateKey, - iv: user.iv, - tag: user.tag - } - - if ( - user?.protectedKey && - user?.protectedKeyIV && - user?.protectedKeyTag - ) { - response.protectedKey = user.protectedKey; - response.protectedKeyIV = user.protectedKeyIV - response.protectedKeyTag = user.protectedKeyTag; - } - - const loginAction = await EELogService.createAction({ - name: ACTION_LOGIN, - userId: user._id + // send MFA code [code] to [email] + await sendMail({ + template: 'emailMfa.handlebars', + subjectLine: 'Infisical MFA code', + recipients: [email], + substitutions: { + code + } }); - loginAction && await EELogService.createLog({ - userId: user._id, - actions: [loginAction], - channel: getChannelFromUserAgent(req.headers['user-agent']), - ipAddress: req.ip + return res.status(200).send({ + mfaEnabled: true, + token }); - - return res.status(200).send(response); } - return res.status(400).send({ - message: 'Failed to authenticate. Try again?' + await checkUserDevice({ + user, + ip: req.ip, + userAgent: req.headers['user-agent'] ?? '' }); + + // issue tokens + const tokens = await issueAuthTokens({ userId: user._id.toString() }); + + // store (refresh) token in httpOnly cookie + res.cookie('jid', tokens.refreshToken, { + httpOnly: true, + path: '/', + sameSite: 'strict', + secure: await getHttpsEnabled() + }); + + // case: user does not have MFA enablgged + // return (access) token in response + + interface ResponseData { + mfaEnabled: boolean; + encryptionVersion: any; + protectedKey?: string; + protectedKeyIV?: string; + protectedKeyTag?: string; + token: string; + publicKey?: string; + encryptedPrivateKey?: string; + iv?: string; + tag?: string; + } + + const response: ResponseData = { + mfaEnabled: false, + encryptionVersion: user.encryptionVersion, + token: tokens.token, + publicKey: user.publicKey, + encryptedPrivateKey: user.encryptedPrivateKey, + iv: user.iv, + tag: user.tag + } + + if ( + user?.protectedKey && + user?.protectedKeyIV && + user?.protectedKeyTag + ) { + response.protectedKey = user.protectedKey; + response.protectedKeyIV = user.protectedKeyIV + response.protectedKeyTag = user.protectedKeyTag; + } + + const loginAction = await EELogService.createAction({ + name: ACTION_LOGIN, + userId: user._id + }); + + loginAction && await EELogService.createLog({ + userId: user._id, + actions: [loginAction], + channel: getChannelFromUserAgent(req.headers['user-agent']), + ipAddress: req.ip + }); + + return res.status(200).send(response); } - ); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to authenticate. Try again?' - }); - } + + return res.status(400).send({ + message: 'Failed to authenticate. Try again?' + }); + } + ); }; /** @@ -237,30 +220,22 @@ export const login2 = async (req: Request, res: Response) => { * @param res */ export const sendMfaToken = async (req: Request, res: Response) => { - try { - const { email } = req.body; + const { email } = req.body; - const code = await TokenService.createToken({ - type: TOKEN_EMAIL_MFA, - email - }); + const code = await TokenService.createToken({ + type: TOKEN_EMAIL_MFA, + email + }); - // send MFA code [code] to [email] - await sendMail({ - template: 'emailMfa.handlebars', - subjectLine: 'Infisical MFA code', - recipients: [email], - substitutions: { - code - } - }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to send MFA code' - }); - } + // send MFA code [code] to [email] + await sendMail({ + template: 'emailMfa.handlebars', + subjectLine: 'Infisical MFA code', + recipients: [email], + substitutions: { + code + } + }); return res.status(200).send({ message: 'Successfully sent new MFA code' diff --git a/backend/src/controllers/v2/environmentController.ts b/backend/src/controllers/v2/environmentController.ts index 4985420fb7..d4f91bacec 100644 --- a/backend/src/controllers/v2/environmentController.ts +++ b/backend/src/controllers/v2/environmentController.ts @@ -1,5 +1,4 @@ import { Request, Response } from 'express'; -import * as Sentry from '@sentry/node'; import { Secret, ServiceToken, @@ -25,30 +24,22 @@ export const createWorkspaceEnvironment = async ( ) => { const { workspaceId } = req.params; const { environmentName, environmentSlug } = req.body; - try { - const workspace = await Workspace.findById(workspaceId).exec(); - if ( - !workspace || - workspace?.environments.find( - ({ name, slug }) => slug === environmentSlug || environmentName === name - ) - ) { - throw new Error('Failed to create workspace environment'); - } - - workspace?.environments.push({ - name: environmentName, - slug: environmentSlug.toLowerCase(), - }); - await workspace.save(); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to create new workspace environment', - }); + const workspace = await Workspace.findById(workspaceId).exec(); + if ( + !workspace || + workspace?.environments.find( + ({ name, slug }) => slug === environmentSlug || environmentName === name + ) + ) { + throw new Error('Failed to create workspace environment'); } + workspace?.environments.push({ + name: environmentName, + slug: environmentSlug.toLowerCase(), + }); + await workspace.save(); + return res.status(200).send({ message: 'Successfully created new environment', workspace: workspaceId, @@ -72,75 +63,67 @@ export const renameWorkspaceEnvironment = async ( ) => { const { workspaceId } = req.params; const { environmentName, environmentSlug, oldEnvironmentSlug } = req.body; - try { - // user should pass both new slug and env name - if (!environmentSlug || !environmentName) { - throw new Error('Invalid environment given.'); - } - - // atomic update the env to avoid conflict - const workspace = await Workspace.findById(workspaceId).exec(); - if (!workspace) { - throw new Error('Failed to create workspace environment'); - } - - const isEnvExist = workspace.environments.some( - ({ name, slug }) => - slug !== oldEnvironmentSlug && - (name === environmentName || slug === environmentSlug) - ); - if (isEnvExist) { - throw new Error('Invalid environment given'); - } - - const envIndex = workspace?.environments.findIndex( - ({ slug }) => slug === oldEnvironmentSlug - ); - if (envIndex === -1) { - throw new Error('Invalid environment given'); - } - - workspace.environments[envIndex].name = environmentName; - workspace.environments[envIndex].slug = environmentSlug.toLowerCase(); - - await workspace.save(); - await Secret.updateMany( - { workspace: workspaceId, environment: oldEnvironmentSlug }, - { environment: environmentSlug } - ); - await SecretVersion.updateMany( - { workspace: workspaceId, environment: oldEnvironmentSlug }, - { environment: environmentSlug } - ); - await ServiceToken.updateMany( - { workspace: workspaceId, environment: oldEnvironmentSlug }, - { environment: environmentSlug } - ); - await ServiceTokenData.updateMany( - { workspace: workspaceId, environment: oldEnvironmentSlug }, - { environment: environmentSlug } - ); - await Integration.updateMany( - { workspace: workspaceId, environment: oldEnvironmentSlug }, - { environment: environmentSlug } - ); - await Membership.updateMany( - { - workspace: workspaceId, - "deniedPermissions.environmentSlug": oldEnvironmentSlug - }, - { $set: { "deniedPermissions.$[element].environmentSlug": environmentSlug } }, - { arrayFilters: [{ "element.environmentSlug": oldEnvironmentSlug }] } - ) - - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to update workspace environment', - }); + // user should pass both new slug and env name + if (!environmentSlug || !environmentName) { + throw new Error('Invalid environment given.'); } + // atomic update the env to avoid conflict + const workspace = await Workspace.findById(workspaceId).exec(); + if (!workspace) { + throw new Error('Failed to create workspace environment'); + } + + const isEnvExist = workspace.environments.some( + ({ name, slug }) => + slug !== oldEnvironmentSlug && + (name === environmentName || slug === environmentSlug) + ); + if (isEnvExist) { + throw new Error('Invalid environment given'); + } + + const envIndex = workspace?.environments.findIndex( + ({ slug }) => slug === oldEnvironmentSlug + ); + if (envIndex === -1) { + throw new Error('Invalid environment given'); + } + + workspace.environments[envIndex].name = environmentName; + workspace.environments[envIndex].slug = environmentSlug.toLowerCase(); + + await workspace.save(); + await Secret.updateMany( + { workspace: workspaceId, environment: oldEnvironmentSlug }, + { environment: environmentSlug } + ); + await SecretVersion.updateMany( + { workspace: workspaceId, environment: oldEnvironmentSlug }, + { environment: environmentSlug } + ); + await ServiceToken.updateMany( + { workspace: workspaceId, environment: oldEnvironmentSlug }, + { environment: environmentSlug } + ); + await ServiceTokenData.updateMany( + { workspace: workspaceId, environment: oldEnvironmentSlug }, + { environment: environmentSlug } + ); + await Integration.updateMany( + { workspace: workspaceId, environment: oldEnvironmentSlug }, + { environment: environmentSlug } + ); + await Membership.updateMany( + { + workspace: workspaceId, + "deniedPermissions.environmentSlug": oldEnvironmentSlug + }, + { $set: { "deniedPermissions.$[element].environmentSlug": environmentSlug } }, + { arrayFilters: [{ "element.environmentSlug": oldEnvironmentSlug }] } + ) + + return res.status(200).send({ message: 'Successfully update environment', workspace: workspaceId, @@ -163,57 +146,48 @@ export const deleteWorkspaceEnvironment = async ( ) => { const { workspaceId } = req.params; const { environmentSlug } = req.body; - try { - // atomic update the env to avoid conflict - const workspace = await Workspace.findById(workspaceId).exec(); - if (!workspace) { - throw new Error('Failed to create workspace environment'); - } - - const envIndex = workspace?.environments.findIndex( - ({ slug }) => slug === environmentSlug - ); - if (envIndex === -1) { - throw new Error('Invalid environment given'); - } - - workspace.environments.splice(envIndex, 1); - await workspace.save(); - - // clean up - await Secret.deleteMany({ - workspace: workspaceId, - environment: environmentSlug, - }); - await SecretVersion.deleteMany({ - workspace: workspaceId, - environment: environmentSlug, - }); - await ServiceToken.deleteMany({ - workspace: workspaceId, - environment: environmentSlug, - }); - await ServiceTokenData.deleteMany({ - workspace: workspaceId, - environment: environmentSlug, - }); - await Integration.deleteMany({ - workspace: workspaceId, - environment: environmentSlug, - }); - await Membership.updateMany( - { workspace: workspaceId }, - { $pull: { deniedPermissions: { environmentSlug: environmentSlug } } } - ) - - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to delete workspace environment', - }); + // atomic update the env to avoid conflict + const workspace = await Workspace.findById(workspaceId).exec(); + if (!workspace) { + throw new Error('Failed to create workspace environment'); } + const envIndex = workspace?.environments.findIndex( + ({ slug }) => slug === environmentSlug + ); + if (envIndex === -1) { + throw new Error('Invalid environment given'); + } + + workspace.environments.splice(envIndex, 1); + await workspace.save(); + + // clean up + await Secret.deleteMany({ + workspace: workspaceId, + environment: environmentSlug, + }); + await SecretVersion.deleteMany({ + workspace: workspaceId, + environment: environmentSlug, + }); + await ServiceToken.deleteMany({ + workspace: workspaceId, + environment: environmentSlug, + }); + await ServiceTokenData.deleteMany({ + workspace: workspaceId, + environment: environmentSlug, + }); + await Integration.deleteMany({ + workspace: workspaceId, + environment: environmentSlug, + }); + await Membership.updateMany( + { workspace: workspaceId }, + { $pull: { deniedPermissions: { environmentSlug: environmentSlug } } } + ) + return res.status(200).send({ message: 'Successfully deleted environment', workspace: workspaceId, diff --git a/backend/src/controllers/v2/organizationsController.ts b/backend/src/controllers/v2/organizationsController.ts index 613206ba3c..3bfd9085e1 100644 --- a/backend/src/controllers/v2/organizationsController.ts +++ b/backend/src/controllers/v2/organizationsController.ts @@ -1,5 +1,4 @@ import { Request, Response } from 'express'; -import * as Sentry from '@sentry/node'; import { Types } from 'mongoose'; import { MembershipOrg, @@ -49,20 +48,11 @@ export const getOrganizationMemberships = async (req: Request, res: Response) => } } */ - let memberships; - try { - const { organizationId } = req.params; + const { organizationId } = req.params; - memberships = await MembershipOrg.find({ + const memberships = await MembershipOrg.find({ organization: organizationId }).populate('user', '+publicKey'); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get organization memberships' - }); - } return res.status(200).send({ memberships @@ -128,26 +118,17 @@ export const updateOrganizationMembership = async (req: Request, res: Response) } } */ - let membership; - try { - const { membershipId } = req.params; - const { role } = req.body; - - membership = await MembershipOrg.findByIdAndUpdate( - membershipId, - { - role - }, { - new: true - } - ); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to update organization membership' - }); - } + const { membershipId } = req.params; + const { role } = req.body; + + const membership = await MembershipOrg.findByIdAndUpdate( + membershipId, + { + role + }, { + new: true + } + ); return res.status(200).send({ membership @@ -197,25 +178,16 @@ export const deleteOrganizationMembership = async (req: Request, res: Response) } } */ - let membership; - try { - const { membershipId } = req.params; - - // delete organization membership - membership = await deleteMembershipOrg({ - membershipOrgId: membershipId - }); + const { membershipId } = req.params; + + // delete organization membership + const membership = await deleteMembershipOrg({ + membershipOrgId: membershipId + }); - await updateSubscriptionOrgQuantity({ + await updateSubscriptionOrgQuantity({ organizationId: membership.organization.toString() }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to delete organization membership' - }); - } return res.status(200).send({ membership @@ -303,4 +275,4 @@ export const getOrganizationServiceAccounts = async (req: Request, res: Response return res.status(200).send({ serviceAccounts }); -} \ No newline at end of file +} diff --git a/backend/src/controllers/v2/serviceTokenDataController.ts b/backend/src/controllers/v2/serviceTokenDataController.ts index 597548f2fe..772ad8bde4 100644 --- a/backend/src/controllers/v2/serviceTokenDataController.ts +++ b/backend/src/controllers/v2/serviceTokenDataController.ts @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/node'; import { Request, Response } from 'express'; import crypto from 'crypto'; import bcrypt from 'bcrypt'; @@ -144,4 +143,4 @@ export const deleteServiceTokenData = async (req: Request, res: Response) => { return res.status(200).send({ serviceTokenData }); -} \ No newline at end of file +} diff --git a/backend/src/controllers/v2/signupController.ts b/backend/src/controllers/v2/signupController.ts index d9e9a0447f..cbaa7a6b6b 100644 --- a/backend/src/controllers/v2/signupController.ts +++ b/backend/src/controllers/v2/signupController.ts @@ -1,5 +1,4 @@ import { Request, Response } from 'express'; -import * as Sentry from '@sentry/node'; import { User, MembershipOrg } from '../../models'; import { completeAccount } from '../../helpers/user'; import { @@ -20,136 +19,128 @@ import { updateSubscriptionOrgQuantity } from '../../helpers/organization'; */ export const completeAccountSignup = async (req: Request, res: Response) => { let user, token, refreshToken; - try { - const { - email, - firstName, - lastName, - protectedKey, - protectedKeyIV, - protectedKeyTag, - publicKey, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt, - verifier, - organizationName - }: { - email: string; - firstName: string; - lastName: string; - protectedKey: string; - protectedKeyIV: string; - protectedKeyTag: string; - publicKey: string; - encryptedPrivateKey: string; - encryptedPrivateKeyIV: string; - encryptedPrivateKeyTag: string; - salt: string; - verifier: string; - organizationName: string; - } = req.body; + const { + email, + firstName, + lastName, + protectedKey, + protectedKeyIV, + protectedKeyTag, + publicKey, + encryptedPrivateKey, + encryptedPrivateKeyIV, + encryptedPrivateKeyTag, + salt, + verifier, + organizationName + }: { + email: string; + firstName: string; + lastName: string; + protectedKey: string; + protectedKeyIV: string; + protectedKeyTag: string; + publicKey: string; + encryptedPrivateKey: string; + encryptedPrivateKeyIV: string; + encryptedPrivateKeyTag: string; + salt: string; + verifier: string; + organizationName: string; + } = req.body; - // get user - user = await User.findOne({ email }); + // get user + user = await User.findOne({ email }); - if (!user || (user && user?.publicKey)) { - // case 1: user doesn't exist. - // case 2: user has already completed account - return res.status(403).send({ - error: 'Failed to complete account for complete user' - }); - } + if (!user || (user && user?.publicKey)) { + // case 1: user doesn't exist. + // case 2: user has already completed account + return res.status(403).send({ + error: 'Failed to complete account for complete user' + }); + } - // complete setting up user's account - user = await completeAccount({ - userId: user._id.toString(), - firstName, - lastName, - encryptionVersion: 2, - protectedKey, - protectedKeyIV, - protectedKeyTag, - publicKey, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt, - verifier - }); + // complete setting up user's account + user = await completeAccount({ + userId: user._id.toString(), + firstName, + lastName, + encryptionVersion: 2, + protectedKey, + protectedKeyIV, + protectedKeyTag, + publicKey, + encryptedPrivateKey, + encryptedPrivateKeyIV, + encryptedPrivateKeyTag, + salt, + verifier + }); - if (!user) - throw new Error('Failed to complete account for non-existent user'); // ensure user is non-null + if (!user) + throw new Error('Failed to complete account for non-existent user'); // ensure user is non-null - // initialize default organization and workspace - await initializeDefaultOrg({ - organizationName, - user - }); + // initialize default organization and workspace + await initializeDefaultOrg({ + organizationName, + user + }); - // update organization membership statuses that are - // invited to completed with user attached - const membershipsToUpdate = await MembershipOrg.find({ - inviteEmail: email, - status: INVITED - }); - - membershipsToUpdate.forEach(async (membership) => { - await updateSubscriptionOrgQuantity({ - organizationId: membership.organization.toString() - }); - }); + // update organization membership statuses that are + // invited to completed with user attached + const membershipsToUpdate = await MembershipOrg.find({ + inviteEmail: email, + status: INVITED + }); + + membershipsToUpdate.forEach(async (membership) => { + await updateSubscriptionOrgQuantity({ + organizationId: membership.organization.toString() + }); + }); - // update organization membership statuses that are - // invited to completed with user attached - await MembershipOrg.updateMany( - { - inviteEmail: email, - status: INVITED - }, - { - user, - status: ACCEPTED - } - ); + // update organization membership statuses that are + // invited to completed with user attached + await MembershipOrg.updateMany( + { + inviteEmail: email, + status: INVITED + }, + { + user, + status: ACCEPTED + } + ); - // issue tokens - const tokens = await issueAuthTokens({ - userId: user._id.toString() - }); + // issue tokens + const tokens = await issueAuthTokens({ + userId: user._id.toString() + }); - token = tokens.token; + token = tokens.token; - // sending a welcome email to new users - if (await getLoopsApiKey()) { - await standardRequest.post("https://app.loops.so/api/v1/events/send", { - "email": email, - "eventName": "Sign Up", - "firstName": firstName, - "lastName": lastName - }, { - headers: { - "Accept": "application/json", - "Authorization": "Bearer " + (await getLoopsApiKey()) - }, - }); - } + // sending a welcome email to new users + if (await getLoopsApiKey()) { + await standardRequest.post("https://app.loops.so/api/v1/events/send", { + "email": email, + "eventName": "Sign Up", + "firstName": firstName, + "lastName": lastName + }, { + headers: { + "Accept": "application/json", + "Authorization": "Bearer " + (await getLoopsApiKey()) + }, + }); + } - // store (refresh) token in httpOnly cookie - res.cookie('jid', tokens.refreshToken, { - httpOnly: true, - path: '/', - sameSite: 'strict', - secure: await getHttpsEnabled() - }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to complete account setup' - }); - } + // store (refresh) token in httpOnly cookie + res.cookie('jid', tokens.refreshToken, { + httpOnly: true, + path: '/', + sameSite: 'strict', + secure: await getHttpsEnabled() + }); return res.status(200).send({ message: 'Successfully set up account', @@ -167,109 +158,101 @@ export const completeAccountSignup = async (req: Request, res: Response) => { */ export const completeAccountInvite = async (req: Request, res: Response) => { let user, token, refreshToken; - try { - const { - email, - firstName, - lastName, - protectedKey, - protectedKeyIV, - protectedKeyTag, - publicKey, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt, - verifier - } = req.body; + const { + email, + firstName, + lastName, + protectedKey, + protectedKeyIV, + protectedKeyTag, + publicKey, + encryptedPrivateKey, + encryptedPrivateKeyIV, + encryptedPrivateKeyTag, + salt, + verifier + } = req.body; - // get user - user = await User.findOne({ email }); + // get user + user = await User.findOne({ email }); - if (!user || (user && user?.publicKey)) { - // case 1: user doesn't exist. - // case 2: user has already completed account - return res.status(403).send({ - error: 'Failed to complete account for complete user' - }); - } + if (!user || (user && user?.publicKey)) { + // case 1: user doesn't exist. + // case 2: user has already completed account + return res.status(403).send({ + error: 'Failed to complete account for complete user' + }); + } - const membershipOrg = await MembershipOrg.findOne({ - inviteEmail: email, - status: INVITED - }); + const membershipOrg = await MembershipOrg.findOne({ + inviteEmail: email, + status: INVITED + }); - if (!membershipOrg) throw new Error('Failed to find invitations for email'); + if (!membershipOrg) throw new Error('Failed to find invitations for email'); - // complete setting up user's account - user = await completeAccount({ - userId: user._id.toString(), - firstName, - lastName, - encryptionVersion: 2, - protectedKey, - protectedKeyIV, - protectedKeyTag, - publicKey, - encryptedPrivateKey, - encryptedPrivateKeyIV, - encryptedPrivateKeyTag, - salt, - verifier - }); + // complete setting up user's account + user = await completeAccount({ + userId: user._id.toString(), + firstName, + lastName, + encryptionVersion: 2, + protectedKey, + protectedKeyIV, + protectedKeyTag, + publicKey, + encryptedPrivateKey, + encryptedPrivateKeyIV, + encryptedPrivateKeyTag, + salt, + verifier + }); - if (!user) - throw new Error('Failed to complete account for non-existent user'); - - // update organization membership statuses that are - // invited to completed with user attached - const membershipsToUpdate = await MembershipOrg.find({ - inviteEmail: email, - status: INVITED - }); - - membershipsToUpdate.forEach(async (membership) => { - await updateSubscriptionOrgQuantity({ - organizationId: membership.organization.toString() - }); - }); + if (!user) + throw new Error('Failed to complete account for non-existent user'); + + // update organization membership statuses that are + // invited to completed with user attached + const membershipsToUpdate = await MembershipOrg.find({ + inviteEmail: email, + status: INVITED + }); + + membershipsToUpdate.forEach(async (membership) => { + await updateSubscriptionOrgQuantity({ + organizationId: membership.organization.toString() + }); + }); - await MembershipOrg.updateMany( - { - inviteEmail: email, - status: INVITED - }, - { - user, - status: ACCEPTED - } - ); + await MembershipOrg.updateMany( + { + inviteEmail: email, + status: INVITED + }, + { + user, + status: ACCEPTED + } + ); - // issue tokens - const tokens = await issueAuthTokens({ - userId: user._id.toString() - }); + // issue tokens + const tokens = await issueAuthTokens({ + userId: user._id.toString() + }); - token = tokens.token; + token = tokens.token; - // store (refresh) token in httpOnly cookie - res.cookie('jid', tokens.refreshToken, { - httpOnly: true, - path: '/', - sameSite: 'strict', - secure: await getHttpsEnabled() - }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to complete account setup' - }); - } + // store (refresh) token in httpOnly cookie + res.cookie('jid', tokens.refreshToken, { + httpOnly: true, + path: '/', + sameSite: 'strict', + secure: await getHttpsEnabled() + }); return res.status(200).send({ message: 'Successfully set up account', user, token }); -}; \ No newline at end of file +}; diff --git a/backend/src/controllers/v2/tagController.ts b/backend/src/controllers/v2/tagController.ts index 199f2401e9..0175b359a8 100644 --- a/backend/src/controllers/v2/tagController.ts +++ b/backend/src/controllers/v2/tagController.ts @@ -1,5 +1,4 @@ import { Request, Response } from 'express'; -import * as Sentry from '@sentry/node'; import { Types } from 'mongoose'; import { Membership, Secret, @@ -69,4 +68,4 @@ export const getWorkspaceTags = async (req: Request, res: Response) => { return res.json({ workspaceTags }) -} \ No newline at end of file +} diff --git a/backend/src/controllers/v2/usersController.ts b/backend/src/controllers/v2/usersController.ts index 9940ddc56b..2d5cdc51ac 100644 --- a/backend/src/controllers/v2/usersController.ts +++ b/backend/src/controllers/v2/usersController.ts @@ -1,5 +1,4 @@ import { Request, Response } from 'express'; -import * as Sentry from '@sentry/node'; import { User, MembershipOrg @@ -37,18 +36,9 @@ export const getMe = async (req: Request, res: Response) => { } } */ - let user; - try { - user = await User - .findById(req.user._id) - .select('+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag'); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get current user' - }); - } + const user = await User + .findById(req.user._id) + .select('+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag'); return res.status(200).send({ user @@ -64,29 +54,20 @@ export const getMe = async (req: Request, res: Response) => { * @returns */ export const updateMyMfaEnabled = async (req: Request, res: Response) => { - let user; - try { - const { isMfaEnabled }: { isMfaEnabled: boolean } = req.body; - req.user.isMfaEnabled = isMfaEnabled; - - if (isMfaEnabled) { - // TODO: adapt this route/controller - // to work for different forms of MFA - req.user.mfaMethods = ['email']; - } else { - req.user.mfaMethods = []; - } - - await req.user.save(); - - user = req.user; - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: "Failed to update current user's MFA status" - }); + const { isMfaEnabled }: { isMfaEnabled: boolean } = req.body; + req.user.isMfaEnabled = isMfaEnabled; + + if (isMfaEnabled) { + // TODO: adapt this route/controller + // to work for different forms of MFA + req.user.mfaMethods = ['email']; + } else { + req.user.mfaMethods = []; } + + await req.user.save(); + + const user = req.user; return res.status(200).send({ user @@ -126,22 +107,13 @@ export const getMyOrganizations = async (req: Request, res: Response) => { } } */ - let organizations; - try { - organizations = ( - await MembershipOrg.find({ - user: req.user._id - }).populate('organization') - ).map((m) => m.organization); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: "Failed to get current user's organizations" - }); - } + const organizations = ( + await MembershipOrg.find({ + user: req.user._id + }).populate('organization') + ).map((m) => m.organization); return res.status(200).send({ organizations }); -} \ No newline at end of file +} diff --git a/backend/src/controllers/v2/workspaceController.ts b/backend/src/controllers/v2/workspaceController.ts index ea673d428e..2f67bea86b 100644 --- a/backend/src/controllers/v2/workspaceController.ts +++ b/backend/src/controllers/v2/workspaceController.ts @@ -1,5 +1,4 @@ import { Request, Response } from 'express'; -import * as Sentry from '@sentry/node'; import { Types } from 'mongoose'; import { Workspace, @@ -47,66 +46,57 @@ interface V2PushSecret { */ export const pushWorkspaceSecrets = async (req: Request, res: Response) => { // upload (encrypted) secrets to workspace with id [workspaceId] - try { - const postHogClient = await TelemetryService.getPostHogClient(); - let { secrets }: { secrets: V2PushSecret[] } = req.body; - const { keys, environment, channel } = req.body; - const { workspaceId } = req.params; + const postHogClient = await TelemetryService.getPostHogClient(); + let { secrets }: { secrets: V2PushSecret[] } = req.body; + const { keys, environment, channel } = req.body; + const { workspaceId } = req.params; - // validate environment - const workspaceEnvs = req.membership.workspace.environments; - if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { - throw new Error('Failed to validate environment'); - } + // validate environment + const workspaceEnvs = req.membership.workspace.environments; + if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { + throw new Error('Failed to validate environment'); + } - // sanitize secrets - secrets = secrets.filter( - (s: V2PushSecret) => s.secretKeyCiphertext !== '' && s.secretValueCiphertext !== '' - ); + // sanitize secrets + secrets = secrets.filter( + (s: V2PushSecret) => s.secretKeyCiphertext !== '' && s.secretValueCiphertext !== '' + ); - await push({ - userId: req.user._id, - workspaceId, - environment, - secrets, - channel: channel ? channel : 'cli', - ipAddress: req.ip - }); + await push({ + userId: req.user._id, + workspaceId, + environment, + secrets, + channel: channel ? channel : 'cli', + ipAddress: req.ip + }); - await pushKeys({ - userId: req.user._id, - workspaceId, - keys - }); + await pushKeys({ + userId: req.user._id, + workspaceId, + keys + }); - if (postHogClient) { - postHogClient.capture({ - event: 'secrets pushed', - distinctId: req.user.email, - properties: { - numberOfSecrets: secrets.length, - environment, - workspaceId, - channel: channel ? channel : 'cli' - } - }); - } + if (postHogClient) { + postHogClient.capture({ + event: 'secrets pushed', + distinctId: req.user.email, + properties: { + numberOfSecrets: secrets.length, + environment, + workspaceId, + channel: channel ? channel : 'cli' + } + }); + } - // trigger event - push secrets - EventService.handleEvent({ - event: eventPushSecrets({ - workspaceId: new Types.ObjectId(workspaceId), - environment - }) - }); - - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to upload workspace secrets' - }); - } + // trigger event - push secrets + EventService.handleEvent({ + event: eventPushSecrets({ + workspaceId: new Types.ObjectId(workspaceId), + environment + }) + }); return res.status(200).send({ message: 'Successfully uploaded workspace secrets' @@ -121,57 +111,49 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => { * @returns */ export const pullSecrets = async (req: Request, res: Response) => { - let secrets; - try { - const postHogClient = await TelemetryService.getPostHogClient(); - const environment: string = req.query.environment as string; - const channel: string = req.query.channel as string; - const { workspaceId } = req.params; + const postHogClient = await TelemetryService.getPostHogClient(); + const environment: string = req.query.environment as string; + const channel: string = req.query.channel as string; + const { workspaceId } = req.params; - let userId; - if (req.user) { - userId = req.user._id.toString(); - } else if (req.serviceTokenData) { - userId = req.serviceTokenData.user.toString(); - } - // validate environment - const workspaceEnvs = req.membership.workspace.environments; - if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { - throw new Error('Failed to validate environment'); - } + let userId; + if (req.user) { + userId = req.user._id.toString(); + } else if (req.serviceTokenData) { + userId = req.serviceTokenData.user.toString(); + } + // validate environment + const workspaceEnvs = req.membership.workspace.environments; + if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { + throw new Error('Failed to validate environment'); + } - secrets = await pull({ - userId, - workspaceId, - environment, - channel: channel ? channel : 'cli', - ipAddress: req.ip - }); + let secrets = await pull({ + userId, + workspaceId, + environment, + channel: channel ? channel : 'cli', + ipAddress: req.ip + }); - if (channel !== 'cli') { - secrets = reformatPullSecrets({ secrets }); - } + if (channel !== 'cli') { + // FIX: Fix this any + secrets = reformatPullSecrets({ secrets }) as any; + } - if (postHogClient) { - // capture secrets pushed event in production - postHogClient.capture({ - distinctId: req.user.email, - event: 'secrets pulled', - properties: { - numberOfSecrets: secrets.length, - environment, - workspaceId, - channel: channel ? channel : 'cli' - } - }); - } - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to pull workspace secrets' - }); - } + if (postHogClient) { + // capture secrets pushed event in production + postHogClient.capture({ + distinctId: req.user.email, + event: 'secrets pulled', + properties: { + numberOfSecrets: secrets.length, + environment, + workspaceId, + channel: channel ? channel : 'cli' + } + }); + } return res.status(200).send({ secrets @@ -208,22 +190,14 @@ export const getWorkspaceKey = async (req: Request, res: Response) => { } */ let key; - try { - const { workspaceId } = req.params; + const { workspaceId } = req.params; - key = await Key.findOne({ - workspace: workspaceId, - receiver: req.user._id - }).populate('sender', '+publicKey'); + key = await Key.findOne({ + workspace: workspaceId, + receiver: req.user._id + }).populate('sender', '+publicKey'); - if (!key) throw new Error('Failed to find workspace key'); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get workspace key' - }); - } + if (!key) throw new Error('Failed to find workspace key'); return res.status(200).json(key); } @@ -231,23 +205,13 @@ export const getWorkspaceServiceTokenData = async ( req: Request, res: Response ) => { - let serviceTokenData; - try { - const { workspaceId } = req.params; + const { workspaceId } = req.params; - serviceTokenData = await ServiceTokenData - .find({ - workspace: workspaceId - }) - .select('+encryptedKey +iv +tag'); - - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get workspace service token data' - }); - } + const serviceTokenData = await ServiceTokenData + .find({ + workspace: workspaceId + }) + .select('+encryptedKey +iv +tag'); return res.status(200).send({ serviceTokenData @@ -294,20 +258,11 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => { } } */ - let memberships; - try { - const { workspaceId } = req.params; + const { workspaceId } = req.params; - memberships = await Membership.find({ - workspace: workspaceId - }).populate('user', '+publicKey'); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get workspace memberships' - }); - } + const memberships = await Membership.find({ + workspace: workspaceId + }).populate('user', '+publicKey'); return res.status(200).send({ memberships @@ -374,29 +329,20 @@ export const updateWorkspaceMembership = async (req: Request, res: Response) => } } */ - let membership; - try { - const { - membershipId - } = req.params; - const { role } = req.body; - - membership = await Membership.findByIdAndUpdate( - membershipId, - { - role - }, { - new: true - } - ); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to update workspace membership' - }); - } - + const { + membershipId + } = req.params; + const { role } = req.body; + + const membership = await Membership.findByIdAndUpdate( + membershipId, + { + role + }, { + new: true + } + ); + return res.status(200).send({ membership }); @@ -445,27 +391,18 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) => } } */ - let membership; - try { - const { - membershipId - } = req.params; - - membership = await Membership.findByIdAndDelete(membershipId); - - if (!membership) throw new Error('Failed to delete workspace membership'); - - await Key.deleteMany({ - receiver: membership.user, - workspace: membership.workspace - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to delete workspace membership' - }); - } + const { + membershipId + } = req.params; + + const membership = await Membership.findByIdAndDelete(membershipId); + + if (!membership) throw new Error('Failed to delete workspace membership'); + + await Key.deleteMany({ + receiver: membership.user, + workspace: membership.workspace + }); return res.status(200).send({ membership @@ -479,32 +416,23 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) => * @returns */ export const toggleAutoCapitalization = async (req: Request, res: Response) => { - let workspace; - try { - const { workspaceId } = req.params; - const { autoCapitalization } = req.body; + const { workspaceId } = req.params; + const { autoCapitalization } = req.body; - workspace = await Workspace.findOneAndUpdate( - { - _id: workspaceId - }, - { - autoCapitalization - }, - { - new: true - } - ); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to change autoCapitalization setting' - }); - } + const workspace = await Workspace.findOneAndUpdate( + { + _id: workspaceId + }, + { + autoCapitalization + }, + { + new: true + } + ); return res.status(200).send({ message: 'Successfully changed autoCapitalization setting', workspace }); -}; \ No newline at end of file +}; diff --git a/backend/src/ee/controllers/v1/actionController.ts b/backend/src/ee/controllers/v1/actionController.ts index b136b0fa4b..4b3117b207 100644 --- a/backend/src/ee/controllers/v1/actionController.ts +++ b/backend/src/ee/controllers/v1/actionController.ts @@ -1,5 +1,4 @@ import { Request, Response } from 'express'; -import * as Sentry from '@sentry/node'; import { Action, SecretVersion } from '../../models'; import { ActionNotFoundError } from '../../../utils/errors'; @@ -28,4 +27,4 @@ export const getAction = async (req: Request, res: Response) => { return res.status(200).send({ action }); -} \ No newline at end of file +} diff --git a/backend/src/ee/controllers/v1/cloudProductsController.ts b/backend/src/ee/controllers/v1/cloudProductsController.ts index 54584ce821..c7d60ca1a6 100644 --- a/backend/src/ee/controllers/v1/cloudProductsController.ts +++ b/backend/src/ee/controllers/v1/cloudProductsController.ts @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/node'; import { Request, Response } from 'express'; import { EELicenseService } from '../../services'; import { getLicenseServerUrl } from '../../../config'; @@ -12,23 +11,18 @@ import { licenseServerKeyRequest } from '../../../config/request'; * @returns */ export const getCloudProducts = async (req: Request, res: Response) => { - try { - const billingCycle = req.query['billing-cycle'] as string; + const billingCycle = req.query['billing-cycle'] as string; - if (EELicenseService.instanceType === 'cloud') { - const { data } = await licenseServerKeyRequest.get( - `${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}` - ); + if (EELicenseService.instanceType === 'cloud') { + const { data } = await licenseServerKeyRequest.get( + `${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}` + ); - return res.status(200).send(data); - } - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); + return res.status(200).send(data); } return res.status(200).send({ head: [], rows: [] }); -} \ No newline at end of file +} diff --git a/backend/src/ee/controllers/v1/secretController.ts b/backend/src/ee/controllers/v1/secretController.ts index 3d1e4b3b31..9ba3c77e39 100644 --- a/backend/src/ee/controllers/v1/secretController.ts +++ b/backend/src/ee/controllers/v1/secretController.ts @@ -1,5 +1,4 @@ import { Request, Response } from "express"; -import * as Sentry from "@sentry/node"; import { Secret } from "../../../models"; import { SecretVersion } from "../../models"; import { EESecretService } from "../../services"; @@ -55,29 +54,20 @@ export const getSecretVersions = async (req: Request, res: Response) => { } } */ - let secretVersions; - try { - const { secretId, workspaceId, environment, folderId } = req.params; + const { secretId, workspaceId, environment, folderId } = req.params; - const offset: number = parseInt(req.query.offset as string); - const limit: number = parseInt(req.query.limit as string); + const offset: number = parseInt(req.query.offset as string); + const limit: number = parseInt(req.query.limit as string); - secretVersions = await SecretVersion.find({ - secret: secretId, - workspace: workspaceId, - environment, - folder: folderId, - }) - .sort({ createdAt: -1 }) - .skip(offset) - .limit(limit); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: "Failed to get secret versions", - }); - } + const secretVersions = await SecretVersion.find({ + secret: secretId, + workspace: workspaceId, + environment, + folder: folderId, + }) + .sort({ createdAt: -1 }) + .skip(offset) + .limit(limit); return res.status(200).send({ secretVersions, @@ -139,74 +129,45 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => { } } */ - let secret; - try { - const { secretId } = req.params; - const { version } = req.body; + const { secretId } = req.params; + const { version } = req.body; - // validate secret version - const oldSecretVersion = await SecretVersion.findOne({ - secret: secretId, - version, - }).select("+secretBlindIndex"); + // validate secret version + const oldSecretVersion = await SecretVersion.findOne({ + secret: secretId, + version, + }).select("+secretBlindIndex"); - if (!oldSecretVersion) throw new Error("Failed to find secret version"); + if (!oldSecretVersion) throw new Error("Failed to find secret version"); - const { - workspace, - type, - user, - environment, - secretBlindIndex, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretValueCiphertext, - secretValueIV, - secretValueTag, - folder, - algorithm, - keyEncoding, - } = oldSecretVersion; + const { + workspace, + type, + user, + environment, + secretBlindIndex, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag, + algorithm, + folder, + keyEncoding, + } = oldSecretVersion; - // update secret - secret = await Secret.findByIdAndUpdate( - secretId, - { - $inc: { - version: 1, - }, - workspace, - type, - user, - environment, - ...(secretBlindIndex ? { secretBlindIndex } : {}), - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretValueCiphertext, - secretValueIV, - secretValueTag, - folderId: folder, - algorithm, - keyEncoding, + // update secret + const secret = await Secret.findByIdAndUpdate( + secretId, + { + $inc: { + version: 1, }, - { - new: true, - } - ); - - if (!secret) throw new Error("Failed to find and update secret"); - - // add new secret version - await new SecretVersion({ - secret: secretId, - version: secret.version, workspace, type, user, environment, - isDeleted: false, ...(secretBlindIndex ? { secretBlindIndex } : {}), secretKeyCiphertext, secretKeyIV, @@ -214,24 +175,44 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => { secretValueCiphertext, secretValueIV, secretValueTag, - folder, + folderId: folder, algorithm, keyEncoding, - }).save(); + }, + { + new: true, + } + ); - // take secret snapshot - await EESecretService.takeSecretSnapshot({ - workspaceId: secret.workspace, - environment, - folderId: folder, - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: "Failed to roll back secret version", - }); - } + if (!secret) throw new Error("Failed to find and update secret"); + + // add new secret version + await new SecretVersion({ + secret: secretId, + version: secret.version, + workspace, + type, + user, + environment, + isDeleted: false, + ...(secretBlindIndex ? { secretBlindIndex } : {}), + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag, + folder, + algorithm, + keyEncoding, + }).save(); + + // take secret snapshot + await EESecretService.takeSecretSnapshot({ + workspaceId: secret.workspace, + environment, + folderId: folder, + }); return res.status(200).send({ secret, diff --git a/backend/src/ee/controllers/v1/secretSnapshotController.ts b/backend/src/ee/controllers/v1/secretSnapshotController.ts index a6ee2ce4d7..34640506a0 100644 --- a/backend/src/ee/controllers/v1/secretSnapshotController.ts +++ b/backend/src/ee/controllers/v1/secretSnapshotController.ts @@ -1,5 +1,4 @@ import { Request, Response } from "express"; -import * as Sentry from "@sentry/node"; import { ISecretVersion, SecretSnapshot, @@ -13,23 +12,14 @@ import { * @returns */ export const getSecretSnapshot = async (req: Request, res: Response) => { - let secretSnapshot; - try { - const { secretSnapshotId } = req.params; + const { secretSnapshotId } = req.params; + const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId) + .lean() + .populate<{ secretVersions: ISecretVersion[] }>("secretVersions") + .populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion"); - secretSnapshot = await SecretSnapshot.findById(secretSnapshotId) - .lean() - .populate<{ secretVersions: ISecretVersion[] }>("secretVersions") - .populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion"); + if (!secretSnapshot) throw new Error("Failed to find secret snapshot"); - if (!secretSnapshot) throw new Error("Failed to find secret snapshot"); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: "Failed to get secret snapshot", - }); - } const folderId = secretSnapshot.folderId; // to show only the folder required secrets secretSnapshot.secretVersions = secretSnapshot.secretVersions.filter( diff --git a/backend/src/ee/controllers/v1/stripeController.ts b/backend/src/ee/controllers/v1/stripeController.ts index 69858c94fe..172df32205 100644 --- a/backend/src/ee/controllers/v1/stripeController.ts +++ b/backend/src/ee/controllers/v1/stripeController.ts @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/node'; import { Request, Response } from 'express'; import Stripe from 'stripe'; import { getStripeSecretKey, getStripeWebhookSecret } from '../../../config'; @@ -10,26 +9,17 @@ import { getStripeSecretKey, getStripeWebhookSecret } from '../../../config'; * @returns */ export const handleWebhook = async (req: Request, res: Response) => { - let event; - try { - const stripe = new Stripe(await getStripeSecretKey(), { - apiVersion: '2022-08-01' - }); + const stripe = new Stripe(await getStripeSecretKey(), { + apiVersion: '2022-08-01' + }); - // check request for valid stripe signature - const sig = req.headers['stripe-signature'] as string; - event = stripe.webhooks.constructEvent( - req.body, - sig, - await getStripeWebhookSecret() - ); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - error: 'Failed to process webhook' - }); - } + // check request for valid stripe signature + const sig = req.headers['stripe-signature'] as string; + const event = stripe.webhooks.constructEvent( + req.body, + sig, + await getStripeWebhookSecret() + ); switch (event.type) { case '': diff --git a/backend/src/ee/controllers/v1/workspaceController.ts b/backend/src/ee/controllers/v1/workspaceController.ts index 409da56b9a..166171ccdc 100644 --- a/backend/src/ee/controllers/v1/workspaceController.ts +++ b/backend/src/ee/controllers/v1/workspaceController.ts @@ -1,5 +1,4 @@ import { Request, Response } from "express"; -import * as Sentry from "@sentry/node"; import { PipelineStage, Types } from "mongoose"; import { Secret } from "../../../models"; import { @@ -69,29 +68,20 @@ export const getWorkspaceSecretSnapshots = async ( } } */ - let secretSnapshots; - try { - const { workspaceId } = req.params; - const { environment, folderId } = req.query; + const { workspaceId } = req.params; + const { environment, folderId } = req.query; - const offset: number = parseInt(req.query.offset as string); - const limit: number = parseInt(req.query.limit as string); + const offset: number = parseInt(req.query.offset as string); + const limit: number = parseInt(req.query.limit as string); - secretSnapshots = await SecretSnapshot.find({ - workspace: workspaceId, - environment, - folderId: folderId || "root", - }) - .sort({ createdAt: -1 }) - .skip(offset) - .limit(limit); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: "Failed to get secret snapshots", - }); - } + const secretSnapshots = await SecretSnapshot.find({ + workspace: workspaceId, + environment, + folderId: folderId || "root", + }) + .sort({ createdAt: -1 }) + .skip(offset) + .limit(limit); return res.status(200).send({ secretSnapshots, @@ -107,23 +97,14 @@ export const getWorkspaceSecretSnapshotsCount = async ( req: Request, res: Response ) => { - let count; - try { - const { workspaceId } = req.params; - const { environment, folderId } = req.query; + const { workspaceId } = req.params; + const { environment, folderId } = req.query; - count = await SecretSnapshot.countDocuments({ - workspace: workspaceId, - environment, - folderId: folderId || "root", - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: "Failed to count number of secret snapshots", - }); - } + const count = await SecretSnapshot.countDocuments({ + workspace: workspaceId, + environment, + folderId: folderId || "root", + }); return res.status(200).send({ count, @@ -191,324 +172,315 @@ export const rollbackWorkspaceSecretSnapshot = async ( } */ - let secrets; - try { - const { workspaceId } = req.params; - const { version, environment, folderId = "root" } = req.body; + const { workspaceId } = req.params; + const { version, environment, folderId = "root" } = req.body; - // validate secret snapshot - const secretSnapshot = await SecretSnapshot.findOne({ - workspace: workspaceId, - version, - environment, - folderId: folderId, + // validate secret snapshot + const secretSnapshot = await SecretSnapshot.findOne({ + workspace: workspaceId, + version, + environment, + folderId: folderId, + }) + .populate<{ secretVersions: ISecretVersion[] }>({ + path: "secretVersions", + select: "+secretBlindIndex", }) - .populate<{ secretVersions: ISecretVersion[] }>({ - path: "secretVersions", - select: "+secretBlindIndex", - }) - .populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion"); + .populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion"); - if (!secretSnapshot) throw new Error("Failed to find secret snapshot"); + if (!secretSnapshot) throw new Error("Failed to find secret snapshot"); - const snapshotFolderTree = secretSnapshot.folderVersion; - const latestFolderTree = await Folder.findOne({ - workspace: workspaceId, - environment, - }); + const snapshotFolderTree = secretSnapshot.folderVersion; + const latestFolderTree = await Folder.findOne({ + workspace: workspaceId, + environment, + }); - const latestFolderVersion = await FolderVersion.findOne({ - environment, - workspace: workspaceId, - "nodes.id": folderId, - }).sort({ "nodes.version": -1 }); + const latestFolderVersion = await FolderVersion.findOne({ + environment, + workspace: workspaceId, + "nodes.id": folderId, + }).sort({ "nodes.version": -1 }); - const oldSecretVersionsObj: Record = {}; - const secretIds: Types.ObjectId[] = []; - const folderIds: string[] = [folderId]; + const oldSecretVersionsObj: Record = {}; + const secretIds: Types.ObjectId[] = []; + const folderIds: string[] = [folderId]; - secretSnapshot.secretVersions.forEach((snapSecVer) => { - oldSecretVersionsObj[snapSecVer.secret.toString()] = snapSecVer; - secretIds.push(snapSecVer.secret); - }); + secretSnapshot.secretVersions.forEach((snapSecVer) => { + oldSecretVersionsObj[snapSecVer.secret.toString()] = snapSecVer; + secretIds.push(snapSecVer.secret); + }); - // the parent node from current latest one - // this will be modified according to the snapshot and latest snapshots - const newFolderTree = - latestFolderTree && searchByFolderId(latestFolderTree.nodes, folderId); + // the parent node from current latest one + // this will be modified according to the snapshot and latest snapshots + const newFolderTree = + latestFolderTree && searchByFolderId(latestFolderTree.nodes, folderId); - if (newFolderTree) { - newFolderTree.children = snapshotFolderTree?.nodes?.children || []; - const queue = [newFolderTree]; - // a bfs algorithm in which we take the latest snapshots of all the folders in a level + if (newFolderTree) { + newFolderTree.children = snapshotFolderTree?.nodes?.children || []; + const queue = [newFolderTree]; + // a bfs algorithm in which we take the latest snapshots of all the folders in a level + while (queue.length) { + const groupByFolderId: Record = {}; + // the original queue is popped out completely to get what ever in a level + // subqueue is filled with all the children thus next level folders + // subQueue will then be transfered to the oriinal queue + const subQueue: TFolderSchema[] = []; + // get everything inside a level while (queue.length) { - const groupByFolderId: Record = {}; - // the original queue is popped out completely to get what ever in a level - // subqueue is filled with all the children thus next level folders - // subQueue will then be transfered to the oriinal queue - const subQueue: TFolderSchema[] = []; - // get everything inside a level - while (queue.length) { - const folder = queue.pop() as TFolderSchema; - folder.children.forEach((el) => { - folderIds.push(el.id); // push ids and data into queu - subQueue.push(el); - // to modify the original tree very fast we keep a reference object - // key with folder id and pointing to the various nodes - groupByFolderId[el.id] = el; - }); - } - // get latest snapshots of all the folder - const matchWsFoldersPipeline = { - $match: { - workspace: new Types.ObjectId(workspaceId), - environment, - folderId: { - $in: Object.keys(groupByFolderId), - }, - }, - }; - const sortByFolderIdAndVersion: PipelineStage = { - $sort: { folderId: 1, version: -1 }, - }; - const pickLatestVersionOfEachFolder = { - $group: { - _id: "$folderId", - latestVersion: { $first: "$version" }, - doc: { - $first: "$$ROOT", - }, - }, - }; - const populateSecVersion = { - $lookup: { - from: SecretVersion.collection.name, - localField: "doc.secretVersions", - foreignField: "_id", - as: "doc.secretVersions", - }, - }; - const populateFolderVersion = { - $lookup: { - from: FolderVersion.collection.name, - localField: "doc.folderVersion", - foreignField: "_id", - as: "doc.folderVersion", - }, - }; - const unwindFolderVerField = { - $unwind: { - path: "$doc.folderVersion", - preserveNullAndEmptyArrays: true, - }, - }; - const latestSnapshotsByFolders: Array<{ doc: typeof secretSnapshot }> = - await SecretSnapshot.aggregate([ - matchWsFoldersPipeline, - sortByFolderIdAndVersion, - pickLatestVersionOfEachFolder, - populateSecVersion, - populateFolderVersion, - unwindFolderVerField, - ]); - - // recursive snapshotting each level - latestSnapshotsByFolders.forEach((snap) => { - // mutate the folder tree to update the nodes to the latest version tree - // we are reconstructing the folder tree by latest snapshots here - if (groupByFolderId[snap.doc.folderId]) { - groupByFolderId[snap.doc.folderId].children = - snap.doc?.folderVersion?.nodes?.children || []; - } - - // push all children of next level snapshots - if (snap.doc.folderVersion?.nodes?.children) { - queue.push(...snap.doc.folderVersion.nodes.children); - } - - snap.doc.secretVersions.forEach((snapSecVer) => { - // record all the secrets - oldSecretVersionsObj[snapSecVer.secret.toString()] = snapSecVer; - secretIds.push(snapSecVer.secret); - }); + const folder = queue.pop() as TFolderSchema; + folder.children.forEach((el) => { + folderIds.push(el.id); // push ids and data into queu + subQueue.push(el); + // to modify the original tree very fast we keep a reference object + // key with folder id and pointing to the various nodes + groupByFolderId[el.id] = el; }); - - queue.push(...subQueue); } - } - - // TODO: fix any - const latestSecretVersionIds = await getLatestSecretVersionIds({ - secretIds, - }); - - // TODO: fix any - const latestSecretVersions: any = ( - await SecretVersion.find( - { - _id: { - $in: latestSecretVersionIds.map((s) => s.versionId), + // get latest snapshots of all the folder + const matchWsFoldersPipeline = { + $match: { + workspace: new Types.ObjectId(workspaceId), + environment, + folderId: { + $in: Object.keys(groupByFolderId), }, }, - "secret version" - ) - ).reduce( - (accumulator, s) => ({ - ...accumulator, - [`${s.secret.toString()}`]: s, - }), - {} - ); + }; + const sortByFolderIdAndVersion: PipelineStage = { + $sort: { folderId: 1, version: -1 }, + }; + const pickLatestVersionOfEachFolder = { + $group: { + _id: "$folderId", + latestVersion: { $first: "$version" }, + doc: { + $first: "$$ROOT", + }, + }, + }; + const populateSecVersion = { + $lookup: { + from: SecretVersion.collection.name, + localField: "doc.secretVersions", + foreignField: "_id", + as: "doc.secretVersions", + }, + }; + const populateFolderVersion = { + $lookup: { + from: FolderVersion.collection.name, + localField: "doc.folderVersion", + foreignField: "_id", + as: "doc.folderVersion", + }, + }; + const unwindFolderVerField = { + $unwind: { + path: "$doc.folderVersion", + preserveNullAndEmptyArrays: true, + }, + }; + const latestSnapshotsByFolders: Array<{ doc: typeof secretSnapshot }> = + await SecretSnapshot.aggregate([ + matchWsFoldersPipeline, + sortByFolderIdAndVersion, + pickLatestVersionOfEachFolder, + populateSecVersion, + populateFolderVersion, + unwindFolderVerField, + ]); - const secDelQuery: Record = { - workspace: workspaceId, - environment, - // undefined means root thus collect all secrets - }; - if (folderId !== "root" && folderIds.length) - secDelQuery.folder = { $in: folderIds }; + // recursive snapshotting each level + latestSnapshotsByFolders.forEach((snap) => { + // mutate the folder tree to update the nodes to the latest version tree + // we are reconstructing the folder tree by latest snapshots here + if (groupByFolderId[snap.doc.folderId]) { + groupByFolderId[snap.doc.folderId].children = + snap.doc?.folderVersion?.nodes?.children || []; + } - // delete existing secrets - await Secret.deleteMany(secDelQuery); - await Folder.deleteOne({ - workspace: workspaceId, - environment, - }); + // push all children of next level snapshots + if (snap.doc.folderVersion?.nodes?.children) { + queue.push(...snap.doc.folderVersion.nodes.children); + } - // add secrets - secrets = await Secret.insertMany( - Object.keys(oldSecretVersionsObj).map((sv) => { - const { - secret: secretId, - workspace, - type, - user, - environment, - secretBlindIndex, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretValueCiphertext, - secretValueIV, - secretValueTag, - createdAt, - algorithm, - keyEncoding, - folder: secFolderId, - } = oldSecretVersionsObj[sv]; - - return { - _id: secretId, - version: latestSecretVersions[secretId.toString()].version + 1, - workspace, - type, - user, - environment, - secretBlindIndex: secretBlindIndex ?? undefined, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretCommentCiphertext: "", - secretCommentIV: "", - secretCommentTag: "", - createdAt, - algorithm, - keyEncoding, - folder: secFolderId, - }; - }) - ); - - // add secret versions - const secretV = await SecretVersion.insertMany( - secrets.map( - ({ - _id, - version, - workspace, - type, - user, - environment, - secretBlindIndex, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretValueCiphertext, - secretValueIV, - secretValueTag, - algorithm, - keyEncoding, - folder: secFolderId, - }) => ({ - _id: new Types.ObjectId(), - secret: _id, - version, - workspace, - type, - user, - environment, - isDeleted: false, - secretBlindIndex: secretBlindIndex ?? undefined, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretValueCiphertext, - secretValueIV, - secretValueTag, - algorithm, - keyEncoding, - folder: secFolderId, - }) - ) - ); - - if (newFolderTree && latestFolderTree) { - // save the updated folder tree to the present one - newFolderTree.version = (latestFolderVersion?.nodes?.version || 0) + 1; - latestFolderTree._id = new Types.ObjectId(); - latestFolderTree.isNew = true; - await latestFolderTree.save(); - - // create new folder version - const newFolderVersion = new FolderVersion({ - workspace: workspaceId, - environment, - nodes: newFolderTree, + snap.doc.secretVersions.forEach((snapSecVer) => { + // record all the secrets + oldSecretVersionsObj[snapSecVer.secret.toString()] = snapSecVer; + secretIds.push(snapSecVer.secret); + }); }); - await newFolderVersion.save(); - } - // update secret versions of restored secrets as not deleted - await SecretVersion.updateMany( + queue.push(...subQueue); + } + } + + // TODO: fix any + const latestSecretVersionIds = await getLatestSecretVersionIds({ + secretIds, + }); + + // TODO: fix any + const latestSecretVersions: any = ( + await SecretVersion.find( { - secret: { - $in: Object.keys(oldSecretVersionsObj).map( - (sv) => oldSecretVersionsObj[sv].secret - ), + _id: { + $in: latestSecretVersionIds.map((s) => s.versionId), }, }, - { - isDeleted: false, - } - ); + "secret version" + ) + ).reduce( + (accumulator, s) => ({ + ...accumulator, + [`${s.secret.toString()}`]: s, + }), + {} + ); - // take secret snapshot - await EESecretService.takeSecretSnapshot({ - workspaceId: new Types.ObjectId(workspaceId), + const secDelQuery: Record = { + workspace: workspaceId, + environment, + // undefined means root thus collect all secrets + }; + if (folderId !== "root" && folderIds.length) + secDelQuery.folder = { $in: folderIds }; + + // delete existing secrets + await Secret.deleteMany(secDelQuery); + await Folder.deleteOne({ + workspace: workspaceId, + environment, + }); + + // add secrets + const secrets = await Secret.insertMany( + Object.keys(oldSecretVersionsObj).map((sv) => { + const { + secret: secretId, + workspace, + type, + user, + environment, + secretBlindIndex, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag, + createdAt, + algorithm, + keyEncoding, + folder: secFolderId, + } = oldSecretVersionsObj[sv]; + + return { + _id: secretId, + version: latestSecretVersions[secretId.toString()].version + 1, + workspace, + type, + user, + environment, + secretBlindIndex: secretBlindIndex ?? undefined, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretCommentCiphertext: "", + secretCommentIV: "", + secretCommentTag: "", + createdAt, + algorithm, + keyEncoding, + folder: secFolderId, + }; + }) + ); + + // add secret versions + const secretV = await SecretVersion.insertMany( + secrets.map( + ({ + _id, + version, + workspace, + type, + user, + environment, + secretBlindIndex, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag, + algorithm, + keyEncoding, + folder: secFolderId, + }) => ({ + _id: new Types.ObjectId(), + secret: _id, + version, + workspace, + type, + user, + environment, + isDeleted: false, + secretBlindIndex: secretBlindIndex ?? undefined, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag, + algorithm, + keyEncoding, + folder: secFolderId, + }) + ) + ); + + if (newFolderTree && latestFolderTree) { + // save the updated folder tree to the present one + newFolderTree.version = (latestFolderVersion?.nodes?.version || 0) + 1; + latestFolderTree._id = new Types.ObjectId(); + latestFolderTree.isNew = true; + await latestFolderTree.save(); + + // create new folder version + const newFolderVersion = new FolderVersion({ + workspace: workspaceId, environment, - folderId, - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: "Failed to roll back secret snapshot", + nodes: newFolderTree, }); + await newFolderVersion.save(); } + // update secret versions of restored secrets as not deleted + await SecretVersion.updateMany( + { + secret: { + $in: Object.keys(oldSecretVersionsObj).map( + (sv) => oldSecretVersionsObj[sv].secret + ), + }, + }, + { + isDeleted: false, + } + ); + + // take secret snapshot + await EESecretService.takeSecretSnapshot({ + workspaceId: new Types.ObjectId(workspaceId), + environment, + folderId, + }); + return res.status(200).send({ secrets, }); @@ -587,39 +559,30 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => { } } */ - let logs; - try { - const { workspaceId } = req.params; + const { workspaceId } = req.params; - const offset: number = parseInt(req.query.offset as string); - const limit: number = parseInt(req.query.limit as string); - const sortBy: string = req.query.sortBy as string; - const userId: string = req.query.userId as string; - const actionNames: string = req.query.actionNames as string; + const offset: number = parseInt(req.query.offset as string); + const limit: number = parseInt(req.query.limit as string); + const sortBy: string = req.query.sortBy as string; + const userId: string = req.query.userId as string; + const actionNames: string = req.query.actionNames as string; - logs = await Log.find({ - workspace: workspaceId, - ...(userId ? { user: userId } : {}), - ...(actionNames - ? { - actionNames: { - $in: actionNames.split(","), - }, - } - : {}), - }) - .sort({ createdAt: sortBy === "recent" ? -1 : 1 }) - .skip(offset) - .limit(limit) - .populate("actions") - .populate("user serviceAccount serviceTokenData"); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: "Failed to get workspace logs", - }); - } + const logs = await Log.find({ + workspace: workspaceId, + ...(userId ? { user: userId } : {}), + ...(actionNames + ? { + actionNames: { + $in: actionNames.split(","), + }, + } + : {}), + }) + .sort({ createdAt: sortBy === "recent" ? -1 : 1 }) + .skip(offset) + .limit(limit) + .populate("actions") + .populate("user serviceAccount serviceTokenData"); return res.status(200).send({ logs, diff --git a/backend/src/ee/services/EELicenseService.ts b/backend/src/ee/services/EELicenseService.ts index a4726073fe..5e39420e54 100644 --- a/backend/src/ee/services/EELicenseService.ts +++ b/backend/src/ee/services/EELicenseService.ts @@ -1,5 +1,4 @@ import NodeCache from 'node-cache'; -import * as Sentry from '@sentry/node'; import { getLicenseKey, getLicenseServerKey, @@ -98,35 +97,29 @@ class EELicenseService { const licenseServerKey = await getLicenseServerKey(); const licenseKey = await getLicenseKey(); - try { - if (licenseServerKey) { - // license server key is present -> validate it - const token = await refreshLicenseServerKeyToken() - - if (token) { - this.instanceType = 'cloud'; - } + if (licenseServerKey) { + // license server key is present -> validate it + const token = await refreshLicenseServerKeyToken() - return; + if (token) { + this.instanceType = 'cloud'; } - if (licenseKey) { - // license key is present -> validate it - const token = await refreshLicenseKeyToken(); - - if (token) { - const { data: { currentPlan } } = await licenseKeyRequest.get( - `${await getLicenseServerUrl()}/api/license/v1/plan` - ); - - this.globalFeatureSet = currentPlan; - this.instanceType = 'enterprise-self-hosted'; - } + return; + } + + if (licenseKey) { + // license key is present -> validate it + const token = await refreshLicenseKeyToken(); + + if (token) { + const { data: { currentPlan } } = await licenseKeyRequest.get( + `${await getLicenseServerUrl()}/api/license/v1/plan` + ); + + this.globalFeatureSet = currentPlan; + this.instanceType = 'enterprise-self-hosted'; } - } catch (err) { - // case: self-hosted free - Sentry.setUser(null); - Sentry.captureException(err); } } @@ -135,4 +128,4 @@ class EELicenseService { } } -export default new EELicenseService(); \ No newline at end of file +export default new EELicenseService(); diff --git a/backend/src/helpers/integration.ts b/backend/src/helpers/integration.ts index 4d26bded9c..90f8b8bdb7 100644 --- a/backend/src/helpers/integration.ts +++ b/backend/src/helpers/integration.ts @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/node'; import { Types } from 'mongoose'; import { Bot, @@ -48,66 +47,59 @@ const handleOAuthExchangeHelper = async ({ code: string; environment: string; }) => { - let integrationAuth; - try { - const bot = await Bot.findOne({ - workspace: workspaceId, - isActive: true + const bot = await Bot.findOne({ + workspace: workspaceId, + isActive: true + }); + + if (!bot) throw new Error('Bot must be enabled for OAuth2 code-token exchange'); + + // exchange code for access and refresh tokens + const res = await exchangeCode({ + integration, + code + }); + + const update: Update = { + workspace: workspaceId, + integration + } + + switch (integration) { + case INTEGRATION_VERCEL: + update.teamId = res.teamId; + break; + case INTEGRATION_NETLIFY: + update.accountId = res.accountId; + break; + } + + const integrationAuth = await IntegrationAuth.findOneAndUpdate({ + workspace: workspaceId, + integration + }, update, { + new: true, + upsert: true + }); + + if (res.refreshToken) { + // case: refresh token returned from exchange + // set integration auth refresh token + await setIntegrationAuthRefreshHelper({ + integrationAuthId: integrationAuth._id.toString(), + refreshToken: res.refreshToken }); - - if (!bot) throw new Error('Bot must be enabled for OAuth2 code-token exchange'); - - // exchange code for access and refresh tokens - const res = await exchangeCode({ - integration, - code + } + + if (res.accessToken) { + // case: access token returned from exchange + // set integration auth access token + await setIntegrationAuthAccessHelper({ + integrationAuthId: integrationAuth._id.toString(), + accessId: null, + accessToken: res.accessToken, + accessExpiresAt: res.accessExpiresAt }); - - const update: Update = { - workspace: workspaceId, - integration - } - - switch (integration) { - case INTEGRATION_VERCEL: - update.teamId = res.teamId; - break; - case INTEGRATION_NETLIFY: - update.accountId = res.accountId; - break; - } - - integrationAuth = await IntegrationAuth.findOneAndUpdate({ - workspace: workspaceId, - integration - }, update, { - new: true, - upsert: true - }); - - if (res.refreshToken) { - // case: refresh token returned from exchange - // set integration auth refresh token - await setIntegrationAuthRefreshHelper({ - integrationAuthId: integrationAuth._id.toString(), - refreshToken: res.refreshToken - }); - } - - if (res.accessToken) { - // case: access token returned from exchange - // set integration auth access token - await setIntegrationAuthAccessHelper({ - integrationAuthId: integrationAuth._id.toString(), - accessId: null, - accessToken: res.accessToken, - accessExpiresAt: res.accessExpiresAt - }); - } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to handle OAuth2 code-token exchange') } return integrationAuth; @@ -125,47 +117,40 @@ const syncIntegrationsHelper = async ({ workspaceId: Types.ObjectId; environment?: string; }) => { - let integrations; - try { - integrations = await Integration.find({ - workspace: workspaceId, - ...(environment ? { - environment - } : {}), - isActive: true, - app: { $ne: null } + const integrations = await Integration.find({ + workspace: workspaceId, + ...(environment ? { + environment + } : {}), + isActive: true, + app: { $ne: null } + }); + + // for each workspace integration, sync/push secrets + // to that integration + for await (const integration of integrations) { + // get workspace, environment (shared) secrets + const secrets = await BotService.getSecrets({ // issue here? + workspaceId: integration.workspace, + environment: integration.environment }); - // for each workspace integration, sync/push secrets - // to that integration - for await (const integration of integrations) { - // get workspace, environment (shared) secrets - const secrets = await BotService.getSecrets({ // issue here? - workspaceId: integration.workspace, - environment: integration.environment - }); + const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth); + if (!integrationAuth) throw new Error('Failed to find integration auth'); + + // get integration auth access token + const access = await getIntegrationAuthAccessHelper({ + integrationAuthId: integration.integrationAuth + }); - const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth); - if (!integrationAuth) throw new Error('Failed to find integration auth'); - - // get integration auth access token - const access = await getIntegrationAuthAccessHelper({ - integrationAuthId: integration.integrationAuth - }); - - // sync secrets to integration - await syncSecrets({ - integration, - integrationAuth, - secrets, - accessId: access.accessId === undefined ? null : access.accessId, - accessToken: access.accessToken - }); - } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to sync secrets to integrations'); + // sync secrets to integration + await syncSecrets({ + integration, + integrationAuth, + secrets, + accessId: access.accessId === undefined ? null : access.accessId, + accessToken: access.accessToken + }); } } @@ -178,30 +163,18 @@ const syncIntegrationsHelper = async ({ * @param {String} refreshToken - decrypted refresh token */ const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => { - let refreshToken; - - try { - const integrationAuth = await IntegrationAuth - .findById(integrationAuthId) - .select('+refreshCiphertext +refreshIV +refreshTag'); + const integrationAuth = await IntegrationAuth + .findById(integrationAuthId) + .select('+refreshCiphertext +refreshIV +refreshTag'); - if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'}); + if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'}); - refreshToken = await BotService.decryptSymmetric({ - workspaceId: integrationAuth.workspace, - ciphertext: integrationAuth.refreshCiphertext as string, - iv: integrationAuth.refreshIV as string, - tag: integrationAuth.refreshTag as string - }); - - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - if(err instanceof RequestError) - throw err - else - throw new Error('Failed to get integration refresh token'); - } + const refreshToken = await BotService.decryptSymmetric({ + workspaceId: integrationAuth.workspace, + ciphertext: integrationAuth.refreshCiphertext as string, + iv: integrationAuth.refreshIV as string, + tag: integrationAuth.refreshTag as string + }); return refreshToken; } @@ -217,50 +190,40 @@ const syncIntegrationsHelper = async ({ const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => { let accessId; let accessToken; - try { - const integrationAuth = await IntegrationAuth - .findById(integrationAuthId) - .select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag'); + const integrationAuth = await IntegrationAuth + .findById(integrationAuthId) + .select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag'); - if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'}); + if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'}); - accessToken = await BotService.decryptSymmetric({ - workspaceId: integrationAuth.workspace, - ciphertext: integrationAuth.accessCiphertext as string, - iv: integrationAuth.accessIV as string, - tag: integrationAuth.accessTag as string - }); + accessToken = await BotService.decryptSymmetric({ + workspaceId: integrationAuth.workspace, + ciphertext: integrationAuth.accessCiphertext as string, + iv: integrationAuth.accessIV as string, + tag: integrationAuth.accessTag as string + }); - if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) { - // there is a access token expiration date - // and refresh token to exchange with the OAuth2 server - - if (integrationAuth.accessExpiresAt < new Date()) { - // access token is expired - const refreshToken = await getIntegrationAuthRefreshHelper({ integrationAuthId }); - accessToken = await exchangeRefresh({ - integrationAuth, - refreshToken - }); - } - } + if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) { + // there is a access token expiration date + // and refresh token to exchange with the OAuth2 server - if (integrationAuth?.accessIdCiphertext && integrationAuth?.accessIdIV && integrationAuth?.accessIdTag) { - accessId = await BotService.decryptSymmetric({ - workspaceId: integrationAuth.workspace, - ciphertext: integrationAuth.accessIdCiphertext as string, - iv: integrationAuth.accessIdIV as string, - tag: integrationAuth.accessIdTag as string + if (integrationAuth.accessExpiresAt < new Date()) { + // access token is expired + const refreshToken = await getIntegrationAuthRefreshHelper({ integrationAuthId }); + accessToken = await exchangeRefresh({ + integrationAuth, + refreshToken }); } - - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - if(err instanceof RequestError) - throw err - else - throw new Error('Failed to get integration access token'); + } + + if (integrationAuth?.accessIdCiphertext && integrationAuth?.accessIdIV && integrationAuth?.accessIdTag) { + accessId = await BotService.decryptSymmetric({ + workspaceId: integrationAuth.workspace, + ciphertext: integrationAuth.accessIdCiphertext as string, + iv: integrationAuth.accessIdIV as string, + tag: integrationAuth.accessIdTag as string + }); } return ({ @@ -285,34 +248,27 @@ const setIntegrationAuthRefreshHelper = async ({ refreshToken: string; }) => { - let integrationAuth; - try { - integrationAuth = await IntegrationAuth - .findById(integrationAuthId); - - if (!integrationAuth) throw new Error('Failed to find integration auth'); - - const obj = await BotService.encryptSymmetric({ - workspaceId: integrationAuth.workspace, - plaintext: refreshToken - }); - - integrationAuth = await IntegrationAuth.findOneAndUpdate({ - _id: integrationAuthId - }, { - refreshCiphertext: obj.ciphertext, - refreshIV: obj.iv, - refreshTag: obj.tag, - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 - }, { - new: true - }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to set integration auth refresh token'); - } + let integrationAuth = await IntegrationAuth + .findById(integrationAuthId); + + if (!integrationAuth) throw new Error('Failed to find integration auth'); + + const obj = await BotService.encryptSymmetric({ + workspaceId: integrationAuth.workspace, + plaintext: refreshToken + }); + + integrationAuth = await IntegrationAuth.findOneAndUpdate({ + _id: integrationAuthId + }, { + refreshCiphertext: obj.ciphertext, + refreshIV: obj.iv, + refreshTag: obj.tag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8 + }, { + new: true + }); return integrationAuth; } @@ -337,46 +293,39 @@ const setIntegrationAuthAccessHelper = async ({ accessToken: string; accessExpiresAt: Date | undefined; }) => { - let integrationAuth; - try { - integrationAuth = await IntegrationAuth.findById(integrationAuthId); - - if (!integrationAuth) throw new Error('Failed to find integration auth'); - - const encryptedAccessTokenObj = await BotService.encryptSymmetric({ + let integrationAuth = await IntegrationAuth.findById(integrationAuthId); + + if (!integrationAuth) throw new Error('Failed to find integration auth'); + + const encryptedAccessTokenObj = await BotService.encryptSymmetric({ + workspaceId: integrationAuth.workspace, + plaintext: accessToken + }); + + let encryptedAccessIdObj; + if (accessId) { + encryptedAccessIdObj = await BotService.encryptSymmetric({ workspaceId: integrationAuth.workspace, - plaintext: accessToken - }); - - let encryptedAccessIdObj; - if (accessId) { - encryptedAccessIdObj = await BotService.encryptSymmetric({ - workspaceId: integrationAuth.workspace, - plaintext: accessId - }); - } - - integrationAuth = await IntegrationAuth.findOneAndUpdate({ - _id: integrationAuthId - }, { - accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined, - accessIdIV: encryptedAccessIdObj?.iv ?? undefined, - accessIdTag: encryptedAccessIdObj?.tag ?? undefined, - accessCiphertext: encryptedAccessTokenObj.ciphertext, - accessIV: encryptedAccessTokenObj.iv, - accessTag: encryptedAccessTokenObj.tag, - accessExpiresAt, - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 - }, { - new: true - }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to save integration auth access token'); + plaintext: accessId + }); } + integrationAuth = await IntegrationAuth.findOneAndUpdate({ + _id: integrationAuthId + }, { + accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined, + accessIdIV: encryptedAccessIdObj?.iv ?? undefined, + accessIdTag: encryptedAccessIdObj?.tag ?? undefined, + accessCiphertext: encryptedAccessTokenObj.ciphertext, + accessIV: encryptedAccessTokenObj.iv, + accessTag: encryptedAccessTokenObj.tag, + accessExpiresAt, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8 + }, { + new: true + }); + return integrationAuth; } diff --git a/backend/src/helpers/membership.ts b/backend/src/helpers/membership.ts index ee85fbb382..074ebe777d 100644 --- a/backend/src/helpers/membership.ts +++ b/backend/src/helpers/membership.ts @@ -1,7 +1,6 @@ -import * as Sentry from '@sentry/node'; -import { Types } from 'mongoose'; -import { Membership, Key } from '../models'; -import { MembershipNotFoundError, BadRequestError } from '../utils/errors'; +import { Types } from "mongoose"; +import { Membership, Key } from "../models"; +import { MembershipNotFoundError, BadRequestError } from "../utils/errors"; /** * Validate that user with id [userId] is a member of workspace with id [workspaceId] @@ -18,23 +17,23 @@ const validateMembership = async ({ }: { userId: Types.ObjectId | string; workspaceId: Types.ObjectId | string; - acceptedRoles?: Array<'admin' | 'member'>; + acceptedRoles?: Array<"admin" | "member">; }) => { const membership = await Membership.findOne({ user: userId, workspace: workspaceId, - }).populate('workspace'); + }).populate("workspace"); if (!membership) { throw MembershipNotFoundError({ - message: 'Failed to find workspace membership', + message: "Failed to find workspace membership", }); } if (acceptedRoles) { if (!acceptedRoles.includes(membership.role)) { throw BadRequestError({ - message: 'Failed authorization for membership role', + message: "Failed authorization for membership role", }); } } @@ -48,15 +47,7 @@ const validateMembership = async ({ * @return {Object} membership - membership */ const findMembership = async (queryObj: any) => { - let membership; - try { - membership = await Membership.findOne(queryObj); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to find membership'); - } - + const membership = await Membership.findOne(queryObj); return membership; }; @@ -77,31 +68,24 @@ const addMemberships = async ({ workspaceId: string; roles: string[]; }): Promise => { - try { - const operations = userIds.map((userId, idx) => { - return { - updateOne: { - filter: { - user: userId, - workspace: workspaceId, - role: roles[idx], - }, - update: { - user: userId, - workspace: workspaceId, - role: roles[idx], - }, - upsert: true, + const operations = userIds.map((userId, idx) => { + return { + updateOne: { + filter: { + user: userId, + workspace: workspaceId, + role: roles[idx], }, - }; - }); - - await Membership.bulkWrite(operations as any); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to add users to workspace'); - } + update: { + user: userId, + workspace: workspaceId, + role: roles[idx], + }, + upsert: true, + }, + }; + }); + await Membership.bulkWrite(operations as any); }; /** @@ -110,24 +94,17 @@ const addMemberships = async ({ * @param {String} obj.membershipId - id of membership to delete */ const deleteMembership = async ({ membershipId }: { membershipId: string }) => { - let deletedMembership; - try { - deletedMembership = await Membership.findOneAndDelete({ - _id: membershipId, - }); + const deletedMembership = await Membership.findOneAndDelete({ + _id: membershipId, + }); - // delete keys associated with the membership - if (deletedMembership?.user) { - // case: membership had a registered user - await Key.deleteMany({ - receiver: deletedMembership.user, - workspace: deletedMembership.workspace, - }); - } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to delete membership'); + // delete keys associated with the membership + if (deletedMembership?.user) { + // case: membership had a registered user + await Key.deleteMany({ + receiver: deletedMembership.user, + workspace: deletedMembership.workspace, + }); } return deletedMembership; diff --git a/backend/src/helpers/nodemailer.ts b/backend/src/helpers/nodemailer.ts index 386db2c392..111573916b 100644 --- a/backend/src/helpers/nodemailer.ts +++ b/backend/src/helpers/nodemailer.ts @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/node'; import fs from 'fs'; import path from 'path'; import handlebars from 'handlebars'; @@ -26,24 +25,19 @@ const sendMail = async ({ substitutions: any; }) => { if (await getSmtpConfigured()) { - try { - const html = fs.readFileSync( - path.resolve(__dirname, '../templates/' + template), - 'utf8' - ); - const temp = handlebars.compile(html); - const htmlToSend = temp(substitutions); + const html = fs.readFileSync( + path.resolve(__dirname, '../templates/' + template), + 'utf8' + ); + const temp = handlebars.compile(html); + const htmlToSend = temp(substitutions); - await smtpTransporter.sendMail({ - from: `"${await getSmtpFromName()}" <${await getSmtpFromAddress()}>`, - to: recipients.join(', '), - subject: subjectLine, - html: htmlToSend - }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - } + await smtpTransporter.sendMail({ + from: `"${await getSmtpFromName()}" <${await getSmtpFromAddress()}>`, + to: recipients.join(', '), + subject: subjectLine, + html: htmlToSend + }); } }; diff --git a/backend/src/helpers/signup.ts b/backend/src/helpers/signup.ts index dfca967369..9a70d0a57b 100644 --- a/backend/src/helpers/signup.ts +++ b/backend/src/helpers/signup.ts @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/node'; import { IUser } from '../models'; import { createOrganization } from './organization'; import { addMembershipsOrg } from './membershipOrg'; @@ -15,28 +14,20 @@ import { TOKEN_EMAIL_CONFIRMATION } from '../variables'; * @returns {Boolean} success - whether or not operation was successful */ const sendEmailVerification = async ({ email }: { email: string }) => { - try { - const token = await TokenService.createToken({ - type: TOKEN_EMAIL_CONFIRMATION, - email - }); + const token = await TokenService.createToken({ + type: TOKEN_EMAIL_CONFIRMATION, + email + }); - // send mail - await sendMail({ - template: 'emailVerification.handlebars', - subjectLine: 'Infisical confirmation code', - recipients: [email], - substitutions: { - code: token - } - }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error( - "Ouch. We weren't able to send your email verification code" - ); - } + // send mail + await sendMail({ + template: 'emailVerification.handlebars', + subjectLine: 'Infisical confirmation code', + recipients: [email], + substitutions: { + code: token + } + }); }; /** @@ -52,17 +43,11 @@ const checkEmailVerification = async ({ email: string; code: string; }) => { - try { - await TokenService.validateToken({ - type: TOKEN_EMAIL_CONFIRMATION, - email, - token: code - }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error("Oops. We weren't able to verify"); - } + await TokenService.validateToken({ + type: TOKEN_EMAIL_CONFIRMATION, + email, + token: code + }); }; /** diff --git a/backend/src/helpers/workspace.ts b/backend/src/helpers/workspace.ts index 6bc8809812..eb74bf06dc 100644 --- a/backend/src/helpers/workspace.ts +++ b/backend/src/helpers/workspace.ts @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/node'; import { Workspace, Bot, @@ -23,31 +22,24 @@ const createWorkspace = async ({ name: string; organizationId: string; }) => { - let workspace; - try { - // create workspace - workspace = await new Workspace({ - name, - organization: organizationId, - autoCapitalization: true - }).save(); - - // initialize bot for workspace - await createBot({ - name: 'Infisical Bot', - workspaceId: workspace._id - }); - - // initialize blind index salt for workspace - await SecretService.createSecretBlindIndexData({ - workspaceId: workspace._id - }); + // create workspace + const workspace = await new Workspace({ + name, + organization: organizationId, + autoCapitalization: true + }).save(); + + // initialize bot for workspace + await createBot({ + name: 'Infisical Bot', + workspaceId: workspace._id + }); + + // initialize blind index salt for workspace + await SecretService.createSecretBlindIndexData({ + workspaceId: workspace._id + }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to create workspace'); - } return workspace; }; @@ -59,25 +51,19 @@ const createWorkspace = async ({ * @param {String} obj.id - id of workspace to delete */ const deleteWorkspace = async ({ id }: { id: string }) => { - try { - await Workspace.deleteOne({ _id: id }); - await Bot.deleteOne({ - workspace: id - }); - await Membership.deleteMany({ - workspace: id - }); - await Secret.deleteMany({ - workspace: id - }); - await Key.deleteMany({ - workspace: id - }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to delete workspace'); - } + await Workspace.deleteOne({ _id: id }); + await Bot.deleteOne({ + workspace: id + }); + await Membership.deleteMany({ + workspace: id + }); + await Secret.deleteMany({ + workspace: id + }); + await Key.deleteMany({ + workspace: id + }); }; export { diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index e6082d7c5e..751d52793e 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -1,4 +1,3 @@ -import * as Sentry from "@sentry/node"; import _ from 'lodash'; import AWS from 'aws-sdk'; import { @@ -61,115 +60,109 @@ const syncSecrets = async ({ accessId: string | null; accessToken: string; }) => { - try { - switch (integration.integration) { - case INTEGRATION_AZURE_KEY_VAULT: - await syncSecretsAzureKeyVault({ + switch (integration.integration) { + case INTEGRATION_AZURE_KEY_VAULT: + await syncSecretsAzureKeyVault({ + integration, + secrets, + accessToken + }); + break; + case INTEGRATION_AWS_PARAMETER_STORE: + await syncSecretsAWSParameterStore({ + integration, + secrets, + accessId, + accessToken + }); + break; + case INTEGRATION_AWS_SECRET_MANAGER: + await syncSecretsAWSSecretManager({ + integration, + secrets, + accessId, + accessToken + }); + break; + case INTEGRATION_HEROKU: + await syncSecretsHeroku({ + integration, + secrets, + accessToken, + }); + break; + case INTEGRATION_VERCEL: + await syncSecretsVercel({ + integration, + integrationAuth, + secrets, + accessToken, + }); + break; + case INTEGRATION_NETLIFY: + await syncSecretsNetlify({ + integration, + integrationAuth, + secrets, + accessToken, + }); + break; + case INTEGRATION_GITHUB: + await syncSecretsGitHub({ + integration, + secrets, + accessToken, + }); + break; + case INTEGRATION_GITLAB: + await syncSecretsGitLab({ + integration, + secrets, + accessToken, + }); + break; + case INTEGRATION_RENDER: + await syncSecretsRender({ + integration, + secrets, + accessToken, + }); + break; + case INTEGRATION_RAILWAY: + await syncSecretsRailway({ + integration, + secrets, + accessToken + }); + break; + case INTEGRATION_FLYIO: + await syncSecretsFlyio({ + integration, + secrets, + accessToken, + }); + break; + case INTEGRATION_CIRCLECI: + await syncSecretsCircleCI({ + integration, + secrets, + accessToken, + }); + break; + case INTEGRATION_TRAVISCI: + await syncSecretsTravisCI({ + integration, + secrets, + accessToken, + }); + break; + case INTEGRATION_SUPABASE: + await syncSecretsSupabase({ integration, secrets, accessToken - }); - break; - case INTEGRATION_AWS_PARAMETER_STORE: - await syncSecretsAWSParameterStore({ - integration, - secrets, - accessId, - accessToken - }); - break; - case INTEGRATION_AWS_SECRET_MANAGER: - await syncSecretsAWSSecretManager({ - integration, - secrets, - accessId, - accessToken - }); - break; - case INTEGRATION_HEROKU: - await syncSecretsHeroku({ - integration, - secrets, - accessToken, - }); - break; - case INTEGRATION_VERCEL: - await syncSecretsVercel({ - integration, - integrationAuth, - secrets, - accessToken, - }); - break; - case INTEGRATION_NETLIFY: - await syncSecretsNetlify({ - integration, - integrationAuth, - secrets, - accessToken, - }); - break; - case INTEGRATION_GITHUB: - await syncSecretsGitHub({ - integration, - secrets, - accessToken, - }); - break; - case INTEGRATION_GITLAB: - await syncSecretsGitLab({ - integration, - secrets, - accessToken, - }); - break; - case INTEGRATION_RENDER: - await syncSecretsRender({ - integration, - secrets, - accessToken, - }); - break; - case INTEGRATION_RAILWAY: - await syncSecretsRailway({ - integration, - secrets, - accessToken - }); - break; - case INTEGRATION_FLYIO: - await syncSecretsFlyio({ - integration, - secrets, - accessToken, - }); - break; - case INTEGRATION_CIRCLECI: - await syncSecretsCircleCI({ - integration, - secrets, - accessToken, - }); - break; - case INTEGRATION_TRAVISCI: - await syncSecretsTravisCI({ - integration, - secrets, - accessToken, - }); - break; - case INTEGRATION_SUPABASE: - await syncSecretsSupabase({ - integration, - secrets, - accessToken - }); - break; - } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to sync secrets to integration'); + }); + break; } }; @@ -189,181 +182,169 @@ const syncSecretsAzureKeyVault = async ({ secrets: any; accessToken: string; }) => { - try { - interface GetAzureKeyVaultSecret { - id: string; // secret URI - attributes: { - enabled: true, - created: number; - updated: number; - recoveryLevel: string; - recoverableDays: number; - } + interface GetAzureKeyVaultSecret { + id: string; // secret URI + attributes: { + enabled: true, + created: number; + updated: number; + recoveryLevel: string; + recoverableDays: number; } - - interface AzureKeyVaultSecret extends GetAzureKeyVaultSecret { - key: string; - } - - /** - * Return all secrets from Azure Key Vault by paginating through URL [url] - * @param {String} url - pagination URL to get next set of secrets from Azure Key Vault - * @returns - */ - const paginateAzureKeyVaultSecrets = async (url: string) => { - let result: GetAzureKeyVaultSecret[] = []; - try { - while (url) { - const res = await standardRequest.get(url, { - headers: { - Authorization: `Bearer ${accessToken}` - } - }); - - result = result.concat(res.data.value); - - url = res.data.nextLink; - } - - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - } - - return result; - } - - const getAzureKeyVaultSecrets = await paginateAzureKeyVaultSecrets(`${integration.app}/secrets?api-version=7.3`); - - let lastSlashIndex: number; - const res = (await Promise.all(getAzureKeyVaultSecrets.map(async (getAzureKeyVaultSecret) => { - if (!lastSlashIndex) { - lastSlashIndex = getAzureKeyVaultSecret.id.lastIndexOf('/'); - } - - const azureKeyVaultSecret = await standardRequest.get(`${getAzureKeyVaultSecret.id}?api-version=7.3`, { + } + + interface AzureKeyVaultSecret extends GetAzureKeyVaultSecret { + key: string; + } + + /** + * Return all secrets from Azure Key Vault by paginating through URL [url] + * @param {String} url - pagination URL to get next set of secrets from Azure Key Vault + * @returns + */ + const paginateAzureKeyVaultSecrets = async (url: string) => { + let result: GetAzureKeyVaultSecret[] = []; + while (url) { + const res = await standardRequest.get(url, { headers: { - 'Authorization': `Bearer ${accessToken}` + Authorization: `Bearer ${accessToken}` } }); - - return ({ - ...azureKeyVaultSecret.data, - key: getAzureKeyVaultSecret.id.substring(lastSlashIndex + 1), - }); - }))) - .reduce((obj: any, secret: any) => ({ - ...obj, - [secret.key]: secret - }), {}); + + result = result.concat(res.data.value); + + url = res.data.nextLink; + } - const setSecrets: { - key: string; - value: string; - }[] = []; + return result; + } + + const getAzureKeyVaultSecrets = await paginateAzureKeyVaultSecrets(`${integration.app}/secrets?api-version=7.3`); + + let lastSlashIndex: number; + const res = (await Promise.all(getAzureKeyVaultSecrets.map(async (getAzureKeyVaultSecret) => { + if (!lastSlashIndex) { + lastSlashIndex = getAzureKeyVaultSecret.id.lastIndexOf('/'); + } + + const azureKeyVaultSecret = await standardRequest.get(`${getAzureKeyVaultSecret.id}?api-version=7.3`, { + headers: { + 'Authorization': `Bearer ${accessToken}` + } + }); - Object.keys(secrets).forEach((key) => { - const hyphenatedKey = key.replace(/_/g, '-'); - if (!(hyphenatedKey in res)) { - // case: secret has been created + return ({ + ...azureKeyVaultSecret.data, + key: getAzureKeyVaultSecret.id.substring(lastSlashIndex + 1), + }); + }))) + .reduce((obj: any, secret: any) => ({ + ...obj, + [secret.key]: secret + }), {}); + + const setSecrets: { + key: string; + value: string; + }[] = []; + + Object.keys(secrets).forEach((key) => { + const hyphenatedKey = key.replace(/_/g, '-'); + if (!(hyphenatedKey in res)) { + // case: secret has been created + setSecrets.push({ + key: hyphenatedKey, + value: secrets[key] + }); + } else { + if (secrets[key] !== res[hyphenatedKey].value) { + // case: secret has been updated setSecrets.push({ key: hyphenatedKey, value: secrets[key] }); - } else { - if (secrets[key] !== res[hyphenatedKey].value) { - // case: secret has been updated - setSecrets.push({ - key: hyphenatedKey, - value: secrets[key] - }); - } } - }); - - const deleteSecrets: AzureKeyVaultSecret[] = []; - - Object.keys(res).forEach((key) => { - const underscoredKey = key.replace(/-/g, '_'); - if (!(underscoredKey in secrets)) { - deleteSecrets.push(res[key]); - } - }); + } + }); + + const deleteSecrets: AzureKeyVaultSecret[] = []; + + Object.keys(res).forEach((key) => { + const underscoredKey = key.replace(/-/g, '_'); + if (!(underscoredKey in secrets)) { + deleteSecrets.push(res[key]); + } + }); - const setSecretAzureKeyVault = async ({ - key, - value, - integration, - accessToken - }: { - key: string; - value: string; - integration: IIntegration; - accessToken: string; - }) => { - let isSecretSet = false; - let maxTries = 6; + const setSecretAzureKeyVault = async ({ + key, + value, + integration, + accessToken + }: { + key: string; + value: string; + integration: IIntegration; + accessToken: string; + }) => { + let isSecretSet = false; + let maxTries = 6; + + while (!isSecretSet && maxTries > 0) { + // try to set secret + try { + await standardRequest.put( + `${integration.app}/secrets/${key}?api-version=7.3`, + { + value + }, + { + headers: { + Authorization: `Bearer ${accessToken}` + } + } + ); + + isSecretSet = true; - while (!isSecretSet && maxTries > 0) { - // try to set secret - try { - await standardRequest.put( - `${integration.app}/secrets/${key}?api-version=7.3`, - { - value - }, + } catch (err) { + const error: any = err; + if (error?.response?.data?.error?.innererror?.code === 'ObjectIsDeletedButRecoverable') { + await standardRequest.post( + `${integration.app}/deletedsecrets/${key}/recover?api-version=7.3`, {}, { headers: { Authorization: `Bearer ${accessToken}` } } ); - - isSecretSet = true; - - } catch (err) { - const error: any = err; - if (error?.response?.data?.error?.innererror?.code === 'ObjectIsDeletedButRecoverable') { - await standardRequest.post( - `${integration.app}/deletedsecrets/${key}/recover?api-version=7.3`, {}, - { - headers: { - Authorization: `Bearer ${accessToken}` - } - } - ); - await new Promise(resolve => setTimeout(resolve, 10000)); - } else { - await new Promise(resolve => setTimeout(resolve, 10000)); - maxTries--; - } + await new Promise(resolve => setTimeout(resolve, 10000)); + } else { + await new Promise(resolve => setTimeout(resolve, 10000)); + maxTries--; } } } - - // Sync/push set secrets - for await (const setSecret of setSecrets) { - const { key, value } = setSecret; - setSecretAzureKeyVault({ - key, - value, - integration, - accessToken - }); - } - - for await (const deleteSecret of deleteSecrets) { - const { key } = deleteSecret; - await standardRequest.delete(`${integration.app}/secrets/${key}?api-version=7.3`, { - headers: { - 'Authorization': `Bearer ${accessToken}` - } - }); - } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to sync secrets to Azure Key Vault'); + } + + // Sync/push set secrets + for await (const setSecret of setSecrets) { + const { key, value } = setSecret; + setSecretAzureKeyVault({ + key, + value, + integration, + accessToken + }); + } + + for await (const deleteSecret of deleteSecrets) { + const { key } = deleteSecret; + await standardRequest.delete(`${integration.app}/secrets/${key}?api-version=7.3`, { + headers: { + 'Authorization': `Bearer ${accessToken}` + } + }); } }; @@ -386,87 +367,81 @@ const syncSecretsAWSParameterStore = async ({ accessId: string | null; accessToken: string; }) => { - try { - if (!accessId) return; + if (!accessId) return; - AWS.config.update({ - region: integration.region, - accessKeyId: accessId, - secretAccessKey: accessToken - }); + AWS.config.update({ + region: integration.region, + accessKeyId: accessId, + secretAccessKey: accessToken + }); - const ssm = new AWS.SSM({ - apiVersion: '2014-11-06', - region: integration.region - }); - - const params = { - Path: integration.path, - Recursive: true, - WithDecryption: true - }; + const ssm = new AWS.SSM({ + apiVersion: '2014-11-06', + region: integration.region + }); + + const params = { + Path: integration.path, + Recursive: true, + WithDecryption: true + }; - const parameterList = (await ssm.getParametersByPath(params).promise()).Parameters - - let awsParameterStoreSecretsObj: { - [key: string]: any // TODO: fix type - } = {}; + const parameterList = (await ssm.getParametersByPath(params).promise()).Parameters + + let awsParameterStoreSecretsObj: { + [key: string]: any // TODO: fix type + } = {}; - if (parameterList) { - awsParameterStoreSecretsObj = parameterList.reduce((obj: any, secret: any) => ({ - ...obj, - [secret.Name.split("/").pop()]: secret - }), {}); - } + if (parameterList) { + awsParameterStoreSecretsObj = parameterList.reduce((obj: any, secret: any) => ({ + ...obj, + [secret.Name.split("/").pop()]: secret + }), {}); + } - // Identify secrets to create - Object.keys(secrets).map(async (key) => { - if (!(key in awsParameterStoreSecretsObj)) { - // case: secret does not exist in AWS parameter store - // -> create secret + // Identify secrets to create + Object.keys(secrets).map(async (key) => { + if (!(key in awsParameterStoreSecretsObj)) { + // case: secret does not exist in AWS parameter store + // -> create secret + await ssm.putParameter({ + Name: `${integration.path}${key}`, + Type: 'SecureString', + Value: secrets[key], + Overwrite: true + }).promise(); + } else { + // case: secret exists in AWS parameter store + + if (awsParameterStoreSecretsObj[key].Value !== secrets[key]) { + // case: secret value doesn't match one in AWS parameter store + // -> update secret await ssm.putParameter({ Name: `${integration.path}${key}`, Type: 'SecureString', Value: secrets[key], Overwrite: true }).promise(); - } else { - // case: secret exists in AWS parameter store - - if (awsParameterStoreSecretsObj[key].Value !== secrets[key]) { - // case: secret value doesn't match one in AWS parameter store - // -> update secret - await ssm.putParameter({ - Name: `${integration.path}${key}`, - Type: 'SecureString', - Value: secrets[key], - Overwrite: true - }).promise(); - } } - }); + } + }); - // Identify secrets to delete - Object.keys(awsParameterStoreSecretsObj).map(async (key) => { - if (!(key in secrets)) { - // case: - // -> delete secret - await ssm.deleteParameter({ - Name: awsParameterStoreSecretsObj[key].Name - }).promise(); - } - }); + // Identify secrets to delete + Object.keys(awsParameterStoreSecretsObj).map(async (key) => { + if (!(key in secrets)) { + // case: + // -> delete secret + await ssm.deleteParameter({ + Name: awsParameterStoreSecretsObj[key].Name + }).promise(); + } + }); - AWS.config.update({ - region: undefined, - accessKeyId: undefined, - secretAccessKey: undefined - }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to sync secrets to AWS Parameter Store'); - } + AWS.config.update({ + region: undefined, + accessKeyId: undefined, + secretAccessKey: undefined + }); } /** @@ -536,11 +511,7 @@ const syncSecretsAWSSecretManager = async ({ Name: integration.app, SecretString: JSON.stringify(secrets) })); - } else { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to sync secrets to AWS Secret Manager'); - } + } AWS.config.update({ region: undefined, accessKeyId: undefined, @@ -565,29 +536,9 @@ const syncSecretsHeroku = async ({ secrets: any; accessToken: string; }) => { - try { - const herokuSecrets = ( - await standardRequest.get( - `${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`, - { - headers: { - Accept: "application/vnd.heroku+json; version=3", - Authorization: `Bearer ${accessToken}`, - 'Accept-Encoding': 'application/json' - }, - } - ) - ).data; - - Object.keys(herokuSecrets).forEach((key) => { - if (!(key in secrets)) { - secrets[key] = null; - } - }); - - await standardRequest.patch( + const herokuSecrets = ( + await standardRequest.get( `${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`, - secrets, { headers: { Accept: "application/vnd.heroku+json; version=3", @@ -595,12 +546,26 @@ const syncSecretsHeroku = async ({ 'Accept-Encoding': 'application/json' }, } - ); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error("Failed to sync secrets to Heroku"); - } + ) + ).data; + + Object.keys(herokuSecrets).forEach((key) => { + if (!(key in secrets)) { + secrets[key] = null; + } + }); + + await standardRequest.patch( + `${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`, + secrets, + { + headers: { + Accept: "application/vnd.heroku+json; version=3", + Authorization: `Bearer ${accessToken}`, + 'Accept-Encoding': 'application/json' + }, + } + ); }; /** @@ -628,127 +593,141 @@ const syncSecretsVercel = async ({ target: string[]; gitBranch?: string; } - - try { - // Get all (decrypted) secrets back from Vercel in - // decrypted format - const params: { [key: string]: string } = { - decrypt: "true", - ...(integrationAuth?.teamId - ? { - teamId: integrationAuth.teamId, + // Get all (decrypted) secrets back from Vercel in + // decrypted format + const params: { [key: string]: string } = { + decrypt: "true", + ...(integrationAuth?.teamId + ? { + teamId: integrationAuth.teamId, + } + : {}), + }; + + const vercelSecrets: VercelSecret[] = (await standardRequest.get( + `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`, + { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + 'Accept-Encoding': 'application/json' + } + } + )) + .data + .envs + .filter((secret: VercelSecret) => { + if (!secret.target.includes(integration.targetEnvironment)) { + // case: secret does not have the same target environment + return false; + } + + if (integration.targetEnvironment === 'preview' && integration.path && integration.path !== secret.gitBranch) { + // case: secret on preview environment does not have same target git branch + return false; + } + + return true; + }); + + // return secret.target.includes(integration.targetEnvironment); + + const res: { [key: string]: VercelSecret } = {}; + + for await (const vercelSecret of vercelSecrets) { + if (vercelSecret.type === 'encrypted') { + // case: secret is encrypted -> need to decrypt + const decryptedSecret = (await standardRequest.get( + `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${vercelSecret.id}`, + { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + 'Accept-Encoding': 'application/json' + } } - : {}), - }; - - const vercelSecrets: VercelSecret[] = (await standardRequest.get( - `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`, + )).data; + + res[vercelSecret.key] = decryptedSecret; + } else { + res[vercelSecret.key] = vercelSecret; + } + } + + const updateSecrets: VercelSecret[] = []; + const deleteSecrets: VercelSecret[] = []; + const newSecrets: VercelSecret[] = []; + + // Identify secrets to create + Object.keys(secrets).map((key) => { + if (!(key in res)) { + // case: secret has been created + newSecrets.push({ + key: key, + value: secrets[key], + type: "encrypted", + target: [integration.targetEnvironment], + ...(integration.path ? { + gitBranch: integration.path + } : {}) + }); + } + }); + + // Identify secrets to update and delete + Object.keys(res).map((key) => { + if (key in secrets) { + if (res[key].value !== secrets[key]) { + // case: secret value has changed + updateSecrets.push({ + id: res[key].id, + key: key, + value: secrets[key], + type: res[key].type, + target: res[key].target.includes(integration.targetEnvironment) + ? [...res[key].target] + : [...res[key].target, integration.targetEnvironment], + ...(integration.path ? { + gitBranch: integration.path + } : {}) + }); + } + } else { + // case: secret has been deleted + deleteSecrets.push({ + id: res[key].id, + key: key, + value: res[key].value, + type: "encrypted", // value doesn't matter + target: [integration.targetEnvironment], + ...(integration.path ? { + gitBranch: integration.path + } : {}) + }); + } + }); + + // Sync/push new secrets + if (newSecrets.length > 0) { + await standardRequest.post( + `${INTEGRATION_VERCEL_API_URL}/v10/projects/${integration.app}/env`, + newSecrets, { params, headers: { - Authorization: `Bearer ${accessToken}`, - 'Accept-Encoding': 'application/json' - } - } - )) - .data - .envs - .filter((secret: VercelSecret) => { - if (!secret.target.includes(integration.targetEnvironment)) { - // case: secret does not have the same target environment - return false; + Authorization: `Bearer ${accessToken}`, + 'Accept-Encoding': 'application/json' + }, } + ); + } - if (integration.targetEnvironment === 'preview' && integration.path && integration.path !== secret.gitBranch) { - // case: secret on preview environment does not have same target git branch - return false; - } - - return true; - }); - - // return secret.target.includes(integration.targetEnvironment); - - const res: { [key: string]: VercelSecret } = {}; - - for await (const vercelSecret of vercelSecrets) { - if (vercelSecret.type === 'encrypted') { - // case: secret is encrypted -> need to decrypt - const decryptedSecret = (await standardRequest.get( - `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${vercelSecret.id}`, - { - params, - headers: { - Authorization: `Bearer ${accessToken}`, - 'Accept-Encoding': 'application/json' - } - } - )).data; - - res[vercelSecret.key] = decryptedSecret; - } else { - res[vercelSecret.key] = vercelSecret; - } - } - - const updateSecrets: VercelSecret[] = []; - const deleteSecrets: VercelSecret[] = []; - const newSecrets: VercelSecret[] = []; - - // Identify secrets to create - Object.keys(secrets).map((key) => { - if (!(key in res)) { - // case: secret has been created - newSecrets.push({ - key: key, - value: secrets[key], - type: "encrypted", - target: [integration.targetEnvironment], - ...(integration.path ? { - gitBranch: integration.path - } : {}) - }); - } - }); - - // Identify secrets to update and delete - Object.keys(res).map((key) => { - if (key in secrets) { - if (res[key].value !== secrets[key]) { - // case: secret value has changed - updateSecrets.push({ - id: res[key].id, - key: key, - value: secrets[key], - type: res[key].type, - target: res[key].target.includes(integration.targetEnvironment) - ? [...res[key].target] - : [...res[key].target, integration.targetEnvironment], - ...(integration.path ? { - gitBranch: integration.path - } : {}) - }); - } - } else { - // case: secret has been deleted - deleteSecrets.push({ - id: res[key].id, - key: key, - value: res[key].value, - type: "encrypted", // value doesn't matter - target: [integration.targetEnvironment], - ...(integration.path ? { - gitBranch: integration.path - } : {}) - }); - } - }); - - // Sync/push new secrets - if (newSecrets.length > 0) { - await standardRequest.post( - `${INTEGRATION_VERCEL_API_URL}/v10/projects/${integration.app}/env`, - newSecrets, + for await (const secret of updateSecrets) { + if (secret.type !== 'sensitive') { + const { id, ...updatedSecret } = secret; + await standardRequest.patch( + `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, + updatedSecret, { params, headers: { @@ -757,41 +736,20 @@ const syncSecretsVercel = async ({ }, } ); - } - - for await (const secret of updateSecrets) { - if (secret.type !== 'sensitive') { - const { id, ...updatedSecret } = secret; - await standardRequest.patch( - `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, - updatedSecret, - { - params, - headers: { - Authorization: `Bearer ${accessToken}`, - 'Accept-Encoding': 'application/json' - }, - } - ); - } - } + } + } - for await (const secret of deleteSecrets) { - await standardRequest.delete( - `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, - { - params, - headers: { - Authorization: `Bearer ${accessToken}`, - 'Accept-Encoding': 'application/json' - }, - } - ); - } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error("Failed to sync secrets to Vercel"); + for await (const secret of deleteSecrets) { + await standardRequest.delete( + `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, + { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + 'Accept-Encoding': 'application/json' + }, + } + ); } }; @@ -814,92 +772,78 @@ const syncSecretsNetlify = async ({ secrets: any; accessToken: string; }) => { - try { - interface NetlifyValue { - id?: string; - context: string; // 'dev' | 'branch-deploy' | 'deploy-preview' | 'production', - value: string; - } + interface NetlifyValue { + id?: string; + context: string; // 'dev' | 'branch-deploy' | 'deploy-preview' | 'production', + value: string; + } - interface NetlifySecret { - key: string; - values: NetlifyValue[]; - } + interface NetlifySecret { + key: string; + values: NetlifyValue[]; + } - interface NetlifySecretsRes { - [index: string]: NetlifySecret; - } + interface NetlifySecretsRes { + [index: string]: NetlifySecret; + } - const getParams = new URLSearchParams({ - context_name: "all", // integration.context or all - site_id: integration.appId, - }); + const getParams = new URLSearchParams({ + context_name: "all", // integration.context or all + site_id: integration.appId, + }); - const res = ( - await standardRequest.get( - `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, - { - params: getParams, - headers: { - Authorization: `Bearer ${accessToken}`, - 'Accept-Encoding': 'application/json' + const res = ( + await standardRequest.get( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, + { + params: getParams, + headers: { + Authorization: `Bearer ${accessToken}`, + 'Accept-Encoding': 'application/json' + }, + } + ) + ).data.reduce( + (obj: any, secret: any) => ({ + ...obj, + [secret.key]: secret, + }), + {} + ); + + const newSecrets: NetlifySecret[] = []; // createEnvVars + const deleteSecrets: string[] = []; // deleteEnvVar + const deleteSecretValues: NetlifySecret[] = []; // deleteEnvVarValue + const updateSecrets: NetlifySecret[] = []; // setEnvVarValue + + // identify secrets to create and update + Object.keys(secrets).map((key) => { + if (!(key in res)) { + // case: Infisical secret does not exist in Netlify -> create secret + newSecrets.push({ + key, + values: [ + { + value: secrets[key], + context: integration.targetEnvironment, }, - } - ) - ).data.reduce( - (obj: any, secret: any) => ({ - ...obj, - [secret.key]: secret, - }), - {} - ); + ], + }); + } else { + // case: Infisical secret exists in Netlify + const contexts = res[key].values.reduce( + (obj: any, value: NetlifyValue) => ({ + ...obj, + [value.context]: value, + }), + {} + ); - const newSecrets: NetlifySecret[] = []; // createEnvVars - const deleteSecrets: string[] = []; // deleteEnvVar - const deleteSecretValues: NetlifySecret[] = []; // deleteEnvVarValue - const updateSecrets: NetlifySecret[] = []; // setEnvVarValue - - // identify secrets to create and update - Object.keys(secrets).map((key) => { - if (!(key in res)) { - // case: Infisical secret does not exist in Netlify -> create secret - newSecrets.push({ - key, - values: [ - { - value: secrets[key], - context: integration.targetEnvironment, - }, - ], - }); - } else { - // case: Infisical secret exists in Netlify - const contexts = res[key].values.reduce( - (obj: any, value: NetlifyValue) => ({ - ...obj, - [value.context]: value, - }), - {} - ); - - if (integration.targetEnvironment in contexts) { - // case: Netlify secret value exists in integration context - if (secrets[key] !== contexts[integration.targetEnvironment].value) { - // case: Infisical and Netlify secret values are different - // -> update Netlify secret context and value - updateSecrets.push({ - key, - values: [ - { - context: integration.targetEnvironment, - value: secrets[key], - }, - ], - }); - } - } else { - // case: Netlify secret value does not exist in integration context - // -> add the new Netlify secret context and value + if (integration.targetEnvironment in contexts) { + // case: Netlify secret value exists in integration context + if (secrets[key] !== contexts[integration.targetEnvironment].value) { + // case: Infisical and Netlify secret values are different + // -> update Netlify secret context and value updateSecrets.push({ key, values: [ @@ -910,49 +854,80 @@ const syncSecretsNetlify = async ({ ], }); } - } - }); - - // identify secrets to delete - // TODO: revise (patch case where 1 context was deleted but others still there - Object.keys(res).map((key) => { - // loop through each key's context - if (!(key in secrets)) { - // case: Netlify secret does not exist in Infisical - - const numberOfValues = res[key].values.length; - - res[key].values.forEach((value: NetlifyValue) => { - if (value.context === integration.targetEnvironment) { - if (numberOfValues <= 1) { - // case: Netlify secret value has less than 1 context -> delete secret - deleteSecrets.push(key); - } else { - // case: Netlify secret value has more than 1 context -> delete secret value context - deleteSecretValues.push({ - key, - values: [ - { - id: value.id, - context: integration.targetEnvironment, - value: value.value, - }, - ], - }); - } - } + } else { + // case: Netlify secret value does not exist in integration context + // -> add the new Netlify secret context and value + updateSecrets.push({ + key, + values: [ + { + context: integration.targetEnvironment, + value: secrets[key], + }, + ], }); } - }); + } + }); - const syncParams = new URLSearchParams({ - site_id: integration.appId, - }); + // identify secrets to delete + // TODO: revise (patch case where 1 context was deleted but others still there + Object.keys(res).map((key) => { + // loop through each key's context + if (!(key in secrets)) { + // case: Netlify secret does not exist in Infisical - if (newSecrets.length > 0) { - await standardRequest.post( - `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, - newSecrets, + const numberOfValues = res[key].values.length; + + res[key].values.forEach((value: NetlifyValue) => { + if (value.context === integration.targetEnvironment) { + if (numberOfValues <= 1) { + // case: Netlify secret value has less than 1 context -> delete secret + deleteSecrets.push(key); + } else { + // case: Netlify secret value has more than 1 context -> delete secret value context + deleteSecretValues.push({ + key, + values: [ + { + id: value.id, + context: integration.targetEnvironment, + value: value.value, + }, + ], + }); + } + } + }); + } + }); + + const syncParams = new URLSearchParams({ + site_id: integration.appId, + }); + + if (newSecrets.length > 0) { + await standardRequest.post( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`, + newSecrets, + { + params: syncParams, + headers: { + Authorization: `Bearer ${accessToken}`, + 'Accept-Encoding': 'application/json' + }, + } + ); + } + + if (updateSecrets.length > 0) { + updateSecrets.forEach(async (secret: NetlifySecret) => { + await standardRequest.patch( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`, + { + context: secret.values[0].context, + value: secret.values[0].value, + }, { params: syncParams, headers: { @@ -961,60 +936,37 @@ const syncSecretsNetlify = async ({ }, } ); - } + }); + } - if (updateSecrets.length > 0) { - updateSecrets.forEach(async (secret: NetlifySecret) => { - await standardRequest.patch( - `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`, - { - context: secret.values[0].context, - value: secret.values[0].value, + if (deleteSecrets.length > 0) { + deleteSecrets.forEach(async (key: string) => { + await standardRequest.delete( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${key}`, + { + params: syncParams, + headers: { + Authorization: `Bearer ${accessToken}`, + 'Accept-Encoding': 'application/json' }, - { - params: syncParams, - headers: { - Authorization: `Bearer ${accessToken}`, - 'Accept-Encoding': 'application/json' - }, - } - ); - }); - } + } + ); + }); + } - if (deleteSecrets.length > 0) { - deleteSecrets.forEach(async (key: string) => { - await standardRequest.delete( - `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${key}`, - { - params: syncParams, - headers: { - Authorization: `Bearer ${accessToken}`, - 'Accept-Encoding': 'application/json' - }, - } - ); - }); - } - - if (deleteSecretValues.length > 0) { - deleteSecretValues.forEach(async (secret: NetlifySecret) => { - await standardRequest.delete( - `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}/value/${secret.values[0].id}`, - { - params: syncParams, - headers: { - Authorization: `Bearer ${accessToken}`, - 'Accept-Encoding': 'application/json' - }, - } - ); - }); - } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error("Failed to sync secrets to Heroku"); + if (deleteSecretValues.length > 0) { + deleteSecretValues.forEach(async (secret: NetlifySecret) => { + await standardRequest.delete( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}/value/${secret.values[0].id}`, + { + params: syncParams, + headers: { + Authorization: `Bearer ${accessToken}`, + 'Accept-Encoding': 'application/json' + }, + } + ); + }); } }; @@ -1035,102 +987,96 @@ const syncSecretsGitHub = async ({ secrets: any; accessToken: string; }) => { - try { - interface GitHubRepoKey { - key_id: string; - key: string; - } + interface GitHubRepoKey { + key_id: string; + key: string; + } - interface GitHubSecret { - name: string; - created_at: string; - updated_at: string; - } + interface GitHubSecret { + name: string; + created_at: string; + updated_at: string; + } - interface GitHubSecretRes { - [index: string]: GitHubSecret; - } + interface GitHubSecretRes { + [index: string]: GitHubSecret; + } - const deleteSecrets: GitHubSecret[] = []; + const deleteSecrets: GitHubSecret[] = []; - const octokit = new Octokit({ - auth: accessToken, - }); + const octokit = new Octokit({ + auth: accessToken, + }); - // const user = (await octokit.request('GET /user', {})).data; - const repoPublicKey: GitHubRepoKey = ( + // const user = (await octokit.request('GET /user', {})).data; + const repoPublicKey: GitHubRepoKey = ( + await octokit.request( + "GET /repos/{owner}/{repo}/actions/secrets/public-key", + { + owner: integration.owner, + repo: integration.app + } + ) + ).data; + + // Get local copy of decrypted secrets. We cannot decrypt them as we dont have access to GH private key + const encryptedSecrets: GitHubSecretRes = ( + await octokit.request("GET /repos/{owner}/{repo}/actions/secrets", { + owner: integration.owner, + repo: integration.app, + }) + ).data.secrets.reduce( + (obj: any, secret: any) => ({ + ...obj, + [secret.name]: secret, + }), + {} + ); + + Object.keys(encryptedSecrets).map(async (key) => { + if (!(key in secrets)) { await octokit.request( - "GET /repos/{owner}/{repo}/actions/secrets/public-key", + "DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", { owner: integration.owner, - repo: integration.app + repo: integration.app, + secret_name: key, } - ) - ).data; + ); + } + }); - // Get local copy of decrypted secrets. We cannot decrypt them as we dont have access to GH private key - const encryptedSecrets: GitHubSecretRes = ( - await octokit.request("GET /repos/{owner}/{repo}/actions/secrets", { - owner: integration.owner, - repo: integration.app, - }) - ).data.secrets.reduce( - (obj: any, secret: any) => ({ - ...obj, - [secret.name]: secret, - }), - {} - ); + Object.keys(secrets).map((key) => { + // let encryptedSecret; + sodium.ready.then(async () => { + // convert secret & base64 key to Uint8Array. + const binkey = sodium.from_base64( + repoPublicKey.key, + sodium.base64_variants.ORIGINAL + ); + const binsec = sodium.from_string(secrets[key]); - Object.keys(encryptedSecrets).map(async (key) => { - if (!(key in secrets)) { - await octokit.request( - "DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}", - { - owner: integration.owner, - repo: integration.app, - secret_name: key, - } - ); - } + // encrypt secret using libsodium + const encBytes = sodium.crypto_box_seal(binsec, binkey); + + // convert encrypted Uint8Array to base64 + const encryptedSecret = sodium.to_base64( + encBytes, + sodium.base64_variants.ORIGINAL + ); + + await octokit.request( + "PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}", + { + owner: integration.owner, + repo: integration.app, + secret_name: key, + encrypted_value: encryptedSecret, + key_id: repoPublicKey.key_id, + } + ); }); - - Object.keys(secrets).map((key) => { - // let encryptedSecret; - sodium.ready.then(async () => { - // convert secret & base64 key to Uint8Array. - const binkey = sodium.from_base64( - repoPublicKey.key, - sodium.base64_variants.ORIGINAL - ); - const binsec = sodium.from_string(secrets[key]); - - // encrypt secret using libsodium - const encBytes = sodium.crypto_box_seal(binsec, binkey); - - // convert encrypted Uint8Array to base64 - const encryptedSecret = sodium.to_base64( - encBytes, - sodium.base64_variants.ORIGINAL - ); - - await octokit.request( - "PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}", - { - owner: integration.owner, - repo: integration.app, - secret_name: key, - encrypted_value: encryptedSecret, - key_id: repoPublicKey.key_id, - } - ); - }); - }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error("Failed to sync secrets to GitHub"); - } + }); }; /** @@ -1149,25 +1095,19 @@ const syncSecretsRender = async ({ secrets: any; accessToken: string; }) => { - try { - await standardRequest.put( - `${INTEGRATION_RENDER_API_URL}/v1/services/${integration.appId}/env-vars`, - Object.keys(secrets).map((key) => ({ - key, - value: secrets[key], - })), - { - headers: { - Authorization: `Bearer ${accessToken}`, - 'Accept-Encoding': 'application/json' - }, - } - ); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error("Failed to sync secrets to Render"); - } + await standardRequest.put( + `${INTEGRATION_RENDER_API_URL}/v1/services/${integration.appId}/env-vars`, + Object.keys(secrets).map((key) => ({ + key, + value: secrets[key], + })), + { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Accept-Encoding': 'application/json' + }, + } + ); }; /** @@ -1186,40 +1126,32 @@ const syncSecretsRailway = async ({ secrets: any; accessToken: string; }) => { - try { + const query = ` + mutation UpsertVariables($input: VariableCollectionUpsertInput!) { + variableCollectionUpsert(input: $input) + } + `; - const query = ` - mutation UpsertVariables($input: VariableCollectionUpsertInput!) { - variableCollectionUpsert(input: $input) - } - `; + const input = { + projectId: integration.appId, + environmentId: integration.targetEnvironmentId, + ...(integration.targetServiceId ? { serviceId: integration.targetServiceId } : {}), + replace: true, + variables: secrets + }; - const input = { - projectId: integration.appId, - environmentId: integration.targetEnvironmentId, - ...(integration.targetServiceId ? { serviceId: integration.targetServiceId } : {}), - replace: true, - variables: secrets - }; - - await standardRequest.post(INTEGRATION_RAILWAY_API_URL, { - query, - variables: { - input, - }, - }, { - headers: { - 'Authorization': `Bearer ${accessToken}`, - 'Content-Type': 'application/json', - 'Accept-Encoding': 'application/json' - }, - }); - - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error("Failed to sync secrets to Railway"); - } + await standardRequest.post(INTEGRATION_RAILWAY_API_URL, { + query, + variables: { + input, + }, + }, { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + 'Accept-Encoding': 'application/json' + }, + }); } /** @@ -1238,120 +1170,113 @@ const syncSecretsFlyio = async ({ secrets: any; accessToken: string; }) => { - try { - // set secrets - const SetSecrets = ` - mutation($input: SetSecretsInput!) { - setSecrets(input: $input) { - release { + // set secrets + const SetSecrets = ` + mutation($input: SetSecretsInput!) { + setSecrets(input: $input) { + release { + id + version + reason + description + user { id - version - reason - description - user { - id - email - name - } - evaluationId - createdAt + email + name } + evaluationId + createdAt } } - `; - - await standardRequest.post(INTEGRATION_FLYIO_API_URL, { - query: SetSecrets, - variables: { - input: { - appId: integration.app, - secrets: Object.entries(secrets).map(([key, value]) => ({ - key, - value, - })), - }, - }, - }, { - headers: { - Authorization: "Bearer " + accessToken, - 'Accept-Encoding': 'application/json', - }, - }); - - // get secrets - interface FlyioSecret { - name: string; - digest: string; - createdAt: string; } + `; - const GetSecrets = `query ($appName: String!) { - app(name: $appName) { - secrets { - name - digest - createdAt - } - } - }`; - - const getSecretsRes = (await standardRequest.post(INTEGRATION_FLYIO_API_URL, { - query: GetSecrets, - variables: { - appName: integration.app, + await standardRequest.post(INTEGRATION_FLYIO_API_URL, { + query: SetSecrets, + variables: { + input: { + appId: integration.app, + secrets: Object.entries(secrets).map(([key, value]) => ({ + key, + value, + })), }, - }, { - headers: { - Authorization: "Bearer " + accessToken, - 'Content-Type': 'application/json', - 'Accept-Encoding': 'application/json', - }, - })).data.data.app.secrets; + }, + }, { + headers: { + Authorization: "Bearer " + accessToken, + 'Accept-Encoding': 'application/json', + }, + }); - const deleteSecretsKeys = getSecretsRes - .filter((secret: FlyioSecret) => !(secret.name in secrets)) - .map((secret: FlyioSecret) => secret.name); - - // unset (delete) secrets - const DeleteSecrets = `mutation($input: UnsetSecretsInput!) { - unsetSecrets(input: $input) { - release { - id - version - reason - description - user { - id - email - name - } - evaluationId - createdAt - } - } - }`; - - await standardRequest.post(INTEGRATION_FLYIO_API_URL, { - query: DeleteSecrets, - variables: { - input: { - appId: integration.app, - keys: deleteSecretsKeys, - }, - }, - }, { - headers: { - Authorization: "Bearer " + accessToken, - "Content-Type": "application/json", - 'Accept-Encoding': 'application/json', - }, - }); - - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error("Failed to sync secrets to Fly.io"); + // get secrets + interface FlyioSecret { + name: string; + digest: string; + createdAt: string; } + + const GetSecrets = `query ($appName: String!) { + app(name: $appName) { + secrets { + name + digest + createdAt + } + } + }`; + + const getSecretsRes = (await standardRequest.post(INTEGRATION_FLYIO_API_URL, { + query: GetSecrets, + variables: { + appName: integration.app, + }, + }, { + headers: { + Authorization: "Bearer " + accessToken, + 'Content-Type': 'application/json', + 'Accept-Encoding': 'application/json', + }, + })).data.data.app.secrets; + + const deleteSecretsKeys = getSecretsRes + .filter((secret: FlyioSecret) => !(secret.name in secrets)) + .map((secret: FlyioSecret) => secret.name); + + // unset (delete) secrets + const DeleteSecrets = `mutation($input: UnsetSecretsInput!) { + unsetSecrets(input: $input) { + release { + id + version + reason + description + user { + id + email + name + } + evaluationId + createdAt + } + } + }`; + + await standardRequest.post(INTEGRATION_FLYIO_API_URL, { + query: DeleteSecrets, + variables: { + input: { + appId: integration.app, + keys: deleteSecretsKeys, + }, + }, + }, { + headers: { + Authorization: "Bearer " + accessToken, + "Content-Type": "application/json", + 'Accept-Encoding': 'application/json', + }, + }); }; /** @@ -1370,68 +1295,62 @@ const syncSecretsCircleCI = async ({ secrets: any; accessToken: string; }) => { - try { - const circleciOrganizationDetail = ( - await standardRequest.get(`${INTEGRATION_CIRCLECI_API_URL}/v2/me/collaborations`, { + const circleciOrganizationDetail = ( + await standardRequest.get(`${INTEGRATION_CIRCLECI_API_URL}/v2/me/collaborations`, { + headers: { + "Circle-Token": accessToken, + "Accept-Encoding": "application/json", + }, + }) + ).data[0]; + + const { slug } = circleciOrganizationDetail; + + // sync secrets to CircleCI + Object.keys(secrets).forEach( + async (key) => + await standardRequest.post( + `${INTEGRATION_CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`, + { + name: key, + value: secrets[key], + }, + { + headers: { + "Circle-Token": accessToken, + "Content-Type": "application/json", + }, + } + ) + ); + + // get secrets from CircleCI + const getSecretsRes = ( + await standardRequest.get( + `${INTEGRATION_CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`, + { headers: { "Circle-Token": accessToken, "Accept-Encoding": "application/json", }, - }) - ).data[0]; + } + ) + ).data?.items; - const { slug } = circleciOrganizationDetail; - - // sync secrets to CircleCI - Object.keys(secrets).forEach( - async (key) => - await standardRequest.post( - `${INTEGRATION_CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`, - { - name: key, - value: secrets[key], - }, - { - headers: { - "Circle-Token": accessToken, - "Content-Type": "application/json", - }, - } - ) - ); - - // get secrets from CircleCI - const getSecretsRes = ( - await standardRequest.get( - `${INTEGRATION_CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar`, + // delete secrets from CircleCI + getSecretsRes.forEach(async (sec: any) => { + if (!(sec.name in secrets)) { + await standardRequest.delete( + `${INTEGRATION_CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar/${sec.name}`, { headers: { "Circle-Token": accessToken, - "Accept-Encoding": "application/json", + "Content-Type": "application/json", }, } - ) - ).data?.items; - - // delete secrets from CircleCI - getSecretsRes.forEach(async (sec: any) => { - if (!(sec.name in secrets)) { - await standardRequest.delete( - `${INTEGRATION_CIRCLECI_API_URL}/v2/project/${slug}/${integration.app}/envvar/${sec.name}`, - { - headers: { - "Circle-Token": accessToken, - "Content-Type": "application/json", - }, - } - ); - } - }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error("Failed to sync secrets to CircleCI"); - } + ); + } + }); }; /** @@ -1450,88 +1369,82 @@ const syncSecretsTravisCI = async ({ secrets: any; accessToken: string; }) => { - try { - // get secrets from travis-ci - const getSecretsRes = ( - await standardRequest.get( + // get secrets from travis-ci + const getSecretsRes = ( + await standardRequest.get( + `${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars?repository_id=${integration.appId}`, + { + headers: { + "Authorization": `token ${accessToken}`, + "Accept-Encoding": "application/json", + }, + } + ) + ) + .data + ?.env_vars + .reduce((obj: any, secret: any) => ({ + ...obj, + [secret.name]: secret + }), {}); + + // add secrets + for await (const key of Object.keys(secrets)) { + if (!(key in getSecretsRes)) { + // case: secret does not exist in travis ci + // -> add secret + await standardRequest.post( `${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars?repository_id=${integration.appId}`, + { + env_var: { + name: key, + value: secrets[key] + } + }, { headers: { "Authorization": `token ${accessToken}`, + "Content-Type": "application/json", "Accept-Encoding": "application/json", }, } - ) - ) - .data - ?.env_vars - .reduce((obj: any, secret: any) => ({ - ...obj, - [secret.name]: secret - }), {}); - - // add secrets - for await (const key of Object.keys(secrets)) { - if (!(key in getSecretsRes)) { - // case: secret does not exist in travis ci - // -> add secret - await standardRequest.post( - `${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars?repository_id=${integration.appId}`, - { - env_var: { - name: key, - value: secrets[key] - } - }, - { - headers: { - "Authorization": `token ${accessToken}`, - "Content-Type": "application/json", - "Accept-Encoding": "application/json", - }, + ); + } else { + // case: secret exists in travis ci + // -> update/set secret + await standardRequest.patch( + `${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars/${getSecretsRes[key].id}?repository_id=${getSecretsRes[key].repository_id}`, + { + env_var: { + name: key, + value: secrets[key], } - ); - } else { - // case: secret exists in travis ci - // -> update/set secret - await standardRequest.patch( - `${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars/${getSecretsRes[key].id}?repository_id=${getSecretsRes[key].repository_id}`, - { - env_var: { - name: key, - value: secrets[key], - } + }, + { + headers: { + "Authorization": `token ${accessToken}`, + "Content-Type": "application/json", + "Accept-Encoding": "application/json", }, - { - headers: { - "Authorization": `token ${accessToken}`, - "Content-Type": "application/json", - "Accept-Encoding": "application/json", - }, - } - ); - } + } + ); } + } - for await (const key of Object.keys(getSecretsRes)) { - if (!(key in secrets)){ - // delete secret - await standardRequest.delete( - `${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars/${getSecretsRes[key].id}?repository_id=${getSecretsRes[key].repository_id}`, - { - headers: { - "Authorization": `token ${accessToken}`, - "Content-Type": "application/json", - "Accept-Encoding": "application/json", - }, - } - ); - } + for await (const key of Object.keys(getSecretsRes)) { + if (!(key in secrets)){ + // delete secret + await standardRequest.delete( + `${INTEGRATION_TRAVISCI_API_URL}/settings/env_vars/${getSecretsRes[key].id}?repository_id=${getSecretsRes[key].repository_id}`, + { + headers: { + "Authorization": `token ${accessToken}`, + "Content-Type": "application/json", + "Accept-Encoding": "application/json", + }, + } + ); } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error("Failed to sync secrets to GitLab"); } } @@ -1552,57 +1465,73 @@ const syncSecretsGitLab = async ({ secrets: any; accessToken: string; }) => { - try { - interface GitLabSecret { - key: string; - value: string; - environment_scope: string; - } + interface GitLabSecret { + key: string; + value: string; + environment_scope: string; + } - const getAllEnvVariables = async (integrationAppId: string, accessToken: string) => { - const gitLabApiUrl = `${INTEGRATION_GITLAB_API_URL}/v4/projects/${integrationAppId}/variables`; - const headers = { - "Authorization": `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }; - - let allEnvVariables: GitLabSecret[] = []; - let url: string | null = `${gitLabApiUrl}?per_page=100`; - - while (url) { - const response: any = await standardRequest.get(url, { headers }); - allEnvVariables = [...allEnvVariables, ...response.data]; - - const linkHeader = response.headers.link; - const nextLink = linkHeader?.split(',').find((part: string) => part.includes('rel="next"')); - - if (nextLink) { - url = nextLink.trim().split(';')[0].slice(1, -1); - } else { - url = null; - } - } - - return allEnvVariables; + const getAllEnvVariables = async (integrationAppId: string, accessToken: string) => { + const gitLabApiUrl = `${INTEGRATION_GITLAB_API_URL}/v4/projects/${integrationAppId}/variables`; + const headers = { + "Authorization": `Bearer ${accessToken}`, + "Accept-Encoding": "application/json", }; + + let allEnvVariables: GitLabSecret[] = []; + let url: string | null = `${gitLabApiUrl}?per_page=100`; + + while (url) { + const response: any = await standardRequest.get(url, { headers }); + allEnvVariables = [...allEnvVariables, ...response.data]; + + const linkHeader = response.headers.link; + const nextLink = linkHeader?.split(',').find((part: string) => part.includes('rel="next"')); + + if (nextLink) { + url = nextLink.trim().split(';')[0].slice(1, -1); + } else { + url = null; + } + } + + return allEnvVariables; + }; - const allEnvVariables = await getAllEnvVariables(integration?.appId, accessToken); - const getSecretsRes: GitLabSecret[] = allEnvVariables.filter((secret: GitLabSecret) => - secret.environment_scope === integration.targetEnvironment - ); + const allEnvVariables = await getAllEnvVariables(integration?.appId, accessToken); + const getSecretsRes: GitLabSecret[] = allEnvVariables.filter((secret: GitLabSecret) => + secret.environment_scope === integration.targetEnvironment + ); - for await (const key of Object.keys(secrets)) { - const existingSecret = getSecretsRes.find((s: any) => s.key == key); - if (!existingSecret) { - await standardRequest.post( - `${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables`, + for await (const key of Object.keys(secrets)) { + const existingSecret = getSecretsRes.find((s: any) => s.key == key); + if (!existingSecret) { + await standardRequest.post( + `${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables`, + { + key: key, + value: secrets[key], + protected: false, + masked: false, + raw: false, + environment_scope: integration.targetEnvironment + }, + { + headers: { + "Authorization": `Bearer ${accessToken}`, + "Content-Type": "application/json", + "Accept-Encoding": "application/json", + }, + } + ) + } else { + // update secret + if (secrets[key] !== existingSecret.value) { + await standardRequest.put( + `${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables/${existingSecret.key}?filter[environment_scope]=${integration.targetEnvironment}`, { - key: key, - value: secrets[key], - protected: false, - masked: false, - raw: false, - environment_scope: integration.targetEnvironment + ...existingSecret, + value: secrets[existingSecret.key] }, { headers: { @@ -1611,46 +1540,23 @@ const syncSecretsGitLab = async ({ "Accept-Encoding": "application/json", }, } - ) - } else { - // update secret - if (secrets[key] !== existingSecret.value) { - await standardRequest.put( - `${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables/${existingSecret.key}?filter[environment_scope]=${integration.targetEnvironment}`, - { - ...existingSecret, - value: secrets[existingSecret.key] - }, - { - headers: { - "Authorization": `Bearer ${accessToken}`, - "Content-Type": "application/json", - "Accept-Encoding": "application/json", - }, - } - ); - } - } - } - - // delete secrets - for await (const sec of getSecretsRes) { - if (!(sec.key in secrets)) { - await standardRequest.delete( - `${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables/${sec.key}?filter[environment_scope]=${integration.targetEnvironment}`, - { - headers: { - "Authorization": `Bearer ${accessToken}`, - }, - } ); } } + } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error("Failed to sync secrets to GitLab"); + // delete secrets + for await (const sec of getSecretsRes) { + if (!(sec.key in secrets)) { + await standardRequest.delete( + `${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables/${sec.key}?filter[environment_scope]=${integration.targetEnvironment}`, + { + headers: { + "Authorization": `Bearer ${accessToken}`, + }, + } + ); + } } } @@ -1671,61 +1577,55 @@ const syncSecretsSupabase = async ({ secrets: any; accessToken: string; }) => { - try { - const { data: getSecretsRes } = await standardRequest.get( - `${INTEGRATION_SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`, - { - headers: { - Authorization: `Bearer ${accessToken}`, - 'Accept-Encoding': 'application/json' - } - } - ); - - // convert the secrets to [{}] format - const modifiedFormatForSecretInjection = Object.keys(secrets).map( - (key) => { - return { - name: key, - value: secrets[key] - }; - } - ); - - await standardRequest.post( - `${INTEGRATION_SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`, - modifiedFormatForSecretInjection, - { - headers: { - Authorization: `Bearer ${accessToken}`, - 'Accept-Encoding': 'application/json' - } - } - ); - - const secretsToDelete: any = []; - getSecretsRes?.forEach((secretObj: any) => { - if (!(secretObj.name in secrets)) { - secretsToDelete.push(secretObj.name); - } - }); - - await standardRequest.delete( - `${INTEGRATION_SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`, - { - headers: { + const { data: getSecretsRes } = await standardRequest.get( + `${INTEGRATION_SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`, + { + headers: { Authorization: `Bearer ${accessToken}`, - 'Content-Type': 'application/json', 'Accept-Encoding': 'application/json' - }, - data: secretsToDelete } - ); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to sync secrets to Supabase'); - } + } + ); + + // convert the secrets to [{}] format + const modifiedFormatForSecretInjection = Object.keys(secrets).map( + (key) => { + return { + name: key, + value: secrets[key] + }; + } + ); + + await standardRequest.post( + `${INTEGRATION_SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`, + modifiedFormatForSecretInjection, + { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Accept-Encoding': 'application/json' + } + } + ); + + const secretsToDelete: any = []; + getSecretsRes?.forEach((secretObj: any) => { + if (!(secretObj.name in secrets)) { + secretsToDelete.push(secretObj.name); + } + }); + + await standardRequest.delete( + `${INTEGRATION_SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + 'Accept-Encoding': 'application/json' + }, + data: secretsToDelete + } + ); }; From 5f5ed5d0a92a6b0ea119ec286b2fd5d119e6ed2d Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Mon, 5 Jun 2023 21:00:23 +0100 Subject: [PATCH 03/19] Change export convention for helper functions --- backend/src/controllers/v1/authController.ts | 17 +++++---- .../src/controllers/v1/passwordController.ts | 26 +++++++++++-- backend/src/helpers/auth.ts | 37 ++++++++----------- backend/src/helpers/bot.ts | 19 +++------- backend/src/helpers/database.ts | 9 +---- backend/src/helpers/event.ts | 6 +-- backend/src/helpers/index.ts | 17 +++++++++ backend/src/helpers/integration.ts | 23 ++++-------- backend/src/helpers/key.ts | 6 +-- backend/src/helpers/membership.ts | 17 +++------ backend/src/helpers/membershipOrg.ts | 17 +++------ backend/src/helpers/nodemailer.ts | 8 ++-- backend/src/helpers/organization.ts | 14 ++----- backend/src/helpers/rateLimiter.ts | 14 ++----- backend/src/helpers/secret.ts | 14 +++---- backend/src/helpers/secrets.ts | 30 +++++---------- backend/src/helpers/signup.ts | 10 ++--- backend/src/helpers/telemetry.ts | 0 backend/src/helpers/token.ts | 8 ++-- backend/src/helpers/workspace.ts | 11 ++---- backend/src/models/user.ts | 2 +- backend/src/services/BotService.ts | 4 +- 22 files changed, 133 insertions(+), 176 deletions(-) create mode 100644 backend/src/helpers/index.ts delete mode 100644 backend/src/helpers/telemetry.ts diff --git a/backend/src/controllers/v1/authController.ts b/backend/src/controllers/v1/authController.ts index 6f6a147bb1..b4854bd3ee 100644 --- a/backend/src/controllers/v1/authController.ts +++ b/backend/src/controllers/v1/authController.ts @@ -13,7 +13,7 @@ import { } from '../../variables'; import { BadRequestError } from '../../utils/errors'; import { EELogService } from '../../ee/services'; -import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this +import { getChannelFromUserAgent } from '../../utils/posthog'; import { getJwtRefreshSecret, getJwtAuthLifetime, @@ -24,6 +24,7 @@ import { declare module 'jsonwebtoken' { export interface UserIDJwtPayload extends jwt.JwtPayload { userId: string; + refreshVersion?: number; } } @@ -173,9 +174,7 @@ export const login2 = async (req: Request, res: Response) => { */ export const logout = async (req: Request, res: Response) => { try { - await clearTokens({ - userId: req.user._id.toString() - }); + await clearTokens(req.user._id); // clear httpOnly cookie res.cookie('jid', '', { @@ -223,7 +222,7 @@ export const checkAuth = async (req: Request, res: Response) => { } /** - * Return new token by redeeming refresh token + * Return new JWT access token by first validating the refresh token * @param req * @param res * @returns @@ -233,7 +232,7 @@ export const getNewToken = async (req: Request, res: Response) => { const refreshToken = req.cookies.jid; if (!refreshToken) { - throw new Error('Failed to find token in request cookies'); + throw new Error('Failed to find refresh token in request cookies'); } const decodedToken = ( @@ -242,12 +241,16 @@ export const getNewToken = async (req: Request, res: Response) => { const user = await User.findOne({ _id: decodedToken.userId - }).select('+publicKey'); + }).select('+publicKey +refreshVersion'); if (!user) throw new Error('Failed to authenticate unfound user'); if (!user?.publicKey) throw new Error('Failed to authenticate not fully set up account'); + if (decodedToken?.refreshVersion !== user.refreshVersion) throw BadRequestError({ + message: 'Failed to validate refresh token' + }); + const token = createToken({ payload: { userId: decodedToken.userId diff --git a/backend/src/controllers/v1/passwordController.ts b/backend/src/controllers/v1/passwordController.ts index f0e5ee3db2..bf8f5f1521 100644 --- a/backend/src/controllers/v1/passwordController.ts +++ b/backend/src/controllers/v1/passwordController.ts @@ -4,12 +4,20 @@ import * as Sentry from '@sentry/node'; const jsrp = require('jsrp'); import * as bigintConversion from 'bigint-conversion'; import { User, BackupPrivateKey, LoginSRPDetail } from '../../models'; -import { createToken } from '../../helpers/auth'; -import { sendMail } from '../../helpers/nodemailer'; +import { + createToken, + sendMail, + clearTokens +} from '../../helpers'; import { TokenService } from '../../services'; import { TOKEN_EMAIL_PASSWORD_RESET } from '../../variables'; import { BadRequestError } from '../../utils/errors'; -import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret } from '../../config'; +import { + getSiteURL, + getJwtSignupLifetime, + getJwtSignupSecret, + getHttpsEnabled +} from '../../config'; /** * Password reset step 1: Send email verification link to email [email] @@ -117,6 +125,7 @@ export const emailPasswordResetVerify = async (req: Request, res: Response) => { */ export const srp1 = async (req: Request, res: Response) => { // return salt, serverPublicKey as part of first step of SRP protocol + try { const { clientPublicKey } = req.body; const user = await User.findOne({ @@ -221,6 +230,17 @@ export const changePassword = async (req: Request, res: Response) => { new: true } ); + + // await clearTokens(user._id); + + // // clear httpOnly cookie + + // res.cookie('jid', '', { + // httpOnly: true, + // path: '/', + // sameSite: 'strict', + // secure: (await getHttpsEnabled()) as boolean + // }); return res.status(200).send({ message: 'Successfully changed password' diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index 2623d31536..c921fcf6f5 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -34,7 +34,7 @@ import { * @param {Object} obj * @param {Object} obj.headers - HTTP request headers object */ -const validateAuthMode = ({ +export const validateAuthMode = ({ headers, acceptedAuthModes }: { @@ -96,7 +96,7 @@ const validateAuthMode = ({ * @param {String} obj.authTokenValue - JWT token value * @returns {User} user - user corresponding to JWT token */ -const getAuthUserPayload = async ({ +export const getAuthUserPayload = async ({ authTokenValue }: { authTokenValue: string; @@ -122,7 +122,7 @@ const getAuthUserPayload = async ({ * @param {String} obj.authTokenValue - service token value * @returns {ServiceTokenData} serviceTokenData - service token data */ -const getAuthSTDPayload = async ({ +export const getAuthSTDPayload = async ({ authTokenValue }: { authTokenValue: string; @@ -168,7 +168,7 @@ const getAuthSTDPayload = async ({ * @param {String} obj.authTokenValue - service account access token value * @returns {ServiceAccount} serviceAccount */ -const getAuthSAAKPayload = async ({ +export const getAuthSAAKPayload = async ({ authTokenValue }: { authTokenValue: string; @@ -197,7 +197,7 @@ const getAuthSAAKPayload = async ({ * @param {String} obj.authTokenValue - API key value * @returns {APIKeyData} apiKeyData - API key data */ -const getAuthAPIKeyPayload = async ({ +export const getAuthAPIKeyPayload = async ({ authTokenValue }: { authTokenValue: string; @@ -254,7 +254,10 @@ const getAuthAPIKeyPayload = async ({ * @return {String} obj.token - issued JWT token * @return {String} obj.refreshToken - issued refresh token */ -const issueAuthTokens = async ({ userId }: { userId: string }) => { +export const issueAuthTokens = async ({ userId }: { userId: string }) => { + + const user = await User.findById(userId).select('+refreshVersion'); + if (!user) throw AccountNotFoundError(); // issue tokens const token = createToken({ @@ -267,7 +270,8 @@ const issueAuthTokens = async ({ userId }: { userId: string }) => { const refreshToken = createToken({ payload: { - userId + userId, + refreshVersion: user.refreshVersion }, expiresIn: await getJwtRefreshLifetime(), secret: await getJwtRefreshSecret() @@ -284,9 +288,9 @@ const issueAuthTokens = async ({ userId }: { userId: string }) => { * @param {Object} obj * @param {String} obj.userId - id of user whose tokens are cleared. */ -const clearTokens = async ({ userId }: { userId: string }): Promise => { +export const clearTokens = async (userId: Types.ObjectId): Promise => { // increment refreshVersion on user by 1 - User.findOneAndUpdate({ + await User.findOneAndUpdate({ _id: userId }, { $inc: { @@ -303,7 +307,7 @@ const clearTokens = async ({ userId }: { userId: string }): Promise => { * @param {String} obj.secret - (JWT) secret such as [JWT_AUTH_SECRET] * @param {String} obj.expiresIn - string describing time span such as '10h' or '7d' */ -const createToken = ({ +export const createToken = ({ payload, expiresIn, secret @@ -315,15 +319,4 @@ const createToken = ({ return jwt.sign(payload, secret, { expiresIn }); -}; - -export { - validateAuthMode, - getAuthUserPayload, - getAuthSTDPayload, - getAuthSAAKPayload, - getAuthAPIKeyPayload, - createToken, - issueAuthTokens, - clearTokens -}; +}; \ No newline at end of file diff --git a/backend/src/helpers/bot.ts b/backend/src/helpers/bot.ts index 04dfd7e365..e93ab6a8bc 100644 --- a/backend/src/helpers/bot.ts +++ b/backend/src/helpers/bot.ts @@ -31,7 +31,7 @@ import { InternalServerError } from "../utils/errors"; * @param {String} obj.name - name of bot * @param {String} obj.workspaceId - id of workspace that bot belongs to */ -const createBot = async ({ +export const createBot = async ({ name, workspaceId, }: { @@ -93,7 +93,7 @@ const createBot = async ({ * @param {String} obj.workspaceId - id of workspace * @param {String} obj.environment - environment */ -const getSecretsHelper = async ({ +export const getSecretsBotHelper = async ({ workspaceId, environment, }: { @@ -136,7 +136,7 @@ const getSecretsHelper = async ({ * @param {String} obj.workspaceId - id of workspace * @returns {String} key - decrypted workspace key */ -const getKey = async ({ workspaceId }: { workspaceId: string }) => { +export const getKey = async ({ workspaceId }: { workspaceId: string }) => { const encryptionKey = await getEncryptionKey(); const rootEncryptionKey = await getRootEncryptionKey(); @@ -194,7 +194,7 @@ const getKey = async ({ workspaceId }: { workspaceId: string }) => { * @param {String} obj1.workspaceId - id of workspace * @param {String} obj1.plaintext - plaintext to encrypt */ -const encryptSymmetricHelper = async ({ +export const encryptSymmetricHelper = async ({ workspaceId, plaintext, }: { @@ -222,7 +222,7 @@ const encryptSymmetricHelper = async ({ * @param {String} obj.iv - iv * @param {String} obj.tag - tag */ -const decryptSymmetricHelper = async ({ +export const decryptSymmetricHelper = async ({ workspaceId, ciphertext, iv, @@ -242,11 +242,4 @@ const decryptSymmetricHelper = async ({ }); return plaintext; -}; - -export { - createBot, - getSecretsHelper, - encryptSymmetricHelper, - decryptSymmetricHelper -}; +}; \ No newline at end of file diff --git a/backend/src/helpers/database.ts b/backend/src/helpers/database.ts index fa8c351552..b9284bbf8c 100644 --- a/backend/src/helpers/database.ts +++ b/backend/src/helpers/database.ts @@ -7,7 +7,7 @@ import { getLogger } from '../utils/logger'; * @param {String} obj.mongoURL - mongo connection string * @returns */ -const initDatabaseHelper = async ({ +export const initDatabaseHelper = async ({ mongoURL }: { mongoURL: string; @@ -30,7 +30,7 @@ const initDatabaseHelper = async ({ /** * Close database conection */ -const closeDatabaseHelper = async () => { +export const closeDatabaseHelper = async () => { return Promise.all([ new Promise((resolve) => { if (mongoose.connection && mongoose.connection.readyState == 1) { @@ -41,9 +41,4 @@ const closeDatabaseHelper = async () => { } }) ]); -} - -export { - initDatabaseHelper, - closeDatabaseHelper } \ No newline at end of file diff --git a/backend/src/helpers/event.ts b/backend/src/helpers/event.ts index c58da5b706..47c2437494 100644 --- a/backend/src/helpers/event.ts +++ b/backend/src/helpers/event.ts @@ -18,7 +18,7 @@ interface Event { * @param {String} obj.event.workspaceId - id of workspace that event is part of * @param {Object} obj.event.payload - payload of event (depends on event) */ -const handleEventHelper = async ({ event }: { event: Event }) => { +export const handleEventHelper = async ({ event }: { event: Event }) => { const { workspaceId, environment } = event; // TODO: moduralize bot check into separate function @@ -37,6 +37,4 @@ const handleEventHelper = async ({ event }: { event: Event }) => { }); break; } -}; - -export { handleEventHelper }; +}; \ No newline at end of file diff --git a/backend/src/helpers/index.ts b/backend/src/helpers/index.ts new file mode 100644 index 0000000000..b9d85f8713 --- /dev/null +++ b/backend/src/helpers/index.ts @@ -0,0 +1,17 @@ +export * from './auth'; +export * from './bot'; +export * from './database'; +export * from './event'; +export * from './integration'; +export * from './key'; +export * from './membership'; +export * from './membershipOrg'; +export * from './nodemailer'; +export * from './organization'; +export * from './rateLimiter'; +export * from './secret'; +export * from './secrets'; +export * from './signup'; +export * from './token'; +export * from './user'; +export * from './workspace'; \ No newline at end of file diff --git a/backend/src/helpers/integration.ts b/backend/src/helpers/integration.ts index 4d26bded9c..316ce1c890 100644 --- a/backend/src/helpers/integration.ts +++ b/backend/src/helpers/integration.ts @@ -37,7 +37,7 @@ interface Update { * @param {String} obj.code - code * @returns {IntegrationAuth} integrationAuth - integration auth after OAuth2 code-token exchange */ -const handleOAuthExchangeHelper = async ({ +export const handleOAuthExchangeHelper = async ({ workspaceId, integration, code, @@ -118,7 +118,7 @@ const handleOAuthExchangeHelper = async ({ * @param {Object} obj * @param {Object} obj.workspaceId - id of workspace */ -const syncIntegrationsHelper = async ({ +export const syncIntegrationsHelper = async ({ workspaceId, environment }: { @@ -177,7 +177,7 @@ const syncIntegrationsHelper = async ({ * @param {String} obj.integrationAuthId - id of integration auth * @param {String} refreshToken - decrypted refresh token */ - const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => { +export const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => { let refreshToken; try { @@ -214,7 +214,7 @@ const syncIntegrationsHelper = async ({ * @param {String} obj.integrationAuthId - id of integration auth * @returns {String} accessToken - decrypted access token */ -const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => { +export const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => { let accessId; let accessToken; try { @@ -277,7 +277,7 @@ const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrati * @param {String} obj.integrationAuthId - id of integration auth * @param {String} obj.refreshToken - refresh token */ -const setIntegrationAuthRefreshHelper = async ({ +export const setIntegrationAuthRefreshHelper = async ({ integrationAuthId, refreshToken }: { @@ -326,7 +326,7 @@ const setIntegrationAuthRefreshHelper = async ({ * @param {String} obj.accessToken - access token * @param {Date} obj.accessExpiresAt - expiration date of access token */ -const setIntegrationAuthAccessHelper = async ({ +export const setIntegrationAuthAccessHelper = async ({ integrationAuthId, accessId, accessToken, @@ -378,13 +378,4 @@ const setIntegrationAuthAccessHelper = async ({ } return integrationAuth; -} - -export { - handleOAuthExchangeHelper, - syncIntegrationsHelper, - getIntegrationAuthRefreshHelper, - getIntegrationAuthAccessHelper, - setIntegrationAuthRefreshHelper, - setIntegrationAuthAccessHelper -} +} \ No newline at end of file diff --git a/backend/src/helpers/key.ts b/backend/src/helpers/key.ts index bbd15ba585..afb4f6396b 100644 --- a/backend/src/helpers/key.ts +++ b/backend/src/helpers/key.ts @@ -17,7 +17,7 @@ interface Key { * @param {String} obj.keys.nonce - nonce for encryption * @param {String} obj.keys.userId - id of receiver user */ -const pushKeys = async ({ +export const pushKeys = async ({ userId, workspaceId, keys @@ -50,6 +50,4 @@ const pushKeys = async ({ workspace: workspaceId })) ); -}; - -export { pushKeys }; +}; \ No newline at end of file diff --git a/backend/src/helpers/membership.ts b/backend/src/helpers/membership.ts index a78100248b..70c42be23e 100644 --- a/backend/src/helpers/membership.ts +++ b/backend/src/helpers/membership.ts @@ -17,7 +17,7 @@ import { * @param {String} obj.workspaceId - id of workspace * @returns {Membership} membership - membership of user with id [userId] for workspace with id [workspaceId] */ -const validateMembership = async ({ +export const validateMembership = async ({ userId, workspaceId, acceptedRoles, @@ -50,7 +50,7 @@ const validateMembership = async ({ * @param {Object} queryObj - query object * @return {Object} membership - membership */ -const findMembership = async (queryObj: any) => { +export const findMembership = async (queryObj: any) => { let membership; try { membership = await Membership.findOne(queryObj); @@ -71,7 +71,7 @@ const findMembership = async (queryObj: any) => { * @param {String} obj.workspaceId - id of workspace. * @param {String[]} obj.roles - roles of users. */ -const addMemberships = async ({ +export const addMemberships = async ({ userIds, workspaceId, roles @@ -112,7 +112,7 @@ const addMemberships = async ({ * @param {Object} obj * @param {String} obj.membershipId - id of membership to delete */ -const deleteMembership = async ({ membershipId }: { membershipId: string }) => { +export const deleteMembership = async ({ membershipId }: { membershipId: string }) => { let deletedMembership; try { deletedMembership = await Membership.findOneAndDelete({ @@ -134,11 +134,4 @@ const deleteMembership = async ({ membershipId }: { membershipId: string }) => { } return deletedMembership; -}; - -export { - validateMembership, - addMemberships, - findMembership, - deleteMembership -}; +}; \ No newline at end of file diff --git a/backend/src/helpers/membershipOrg.ts b/backend/src/helpers/membershipOrg.ts index b5f4bb3663..f29f3cec02 100644 --- a/backend/src/helpers/membershipOrg.ts +++ b/backend/src/helpers/membershipOrg.ts @@ -18,7 +18,7 @@ import { * @param {Types.ObjectId} obj.organizationId * @param {String[]} obj.acceptedRoles */ -const validateMembershipOrg = async ({ +export const validateMembershipOrg = async ({ userId, organizationId, acceptedRoles, @@ -59,7 +59,7 @@ const validateMembershipOrg = async ({ * @param {Object} queryObj - query object * @return {Object} membershipOrg - membership */ -const findMembershipOrg = (queryObj: any) => { +export const findMembershipOrg = (queryObj: any) => { const membershipOrg = MembershipOrg.findOne(queryObj); return membershipOrg; }; @@ -72,7 +72,7 @@ const findMembershipOrg = (queryObj: any) => { * @param {String} obj.organizationId - id of organization. * @param {String[]} obj.roles - roles of users. */ -const addMembershipsOrg = async ({ +export const addMembershipsOrg = async ({ userIds, organizationId, roles, @@ -111,7 +111,7 @@ const addMembershipsOrg = async ({ * @param {Object} obj * @param {String} obj.membershipOrgId - id of organization membership to delete */ -const deleteMembershipOrg = async ({ +export const deleteMembershipOrg = async ({ membershipOrgId }: { membershipOrgId: string; @@ -148,11 +148,4 @@ const deleteMembershipOrg = async ({ } return deletedMembershipOrg; -}; - -export { - validateMembershipOrg, - findMembershipOrg, - addMembershipsOrg, - deleteMembershipOrg -}; +}; \ No newline at end of file diff --git a/backend/src/helpers/nodemailer.ts b/backend/src/helpers/nodemailer.ts index 78e8a12a03..c4f3de9b72 100644 --- a/backend/src/helpers/nodemailer.ts +++ b/backend/src/helpers/nodemailer.ts @@ -14,7 +14,7 @@ let smtpTransporter: nodemailer.Transporter; * @param {String[]} obj.recipients - email addresses of people to send email to * @param {Object} obj.substitutions - object containing template substitutions */ -const sendMail = async ({ +export const sendMail = async ({ template, subjectLine, recipients, @@ -48,8 +48,6 @@ const sendMail = async ({ } }; -const setTransporter = (transporter: nodemailer.Transporter) => { +export const setTransporter = (transporter: nodemailer.Transporter) => { smtpTransporter = transporter; -}; - -export { sendMail, setTransporter }; +}; \ No newline at end of file diff --git a/backend/src/helpers/organization.ts b/backend/src/helpers/organization.ts index 74c5df71d8..71e8c15ce1 100644 --- a/backend/src/helpers/organization.ts +++ b/backend/src/helpers/organization.ts @@ -28,7 +28,7 @@ import { * @param {String} obj.email - POC email that will receive invoice info * @param {Object} organization - new organization */ -const createOrganization = async ({ +export const createOrganization = async ({ name, email, }: { @@ -70,7 +70,7 @@ const createOrganization = async ({ * @return {Object} obj.stripeSubscription - new stripe subscription * @return {Subscription} obj.subscription - new subscription */ -const initSubscriptionOrg = async ({ +export const initSubscriptionOrg = async ({ organizationId, }: { organizationId: Types.ObjectId; @@ -125,7 +125,7 @@ const initSubscriptionOrg = async ({ * @param {Object} obj * @param {Number} obj.organizationId - id of subscription's organization */ -const updateSubscriptionOrgQuantity = async ({ +export const updateSubscriptionOrgQuantity = async ({ organizationId, }: { organizationId: string; @@ -171,10 +171,4 @@ const updateSubscriptionOrgQuantity = async ({ } return stripeSubscription; -}; - -export { - createOrganization, - initSubscriptionOrg, - updateSubscriptionOrgQuantity -}; +}; \ No newline at end of file diff --git a/backend/src/helpers/rateLimiter.ts b/backend/src/helpers/rateLimiter.ts index b082bceb3c..97921d8a3c 100644 --- a/backend/src/helpers/rateLimiter.ts +++ b/backend/src/helpers/rateLimiter.ts @@ -1,7 +1,7 @@ import rateLimit from 'express-rate-limit'; // 120 requests per minute -const apiLimiter = rateLimit({ +export const apiLimiter = rateLimit({ windowMs: 60 * 1000, max: 240, standardHeaders: true, @@ -20,23 +20,17 @@ const authLimit = rateLimit({ }); // 10 requests per hour -const passwordLimiter = rateLimit({ +export const passwordLimiter = rateLimit({ windowMs: 60 * 60 * 1000, max: 10, standardHeaders: true, legacyHeaders: false }); -const authLimiter = (req: any, res: any, next: any) => { +export const authLimiter = (req: any, res: any, next: any) => { if (process.env.NODE_ENV === 'production') { authLimit(req, res, next); } else { next(); } -}; - -export { - apiLimiter, - authLimiter, - passwordLimiter -}; +}; \ No newline at end of file diff --git a/backend/src/helpers/secret.ts b/backend/src/helpers/secret.ts index 9b81d79c9e..d6fcb873f5 100644 --- a/backend/src/helpers/secret.ts +++ b/backend/src/helpers/secret.ts @@ -62,7 +62,7 @@ interface Update { * @param {String} obj.environment - environment for secrets * @param {Object[]} obj.secrets - secrets to push */ -const v1PushSecrets = async ({ +export const v1PushSecrets = async ({ userId, workspaceId, environment, @@ -305,7 +305,7 @@ const v1PushSecrets = async ({ * @param {String} obj.channel - channel (web/cli/auto) * @param {String} obj.ipAddress - ip address of request to push secrets */ -const v2PushSecrets = async ({ +export const v2PushSecrets = async ({ userId, workspaceId, environment, @@ -530,7 +530,7 @@ const v2PushSecrets = async ({ * @param {String} obj.workspaceId - id of workspace to pull from * @param {String} obj.environment - environment for secrets */ -const getSecrets = async ({ +export const getSecrets = async ({ userId, workspaceId, environment, @@ -570,7 +570,7 @@ const getSecrets = async ({ * @param {String} obj.channel - channel (web/cli/auto) * @param {String} obj.ipAddress - ip address of request to push secrets */ -const pullSecrets = async ({ +export const pullSecrets = async ({ userId, workspaceId, environment, @@ -614,7 +614,7 @@ const pullSecrets = async ({ * @param {Object} obj * @param {Object} obj.secrets */ -const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => { +export const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => { const reformatedSecrets = secrets.map((s) => ({ _id: s._id, workspace: s.workspace, @@ -644,6 +644,4 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => { })); return reformatedSecrets; -}; - -export { v1PushSecrets, v2PushSecrets, pullSecrets, reformatPullSecrets }; +}; \ No newline at end of file diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index 562c173fa5..8042b2c7e9 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -52,7 +52,7 @@ import { * @param {Object} obj * @param {Types.ObjectId} obj.workspaceId */ -const createSecretBlindIndexDataHelper = async ({ +export const createSecretBlindIndexDataHelper = async ({ workspaceId }: { workspaceId: Types.ObjectId; @@ -106,7 +106,7 @@ const createSecretBlindIndexDataHelper = async ({ * @param {Types.ObjectId} obj.workspaceId - id of workspace to get salt for * @returns */ -const getSecretBlindIndexSaltHelper = async ({ +export const getSecretBlindIndexSaltHelper = async ({ workspaceId }: { workspaceId: Types.ObjectId; @@ -150,7 +150,7 @@ const getSecretBlindIndexSaltHelper = async ({ * @param {String} obj.secretName - name of secret to generate blind index for * @param {String} obj.salt - base64-salt */ - const generateSecretBlindIndexWithSaltHelper = async ({ +export const generateSecretBlindIndexWithSaltHelper = async ({ secretName, salt }: { @@ -179,7 +179,7 @@ const getSecretBlindIndexSaltHelper = async ({ * @param {Stringj} obj.secretName - name of secret to generate blind index for * @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to */ -const generateSecretBlindIndexHelper = async ({ +export const generateSecretBlindIndexHelper = async ({ secretName, workspaceId }: { @@ -220,7 +220,7 @@ const generateSecretBlindIndexHelper = async ({ * @param {AuthData} obj.authData - authentication data on request * @returns */ -const createSecretHelper = async ({ +export const createSecretHelper = async ({ secretName, workspaceId, environment, @@ -362,7 +362,7 @@ const createSecretHelper = async ({ * @param {AuthData} obj.authData - authentication data on request * @returns */ -const getSecretsHelper = async ({ +export const getSecretsHelper = async ({ workspaceId, environment, authData @@ -434,7 +434,7 @@ const getSecretsHelper = async ({ * @param {AuthData} obj.authData - authentication data on request * @returns */ -const getSecretHelper = async ({ +export const getSecretHelper = async ({ secretName, workspaceId, environment, @@ -519,7 +519,7 @@ const getSecretHelper = async ({ * @param {AuthData} obj.authData - authentication data on request * @returns */ -const updateSecretHelper = async ({ +export const updateSecretHelper = async ({ secretName, workspaceId, environment, @@ -656,7 +656,7 @@ const updateSecretHelper = async ({ * @param {AuthData} obj.authData - authentication data on request * @returns */ -const deleteSecretHelper = async ({ +export const deleteSecretHelper = async ({ secretName, workspaceId, environment, @@ -754,16 +754,4 @@ const deleteSecretHelper = async ({ secrets, secret }); -} - -export { - createSecretBlindIndexDataHelper, - getSecretBlindIndexSaltHelper, - generateSecretBlindIndexWithSaltHelper, - generateSecretBlindIndexHelper, - createSecretHelper, - getSecretsHelper, - getSecretHelper, - updateSecretHelper, - deleteSecretHelper } \ No newline at end of file diff --git a/backend/src/helpers/signup.ts b/backend/src/helpers/signup.ts index dfca967369..7a97463b24 100644 --- a/backend/src/helpers/signup.ts +++ b/backend/src/helpers/signup.ts @@ -14,7 +14,7 @@ import { TOKEN_EMAIL_CONFIRMATION } from '../variables'; * @param {String} obj.email - email * @returns {Boolean} success - whether or not operation was successful */ -const sendEmailVerification = async ({ email }: { email: string }) => { +export const sendEmailVerification = async ({ email }: { email: string }) => { try { const token = await TokenService.createToken({ type: TOKEN_EMAIL_CONFIRMATION, @@ -45,7 +45,7 @@ const sendEmailVerification = async ({ email }: { email: string }) => { * @param {String} obj.email - emai * @param {String} obj.code - code that was sent to [email] */ -const checkEmailVerification = async ({ +export const checkEmailVerification = async ({ email, code }: { @@ -72,7 +72,7 @@ const checkEmailVerification = async ({ * @param {String} obj.organizationName - name of organization to initialize * @param {IUser} obj.user - user who we are initializing for */ -const initializeDefaultOrg = async ({ +export const initializeDefaultOrg = async ({ organizationName, user }: { @@ -96,6 +96,4 @@ const initializeDefaultOrg = async ({ } catch (err) { throw new Error(`Failed to initialize default organization and workspace [err=${err}]`); } -}; - -export { sendEmailVerification, checkEmailVerification, initializeDefaultOrg }; +}; \ No newline at end of file diff --git a/backend/src/helpers/telemetry.ts b/backend/src/helpers/telemetry.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/src/helpers/token.ts b/backend/src/helpers/token.ts index b44641daea..66bae1a153 100644 --- a/backend/src/helpers/token.ts +++ b/backend/src/helpers/token.ts @@ -20,7 +20,7 @@ import { getSaltRounds } from "../config"; * @param {Types.ObjectId} obj.organizationId * @returns {String} token - the created token */ -const createTokenHelper = async ({ +export const createTokenHelper = async ({ type, email, phoneNumber, @@ -121,7 +121,7 @@ const createTokenHelper = async ({ * @param {String} obj.email - email associated with the token * @param {String} obj.token - value of the token */ -const validateTokenHelper = async ({ +export const validateTokenHelper = async ({ type, email, phoneNumber, @@ -212,6 +212,4 @@ const validateTokenHelper = async ({ // case: token is valid await TokenData.findByIdAndDelete(tokenData._id); -}; - -export { createTokenHelper, validateTokenHelper }; +}; \ No newline at end of file diff --git a/backend/src/helpers/workspace.ts b/backend/src/helpers/workspace.ts index 6bc8809812..701bc94bf2 100644 --- a/backend/src/helpers/workspace.ts +++ b/backend/src/helpers/workspace.ts @@ -16,7 +16,7 @@ import { SecretService } from '../services'; * @param {String} organizationId - id of organization to create workspace in * @param {Object} workspace - new workspace */ -const createWorkspace = async ({ +export const createWorkspace = async ({ name, organizationId }: { @@ -58,7 +58,7 @@ const createWorkspace = async ({ * @param {Object} obj * @param {String} obj.id - id of workspace to delete */ -const deleteWorkspace = async ({ id }: { id: string }) => { +export const deleteWorkspace = async ({ id }: { id: string }) => { try { await Workspace.deleteOne({ _id: id }); await Bot.deleteOne({ @@ -78,9 +78,4 @@ const deleteWorkspace = async ({ id }: { id: string }) => { Sentry.captureException(err); throw new Error('Failed to delete workspace'); } -}; - -export { - createWorkspace, - deleteWorkspace -}; +}; \ No newline at end of file diff --git a/backend/src/models/user.ts b/backend/src/models/user.ts index 4059ce8e0c..45f248924a 100644 --- a/backend/src/models/user.ts +++ b/backend/src/models/user.ts @@ -15,7 +15,7 @@ export interface IUser extends Document { tag?: string; salt?: string; verifier?: string; - refreshVersion?: number; + refreshVersion: number; isMfaEnabled: boolean; mfaMethods: boolean; devices: { diff --git a/backend/src/services/BotService.ts b/backend/src/services/BotService.ts index 4e8118fbea..2c0db03550 100644 --- a/backend/src/services/BotService.ts +++ b/backend/src/services/BotService.ts @@ -1,6 +1,6 @@ import { Types } from 'mongoose'; import { - getSecretsHelper, + getSecretsBotHelper, encryptSymmetricHelper, decryptSymmetricHelper } from '../helpers/bot'; @@ -25,7 +25,7 @@ class BotService { workspaceId: Types.ObjectId; environment: string; }) { - return await getSecretsHelper({ + return await getSecretsBotHelper({ workspaceId, environment }); From b9dad5c3f087d98dc42ca078b1b82930ad29f63b Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 6 Jun 2023 11:25:08 +0100 Subject: [PATCH 04/19] Begin preliminary tokenVersion impl --- backend/src/controllers/v1/authController.ts | 29 +++- .../src/controllers/v1/passwordController.ts | 16 +-- backend/src/controllers/v2/authController.ts | 2 - backend/src/controllers/v3/authController.ts | 6 +- backend/src/helpers/auth.ts | 53 ++++++-- backend/src/index.ts | 127 ++++++++++-------- backend/src/models/index.ts | 5 +- backend/src/models/tokenVersion.ts | 37 +++++ backend/src/models/user.ts | 6 - .../utilities/cryptography/changePassword.ts | 4 + 10 files changed, 195 insertions(+), 90 deletions(-) create mode 100644 backend/src/models/tokenVersion.ts diff --git a/backend/src/controllers/v1/authController.ts b/backend/src/controllers/v1/authController.ts index 7b323309a1..ddcbd6051d 100644 --- a/backend/src/controllers/v1/authController.ts +++ b/backend/src/controllers/v1/authController.ts @@ -4,14 +4,21 @@ import jwt from 'jsonwebtoken'; import * as bigintConversion from 'bigint-conversion'; // eslint-disable-next-line @typescript-eslint/no-var-requires const jsrp = require('jsrp'); -import { User, LoginSRPDetail } from '../../models'; +import { + User, + LoginSRPDetail, + TokenVersion +} from '../../models'; import { createToken, issueAuthTokens, clearTokens } from '../../helpers/auth'; import { checkUserDevice } from '../../helpers/user'; import { ACTION_LOGIN, ACTION_LOGOUT } from '../../variables'; -import { BadRequestError } from '../../utils/errors'; +import { + BadRequestError, + UnauthorizedRequestError +} from '../../utils/errors'; import { EELogService } from '../../ee/services'; import { getChannelFromUserAgent } from '../../utils/posthog'; import { @@ -241,19 +248,31 @@ export const getNewToken = async (req: Request, res: Response) => { const user = await User.findOne({ _id: decodedToken.userId - }).select('+publicKey +refreshVersion'); + }).select('+publicKey +refreshVersion +accessVersion'); if (!user) throw new Error('Failed to authenticate unfound user'); if (!user?.publicKey) throw new Error('Failed to authenticate not fully set up account'); + + const tokenVersion = await TokenVersion.findOne({ + _id: decodedToken.tokenVersionId, + user: user._id + }); - if (decodedToken?.refreshVersion !== user.refreshVersion) throw BadRequestError({ + console.log('tokenVersion: ', tokenVersion); + + if (!tokenVersion) throw UnauthorizedRequestError({ + message: 'Failed to validate refresh token' + }); + + if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) throw BadRequestError({ message: 'Failed to validate refresh token' }); const token = createToken({ payload: { - userId: decodedToken.userId + userId: decodedToken.userId, + accessVersion: tokenVersion.refreshVersion }, expiresIn: await getJwtAuthLifetime(), secret: await getJwtAuthSecret() diff --git a/backend/src/controllers/v1/passwordController.ts b/backend/src/controllers/v1/passwordController.ts index bf8f5f1521..c066bf793f 100644 --- a/backend/src/controllers/v1/passwordController.ts +++ b/backend/src/controllers/v1/passwordController.ts @@ -231,16 +231,16 @@ export const changePassword = async (req: Request, res: Response) => { } ); - // await clearTokens(user._id); + await clearTokens(user._id); - // // clear httpOnly cookie + // clear httpOnly cookie - // res.cookie('jid', '', { - // httpOnly: true, - // path: '/', - // sameSite: 'strict', - // secure: (await getHttpsEnabled()) as boolean - // }); + res.cookie('jid', '', { + httpOnly: true, + path: '/', + sameSite: 'strict', + secure: (await getHttpsEnabled()) as boolean + }); return res.status(200).send({ message: 'Successfully changed password' diff --git a/backend/src/controllers/v2/authController.ts b/backend/src/controllers/v2/authController.ts index a4ee36aae4..ff29c700a8 100644 --- a/backend/src/controllers/v2/authController.ts +++ b/backend/src/controllers/v2/authController.ts @@ -235,8 +235,6 @@ export const login2 = async (req: Request, res: Response) => { } }; -import { validateUserEmail } from '../../validation'; - /** * Send MFA token to email [email] * @param req diff --git a/backend/src/controllers/v3/authController.ts b/backend/src/controllers/v3/authController.ts index 61edffb420..59e3ef0434 100644 --- a/backend/src/controllers/v3/authController.ts +++ b/backend/src/controllers/v3/authController.ts @@ -113,7 +113,7 @@ export const login2 = async (req: Request, res: Response) => { const user = await User.findOne({ email, - }).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag'); + }).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices'); if (!user) throw new Error('Failed to find user'); @@ -183,6 +183,10 @@ export const login2 = async (req: Request, res: Response) => { userAgent: req.headers['user-agent'] ?? '' }); + console.log('logged in, issue tokens'); + console.log('ip: ', req.ip); + console.log('userAgent: ', req.headers['user-agent']); + // issue tokens const tokens = await issueAuthTokens({ userId: user._id.toString() }); diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index 2114f27301..be4fe4b491 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -6,7 +6,8 @@ import { User, ServiceTokenData, ServiceAccount, - APIKeyData + APIKeyData, + TokenVersion } from '../models'; import { AccountNotFoundError, @@ -108,11 +109,32 @@ export const getAuthUserPayload = async ({ const user = await User.findOne({ _id: decodedToken.userId - }).select('+publicKey'); + }).select('+publicKey +accessVersion'); - if (!user) throw AccountNotFoundError({ message: 'Failed to find User' }); + if (!user) throw AccountNotFoundError({ message: 'Failed to find user' }); - if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate User with partially set up account' }); + if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate user with partially set up account' }); + + console.log('getAuthUserPayload'); + + const tokenVersion = await TokenVersion.findOne({ + _id: decodedToken.tokenVersionId, + user: user._id + }); + + console.log('tokenVersion: ', tokenVersion); + + if (!tokenVersion) throw UnauthorizedRequestError({ + message: 'Failed to validate access token' + }); + + if (decodedToken.accessVersion !== tokenVersion.accessVersion) { + console.log('incorrect version'); + + throw UnauthorizedRequestError({ + message: 'Failed to validate access token' + }); + } return user; } @@ -256,14 +278,23 @@ export const getAuthAPIKeyPayload = async ({ * @return {String} obj.refreshToken - issued refresh token */ export const issueAuthTokens = async ({ userId }: { userId: string }) => { + + // TODO: create tokenVersion here + // TODO: include some kind of (channel) name here - const user = await User.findById(userId).select('+refreshVersion'); - if (!user) throw AccountNotFoundError(); + const tokenVersion = await new TokenVersion({ + user: new Types.ObjectId(userId), + name: '', // improve to channel + refreshVersion: 0, + accessVersion: 0 + }); // issue tokens const token = createToken({ payload: { - userId + userId, + tokenVersionId: tokenVersion._id.toString(), + accessVersion: tokenVersion.accessVersion }, expiresIn: await getJwtAuthLifetime(), secret: await getJwtAuthSecret() @@ -272,7 +303,8 @@ export const issueAuthTokens = async ({ userId }: { userId: string }) => { const refreshToken = createToken({ payload: { userId, - refreshVersion: user.refreshVersion + tokenVersionId: tokenVersion._id.toString(), + refreshVersion: tokenVersion.refreshVersion }, expiresIn: await getJwtRefreshLifetime(), secret: await getJwtRefreshSecret() @@ -291,11 +323,14 @@ export const issueAuthTokens = async ({ userId }: { userId: string }) => { */ export const clearTokens = async (userId: Types.ObjectId): Promise => { // increment refreshVersion on user by 1 + + // change this await User.findOneAndUpdate({ _id: userId }, { $inc: { - refreshVersion: 1 + refreshVersion: 1, + accessVersion: 1 } }); }; diff --git a/backend/src/index.ts b/backend/src/index.ts index 76790f368b..dd6642141d 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,6 +1,6 @@ import dotenv from "dotenv"; dotenv.config(); -import express, { Request, NextFunction, Response } from "express"; +import express from "express"; import helmet from "helmet"; import cors from "cors"; import { DatabaseService } from "./services"; @@ -9,9 +9,10 @@ import { setUpHealthEndpoint } from "./services/health"; import cookieParser from "cookie-parser"; import swaggerUi = require("swagger-ui-express"); // eslint-disable-next-line @typescript-eslint/no-var-requires -const swaggerFile = require('../spec.json'); +const swaggerFile = require("../spec.json"); // eslint-disable-next-line @typescript-eslint/no-var-requires -import { apiLimiter } from './helpers/rateLimiter'; +const requestIp = require("request-ip"); +import { apiLimiter } from "./helpers/rateLimiter"; import { workspace as eeWorkspaceRouter, secret as eeSecretRouter, @@ -73,81 +74,91 @@ const main = async () => { await EELicenseService.initGlobalFeatureSet(); const app = express(); - app.enable('trust proxy'); + app.enable("trust proxy"); app.use(express.json()); app.use(cookieParser()); app.use( - cors({ - credentials: true, - origin: await getSiteURL() - }) + cors({ + credentials: true, + origin: await getSiteURL(), + }) ); - if ((await getNodeEnv()) === 'production') { - // enable app-wide rate-limiting + helmet security - // in production - app.disable('x-powered-by'); - app.use(apiLimiter); - app.use(helmet()); + app.use(requestIp.mw()); + + if ((await getNodeEnv()) === "production") { + // enable app-wide rate-limiting + helmet security + // in production + app.disable("x-powered-by"); + app.use(apiLimiter); + app.use(helmet()); } // (EE) routes - app.use('/api/v1/secret', eeSecretRouter); - app.use('/api/v1/secret-snapshot', eeSecretSnapshotRouter); - app.use('/api/v1/workspace', eeWorkspaceRouter); - app.use('/api/v1/action', eeActionRouter); - app.use('/api/v1/organizations', eeOrganizationsRouter); - app.use('/api/v1/cloud-products', eeCloudProductsRouter); + app.use("/api/v1/secret", eeSecretRouter); + app.use("/api/v1/secret-snapshot", eeSecretSnapshotRouter); + app.use("/api/v1/workspace", eeWorkspaceRouter); + app.use("/api/v1/action", eeActionRouter); + app.use("/api/v1/organizations", eeOrganizationsRouter); + app.use("/api/v1/cloud-products", eeCloudProductsRouter); // v1 routes (default) - app.use('/api/v1/signup', v1SignupRouter); - app.use('/api/v1/auth', v1AuthRouter); - app.use('/api/v1/bot', v1BotRouter); - app.use('/api/v1/user', v1UserRouter); - app.use('/api/v1/user-action', v1UserActionRouter); - app.use('/api/v1/organization', v1OrganizationRouter); - app.use('/api/v1/workspace', v1WorkspaceRouter); - app.use('/api/v1/membership-org', v1MembershipOrgRouter); - app.use('/api/v1/membership', v1MembershipRouter); - app.use('/api/v1/key', v1KeyRouter); - app.use('/api/v1/invite-org', v1InviteOrgRouter); - app.use('/api/v1/secret', v1SecretRouter); // deprecate - app.use('/api/v1/service-token', v1ServiceTokenRouter); // deprecate - app.use('/api/v1/password', v1PasswordRouter); - app.use('/api/v1/stripe', v1StripeRouter); - app.use('/api/v1/integration', v1IntegrationRouter); - app.use('/api/v1/integration-auth', v1IntegrationAuthRouter); - app.use('/api/v1/folder', v1SecretsFolder) + app.use("/api/v1/signup", v1SignupRouter); + app.use("/api/v1/auth", v1AuthRouter); + app.use("/api/v1/bot", v1BotRouter); + app.use("/api/v1/user", v1UserRouter); + app.use("/api/v1/user-action", v1UserActionRouter); + app.use("/api/v1/organization", v1OrganizationRouter); + app.use("/api/v1/workspace", v1WorkspaceRouter); + app.use("/api/v1/membership-org", v1MembershipOrgRouter); + app.use("/api/v1/membership", v1MembershipRouter); + app.use("/api/v1/key", v1KeyRouter); + app.use("/api/v1/invite-org", v1InviteOrgRouter); + app.use("/api/v1/secret", v1SecretRouter); // deprecate + app.use("/api/v1/service-token", v1ServiceTokenRouter); // deprecate + app.use("/api/v1/password", v1PasswordRouter); + app.use("/api/v1/stripe", v1StripeRouter); + app.use("/api/v1/integration", v1IntegrationRouter); + app.use("/api/v1/integration-auth", v1IntegrationAuthRouter); + app.use("/api/v1/folders", v1SecretsFolder); // v2 routes (improvements) - app.use('/api/v2/signup', v2SignupRouter); - app.use('/api/v2/auth', v2AuthRouter); - app.use('/api/v2/users', v2UsersRouter); - app.use('/api/v2/organizations', v2OrganizationsRouter); - app.use('/api/v2/workspace', v2EnvironmentRouter); - app.use('/api/v2/workspace', v2TagsRouter); - app.use('/api/v2/workspace', v2WorkspaceRouter); - app.use('/api/v2/secret', v2SecretRouter); // deprecate - app.use('/api/v2/secrets', v2SecretsRouter); // note: in the process of moving to v3/secrets - app.use('/api/v2/service-token', v2ServiceTokenDataRouter); - app.use('/api/v2/service-accounts', v2ServiceAccountsRouter); // new - app.use('/api/v2/api-key', v2APIKeyDataRouter); + app.use("/api/v2/signup", v2SignupRouter); + app.use("/api/v2/auth", v2AuthRouter); + app.use("/api/v2/users", v2UsersRouter); + app.use("/api/v2/organizations", v2OrganizationsRouter); + app.use("/api/v2/workspace", v2EnvironmentRouter); + app.use("/api/v2/workspace", v2TagsRouter); + app.use("/api/v2/workspace", v2WorkspaceRouter); + app.use("/api/v2/secret", v2SecretRouter); // deprecate + app.use("/api/v2/secrets", v2SecretsRouter); // note: in the process of moving to v3/secrets + app.use("/api/v2/service-token", v2ServiceTokenDataRouter); + app.use("/api/v2/service-accounts", v2ServiceAccountsRouter); // new + app.use("/api/v2/api-key", v2APIKeyDataRouter); // v3 routes (experimental) - app.use('/api/v3/secrets', v3SecretsRouter); - app.use('/api/v3/workspaces', v3WorkspacesRouter); + app.use("/api/v3/auth", v3AuthRouter); + app.use("/api/v3/secrets", v3SecretsRouter); + app.use("/api/v3/workspaces", v3WorkspacesRouter); + app.use("/api/v3/signup", v3SignupRouter); - // api docs - app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile)) + // api docs + app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(swaggerFile)); // server status - app.use('/api', healthCheck) + app.use("/api", healthCheck); //* Handle unrouted requests and respond with proper error message as well as status code - app.use((req: Request, res: Response, next: NextFunction) => { + app.use((req, res, next) => { if (res.headersSent) return next(); - next(RouteNotFoundError({ message: `The requested source '(${req.method})${req.url}' was not found` })) - }) + next( + RouteNotFoundError({ + message: `The requested source '(${req.method})${req.url}' was not found`, + }) + ); + }); + + app.use(requestErrorHandler); const server = app.listen(await getPort(), async () => { (await getLogger("backend-main")).info( diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index 843296b550..cb4ee672af 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -22,6 +22,7 @@ import Workspace, { IWorkspace } from './workspace'; import ServiceTokenData, { IServiceTokenData } from './serviceTokenData'; import APIKeyData, { IAPIKeyData } from './apiKeyData'; import LoginSRPDetail, { ILoginSRPDetail } from './loginSRPDetail'; +import TokenVersion, { ITokenVersion } from './tokenVersion'; export { AuthProvider, @@ -72,5 +73,7 @@ export { APIKeyData, IAPIKeyData, LoginSRPDetail, - ILoginSRPDetail + ILoginSRPDetail, + TokenVersion, + ITokenVersion }; diff --git a/backend/src/models/tokenVersion.ts b/backend/src/models/tokenVersion.ts new file mode 100644 index 0000000000..c591809db8 --- /dev/null +++ b/backend/src/models/tokenVersion.ts @@ -0,0 +1,37 @@ +import { Schema, model, Types, Document } from 'mongoose'; + +export interface ITokenVersion extends Document { + user: Types.ObjectId; + name: string; + refreshVersion: number; + accessVersion: number; +} + +const tokenVersionSchema = new Schema( + { + user: { + type: Schema.Types.ObjectId, + ref: 'User', + required: true + }, + name: { + type: String, + required: true + }, + refreshVersion: { + type: Number, + required: true + }, + accessVersion: { + type: Number, + required: true + } + }, + { + timestamps: true + } +); + +const TokenVersion = model('TokenVersion', tokenVersionSchema); + +export default TokenVersion; \ No newline at end of file diff --git a/backend/src/models/user.ts b/backend/src/models/user.ts index 31163ebe8a..1c12720f82 100644 --- a/backend/src/models/user.ts +++ b/backend/src/models/user.ts @@ -21,7 +21,6 @@ export interface IUser extends Document { tag?: string; salt?: string; verifier?: string; - refreshVersion: number; isMfaEnabled: boolean; mfaMethods: boolean; devices: { @@ -91,11 +90,6 @@ const userSchema = new Schema( type: String, select: false }, - refreshVersion: { - type: Number, - default: 0, - select: false - }, isMfaEnabled: { type: Boolean, default: false diff --git a/frontend/src/components/utilities/cryptography/changePassword.ts b/frontend/src/components/utilities/cryptography/changePassword.ts index e5b8fffb5d..4f1eb85c30 100644 --- a/frontend/src/components/utilities/cryptography/changePassword.ts +++ b/frontend/src/components/utilities/cryptography/changePassword.ts @@ -125,6 +125,10 @@ const changePassword = async ( setPasswordChanged(true); setCurrentPassword(''); setNewPassword(''); + + window.location.href = '/login'; + + // move to login page } catch (error) { setCurrentPasswordError(true); console.log(error); From 846f5c6680a963ded6fc1119ec7664bef29720f8 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 6 Jun 2023 16:36:52 +0100 Subject: [PATCH 05/19] Upgraded JWT invalidation/session logic to separate TokenVersion model. --- backend/src/controllers/v1/authController.ts | 37 ++++++--- backend/src/controllers/v2/authController.ts | 16 ++-- .../src/controllers/v2/signupController.ts | 8 +- backend/src/controllers/v3/authController.ts | 10 +-- .../src/controllers/v3/signupController.ts | 4 +- backend/src/helpers/auth.ts | 76 +++++++++++-------- backend/src/interfaces/middleware/index.ts | 2 + backend/src/middleware/requireAuth.ts | 9 ++- backend/src/models/tokenVersion.ts | 14 +++- backend/src/routes/v1/auth.ts | 14 +++- backend/src/types/express/index.d.ts | 2 + frontend/src/hooks/api/auth/index.tsx | 4 +- frontend/src/hooks/api/auth/queries.tsx | 9 +++ frontend/src/pages/settings/personal/[id].tsx | 29 ++++++- 14 files changed, 170 insertions(+), 64 deletions(-) diff --git a/backend/src/controllers/v1/authController.ts b/backend/src/controllers/v1/authController.ts index ddcbd6051d..7bf86cffc7 100644 --- a/backend/src/controllers/v1/authController.ts +++ b/backend/src/controllers/v1/authController.ts @@ -13,7 +13,8 @@ import { createToken, issueAuthTokens, clearTokens } from '../../helpers/auth'; import { checkUserDevice } from '../../helpers/user'; import { ACTION_LOGIN, - ACTION_LOGOUT + ACTION_LOGOUT, + AUTH_MODE_JWT } from '../../variables'; import { BadRequestError, @@ -127,7 +128,11 @@ export const login2 = async (req: Request, res: Response) => { userAgent: req.headers['user-agent'] ?? '' }); - const tokens = await issueAuthTokens({ userId: user._id.toString() }); + const tokens = await issueAuthTokens({ + userId: user._id, + ip: req.ip, + userAgent: req.headers['user-agent'] ?? '' + }); // store (refresh) token in httpOnly cookie res.cookie('jid', tokens.refreshToken, { @@ -181,7 +186,10 @@ export const login2 = async (req: Request, res: Response) => { */ export const logout = async (req: Request, res: Response) => { try { - await clearTokens(req.user._id); + + if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User && req.authData.tokenVersionId) { + await clearTokens(req.authData.tokenVersionId) + } // clear httpOnly cookie res.cookie('jid', '', { @@ -216,6 +224,21 @@ export const logout = async (req: Request, res: Response) => { }); }; +export const revokeAllSessions = async (req: Request, res: Response) => { + await TokenVersion.updateMany({ + user: req.user._id + }, { + $inc: { + refreshVersion: 1, + accessVersion: 1 + } + }); + + return res.status(200).send({ + message: 'Successfully revoked all sessions.' + }); +} + /** * Return user is authenticated * @param req @@ -254,12 +277,7 @@ export const getNewToken = async (req: Request, res: Response) => { if (!user?.publicKey) throw new Error('Failed to authenticate not fully set up account'); - const tokenVersion = await TokenVersion.findOne({ - _id: decodedToken.tokenVersionId, - user: user._id - }); - - console.log('tokenVersion: ', tokenVersion); + const tokenVersion = await TokenVersion.findById(decodedToken.tokenVersionId); if (!tokenVersion) throw UnauthorizedRequestError({ message: 'Failed to validate refresh token' @@ -272,6 +290,7 @@ export const getNewToken = async (req: Request, res: Response) => { const token = createToken({ payload: { userId: decodedToken.userId, + tokenVersionId: tokenVersion._id.toString(), accessVersion: tokenVersion.refreshVersion }, expiresIn: await getJwtAuthLifetime(), diff --git a/backend/src/controllers/v2/authController.ts b/backend/src/controllers/v2/authController.ts index ff29c700a8..99f01d241a 100644 --- a/backend/src/controllers/v2/authController.ts +++ b/backend/src/controllers/v2/authController.ts @@ -22,10 +22,6 @@ import { getHttpsEnabled } from '../../config'; -// note: move this out -import path from 'path'; -import fs from 'fs'; - declare module 'jsonwebtoken' { export interface UserIDJwtPayload extends jwt.JwtPayload { userId: string; @@ -160,7 +156,11 @@ export const login2 = async (req: Request, res: Response) => { }); // issue tokens - const tokens = await issueAuthTokens({ userId: user._id.toString() }); + const tokens = await issueAuthTokens({ + userId: user._id, + ip: req.ip, + userAgent: req.headers['user-agent'] ?? '' + }); // store (refresh) token in httpOnly cookie res.cookie('jid', tokens.refreshToken, { @@ -301,7 +301,11 @@ export const verifyMfaToken = async (req: Request, res: Response) => { }); // issue tokens - const tokens = await issueAuthTokens({ userId: user._id.toString() }); + const tokens = await issueAuthTokens({ + userId: user._id, + ip: req.ip, + userAgent: req.headers['user-agent'] ?? '' + }); // store (refresh) token in httpOnly cookie res.cookie('jid', tokens.refreshToken, { diff --git a/backend/src/controllers/v2/signupController.ts b/backend/src/controllers/v2/signupController.ts index d9e9a0447f..ec73da75c6 100644 --- a/backend/src/controllers/v2/signupController.ts +++ b/backend/src/controllers/v2/signupController.ts @@ -116,7 +116,9 @@ export const completeAccountSignup = async (req: Request, res: Response) => { // issue tokens const tokens = await issueAuthTokens({ - userId: user._id.toString() + userId: user._id, + ip: req.ip, + userAgent: req.headers['user-agent'] ?? '' }); token = tokens.token; @@ -247,7 +249,9 @@ export const completeAccountInvite = async (req: Request, res: Response) => { // issue tokens const tokens = await issueAuthTokens({ - userId: user._id.toString() + userId: user._id, + ip: req.ip, + userAgent: req.headers['user-agent'] ?? '' }); token = tokens.token; diff --git a/backend/src/controllers/v3/authController.ts b/backend/src/controllers/v3/authController.ts index 59e3ef0434..cd3ee140ad 100644 --- a/backend/src/controllers/v3/authController.ts +++ b/backend/src/controllers/v3/authController.ts @@ -183,12 +183,12 @@ export const login2 = async (req: Request, res: Response) => { userAgent: req.headers['user-agent'] ?? '' }); - console.log('logged in, issue tokens'); - console.log('ip: ', req.ip); - console.log('userAgent: ', req.headers['user-agent']); - // issue tokens - const tokens = await issueAuthTokens({ userId: user._id.toString() }); + const tokens = await issueAuthTokens({ + userId: user._id, + ip: req.ip, + userAgent: req.headers['user-agent'] ?? '' + }); // store (refresh) token in httpOnly cookie res.cookie('jid', tokens.refreshToken, { diff --git a/backend/src/controllers/v3/signupController.ts b/backend/src/controllers/v3/signupController.ts index 61b4612dc4..6c09b71375 100644 --- a/backend/src/controllers/v3/signupController.ts +++ b/backend/src/controllers/v3/signupController.ts @@ -137,7 +137,9 @@ export const completeAccountSignup = async (req: Request, res: Response) => { // issue tokens const tokens = await issueAuthTokens({ - userId: user._id.toString() + userId: user._id, + ip: req.ip, + userAgent: req.headers['user-agent'] ?? '' }); token = tokens.token; diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index be4fe4b491..ce61f3650e 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -7,7 +7,8 @@ import { ServiceTokenData, ServiceAccount, APIKeyData, - TokenVersion + TokenVersion, + ITokenVersion } from '../models'; import { AccountNotFoundError, @@ -108,35 +109,32 @@ export const getAuthUserPayload = async ({ ); const user = await User.findOne({ - _id: decodedToken.userId + _id: new Types.ObjectId(decodedToken.userId) }).select('+publicKey +accessVersion'); if (!user) throw AccountNotFoundError({ message: 'Failed to find user' }); if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate user with partially set up account' }); - console.log('getAuthUserPayload'); - - const tokenVersion = await TokenVersion.findOne({ - _id: decodedToken.tokenVersionId, + const tokenVersion = await TokenVersion.findOneAndUpdate({ + _id: new Types.ObjectId(decodedToken.tokenVersionId), user: user._id + }, { + lastUsed: new Date() }); - console.log('tokenVersion: ', tokenVersion); - if (!tokenVersion) throw UnauthorizedRequestError({ message: 'Failed to validate access token' }); - if (decodedToken.accessVersion !== tokenVersion.accessVersion) { - console.log('incorrect version'); + if (decodedToken.accessVersion !== tokenVersion.accessVersion) throw UnauthorizedRequestError({ + message: 'Failed to validate access token' + }); - throw UnauthorizedRequestError({ - message: 'Failed to validate access token' - }); - } - - return user; + return ({ + user, + tokenVersionId: tokenVersion._id + }); } /** @@ -277,17 +275,36 @@ export const getAuthAPIKeyPayload = async ({ * @return {String} obj.token - issued JWT token * @return {String} obj.refreshToken - issued refresh token */ -export const issueAuthTokens = async ({ userId }: { userId: string }) => { - - // TODO: create tokenVersion here - // TODO: include some kind of (channel) name here +export const issueAuthTokens = async ({ + userId, + ip, + userAgent +}: { + userId: Types.ObjectId; + ip: string; + userAgent: string; +}) => { + let tokenVersion: ITokenVersion | null; - const tokenVersion = await new TokenVersion({ - user: new Types.ObjectId(userId), - name: '', // improve to channel - refreshVersion: 0, - accessVersion: 0 + // continue with (session) token version matching existing ip and user agent + tokenVersion = await TokenVersion.findOne({ + user: userId, + ip, + userAgent }); + + if (!tokenVersion) { + // case: no existing ip and user agent exists + // -> create new (session) token version for ip and user agent + tokenVersion = await new TokenVersion({ + user: userId, + refreshVersion: 0, + accessVersion: 0, + ip, + userAgent, + lastUsed: new Date() + }).save(); + } // issue tokens const token = createToken({ @@ -321,12 +338,11 @@ export const issueAuthTokens = async ({ userId }: { userId: string }) => { * @param {Object} obj * @param {String} obj.userId - id of user whose tokens are cleared. */ -export const clearTokens = async (userId: Types.ObjectId): Promise => { +export const clearTokens = async (tokenVersionId: Types.ObjectId): Promise => { // increment refreshVersion on user by 1 - - // change this - await User.findOneAndUpdate({ - _id: userId + + await TokenVersion.findOneAndUpdate({ + _id: tokenVersionId }, { $inc: { refreshVersion: 1, diff --git a/backend/src/interfaces/middleware/index.ts b/backend/src/interfaces/middleware/index.ts index 34230de056..a368a3be15 100644 --- a/backend/src/interfaces/middleware/index.ts +++ b/backend/src/interfaces/middleware/index.ts @@ -1,3 +1,4 @@ +import { Types } from 'mongoose'; import { IUser, IServiceAccount, @@ -10,4 +11,5 @@ export interface AuthData { authChannel: string; authIP: string; authUserAgent: string; + tokenVersionId?: Types.ObjectId; } \ No newline at end of file diff --git a/backend/src/middleware/requireAuth.ts b/backend/src/middleware/requireAuth.ts index 9ada0628be..17b7d757af 100644 --- a/backend/src/middleware/requireAuth.ts +++ b/backend/src/middleware/requireAuth.ts @@ -71,10 +71,12 @@ const requireAuth = ({ req.user = authPayload; break; default: - authPayload = await getAuthUserPayload({ + const { user, tokenVersionId } = await getAuthUserPayload({ authTokenValue }); - req.user = authPayload; + authPayload = user; + req.user = user; + req.tokenVersionId = tokenVersionId; break; } @@ -89,7 +91,8 @@ const requireAuth = ({ authPayload, // User, ServiceAccount, ServiceTokenData authChannel: getChannelFromUserAgent(req.headers['user-agent']), authIP: req.ip, - authUserAgent: req.headers['user-agent'] ?? 'other' + authUserAgent: req.headers['user-agent'] ?? 'other', + tokenVersionId: req.tokenVersionId } return next(); diff --git a/backend/src/models/tokenVersion.ts b/backend/src/models/tokenVersion.ts index c591809db8..890c0a2723 100644 --- a/backend/src/models/tokenVersion.ts +++ b/backend/src/models/tokenVersion.ts @@ -2,9 +2,11 @@ import { Schema, model, Types, Document } from 'mongoose'; export interface ITokenVersion extends Document { user: Types.ObjectId; - name: string; + ip: string; + userAgent: string; refreshVersion: number; accessVersion: number; + lastUsed: Date; } const tokenVersionSchema = new Schema( @@ -14,7 +16,11 @@ const tokenVersionSchema = new Schema( ref: 'User', required: true }, - name: { + ip: { + type: String, + required: true + }, + userAgent: { type: String, required: true }, @@ -25,6 +31,10 @@ const tokenVersionSchema = new Schema( accessVersion: { type: Number, required: true + }, + lastUsed: { + type: Date, + required: true } }, { diff --git a/backend/src/routes/v1/auth.ts b/backend/src/routes/v1/auth.ts index 20f5ec9b86..448bf24ea0 100644 --- a/backend/src/routes/v1/auth.ts +++ b/backend/src/routes/v1/auth.ts @@ -44,8 +44,6 @@ router.post( authController.checkAuth ); - - router.get( '/redirect/google', authLimiter, @@ -53,12 +51,20 @@ router.get( scope: ['profile', 'email'], session: false, }), -) +); router.get( '/callback/google', passport.authenticate('google', { failureRedirect: '/login/provider/error', session: false }), authController.handleAuthProviderCallback, -) +); + +router.delete( + '/sessions', + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT] + }), + authController.revokeAllSessions +); export default router; diff --git a/backend/src/types/express/index.d.ts b/backend/src/types/express/index.d.ts index 7cd5700205..cbf3906493 100644 --- a/backend/src/types/express/index.d.ts +++ b/backend/src/types/express/index.d.ts @@ -1,4 +1,5 @@ import * as express from 'express'; +import { Types } from 'mongoose'; import { IUser, IServiceAccount, @@ -39,6 +40,7 @@ declare global { serviceTokenData: any; apiKeyData: any; query?: any; + tokenVersionId?: Types.ObjectId; authData: AuthData; requestData: { [key: string]: string diff --git a/frontend/src/hooks/api/auth/index.tsx b/frontend/src/hooks/api/auth/index.tsx index de14c3bf42..195840efbb 100644 --- a/frontend/src/hooks/api/auth/index.tsx +++ b/frontend/src/hooks/api/auth/index.tsx @@ -1,4 +1,6 @@ export { useGetAuthToken, useSendMfaToken, - useVerifyMfaToken} from './queries' + useVerifyMfaToken, + useRevokeAllSessions +} from './queries' diff --git a/frontend/src/hooks/api/auth/queries.tsx b/frontend/src/hooks/api/auth/queries.tsx index 23e0c50fcb..b5f8f24065 100644 --- a/frontend/src/hooks/api/auth/queries.tsx +++ b/frontend/src/hooks/api/auth/queries.tsx @@ -49,3 +49,12 @@ export const useGetAuthToken = () => onSuccess: (data) => setAuthToken(data.token), retry: 0 }); + +export const useRevokeAllSessions = () => { + return useMutation({ + mutationFn: async () => { + const { data } = await apiRequest.delete('/api/v1/auth/sessions'); + return data; + } + }); +} \ No newline at end of file diff --git a/frontend/src/pages/settings/personal/[id].tsx b/frontend/src/pages/settings/personal/[id].tsx index 2b428aeca7..a1b7703f2b 100644 --- a/frontend/src/pages/settings/personal/[id].tsx +++ b/frontend/src/pages/settings/personal/[id].tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import Head from 'next/head'; import { useRouter } from 'next/router'; -import { faCheck, faPlus, faX } from '@fortawesome/free-solid-svg-icons'; +import { faCheck, faPlus, faX, faBan } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import Button from '@app/components/basic/buttons/Button'; @@ -18,6 +18,9 @@ import { SecuritySection } from '@app/views/Settings/PersonalSettingsPage/Securi import AddApiKeyDialog from '../../../components/basic/dialog/AddApiKeyDialog'; import getAPIKeys from '../../api/apiKey/getAPIKeys'; import getUser from '../../api/user/getUser'; +import { + useRevokeAllSessions +} from '@app/hooks/api'; export default function PersonalSettings() { const [personalEmail, setPersonalEmail] = useState(''); @@ -34,6 +37,8 @@ export default function PersonalSettings() { const [backupKeyError, setBackupKeyError] = useState(false); const [isAddApiKeyDialogOpen, setIsAddApiKeyDialogOpen] = useState(false); const [apiKeys, setApiKeys] = useState([]); + + const revokeAllSessions = useRevokeAllSessions(); const { t, i18n } = useTranslation(); const router = useRouter(); @@ -254,6 +259,28 @@ export default function PersonalSettings() { /> +
+
+

+ Sessions +

+
+
+
+

+ Logging into Infisical via browser or CLI creates a session. Revoking all sessions logs your account out all active sessions across all browsers and CLIs. +

+
From c5be497052307b47a0362b64ef0fe2169d9fa842 Mon Sep 17 00:00:00 2001 From: Tuan Dang Date: Tue, 6 Jun 2023 23:06:44 +0100 Subject: [PATCH 06/19] Strengthen password requirement --- .../src/controllers/v1/passwordController.ts | 9 +- .../src/components/signup/UserInfoStep.tsx | 87 +++++++------- .../utilities/checks/PasswordCheck.ts | 1 + .../utilities/checks/checkPassword.ts | 65 +++++++++++ frontend/src/pages/settings/personal/[id].tsx | 109 +++++++----------- frontend/src/pages/signupinvite.tsx | 98 +++++++--------- 6 files changed, 198 insertions(+), 171 deletions(-) create mode 100644 frontend/src/components/utilities/checks/checkPassword.ts diff --git a/backend/src/controllers/v1/passwordController.ts b/backend/src/controllers/v1/passwordController.ts index c066bf793f..a8d2bae858 100644 --- a/backend/src/controllers/v1/passwordController.ts +++ b/backend/src/controllers/v1/passwordController.ts @@ -10,7 +10,10 @@ import { clearTokens } from '../../helpers'; import { TokenService } from '../../services'; -import { TOKEN_EMAIL_PASSWORD_RESET } from '../../variables'; +import { + TOKEN_EMAIL_PASSWORD_RESET, + AUTH_MODE_JWT +} from '../../variables'; import { BadRequestError } from '../../utils/errors'; import { getSiteURL, @@ -231,7 +234,9 @@ export const changePassword = async (req: Request, res: Response) => { } ); - await clearTokens(user._id); + if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User && req.authData.tokenVersionId) { + await clearTokens(req.authData.tokenVersionId) + } // clear httpOnly cookie diff --git a/frontend/src/components/signup/UserInfoStep.tsx b/frontend/src/components/signup/UserInfoStep.tsx index 314e1e13a5..4c9ef4048a 100644 --- a/frontend/src/components/signup/UserInfoStep.tsx +++ b/frontend/src/components/signup/UserInfoStep.tsx @@ -2,7 +2,7 @@ import crypto from 'crypto'; import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { faCheck, faXmark } from '@fortawesome/free-solid-svg-icons'; +import { faXmark } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import jsrp from 'jsrp'; import nacl from 'tweetnacl'; @@ -13,7 +13,7 @@ import getOrganizations from '@app/pages/api/organization/getOrgs'; import ProjectService from '@app/services/ProjectService'; import InputField from '../basic/InputField'; -import passwordCheck from '../utilities/checks/PasswordCheck'; +import checkPassword from '../utilities/checks/checkPassword'; import Aes256Gcm from '../utilities/cryptography/aes-256-gcm'; import { deriveArgonKey } from '../utilities/cryptography/crypto'; import { saveTokenToLocalStorage } from '../utilities/saveTokenToLocalStorage'; @@ -37,6 +37,15 @@ interface UserInfoStepProps { providerAuthToken?: string; } +type Errors = { + length?: string, + upperCase?: string, + lowerCase?: string, + number?: string, + specialChar?: string, + repeatedChar?: string, +}; + /** * This is the step of the sign up flow where people provife their name/surname and password * @param {object} obj @@ -69,6 +78,8 @@ export default function UserInfoStep({ const [passwordErrorNumber, setPasswordErrorNumber] = useState(false); const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false); + const [errors, setErrors] = useState({}); + const [isLoading, setIsLoading] = useState(false); const { t } = useTranslation(); @@ -89,12 +100,10 @@ export default function UserInfoStep({ } else { setOrganizationNameError(false); } - errorCheck = passwordCheck({ + + errorCheck = checkPassword({ password, - setPasswordErrorLength, - setPasswordErrorNumber, - setPasswordErrorLowerCase, - errorCheck + setErrors }); if (!errorCheck) { @@ -248,12 +257,9 @@ export default function UserInfoStep({ label={t('section.password.password')} onChangeHandler={(pass: string) => { setPassword(pass); - passwordCheck({ + checkPassword({ password: pass, - setPasswordErrorLength, - setPasswordErrorNumber, - setPasswordErrorLowerCase, - errorCheck: false + setErrors }); }} type="password" @@ -263,44 +269,29 @@ export default function UserInfoStep({ autoComplete="new-password" id="new-password" /> - {passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber ? ( + {Object.keys(errors).length > 0 && (
-
{t('section.password.validate-base')}
-
- {passwordErrorLength ? ( - - ) : ( - - )} -
- {t('section.password.validate-length')} -
-
-
- {passwordErrorLowerCase ? ( - - ) : ( - - )} -
- {t('section.password.validate-case')} -
-
-
- {passwordErrorNumber ? ( - - ) : ( - - )} -
- {t('section.password.validate-number')} -
-
+
{t('section.password.validate-base')}
+ {Object.keys(errors).map((key) => { + if (errors[key as keyof Errors]) { + return ( +
+
+ +
+

+ {errors[key as keyof Errors]} +

+
+ ); + } + + return null; + })}
- ) : ( -
)}
diff --git a/frontend/src/components/utilities/checks/PasswordCheck.ts b/frontend/src/components/utilities/checks/PasswordCheck.ts index e84cba761b..5fb9dfe2cc 100644 --- a/frontend/src/components/utilities/checks/PasswordCheck.ts +++ b/frontend/src/components/utilities/checks/PasswordCheck.ts @@ -17,6 +17,7 @@ const passwordCheck = ({ setPasswordErrorLowerCase, errorCheck }: PasswordCheckProps) => { + if (!password || password.length < 14) { setPasswordErrorLength(true); errorCheck = true; diff --git a/frontend/src/components/utilities/checks/checkPassword.ts b/frontend/src/components/utilities/checks/checkPassword.ts new file mode 100644 index 0000000000..e4c5ccbbb7 --- /dev/null +++ b/frontend/src/components/utilities/checks/checkPassword.ts @@ -0,0 +1,65 @@ +type Errors = { + length?: string, + upperCase?: string, + lowerCase?: string, + number?: string, + specialChar?: string, + repeatedChar?: string, + }; + +interface CheckPasswordParams { + password: string; + setErrors: (value: Errors) => void; +} + +/** + * Validate that the password [password] is at least: + * - 8 characters long + * - Contains 1 uppercase character (A-Z) + * - Contains 1 lowercase character (a-z) + * - Contains 1 number (0-9) + * - Does not contain 3 repeat, consecutive characters + * + * The function returns whether or not the password [password] + * passes the minimum requirements above. It sets errors on + * an erorr object via [setErrors]. + * + * @param {Object} obj + * @param {String} obj.password - the password to check + * @param {Function} obj.setErrors - set state function to set error object + */ +const checkPassword = ({ + password, + setErrors +}: CheckPasswordParams): boolean => { + let errors: Errors = {}; + + if (password.length < 8) { + errors.length = "8 characters"; + } + + if (!/[A-Z]/.test(password)) { + errors.upperCase = "1 uppercase character (A-Z)"; + } + + if (!/[a-z]/.test(password)) { + errors.lowerCase = "1 lowercase character (a-z)"; + } + + if (!/[0-9]/.test(password)) { + errors.number = "1 number (0-9)"; + } + + if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) { + errors.specialChar = "1 special character (!@#$%^&*(),.?)"; + } + + if (/([A-Za-z0-9])\1\1\1/.test(password)) { + errors.repeatedChar = "No 3 repeat, consecutive characters"; + } + + setErrors(errors); + return Object.keys(errors).length > 0; +} + +export default checkPassword; \ No newline at end of file diff --git a/frontend/src/pages/settings/personal/[id].tsx b/frontend/src/pages/settings/personal/[id].tsx index a1b7703f2b..c7f7296cd4 100644 --- a/frontend/src/pages/settings/personal/[id].tsx +++ b/frontend/src/pages/settings/personal/[id].tsx @@ -2,7 +2,7 @@ import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import Head from 'next/head'; import { useRouter } from 'next/router'; -import { faCheck, faPlus, faX, faBan } from '@fortawesome/free-solid-svg-icons'; +import { faCheck, faPlus, faXmark, faBan } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import Button from '@app/components/basic/buttons/Button'; @@ -10,7 +10,7 @@ import InputField from '@app/components/basic/InputField'; import ListBox from '@app/components/basic/Listbox'; import ApiKeyTable from '@app/components/basic/table/ApiKeyTable'; import NavHeader from '@app/components/navigation/NavHeader'; -import passwordCheck from '@app/components/utilities/checks/PasswordCheck'; +import checkPassword from '@app/components/utilities/checks/checkPassword'; import changePassword from '@app/components/utilities/cryptography/changePassword'; import issueBackupKey from '@app/components/utilities/cryptography/issueBackupKey'; import { SecuritySection } from '@app/views/Settings/PersonalSettingsPage/SecuritySection/SecuritySection'; @@ -22,12 +22,18 @@ import { useRevokeAllSessions } from '@app/hooks/api'; +type Errors = { + length?: string, + upperCase?: string, + lowerCase?: string, + number?: string, + specialChar?: string, + repeatedChar?: string, +}; + export default function PersonalSettings() { const [personalEmail, setPersonalEmail] = useState(''); const [personalName, setPersonalName] = useState(''); - const [passwordErrorLength, setPasswordErrorLength] = useState(false); - const [passwordErrorNumber, setPasswordErrorNumber] = useState(false); - const [passwordErrorLowerCase, setPasswordErrorLowerCase] = useState(false); const [currentPasswordError, setCurrentPasswordError] = useState(false); const [currentPassword, setCurrentPassword] = useState(''); const [newPassword, setNewPassword] = useState(''); @@ -37,6 +43,7 @@ export default function PersonalSettings() { const [backupKeyError, setBackupKeyError] = useState(false); const [isAddApiKeyDialogOpen, setIsAddApiKeyDialogOpen] = useState(false); const [apiKeys, setApiKeys] = useState([]); + const [errors, setErrors] = useState({}); const revokeAllSessions = useRevokeAllSessions(); @@ -159,78 +166,52 @@ export default function PersonalSettings() { label={t('section.password.new') as string} onChangeHandler={(password) => { setNewPassword(password); - passwordCheck({ + checkPassword({ password, - setPasswordErrorLength, - setPasswordErrorNumber, - setPasswordErrorLowerCase, - errorCheck: false + setErrors }); }} type="password" value={newPassword} isRequired - error={passwordErrorLength && passwordErrorLowerCase && passwordErrorNumber} + error={Object.keys(errors).length > 0} autoComplete="new-password" id="new-password" />
- {passwordErrorLength || passwordErrorLowerCase || passwordErrorNumber ? ( -
-
- {t('section.password.validate-base')} -
-
- {passwordErrorLength ? ( - - ) : ( - - )} -
- {t('section.password.validate-length')} -
-
-
- {passwordErrorLowerCase ? ( - - ) : ( - - )} -
- {t('section.password.validate-case')} -
-
-
- {passwordErrorNumber ? ( - - ) : ( - - )} -
- {t('section.password.validate-number')} -
-
+ {Object.keys(errors).length > 0 && ( +
+
{t('section.password.validate-base')}
+ {Object.keys(errors).map((key) => { + if (errors[key as keyof Errors]) { + return ( +
+
+ +
+

+ {errors[key as keyof Errors]} +

+
+ ); + } + + return null; + })}
- ) : ( -
)}