Initial commit: Angular frontend and Expressjs backend
This commit is contained in:
42
worklenz-backend/src/passport/deserialize.ts
Normal file
42
worklenz-backend/src/passport/deserialize.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import moment from "moment";
|
||||
import db from "../config/db";
|
||||
import {IDeserializeCallback} from "../interfaces/deserialize-callback";
|
||||
import {IPassportSession} from "../interfaces/passport-session";
|
||||
|
||||
async function setLastActive(id: string) {
|
||||
try {
|
||||
await db.query("UPDATE users SET last_active = CURRENT_TIMESTAMP WHERE id = $1;", [id]);
|
||||
} catch (error) {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
async function clearEmailInvitations(email: string, teamId: string) {
|
||||
try {
|
||||
await db.query("DELETE FROM email_invitations WHERE email = $1 AND team_id = $2;", [email, teamId]);
|
||||
} catch (error) {
|
||||
// ignored
|
||||
}
|
||||
}
|
||||
|
||||
// Check whether the user still exists on the database
|
||||
export async function deserialize(id: string, done: IDeserializeCallback) {
|
||||
try {
|
||||
const q = `SELECT deserialize_user($1) AS user;`;
|
||||
const result = await db.query(q, [id]);
|
||||
if (result.rows.length) {
|
||||
const [data] = result.rows;
|
||||
if (data?.user) {
|
||||
data.user.is_member = !!data.user.team_member_id;
|
||||
|
||||
void setLastActive(data.user.id);
|
||||
void clearEmailInvitations(data.user.email, data.user.team_id);
|
||||
|
||||
return done(null, data.user as IPassportSession);
|
||||
}
|
||||
}
|
||||
return done(null, null);
|
||||
} catch (error) {
|
||||
return done(error, null);
|
||||
}
|
||||
}
|
||||
20
worklenz-backend/src/passport/index.ts
Normal file
20
worklenz-backend/src/passport/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import {PassportStatic} from "passport";
|
||||
|
||||
import {deserialize} from "./deserialize";
|
||||
import {serialize} from "./serialize";
|
||||
|
||||
import GoogleLogin from "./passport-strategies/passport-google";
|
||||
import LocalLogin from "./passport-strategies/passport-local-login";
|
||||
import LocalSignup from "./passport-strategies/passport-local-signup";
|
||||
|
||||
/**
|
||||
* Use any passport middleware before the serialize and deserialize
|
||||
* @param {Passport} passport
|
||||
*/
|
||||
export default (passport: PassportStatic) => {
|
||||
passport.use("local-login", LocalLogin);
|
||||
passport.use("local-signup", LocalSignup);
|
||||
passport.use(GoogleLogin);
|
||||
passport.serializeUser(serialize);
|
||||
passport.deserializeUser(deserialize);
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export const SUCCESS_KEY = "success";
|
||||
export const ERROR_KEY = "error";
|
||||
@@ -0,0 +1,73 @@
|
||||
import GoogleStrategy from "passport-google-oauth20";
|
||||
import {sendWelcomeEmail} from "../../shared/email-templates";
|
||||
import {log_error} from "../../shared/utils";
|
||||
import db from "../../config/db";
|
||||
import {ERROR_KEY} from "./passport-constants";
|
||||
import {Request} from "express";
|
||||
|
||||
async function handleGoogleLogin(req: Request, _accessToken: string, _refreshToken: string, profile: GoogleStrategy.Profile, done: GoogleStrategy.VerifyCallback) {
|
||||
try {
|
||||
const body: any = profile;
|
||||
if (Array.isArray(profile.emails) && profile.emails.length) body.email = profile.emails[0].value;
|
||||
if (Array.isArray(profile.photos) && profile.photos.length) body.picture = profile.photos[0].value;
|
||||
|
||||
// Check for existing accounts signed up using OAuth
|
||||
const localAccountResult = await db.query("SELECT 1 FROM users WHERE email = $1 AND password IS NOT NULL;", [body.email]);
|
||||
if (localAccountResult.rowCount) {
|
||||
const message = `No Google account exists for email ${body.email}.`;
|
||||
(req.session as any).error = message;
|
||||
return done(null, undefined, req.flash(ERROR_KEY, message));
|
||||
}
|
||||
|
||||
// If the user came from an invitation, this exists
|
||||
const state = JSON.parse(req.query.state as string || "{}");
|
||||
if (state) {
|
||||
body.team = state.team;
|
||||
body.member_id = state.teamMember;
|
||||
}
|
||||
|
||||
const q1 = `SELECT id, google_id, name, email, active_team
|
||||
FROM users
|
||||
WHERE google_id = $1
|
||||
OR email = $2;`;
|
||||
const result1 = await db.query(q1, [body.id, body.email]);
|
||||
|
||||
if (result1.rowCount) { // Login
|
||||
const [user] = result1.rows;
|
||||
|
||||
// Update active team of users who came from an invitation
|
||||
try {
|
||||
await db.query("SELECT set_active_team($1, $2);", [user.id || null, state.team || null]);
|
||||
} catch (error) {
|
||||
log_error(error, user);
|
||||
}
|
||||
|
||||
if (user)
|
||||
return done(null, user);
|
||||
|
||||
} else { // Register
|
||||
const q2 = `SELECT register_google_user($1) AS user;`;
|
||||
const result2 = await db.query(q2, [JSON.stringify(body)]);
|
||||
const [data] = result2.rows;
|
||||
|
||||
sendWelcomeEmail(data.user.email, body.displayName);
|
||||
return done(null, data.user, {message: "User successfully logged in"});
|
||||
}
|
||||
|
||||
return done(null);
|
||||
} catch (error: any) {
|
||||
return done(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Passport strategy for authenticate with google
|
||||
* http://www.passportjs.org/packages/passport-google-oauth20/
|
||||
*/
|
||||
export default new GoogleStrategy.Strategy({
|
||||
clientID: process.env.GOOGLE_CLIENT_ID as string,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
|
||||
callbackURL: process.env.GOOGLE_CALLBACK_URL as string,
|
||||
passReqToCallback: true
|
||||
},
|
||||
(req, _accessToken, _refreshToken, profile, done) => void handleGoogleLogin(req, _accessToken, _refreshToken, profile, done));
|
||||
@@ -0,0 +1,46 @@
|
||||
import bcrypt from "bcrypt";
|
||||
import {Strategy as LocalStrategy} from "passport-local";
|
||||
|
||||
import {log_error} from "../../shared/utils";
|
||||
import db from "../../config/db";
|
||||
import {Request} from "express";
|
||||
|
||||
async function handleLogin(req: Request, email: string, password: string, done: any) {
|
||||
(req.session as any).flash = {};
|
||||
|
||||
if (!email || !password)
|
||||
return done(null, false, {message: "Invalid credentials."});
|
||||
|
||||
try {
|
||||
// select the user from the database based on the username
|
||||
const q = `SELECT id, email, google_id, password
|
||||
FROM users
|
||||
WHERE email = $1
|
||||
AND google_id IS NULL;`;
|
||||
const result = await db.query(q, [email]);
|
||||
const [data] = result.rows;
|
||||
|
||||
// Check user existence
|
||||
if (!data?.password)
|
||||
return done(null, false, {message: "Invalid credentials."});
|
||||
|
||||
// Compare the password & email
|
||||
if (bcrypt.compareSync(password, data.password) && email === data.email) {
|
||||
delete data.password;
|
||||
|
||||
req.logout(() => true);
|
||||
return done(false, data, {message: "User successfully logged in"});
|
||||
}
|
||||
|
||||
return done(null, false, {message: "Invalid credentials."});
|
||||
} catch (error) {
|
||||
log_error(error, req.body);
|
||||
return done(error);
|
||||
}
|
||||
}
|
||||
|
||||
export default new LocalStrategy({
|
||||
usernameField: "email", // = email
|
||||
passwordField: "password",
|
||||
passReqToCallback: true
|
||||
}, (req, email, password, done) => void handleLogin(req, email, password, done));
|
||||
@@ -0,0 +1,96 @@
|
||||
import bcrypt from "bcrypt";
|
||||
import {Strategy as LocalStrategy} from "passport-local";
|
||||
|
||||
import {DEFAULT_ERROR_MESSAGE} from "../../shared/constants";
|
||||
import {sendWelcomeEmail} from "../../shared/email-templates";
|
||||
import {log_error} from "../../shared/utils";
|
||||
|
||||
import db from "../../config/db";
|
||||
import {Request} from "express";
|
||||
import {ERROR_KEY, SUCCESS_KEY} from "./passport-constants";
|
||||
|
||||
async function isGoogleAccountFound(email: string) {
|
||||
const q = `
|
||||
SELECT 1
|
||||
FROM users
|
||||
WHERE email = $1
|
||||
AND google_id IS NOT NULL;
|
||||
`;
|
||||
const result = await db.query(q, [email]);
|
||||
return !!result.rowCount;
|
||||
}
|
||||
|
||||
async function registerUser(password: string, team_id: string, name: string, team_name: string, email: string, timezone: string, team_member_id: string) {
|
||||
const salt = bcrypt.genSaltSync(10);
|
||||
const encryptedPassword = bcrypt.hashSync(password, salt);
|
||||
|
||||
const teamId = team_id || null;
|
||||
const q = "SELECT register_user($1) AS user;";
|
||||
|
||||
const body = {
|
||||
name,
|
||||
team_name,
|
||||
email,
|
||||
password: encryptedPassword,
|
||||
timezone,
|
||||
invited_team_id: teamId,
|
||||
team_member_id,
|
||||
};
|
||||
|
||||
const result = await db.query(q, [JSON.stringify(body)]);
|
||||
const [data] = result.rows;
|
||||
return data.user;
|
||||
}
|
||||
|
||||
async function handleSignUp(req: Request, email: string, password: string, done: any) {
|
||||
(req.session as any).flash = {};
|
||||
// team = Invited team_id if req.body.from_invitation is true
|
||||
const {name, team_name, team_member_id, team_id, timezone} = req.body;
|
||||
|
||||
if (!team_name) return done(null, null, req.flash(ERROR_KEY, "Team name is required"));
|
||||
|
||||
const googleAccountFound = await isGoogleAccountFound(email);
|
||||
if (googleAccountFound)
|
||||
return done(null, null, req.flash(ERROR_KEY, `${req.body.email} is already linked with a Google account.`));
|
||||
|
||||
try {
|
||||
const user = await registerUser(password, team_id, name, team_name, email, timezone, team_member_id);
|
||||
sendWelcomeEmail(email, name);
|
||||
|
||||
setTimeout(() => {
|
||||
return done(null, user, req.flash(SUCCESS_KEY, "Registration successful. Please check your email for verification."));
|
||||
}, 500);
|
||||
|
||||
} catch (error: any) {
|
||||
const message = (error?.message) || "";
|
||||
|
||||
if (message === "ERROR_INVALID_JOINING_EMAIL") {
|
||||
return done(null, null, req.flash(ERROR_KEY, `No invitations found for email ${req.body.email}.`));
|
||||
}
|
||||
|
||||
// if error.message is "email already exists" then it should have the email address in the error message after ":".
|
||||
if (message.includes("EMAIL_EXISTS_ERROR") || error.constraint === "users_google_id_uindex") {
|
||||
const [, value] = error.message.split(":");
|
||||
return done(null, null, req.flash(ERROR_KEY, `Worklenz account already exists for email ${value}.`));
|
||||
}
|
||||
|
||||
if (message.includes("TEAM_NAME_EXISTS_ERROR")) {
|
||||
const [, value] = error.message.split(":");
|
||||
return done(null, null, req.flash(ERROR_KEY, `Team name "${value}" already exists. Please choose a different team name.`));
|
||||
}
|
||||
|
||||
// The Team name is already taken.
|
||||
if (error.constraint === "teams_url_uindex" || error.constraint === "teams_name_uindex") {
|
||||
return done(null, null, req.flash(ERROR_KEY, `Team name "${team_name}" is already taken. Please choose a different team name.`));
|
||||
}
|
||||
|
||||
log_error(error, req.body);
|
||||
return done(null, null, req.flash(ERROR_KEY, DEFAULT_ERROR_MESSAGE));
|
||||
}
|
||||
}
|
||||
|
||||
export default new LocalStrategy({
|
||||
usernameField: "email",
|
||||
passwordField: "password",
|
||||
passReqToCallback: true
|
||||
}, (req, email, password, done) => void handleSignUp(req, email, password, done));
|
||||
7
worklenz-backend/src/passport/serialize.ts
Normal file
7
worklenz-backend/src/passport/serialize.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import {ISerializeCallback} from "../interfaces/serialize-callback";
|
||||
import {IPassportSession} from "../interfaces/passport-session";
|
||||
|
||||
// Parse the user id to deserialize function
|
||||
export function serialize($user: IPassportSession, done: ISerializeCallback) {
|
||||
done(null, $user?.id ?? null);
|
||||
}
|
||||
Reference in New Issue
Block a user