beta trying better practice making sure an e-mail address works before creating the account

This commit is contained in:
adueck 2023-12-17 10:47:00 +04:00
parent 5c67fb5179
commit d2a11a8004
5 changed files with 420 additions and 339 deletions

View File

@ -3,14 +3,14 @@ import inProd from "./inProd";
import env from "./env-vars"; import env from "./env-vars";
import * as T from "../../../website/src/types/account-types"; import * as T from "../../../website/src/types/account-types";
type Address = string | { name: string, address: string }; type Address = string | { name: string; address: string };
const adminAddress: Address = { const adminAddress: Address = {
name: "LingDocs Admin", name: "LingDocs Admin",
address: "admin@lingdocs.com", address: "admin@lingdocs.com",
}; };
function getAddress(user: T.LingdocsUser): Address { export function getAddress(user: T.LingdocsUser): Address {
// TODO: Guard against "" // TODO: Guard against ""
if (!user.name) return user.email || ""; if (!user.name) return user.email || "";
return { return {
@ -39,19 +39,34 @@ async function sendEmail(to: Address, subject: string, text: string) {
} }
// TODO: MAKE THIS A URL ACROSS PROJECT // TODO: MAKE THIS A URL ACROSS PROJECT
const baseURL = inProd ? "https://account.lingdocs.com" : "http://localhost:4000"; const baseURL = inProd
? "https://account.lingdocs.com"
: "http://localhost:4000";
export async function sendVerificationEmail(user: T.LingdocsUser, token: T.URLToken) { export async function sendVerificationEmail({
name,
uid,
email,
token,
}: {
name: string;
uid: T.UUID;
email: string;
token: T.URLToken;
}) {
const subject = "Please Verify Your E-mail"; const subject = "Please Verify Your E-mail";
const content = `Hello ${user.name}, const content = `Hello ${name},
Please verify your email by visiting this link: ${baseURL}/email-verification/${user.userId}/${token} Please verify your email by visiting this link: ${baseURL}/email-verification/${uid}/${token}
LingDocs Admin`; LingDocs Admin`;
await sendEmail(getAddress(user), subject, content); await sendEmail(email, subject, content);
} }
export async function sendPasswordResetEmail(user: T.LingdocsUser, token: T.URLToken) { export async function sendPasswordResetEmail(
user: T.LingdocsUser,
token: T.URLToken
) {
const subject = "Reset Your Password"; const subject = "Reset Your Password";
const content = `Hello ${user.name}, const content = `Hello ${user.name},
@ -75,7 +90,9 @@ LingDocs Admin`;
await sendEmail(getAddress(user), subject, content); await sendEmail(getAddress(user), subject, content);
} }
export async function sendUpgradeRequestToAdmin(userWantingToUpgrade: T.LingdocsUser) { export async function sendUpgradeRequestToAdmin(
userWantingToUpgrade: T.LingdocsUser
) {
const subject = "Account Upgrade Request"; const subject = "Account Upgrade Request";
const content = `${userWantingToUpgrade.name} - ${userWantingToUpgrade.email} - ${userWantingToUpgrade.userId} is requesting to upgrade to student.`; const content = `${userWantingToUpgrade.name} - ${userWantingToUpgrade.email} - ${userWantingToUpgrade.userId} is requesting to upgrade to student.`;
await sendEmail(adminAddress, subject, content); await sendEmail(adminAddress, subject, content);

View File

@ -5,10 +5,7 @@ import {
updateLingdocsUser, updateLingdocsUser,
deleteCouchDbAuthUser, deleteCouchDbAuthUser,
} from "../lib/couch-db"; } from "../lib/couch-db";
import { import { getHash, getEmailTokenAndHash } from "../lib/password-utils";
getHash,
getEmailTokenAndHash,
} from "../lib/password-utils";
import { getTimestamp } from "../lib/time-utils"; import { getTimestamp } from "../lib/time-utils";
import { import {
sendVerificationEmail, sendVerificationEmail,
@ -31,33 +28,43 @@ export function canRemoveOneOutsideProvider(user: T.LingdocsUser): boolean {
if (user.email && user.password) { if (user.email && user.password) {
return true; return true;
} }
const providersPresent = outsideProviders.filter((provider) => !!user[provider]); const providersPresent = outsideProviders.filter(
(provider) => !!user[provider]
);
return providersPresent.length > 1; return providersPresent.length > 1;
} }
export function getVerifiedEmail({ emails }: T.ProviderProfile): string | false { export function getVerifiedEmail({
return ( emails,
emails }: T.ProviderProfile): string | false {
&& emails.length return emails &&
emails.length &&
// @ts-ignore // @ts-ignore
&& emails[0].verified emails[0].verified
) ? emails[0].value : false; ? emails[0].value
: false;
} }
export function getEmailFromGoogleProfile(profile: T.GoogleProfile): { email: string | undefined, verified: boolean } { export function getEmailFromGoogleProfile(profile: T.GoogleProfile): {
email: string | undefined;
verified: boolean;
} {
if (!profile.emails || profile.emails.length === 0) { if (!profile.emails || profile.emails.length === 0) {
return { email: undefined, verified: false }; return { email: undefined, verified: false };
} }
const em = profile.emails[0]; const em = profile.emails[0];
// @ts-ignore // but the verified value *is* there - if not it's still safe // @ts-ignore // but the verified value *is* there - if not it's still safe
const verified = !!em.verified const verified = !!em.verified;
return { return {
email: em.value, email: em.value,
verified, verified,
}; };
} }
export async function upgradeUser(userId: T.UUID, subscription?: T.StripeSubscription): Promise<T.UpgradeUserResponse> { export async function upgradeUser(
userId: T.UUID,
subscription?: T.StripeSubscription
): Promise<T.UpgradeUserResponse> {
// add user to couchdb authentication db // add user to couchdb authentication db
const { password, userDbName } = await addCouchDbAuthUser(userId); const { password, userDbName } = await addCouchDbAuthUser(userId);
// // create user db // // create user db
@ -79,7 +86,10 @@ export async function upgradeUser(userId: T.UUID, subscription?: T.StripeSubscri
}; };
} }
export async function downgradeUser(userId: T.UUID, subscriptionId?: string): Promise<T.DowngradeUserResponse> { export async function downgradeUser(
userId: T.UUID,
subscriptionId?: string
): Promise<T.DowngradeUserResponse> {
await deleteCouchDbAuthUser(userId); await deleteCouchDbAuthUser(userId);
if (subscriptionId) { if (subscriptionId) {
stripe.subscriptions.del(subscriptionId); stripe.subscriptions.del(subscriptionId);
@ -108,21 +118,27 @@ export async function denyUserUpgradeRequest(userId: T.UUID): Promise<void> {
}); });
} }
export async function createNewUser(input: { export async function createNewUser(
strategy: "local", input:
email: string, | {
name: string, strategy: "local";
passwordPlainText: string, email: string;
} | { name: string;
strategy: "github", passwordPlainText: string;
profile: T.GitHubProfile, }
} | { | {
strategy: "google", strategy: "github";
profile: T.GoogleProfile, profile: T.GitHubProfile;
} | { }
strategy: "twitter", | {
profile: T.TwitterProfile, strategy: "google";
}): Promise<T.LingdocsUser> { profile: T.GoogleProfile;
}
| {
strategy: "twitter";
profile: T.TwitterProfile;
}
): Promise<T.LingdocsUser> {
const userId = getUUID(); const userId = getUUID();
const now = getTimestamp(); const now = getTimestamp();
if (input.strategy === "local") { if (input.strategy === "local") {
@ -141,8 +157,13 @@ export async function createNewUser(input: {
lastLogin: now, lastLogin: now,
lastActive: now, lastActive: now,
}; };
await sendVerificationEmail({
name: input.name,
uid: userId,
email: input.email || "",
token: email.token,
});
const user = await insertLingdocsUser(newUser); const user = await insertLingdocsUser(newUser);
sendVerificationEmail(user, email.token).catch(console.error);
return user; return user;
} }
// GitHub || Twitter // GitHub || Twitter
@ -179,9 +200,14 @@ export async function createNewUser(input: {
lastActive: now, lastActive: now,
accountCreated: now, accountCreated: now,
level: "basic", level: "basic",
} };
const user = await insertLingdocsUser(newUser); const user = await insertLingdocsUser(newUser);
sendVerificationEmail(user, em.token); sendVerificationEmail({
name: newUser.name,
uid: newUser.userId,
email: newUser.email || "",
token: em.token,
}).catch(console.error);
return user; return user;
} }
const newUser: T.LingdocsUser = { const newUser: T.LingdocsUser = {
@ -196,7 +222,7 @@ export async function createNewUser(input: {
lastActive: now, lastActive: now,
accountCreated: now, accountCreated: now,
level: "basic", level: "basic",
} };
const user = await insertLingdocsUser(newUser); const user = await insertLingdocsUser(newUser);
return user; return user;
} }

View File

@ -13,9 +13,7 @@ import {
sendUpgradeRequestToAdmin, sendUpgradeRequestToAdmin,
sendVerificationEmail, sendVerificationEmail,
} from "../lib/mail-utils"; } from "../lib/mail-utils";
import { import { upgradeUser } from "../lib/user-utils";
upgradeUser,
} from "../lib/user-utils";
import * as T from "../../../website/src/types/account-types"; import * as T from "../../../website/src/types/account-types";
import env from "../lib/env-vars"; import env from "../lib/env-vars";
@ -70,29 +68,46 @@ apiRouter.post("/password", async (req, res, next) => {
const { oldPassword, password, passwordConfirmed } = req.body; const { oldPassword, password, passwordConfirmed } = req.body;
const addingFirstPassword = !req.user.password; const addingFirstPassword = !req.user.password;
if (!oldPassword && !addingFirstPassword) { if (!oldPassword && !addingFirstPassword) {
return sendResponse(res, { ok: false, error: "Please enter your old password" }); return sendResponse(res, {
ok: false,
error: "Please enter your old password",
});
} }
if (!password) { if (!password) {
return sendResponse(res, { ok: false, error: "Please enter a new password" }); return sendResponse(res, {
ok: false,
error: "Please enter a new password",
});
} }
if (!req.user.email) { if (!req.user.email) {
return sendResponse(res, { ok: false, error: "You need to add an e-mail address first" }); return sendResponse(res, {
ok: false,
error: "You need to add an e-mail address first",
});
} }
if (req.user.password) { if (req.user.password) {
const matchedOld = await compareToHash(oldPassword, req.user.password) || !req.user.password; const matchedOld =
(await compareToHash(oldPassword, req.user.password)) ||
!req.user.password;
if (!matchedOld) { if (!matchedOld) {
return sendResponse(res, { ok: false, error: "Incorrect old password" }); return sendResponse(res, { ok: false, error: "Incorrect old password" });
} }
} }
if (password !== passwordConfirmed) { if (password !== passwordConfirmed) {
return sendResponse(res, { ok: false, error: "New passwords do not match" }); return sendResponse(res, {
ok: false,
error: "New passwords do not match",
});
} }
if (password.length < 6) { if (password.length < 6) {
return sendResponse(res, { ok: false, error: "New password too short" }); return sendResponse(res, { ok: false, error: "New password too short" });
} }
const hash = await getHash(password); const hash = await getHash(password);
await updateLingdocsUser(req.user.userId, { password: hash }); await updateLingdocsUser(req.user.userId, { password: hash });
sendResponse(res, { ok: true, message: addingFirstPassword ? "Password added" : "Password changed" }); sendResponse(res, {
ok: true,
message: addingFirstPassword ? "Password added" : "Password changed",
});
}); });
/** /**
@ -102,10 +117,19 @@ apiRouter.put("/email-verification", async (req, res, next) => {
try { try {
if (!req.user) throw new Error("user not found"); if (!req.user) throw new Error("user not found");
const { token, hash } = await getEmailTokenAndHash(); const { token, hash } = await getEmailTokenAndHash();
const u = await updateLingdocsUser(req.user.userId, { emailVerified: hash }); const u = await updateLingdocsUser(req.user.userId, {
sendVerificationEmail(u, token).then(() => { emailVerified: hash,
});
sendVerificationEmail({
name: u.name,
uid: u.userId,
email: u.email || "",
token,
})
.then(() => {
sendResponse(res, { ok: true, message: "e-mail verification sent" }); sendResponse(res, { ok: true, message: "e-mail verification sent" });
}).catch((err) => { })
.catch((err) => {
sendResponse(res, { ok: false, error: err }); sendResponse(res, { ok: false, error: err });
}); });
} catch (e) { } catch (e) {
@ -153,7 +177,9 @@ apiRouter.post("/user/upgradeToStudentRequest", async (req, res, next) => {
return; return;
} }
sendUpgradeRequestToAdmin(req.user).catch(console.error); sendUpgradeRequestToAdmin(req.user).catch(console.error);
await updateLingdocsUser(req.user.userId, { upgradeToStudentRequest: "waiting" }); await updateLingdocsUser(req.user.userId, {
upgradeToStudentRequest: "waiting",
});
res.send({ ok: true, message: "request for upgrade sent" }); res.send({ ok: true, message: "request for upgrade sent" });
} catch (e) { } catch (e) {
next(e); next(e);
@ -171,7 +197,7 @@ apiRouter.delete("/user", async (req, res, next) => {
} catch (e) { } catch (e) {
next(e); next(e);
} }
}) });
/** /**
* signs out the user signed in * signs out the user signed in

View File

@ -22,6 +22,7 @@ import { upgradeUser, denyUserUpgradeRequest } from "../lib/user-utils";
import { validateReCaptcha } from "../lib/recaptcha"; import { validateReCaptcha } from "../lib/recaptcha";
import { getTimestamp } from "../lib/time-utils"; import { getTimestamp } from "../lib/time-utils";
import { import {
getAddress,
sendPasswordResetEmail, sendPasswordResetEmail,
sendVerificationEmail, sendVerificationEmail,
} from "../lib/mail-utils"; } from "../lib/mail-utils";
@ -74,7 +75,13 @@ const authRouter = (passport: PassportStatic) => {
email, email,
emailVerified: hash, emailVerified: hash,
}); });
sendVerificationEmail(updated, token).catch(console.error); // TODO: AWAIT THE E-MAIL SEND TO MAKE SURE THE E-MAIL WORKS!
sendVerificationEmail({
name: updated.name,
uid: updated.userId,
email: updated.email || "",
token,
});
return res.render(page, { return res.render(page, {
user: updated, user: updated,
error: null, error: null,
@ -170,6 +177,7 @@ const authRouter = (passport: PassportStatic) => {
const { email, password, name } = req.body; const { email, password, name } = req.body;
const existingUser = await getLingdocsUser("email", email); const existingUser = await getLingdocsUser("email", email);
if (existingUser) return res.send("User Already Exists"); if (existingUser) return res.send("User Already Exists");
try {
const user = await createNewUser({ const user = await createNewUser({
strategy: "local", strategy: "local",
email, email,
@ -180,6 +188,10 @@ const authRouter = (passport: PassportStatic) => {
if (err) return next(err); if (err) return next(err);
return res.send({ ok: true, user }); return res.send({ ok: true, user });
}); });
} catch (e) {
console.error(e);
return res.send("Invalid E-mail");
}
} catch (e) { } catch (e) {
next(e); next(e);
} }

View File

@ -29,7 +29,7 @@
<h1 class="h3 mb-4 fw-normal">Sign in to LingDocs</h1> <h1 class="h3 mb-4 fw-normal">Sign in to LingDocs</h1>
<!-- <p class="small mb-2">New? Enter an e-mail and password to sign up</p> --> <!-- <p class="small mb-2">New? Enter an e-mail and password to sign up</p> -->
<div class="form-floating mt-3"> <div class="form-floating mt-3">
<input type="email" pattern='(?:[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\])' required class="form-control" id="emailInput" placeholder="name@example.com"> <input type="email" required class="form-control" id="emailInput" placeholder="name@example.com">
<label for="floatingInput">Email address</label> <label for="floatingInput">Email address</label>
</div> </div>
<div class="form-floating"> <div class="form-floating">