diff --git a/.gitignore b/.gitignore index 7463ee5..e15eca4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,28 +1,29 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -client/node_modules -server/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# production -/build -client/build - -# misc -.DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local - -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -.eslintcache -debug.log +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +client/node_modules +server/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build +client/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +.eslintcache +debug.log +.aider* diff --git a/README.md b/README.md index adc7944..a58b2c4 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,13 @@ npm run build 3. Run the server: +Modify the server/src/.env file with your secrets `SESSION_SECRET=` and `AUTH_JWT_SECRET=` are required and can be a long-ish random string. All others are optional. + +If you don't provide sendgrid API key then the emails are logged to console. + ``` cd server +npm install npm start ``` @@ -41,7 +46,13 @@ npm start To run the Seqoio server in Docker instead of natively, simply run this: ``` -docker-compose -f ./docker-compose.yml up -d --build --force-recreate +docker compose -f ./docker-compose.yml up -d --build --force-recreate +``` + +Afterwards run this to inspect the logs: + +``` +docker logs app ``` ## Authentication diff --git a/client/package.json b/client/package.json index ebc8b15..a9b22a1 100644 --- a/client/package.json +++ b/client/package.json @@ -29,7 +29,7 @@ "web-vitals": "^2.1.0" }, "scripts": { - "start": "react-scripts start", + "start": "NODE_OPTIONS=--openssl-legacy-provider react-scripts start", "build": "react-scripts build", "test": "react-scripts test", "eject": "react-scripts eject", diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml index db6b0cb..24669af 100644 --- a/docker-compose-prod.yml +++ b/docker-compose-prod.yml @@ -1,5 +1,3 @@ -version: "3" - services: db: diff --git a/docker-compose.yml b/docker-compose.yml index 8bf8545..cfe4650 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3" - services: db: diff --git a/server/package-lock.json b/server/package-lock.json index e2737a1..5736b25 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1061,6 +1061,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -2670,7 +2671,8 @@ "node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true }, "node_modules/http-cache-semantics": { "version": "4.1.0", @@ -3118,7 +3120,8 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true }, "node_modules/isstream": { "version": "0.1.2", @@ -3595,6 +3598,7 @@ "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "optional": true, "dependencies": { "minimist": "^1.2.5" }, @@ -8092,7 +8096,8 @@ "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true }, "node_modules/to-fast-properties": { "version": "2.0.0", @@ -8488,6 +8493,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -9422,6 +9428,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -10649,7 +10656,8 @@ "hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true }, "http-cache-semantics": { "version": "4.1.0", @@ -10959,7 +10967,8 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true }, "isstream": { "version": "0.1.2", @@ -11373,6 +11382,7 @@ "version": "0.5.5", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "optional": true, "requires": { "minimist": "^1.2.5" } @@ -14680,7 +14690,8 @@ "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=" + "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", + "dev": true }, "to-fast-properties": { "version": "2.0.0", @@ -14996,6 +15007,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "requires": { "isexe": "^2.0.0" } diff --git a/server/src/authentication/middleware/authentication.js b/server/src/authentication/middleware/authentication.js index 868cdb8..053611f 100644 --- a/server/src/authentication/middleware/authentication.js +++ b/server/src/authentication/middleware/authentication.js @@ -242,141 +242,158 @@ module.exports = function (passport) { }) ); - passport.use( - new OIDCStrategy( - { - identityMetadata: config.authentication.microsoft.identityMetadata, - clientID: config.authentication.microsoft.clientID, - responseType: config.authentication.microsoft.responseType, - responseMode: config.authentication.microsoft.responseMode, - redirectUrl: config.authentication.microsoft.redirectUrl, - allowHttpForRedirectUrl: - config.authentication.microsoft.allowHttpForRedirectUrl, - clientSecret: config.authentication.microsoft.clientSecret, - validateIssuer: config.authentication.microsoft.validateIssuer, - isB2C: config.authentication.microsoft.isB2C, - issuer: config.authentication.microsoft.issuer, - passReqToCallback: config.authentication.microsoft.passReqToCallback, - scope: config.authentication.microsoft.scope, - loggingLevel: config.authentication.microsoft.loggingLevel, - nonceLifetime: config.authentication.microsoft.nonceLifetime, - nonceMaxAmount: config.authentication.microsoft.nonceMaxAmount, - useCookieInsteadOfSession: - config.authentication.microsoft.useCookieInsteadOfSession, - cookieEncryptionKeys: - config.authentication.microsoft.cookieEncryptionKeys, - clockSkew: config.authentication.microsoft.clockSkew, - loggingNoPII: false, - }, - async (iss, sub, profile, accessToken, refreshToken, done) => { - const provider = "azure-ad"; - const oid = profile._json.oid; - const email = profile._json.email; - - let user = await UserModel.findOne({ provider, providerId: oid }); - if (user) { - log.info("Azure-AD user found"); - return done(null, user); - } - - user = await UserModel.findOne({ email }); - if (user) { - if (user.state === UserStates.VERIFY) { - log.warn("Waiting for email verification"); - return done(null, false, { - message: texts.allreadyWaitingForEmailVerification, - }); - } else { - log.warn("Known email"); - return done(null, false, { - message: formatString(texts.wrongProvider, user.provider), - }); + if ( + !config.authentication.microsoft.clientID || + !config.authentication.microsoft.clientSecret + ) { + console.log( + "Please provide AUTH_MICROSOFT_CLIENT_ID= and AUTH_MICROSOFT_CLIENT_SECRET= via .env if you want to enable Micosoft authentication" + ); + } else { + passport.use( + new OIDCStrategy( + { + identityMetadata: config.authentication.microsoft.identityMetadata, + clientID: config.authentication.microsoft.clientID, + responseType: config.authentication.microsoft.responseType, + responseMode: config.authentication.microsoft.responseMode, + redirectUrl: config.authentication.microsoft.redirectUrl, + allowHttpForRedirectUrl: + config.authentication.microsoft.allowHttpForRedirectUrl, + clientSecret: config.authentication.microsoft.clientSecret, + validateIssuer: config.authentication.microsoft.validateIssuer, + isB2C: config.authentication.microsoft.isB2C, + issuer: config.authentication.microsoft.issuer, + passReqToCallback: config.authentication.microsoft.passReqToCallback, + scope: config.authentication.microsoft.scope, + loggingLevel: config.authentication.microsoft.loggingLevel, + nonceLifetime: config.authentication.microsoft.nonceLifetime, + nonceMaxAmount: config.authentication.microsoft.nonceMaxAmount, + useCookieInsteadOfSession: + config.authentication.microsoft.useCookieInsteadOfSession, + cookieEncryptionKeys: + config.authentication.microsoft.cookieEncryptionKeys, + clockSkew: config.authentication.microsoft.clockSkew, + loggingNoPII: false, + }, + async (iss, sub, profile, accessToken, refreshToken, done) => { + const provider = "azure-ad"; + const oid = profile._json.oid; + const email = profile._json.email; + let user = await UserModel.findOne({ provider, providerId: oid }); + if (user) { + log.info("Azure-AD user found"); + return done(null, user); + } + user = await UserModel.findOne({ email }); + if (user) { + if (user.state === UserStates.VERIFY) { + log.warn("Waiting for email verification"); + return done(null, false, { + message: texts.allreadyWaitingForEmailVerification, + }); + } else { + log.warn("Known email"); + return done(null, false, { + message: formatString(texts.wrongProvider, user.provider), + }); + } } + log.info("Updating Azure-AD user"); + log.info("Create Azure-AD user"); + user = await UserModel.create({ + oid, + state: UserStates.ACTIVE, + roles: DefaultRoles, + provider, + providerId: oid, + email, + }); + await emails.sendWelcomeEmail(user.email); + done(null, user); } + ) + ); + } + + if ( + !config.authentication.google.clientID || + !config.authentication.google.clientSecret + ) { + console.log( + "Please provide AUTH_GOOGLE_CLIENT_ID= and AUTH_GOOGLE_CLIENT_SECRET= via .env if you want to enable Google authentication" + ); + } else { + passport.use( + new GoogleStrategy( + { + clientID: config.authentication.google.clientID, + clientSecret: config.authentication.google.clientSecret, + callbackURL: config.authentication.google.redirectUrl, + scope: config.authentication.google.scope, + }, + async (accessToken, refreshToken, profile, done) => { + const provider = "google"; + const oid = profile.id; + const email = profile._json.email; + + let user = await UserModel.findOne({ provider, providerId: oid }); + if (user) { + log.info("Google user found"); + return done(null, user); + } - log.info("Updating Azure-AD user"); - log.info("Create Azure-AD user"); - user = await UserModel.create({ - oid, - state: UserStates.ACTIVE, - roles: DefaultRoles, - provider, - providerId: oid, - email, - }); + user = await UserModel.findOne({ email }); + if (user) { + if (user.state === UserStates.VERIFY) { + log.warn("Waiting for email verification"); + return done(null, false, { + message: texts.allreadyWaitingForEmailVerification, + }); + } else { + log.warn("Known email"); + return done(null, false, { + message: formatString(texts.wrongProvider, user.provider), + }); + } + } - await emails.sendWelcomeEmail(user.email); + log.info("Create Google user"); + user = await UserModel.create({ + oid, + state: UserStates.ACTIVE, + roles: DefaultRoles, + provider, + providerId: oid, + email, + }); - done(null, user); - } - ) - ); + await emails.sendWelcomeEmail(user.email); - passport.use( - new GoogleStrategy( - { - clientID: config.authentication.google.clientID, - clientSecret: config.authentication.google.clientSecret, - callbackURL: config.authentication.google.redirectUrl, - scope: config.authentication.google.scope, - }, - async (accessToken, refreshToken, profile, done) => { - const provider = "google"; - const oid = profile.id; - const email = profile._json.email; - - let user = await UserModel.findOne({ provider, providerId: oid }); - if (user) { - log.info("Google user found"); - return done(null, user); + done(null, user); } - - user = await UserModel.findOne({ email }); - if (user) { - if (user.state === UserStates.VERIFY) { - log.warn("Waiting for email verification"); - return done(null, false, { - message: texts.allreadyWaitingForEmailVerification, - }); - } else { - log.warn("Known email"); - return done(null, false, { - message: formatString(texts.wrongProvider, user.provider), - }); - } + ) + ); + } + + if (!config.jwtSecret) { + console.log("Please provide AUTH_JWT_SECRET= via .env!"); + process.exit(1); + } else { + const jwtOptions = { + jwtFromRequest: (req) => (req && req.cookies ? req.cookies.token : null), + secretOrKey: config.jwtSecret, + }; + passport.use( + new JwtStrategy(jwtOptions, async (token, done) => { + try { + return done(null, { id: token.id, roles: token.roles }); + } catch (error) { + done(error); } - - log.info("Create Google user"); - user = await UserModel.create({ - oid, - state: UserStates.ACTIVE, - roles: DefaultRoles, - provider, - providerId: oid, - email, - }); - - await emails.sendWelcomeEmail(user.email); - - done(null, user); - } - ) - ); - - const jwtOptions = { - jwtFromRequest: (req) => (req && req.cookies ? req.cookies.token : null), - secretOrKey: config.jwtSecret, - }; - - passport.use( - new JwtStrategy(jwtOptions, async (token, done) => { - try { - return done(null, { id: token.id, roles: token.roles }); - } catch (error) { - done(error); - } - }) - ); + }) + ); + } return passport; }; diff --git a/server/src/authentication/middleware/email.js b/server/src/authentication/middleware/email.js index c26b33a..0886407 100644 --- a/server/src/authentication/middleware/email.js +++ b/server/src/authentication/middleware/email.js @@ -1,97 +1,105 @@ -const config = require("../../config/config"); -const formatString = require("util").format; -const nodemailer = require("nodemailer"); -const nodemailerSendgrid = require("nodemailer-sendgrid"); - -const transport = nodemailer.createTransport( - nodemailerSendgrid({ - apiKey: config.sendGrid.apiKey, - }) -); - -const texts = { - emailFrom: "mail@mroc.de", - emailSignupSubject: "Please confirm your email address for Seqoio", - emailSignup: `Hey, - - Please confirm your email address to finish your Seqoio account. - - To do so, please click on the following link, or paste this into your browser to complete the process: - %s - - If you did not request this, please ignore this email. - - Matthias from Seqoio`, - emailSetPasswordSubject: "Seqoio password reset", - emailSetPassword: `Hey, - - You are receiving this email because you or someone else has requested the reset of the password for your account. - - Please click on the following link, or paste this into your browser to complete the process: - %s - - If you did not request this, please ignore this email and your password will remain unchanged. - - Matthias from Seqoio`, - emailPasswordChangedSubject: "Seqoio password has been updated", - emailPasswordChangedBody: `Hey, - - The password for your Seqoio account was successfully updated! - - Matthias from Seqoio`, - - emailSubject: "Welcome to Seqoio", - emailBody: `Hey, - - Welcome to the Seqoio, we're happy to have you on board! - - Matthias from Seqoio`, -}; - - -async function sendSignupEmail(email, url) { - const resetEmail = { - to: email, - from: texts.emailFrom, - subject: texts.emailSignupSubject, - text: formatString(texts.emailSignup, url), - }; - await transport.sendMail(resetEmail); -} - -async function sendSetPasswordEmail(email, url) { - const resetEmail = { - to: email, - from: texts.emailFrom, - subject: texts.emailSetPasswordSubject, - text: formatString(texts.emailSetPassword, url), - }; - await transport.sendMail(resetEmail); -} - -async function sendSetPasswordConfirmation(email) { - const resetEmail = { - to: email, - from: texts.emailFrom, - subject: texts.emailPasswordChangedSubject, - text: texts.emailPasswordChangedBody, - }; - await transport.sendMail(resetEmail); -} - -async function sendWelcomeEmail(email) { - const welcomeEmail = { - to: email, - from: texts.emailFrom, - subject: texts.emailSubject, - text: texts.emailBody, - }; - await transport.sendMail(welcomeEmail); -} - -module.exports = { - sendWelcomeEmail, - sendSignupEmail, - sendSetPasswordEmail, - sendSetPasswordConfirmation, -}; +const config = require("../../config/config"); +const formatString = require("util").format; +const nodemailer = require("nodemailer"); +const nodemailerSendgrid = require("nodemailer-sendgrid"); +const console = require("console"); + +const transport = nodemailer.createTransport( + nodemailerSendgrid({ + apiKey: config.sendGrid.apiKey, + }) +); + +async function sendMail(emailOptions) { + if (!config.sendGrid.apiKey) { + console.log("Email would be sent (API key not set):", emailOptions); + return; + } + await transport.sendMail(emailOptions); +} + +const texts = { + emailFrom: "mail@mroc.de", + emailSignupSubject: "Please confirm your email address for Seqoio", + emailSignup: `Hey, + + Please confirm your email address to finish your Seqoio account. + + To do so, please click on the following link, or paste this into your browser to complete the process: + %s + + If you did not request this, please ignore this email. + + Matthias from Seqoio`, + emailSetPasswordSubject: "Seqoio password reset", + emailSetPassword: `Hey, + + You are receiving this email because you or someone else has requested the reset of the password for your account. + + Please click on the following link, or paste this into your browser to complete the process: + %s + + If you did not request this, please ignore this email and your password will remain unchanged. + + Matthias from Seqoio`, + emailPasswordChangedSubject: "Seqoio password has been updated", + emailPasswordChangedBody: `Hey, + + The password for your Seqoio account was successfully updated! + + Matthias from Seqoio`, + + emailSubject: "Welcome to Seqoio", + emailBody: `Hey, + + Welcome to the Seqoio, we're happy to have you on board! + + Matthias from Seqoio`, +}; + +async function sendSignupEmail(email, url) { + const resetEmail = { + to: email, + from: texts.emailFrom, + subject: texts.emailSignupSubject, + text: formatString(texts.emailSignup, url), + }; + await sendMail(resetEmail); +} + +async function sendSetPasswordEmail(email, url) { + const resetEmail = { + to: email, + from: texts.emailFrom, + subject: texts.emailSetPasswordSubject, + text: formatString(texts.emailSetPassword, url), + }; + await sendMail(resetEmail); +} + +async function sendSetPasswordConfirmation(email) { + const resetEmail = { + to: email, + from: texts.emailFrom, + subject: texts.emailPasswordChangedSubject, + text: texts.emailPasswordChangedBody, + }; + await sendMail(resetEmail); +} + +async function sendWelcomeEmail(email) { + const welcomeEmail = { + to: email, + from: texts.emailFrom, + subject: texts.emailSubject, + text: texts.emailBody, + }; + await sendMail(welcomeEmail); +} + +module.exports = { + sendWelcomeEmail, + sendSignupEmail, + sendSetPasswordEmail, + sendSetPasswordConfirmation, +};