Compare commits
4 Commits
5c67fb5179
...
51e22a4611
Author | SHA1 | Date |
---|---|---|
adueck | 51e22a4611 | |
adueck | f84c179e37 | |
adueck | 9acd645478 | |
adueck | d2a11a8004 |
|
@ -3,68 +3,83 @@ 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 {
|
||||||
name: user.name,
|
name: user.name,
|
||||||
address: user.email || "",
|
address: user.email || "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const transporter = nodemailer.createTransport({
|
const transporter = nodemailer.createTransport({
|
||||||
host: env.emailHost,
|
host: env.emailHost,
|
||||||
port: 465,
|
port: 465,
|
||||||
secure: true,
|
secure: true,
|
||||||
auth: {
|
auth: {
|
||||||
user: env.emailUser,
|
user: env.emailUser,
|
||||||
pass: env.emailPass,
|
pass: env.emailPass,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
async function sendEmail(to: Address, subject: string, text: string) {
|
async function sendEmail(to: Address, subject: string, text: string) {
|
||||||
await transporter.sendMail({
|
return await transporter.sendMail({
|
||||||
from: adminAddress,
|
from: adminAddress,
|
||||||
to,
|
to,
|
||||||
subject,
|
subject,
|
||||||
text,
|
text,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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({
|
||||||
const subject = "Please Verify Your E-mail";
|
name,
|
||||||
const content = `Hello ${user.name},
|
uid,
|
||||||
|
email,
|
||||||
|
token,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
uid: T.UUID;
|
||||||
|
email: string;
|
||||||
|
token: T.URLToken;
|
||||||
|
}) {
|
||||||
|
const subject = "Please Verify Your E-mail";
|
||||||
|
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(
|
||||||
const subject = "Reset Your Password";
|
user: T.LingdocsUser,
|
||||||
const content = `Hello ${user.name},
|
token: T.URLToken
|
||||||
|
) {
|
||||||
|
const subject = "Reset Your Password";
|
||||||
|
const content = `Hello ${user.name},
|
||||||
|
|
||||||
Please visit this link to reset your password: ${baseURL}/password-reset/${user.userId}/${token}
|
Please visit this link to reset your password: ${baseURL}/password-reset/${user.userId}/${token}
|
||||||
|
|
||||||
LingDocs Admin`;
|
LingDocs Admin`;
|
||||||
|
|
||||||
await sendEmail(getAddress(user), subject, content);
|
await sendEmail(getAddress(user), subject, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sendAccountUpgradeMessage(user: T.LingdocsUser) {
|
export async function sendAccountUpgradeMessage(user: T.LingdocsUser) {
|
||||||
const subject = "You're Upgraded to Student";
|
const subject = "You're Upgraded to Student";
|
||||||
const content = `Hello ${user.name},
|
const content = `Hello ${user.name},
|
||||||
|
|
||||||
Congratulations on your upgrade to a LingDocs Student account! 👨🎓
|
Congratulations on your upgrade to a LingDocs Student account! 👨🎓
|
||||||
|
|
||||||
|
@ -72,11 +87,13 @@ Now you can start using your wordlist in the dictionary. It will automatically s
|
||||||
|
|
||||||
LingDocs Admin`;
|
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(
|
||||||
const subject = "Account Upgrade Request";
|
userWantingToUpgrade: T.LingdocsUser
|
||||||
const content = `${userWantingToUpgrade.name} - ${userWantingToUpgrade.email} - ${userWantingToUpgrade.userId} is requesting to upgrade to student.`;
|
) {
|
||||||
await sendEmail(adminAddress, subject, content);
|
const subject = "Account Upgrade Request";
|
||||||
|
const content = `${userWantingToUpgrade.name} - ${userWantingToUpgrade.email} - ${userWantingToUpgrade.userId} is requesting to upgrade to student.`;
|
||||||
|
await sendEmail(adminAddress, subject, content);
|
||||||
}
|
}
|
|
@ -1,18 +1,15 @@
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
import {
|
import {
|
||||||
insertLingdocsUser,
|
insertLingdocsUser,
|
||||||
addCouchDbAuthUser,
|
addCouchDbAuthUser,
|
||||||
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,
|
||||||
sendAccountUpgradeMessage,
|
sendAccountUpgradeMessage,
|
||||||
} from "../lib/mail-utils";
|
} from "../lib/mail-utils";
|
||||||
import { outsideProviders } from "../middleware/setup-passport";
|
import { outsideProviders } from "../middleware/setup-passport";
|
||||||
import * as T from "../../../website/src/types/account-types";
|
import * as T from "../../../website/src/types/account-types";
|
||||||
|
@ -20,183 +17,212 @@ import env from "../lib/env-vars";
|
||||||
import Stripe from "stripe";
|
import Stripe from "stripe";
|
||||||
|
|
||||||
const stripe = new Stripe(env.stripeSecretKey, {
|
const stripe = new Stripe(env.stripeSecretKey, {
|
||||||
apiVersion: "2022-08-01",
|
apiVersion: "2022-08-01",
|
||||||
});
|
});
|
||||||
|
|
||||||
function getUUID(): T.UUID {
|
function getUUID(): T.UUID {
|
||||||
return uuidv4() as T.UUID;
|
return uuidv4() as T.UUID;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function canRemoveOneOutsideProvider(user: T.LingdocsUser): boolean {
|
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(
|
||||||
return providersPresent.length > 1;
|
(provider) => !!user[provider]
|
||||||
|
);
|
||||||
|
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 &&
|
||||||
// @ts-ignore
|
emails.length &&
|
||||||
&& emails[0].verified
|
// @ts-ignore
|
||||||
) ? emails[0].value : false;
|
emails[0].verified
|
||||||
|
? emails[0].value
|
||||||
|
: false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getEmailFromGoogleProfile(profile: T.GoogleProfile): { email: string | undefined, verified: boolean } {
|
export function getEmailFromGoogleProfile(profile: T.GoogleProfile): {
|
||||||
if (!profile.emails || profile.emails.length === 0) {
|
email: string | undefined;
|
||||||
return { email: undefined, verified: false };
|
verified: boolean;
|
||||||
}
|
} {
|
||||||
const em = profile.emails[0];
|
if (!profile.emails || profile.emails.length === 0) {
|
||||||
// @ts-ignore // but the verified value *is* there - if not it's still safe
|
return { email: undefined, verified: false };
|
||||||
const verified = !!em.verified
|
}
|
||||||
return {
|
const em = profile.emails[0];
|
||||||
email: em.value,
|
// @ts-ignore // but the verified value *is* there - if not it's still safe
|
||||||
verified,
|
const verified = !!em.verified;
|
||||||
};
|
return {
|
||||||
|
email: em.value,
|
||||||
|
verified,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function upgradeUser(userId: T.UUID, subscription?: T.StripeSubscription): Promise<T.UpgradeUserResponse> {
|
export async function upgradeUser(
|
||||||
// add user to couchdb authentication db
|
userId: T.UUID,
|
||||||
const { password, userDbName } = await addCouchDbAuthUser(userId);
|
subscription?: T.StripeSubscription
|
||||||
// // create user db
|
): Promise<T.UpgradeUserResponse> {
|
||||||
// update LingdocsUser
|
// add user to couchdb authentication db
|
||||||
const user = await updateLingdocsUser(userId, {
|
const { password, userDbName } = await addCouchDbAuthUser(userId);
|
||||||
level: "student",
|
// // create user db
|
||||||
wordlistDbName: userDbName,
|
// update LingdocsUser
|
||||||
couchDbPassword: password,
|
const user = await updateLingdocsUser(userId, {
|
||||||
upgradeToStudentRequest: undefined,
|
level: "student",
|
||||||
subscription,
|
wordlistDbName: userDbName,
|
||||||
});
|
couchDbPassword: password,
|
||||||
if (user.email) {
|
upgradeToStudentRequest: undefined,
|
||||||
sendAccountUpgradeMessage(user).catch(console.error);
|
subscription,
|
||||||
}
|
});
|
||||||
return {
|
if (user.email) {
|
||||||
ok: true,
|
sendAccountUpgradeMessage(user).catch(console.error);
|
||||||
message: "user upgraded to student",
|
}
|
||||||
user,
|
return {
|
||||||
};
|
ok: true,
|
||||||
|
message: "user upgraded to student",
|
||||||
|
user,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function downgradeUser(userId: T.UUID, subscriptionId?: string): Promise<T.DowngradeUserResponse> {
|
export async function downgradeUser(
|
||||||
await deleteCouchDbAuthUser(userId);
|
userId: T.UUID,
|
||||||
if (subscriptionId) {
|
subscriptionId?: string
|
||||||
stripe.subscriptions.del(subscriptionId);
|
): Promise<T.DowngradeUserResponse> {
|
||||||
}
|
await deleteCouchDbAuthUser(userId);
|
||||||
const user = await updateLingdocsUser(userId, {
|
if (subscriptionId) {
|
||||||
level: "basic",
|
stripe.subscriptions.del(subscriptionId);
|
||||||
wordlistDbName: undefined,
|
}
|
||||||
couchDbPassword: undefined,
|
const user = await updateLingdocsUser(userId, {
|
||||||
upgradeToStudentRequest: undefined,
|
level: "basic",
|
||||||
subscription: undefined,
|
wordlistDbName: undefined,
|
||||||
});
|
couchDbPassword: undefined,
|
||||||
if (user.email) {
|
upgradeToStudentRequest: undefined,
|
||||||
// TODO
|
subscription: undefined,
|
||||||
// sendAccountDowngradeMessage(user).catch(console.error);
|
});
|
||||||
}
|
if (user.email) {
|
||||||
return {
|
// TODO
|
||||||
ok: true,
|
// sendAccountDowngradeMessage(user).catch(console.error);
|
||||||
message: "user downgraded to basic",
|
}
|
||||||
user,
|
return {
|
||||||
};
|
ok: true,
|
||||||
|
message: "user downgraded to basic",
|
||||||
|
user,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function denyUserUpgradeRequest(userId: T.UUID): Promise<void> {
|
export async function denyUserUpgradeRequest(userId: T.UUID): Promise<void> {
|
||||||
await updateLingdocsUser(userId, {
|
await updateLingdocsUser(userId, {
|
||||||
upgradeToStudentRequest: "denied",
|
upgradeToStudentRequest: "denied",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
const userId = getUUID();
|
}
|
||||||
const now = getTimestamp();
|
| {
|
||||||
if (input.strategy === "local") {
|
strategy: "twitter";
|
||||||
const email = await getEmailTokenAndHash();
|
profile: T.TwitterProfile;
|
||||||
const password = await getHash(input.passwordPlainText);
|
}
|
||||||
const newUser: T.LingdocsUser = {
|
): Promise<T.LingdocsUser> {
|
||||||
_id: userId,
|
const userId = getUUID();
|
||||||
userId,
|
const now = getTimestamp();
|
||||||
email: input.email,
|
if (input.strategy === "local") {
|
||||||
emailVerified: email.hash,
|
const email = await getEmailTokenAndHash();
|
||||||
name: input.name,
|
const password = await getHash(input.passwordPlainText);
|
||||||
password,
|
|
||||||
level: "basic",
|
|
||||||
tests: [],
|
|
||||||
accountCreated: now,
|
|
||||||
lastLogin: now,
|
|
||||||
lastActive: now,
|
|
||||||
};
|
|
||||||
const user = await insertLingdocsUser(newUser);
|
|
||||||
sendVerificationEmail(user, email.token).catch(console.error);
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
// GitHub || Twitter
|
|
||||||
if (input.strategy === "github" || input.strategy === "twitter") {
|
|
||||||
const newUser: T.LingdocsUser = {
|
|
||||||
_id: userId,
|
|
||||||
userId,
|
|
||||||
emailVerified: false,
|
|
||||||
name: input.profile.displayName,
|
|
||||||
[input.strategy]: input.profile,
|
|
||||||
level: "basic",
|
|
||||||
tests: [],
|
|
||||||
accountCreated: now,
|
|
||||||
lastLogin: now,
|
|
||||||
lastActive: now,
|
|
||||||
};
|
|
||||||
const user = await insertLingdocsUser(newUser);
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
// Google
|
|
||||||
// TODO: Add e-mail in here
|
|
||||||
const { email, verified } = getEmailFromGoogleProfile(input.profile);
|
|
||||||
if (email && !verified) {
|
|
||||||
const em = await getEmailTokenAndHash();
|
|
||||||
const newUser: T.LingdocsUser = {
|
|
||||||
_id: userId,
|
|
||||||
userId,
|
|
||||||
email,
|
|
||||||
emailVerified: em.hash,
|
|
||||||
name: input.profile.displayName,
|
|
||||||
google: input.profile,
|
|
||||||
lastLogin: now,
|
|
||||||
tests: [],
|
|
||||||
lastActive: now,
|
|
||||||
accountCreated: now,
|
|
||||||
level: "basic",
|
|
||||||
}
|
|
||||||
const user = await insertLingdocsUser(newUser);
|
|
||||||
sendVerificationEmail(user, em.token);
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
const newUser: T.LingdocsUser = {
|
const newUser: T.LingdocsUser = {
|
||||||
_id: userId,
|
_id: userId,
|
||||||
userId,
|
userId,
|
||||||
email,
|
email: input.email,
|
||||||
emailVerified: verified,
|
emailVerified: email.hash,
|
||||||
name: input.profile.displayName,
|
name: input.name,
|
||||||
google: input.profile,
|
password,
|
||||||
lastLogin: now,
|
level: "basic",
|
||||||
tests: [],
|
tests: [],
|
||||||
lastActive: now,
|
accountCreated: now,
|
||||||
accountCreated: now,
|
lastLogin: now,
|
||||||
level: "basic",
|
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);
|
||||||
return user;
|
return user;
|
||||||
|
}
|
||||||
|
// GitHub || Twitter
|
||||||
|
if (input.strategy === "github" || input.strategy === "twitter") {
|
||||||
|
const newUser: T.LingdocsUser = {
|
||||||
|
_id: userId,
|
||||||
|
userId,
|
||||||
|
emailVerified: false,
|
||||||
|
name: input.profile.displayName,
|
||||||
|
[input.strategy]: input.profile,
|
||||||
|
level: "basic",
|
||||||
|
tests: [],
|
||||||
|
accountCreated: now,
|
||||||
|
lastLogin: now,
|
||||||
|
lastActive: now,
|
||||||
|
};
|
||||||
|
const user = await insertLingdocsUser(newUser);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
// Google
|
||||||
|
// TODO: Add e-mail in here
|
||||||
|
const { email, verified } = getEmailFromGoogleProfile(input.profile);
|
||||||
|
if (email && !verified) {
|
||||||
|
const em = await getEmailTokenAndHash();
|
||||||
|
const newUser: T.LingdocsUser = {
|
||||||
|
_id: userId,
|
||||||
|
userId,
|
||||||
|
email,
|
||||||
|
emailVerified: em.hash,
|
||||||
|
name: input.profile.displayName,
|
||||||
|
google: input.profile,
|
||||||
|
lastLogin: now,
|
||||||
|
tests: [],
|
||||||
|
lastActive: now,
|
||||||
|
accountCreated: now,
|
||||||
|
level: "basic",
|
||||||
|
};
|
||||||
|
const user = await insertLingdocsUser(newUser);
|
||||||
|
sendVerificationEmail({
|
||||||
|
name: newUser.name,
|
||||||
|
uid: newUser.userId,
|
||||||
|
email: newUser.email || "",
|
||||||
|
token: em.token,
|
||||||
|
}).catch(console.error);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
const newUser: T.LingdocsUser = {
|
||||||
|
_id: userId,
|
||||||
|
userId,
|
||||||
|
email,
|
||||||
|
emailVerified: verified,
|
||||||
|
name: input.profile.displayName,
|
||||||
|
google: input.profile,
|
||||||
|
lastLogin: now,
|
||||||
|
tests: [],
|
||||||
|
lastActive: now,
|
||||||
|
accountCreated: now,
|
||||||
|
level: "basic",
|
||||||
|
};
|
||||||
|
const user = await insertLingdocsUser(newUser);
|
||||||
|
return user;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,184 +1,210 @@
|
||||||
import express, { Response } from "express";
|
import express, { Response } from "express";
|
||||||
import {
|
import {
|
||||||
deleteLingdocsUser,
|
deleteLingdocsUser,
|
||||||
getLingdocsUser,
|
getLingdocsUser,
|
||||||
updateLingdocsUser,
|
updateLingdocsUser,
|
||||||
} from "../lib/couch-db";
|
} from "../lib/couch-db";
|
||||||
import {
|
import {
|
||||||
getHash,
|
getHash,
|
||||||
compareToHash,
|
compareToHash,
|
||||||
getEmailTokenAndHash,
|
getEmailTokenAndHash,
|
||||||
} from "../lib/password-utils";
|
} from "../lib/password-utils";
|
||||||
import {
|
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";
|
||||||
|
|
||||||
// TODO: ADD PROPER ERROR HANDLING THAT WILL RETURN JSON ALWAYS
|
// TODO: ADD PROPER ERROR HANDLING THAT WILL RETURN JSON ALWAYS
|
||||||
|
|
||||||
function sendResponse(res: Response, payload: T.APIResponse) {
|
function sendResponse(res: Response, payload: T.APIResponse) {
|
||||||
return res.send(payload);
|
return res.send(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
const apiRouter = express.Router();
|
const apiRouter = express.Router();
|
||||||
|
|
||||||
// Guard all api with authentication
|
// Guard all api with authentication
|
||||||
apiRouter.use((req, res, next) => {
|
apiRouter.use((req, res, next) => {
|
||||||
if (req.isAuthenticated()) {
|
if (req.isAuthenticated()) {
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
const r: T.APIResponse = { ok: false, error: "401 Unauthorized" };
|
const r: T.APIResponse = { ok: false, error: "401 Unauthorized" };
|
||||||
return res.status(401).send(r);
|
return res.status(401).send(r);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* gets the LingdocsUser object for the user signed in
|
* gets the LingdocsUser object for the user signed in
|
||||||
*/
|
*/
|
||||||
apiRouter.get("/user", (req, res, next) => {
|
apiRouter.get("/user", (req, res, next) => {
|
||||||
if (!req.user) return next("user not found");
|
if (!req.user) return next("user not found");
|
||||||
sendResponse(res, { ok: true, user: req.user });
|
sendResponse(res, { ok: true, user: req.user });
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* adds (passed) test results to the record of the user signed in
|
* adds (passed) test results to the record of the user signed in
|
||||||
*/
|
*/
|
||||||
apiRouter.put("/user/tests", async (req, res, next) => {
|
apiRouter.put("/user/tests", async (req, res, next) => {
|
||||||
if (!req.user) return next("user not found");
|
if (!req.user) return next("user not found");
|
||||||
try {
|
try {
|
||||||
const { tests } = req.body as T.PostTestResultsBody;
|
const { tests } = req.body as T.PostTestResultsBody;
|
||||||
await updateLingdocsUser(req.user.userId, { tests });
|
await updateLingdocsUser(req.user.userId, { tests });
|
||||||
sendResponse(res, {
|
sendResponse(res, {
|
||||||
ok: true,
|
ok: true,
|
||||||
message: "posted test results",
|
message: "posted test results",
|
||||||
tests,
|
tests,
|
||||||
});
|
});
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
next(e);
|
next(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* receives a request to change or add a user's own password
|
* receives a request to change or add a user's own password
|
||||||
*/
|
*/
|
||||||
apiRouter.post("/password", async (req, res, next) => {
|
apiRouter.post("/password", async (req, res, next) => {
|
||||||
if (!req.user) return next("user not found");
|
if (!req.user) return next("user not found");
|
||||||
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) {
|
||||||
|
return sendResponse(res, {
|
||||||
|
ok: false,
|
||||||
|
error: "Please enter a new password",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (!req.user.email) {
|
||||||
|
return sendResponse(res, {
|
||||||
|
ok: false,
|
||||||
|
error: "You need to add an e-mail address first",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (req.user.password) {
|
||||||
|
const matchedOld =
|
||||||
|
(await compareToHash(oldPassword, req.user.password)) ||
|
||||||
|
!req.user.password;
|
||||||
|
if (!matchedOld) {
|
||||||
|
return sendResponse(res, { ok: false, error: "Incorrect old password" });
|
||||||
}
|
}
|
||||||
if (!password) {
|
}
|
||||||
return sendResponse(res, { ok: false, error: "Please enter a new password" });
|
if (password !== passwordConfirmed) {
|
||||||
}
|
return sendResponse(res, {
|
||||||
if (!req.user.email) {
|
ok: false,
|
||||||
return sendResponse(res, { ok: false, error: "You need to add an e-mail address first" });
|
error: "New passwords do not match",
|
||||||
}
|
});
|
||||||
if (req.user.password) {
|
}
|
||||||
const matchedOld = await compareToHash(oldPassword, req.user.password) || !req.user.password;
|
if (password.length < 6) {
|
||||||
if (!matchedOld) {
|
return sendResponse(res, { ok: false, error: "New password too short" });
|
||||||
return sendResponse(res, { ok: false, error: "Incorrect old password" });
|
}
|
||||||
}
|
const hash = await getHash(password);
|
||||||
}
|
await updateLingdocsUser(req.user.userId, { password: hash });
|
||||||
if (password !== passwordConfirmed) {
|
sendResponse(res, {
|
||||||
return sendResponse(res, { ok: false, error: "New passwords do not match" });
|
ok: true,
|
||||||
}
|
message: addingFirstPassword ? "Password added" : "Password changed",
|
||||||
if (password.length < 6) {
|
});
|
||||||
return sendResponse(res, {ok: false, error: "New password too short" });
|
|
||||||
}
|
|
||||||
const hash = await getHash(password);
|
|
||||||
await updateLingdocsUser(req.user.userId, { password: hash });
|
|
||||||
sendResponse(res, { ok: true, message: addingFirstPassword ? "Password added" : "Password changed" });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* receives a request to generate a new e-mail verification token and send e-mail
|
* receives a request to generate a new e-mail verification token and send e-mail
|
||||||
*/
|
*/
|
||||||
apiRouter.put("/email-verification", async (req, res, next) => {
|
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,
|
||||||
sendResponse(res, { ok: true, message: "e-mail verification sent" });
|
});
|
||||||
}).catch((err) => {
|
sendVerificationEmail({
|
||||||
sendResponse(res, { ok: false, error: err });
|
name: u.name,
|
||||||
});
|
uid: u.userId,
|
||||||
} catch (e) {
|
email: u.email || "",
|
||||||
next(e);
|
token,
|
||||||
}
|
})
|
||||||
|
.then(() => {
|
||||||
|
sendResponse(res, { ok: true, message: "e-mail verification sent" });
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
sendResponse(res, { ok: false, error: err });
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
next(e);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
apiRouter.put("/user/upgrade", async (req, res, next) => {
|
apiRouter.put("/user/upgrade", async (req, res, next) => {
|
||||||
if (!req.user) throw new Error("user not found");
|
if (!req.user) throw new Error("user not found");
|
||||||
try {
|
try {
|
||||||
const givenPassword = (req.body.password || "") as string;
|
const givenPassword = (req.body.password || "") as string;
|
||||||
const studentPassword = env.upgradePassword;
|
const studentPassword = env.upgradePassword;
|
||||||
if (givenPassword.toLowerCase().trim() !== studentPassword.toLowerCase()) {
|
if (givenPassword.toLowerCase().trim() !== studentPassword.toLowerCase()) {
|
||||||
const wrongPass: T.UpgradeUserResponse = {
|
const wrongPass: T.UpgradeUserResponse = {
|
||||||
ok: false,
|
ok: false,
|
||||||
error: "incorrect password",
|
error: "incorrect password",
|
||||||
};
|
};
|
||||||
res.send(wrongPass);
|
res.send(wrongPass);
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
const { userId } = req.user;
|
|
||||||
const user = await getLingdocsUser("userId", userId);
|
|
||||||
if (!user) throw new Error("user lost");
|
|
||||||
if (user.level !== "basic") {
|
|
||||||
const alreadyUpgraded: T.UpgradeUserResponse = {
|
|
||||||
ok: true,
|
|
||||||
message: "user already upgraded",
|
|
||||||
user,
|
|
||||||
};
|
|
||||||
res.send(alreadyUpgraded);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const upgraded: T.UpgradeUserResponse = await upgradeUser(userId);
|
|
||||||
res.send(upgraded);
|
|
||||||
} catch (e) {
|
|
||||||
next(e);
|
|
||||||
}
|
}
|
||||||
|
const { userId } = req.user;
|
||||||
|
const user = await getLingdocsUser("userId", userId);
|
||||||
|
if (!user) throw new Error("user lost");
|
||||||
|
if (user.level !== "basic") {
|
||||||
|
const alreadyUpgraded: T.UpgradeUserResponse = {
|
||||||
|
ok: true,
|
||||||
|
message: "user already upgraded",
|
||||||
|
user,
|
||||||
|
};
|
||||||
|
res.send(alreadyUpgraded);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const upgraded: T.UpgradeUserResponse = await upgradeUser(userId);
|
||||||
|
res.send(upgraded);
|
||||||
|
} catch (e) {
|
||||||
|
next(e);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
apiRouter.post("/user/upgradeToStudentRequest", async (req, res, next) => {
|
apiRouter.post("/user/upgradeToStudentRequest", async (req, res, next) => {
|
||||||
if (!req.user) throw new Error("user not found");
|
if (!req.user) throw new Error("user not found");
|
||||||
try {
|
try {
|
||||||
if (req.user.level === "student" || req.user.level === "editor") {
|
if (req.user.level === "student" || req.user.level === "editor") {
|
||||||
res.send({ ok: true, message: "user already upgraded" });
|
res.send({ ok: true, message: "user already upgraded" });
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
sendUpgradeRequestToAdmin(req.user).catch(console.error);
|
|
||||||
await updateLingdocsUser(req.user.userId, { upgradeToStudentRequest: "waiting" });
|
|
||||||
res.send({ ok: true, message: "request for upgrade sent" });
|
|
||||||
} catch (e) {
|
|
||||||
next(e);
|
|
||||||
}
|
}
|
||||||
|
sendUpgradeRequestToAdmin(req.user).catch(console.error);
|
||||||
|
await updateLingdocsUser(req.user.userId, {
|
||||||
|
upgradeToStudentRequest: "waiting",
|
||||||
|
});
|
||||||
|
res.send({ ok: true, message: "request for upgrade sent" });
|
||||||
|
} catch (e) {
|
||||||
|
next(e);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* deletes a users own account
|
* deletes a users own account
|
||||||
*/
|
*/
|
||||||
apiRouter.delete("/user", async (req, res, next) => {
|
apiRouter.delete("/user", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
if (!req.user) throw new Error("user not found");
|
if (!req.user) throw new Error("user not found");
|
||||||
await deleteLingdocsUser(req.user.userId);
|
await deleteLingdocsUser(req.user.userId);
|
||||||
sendResponse(res, { ok: true, message: "user deleted" });
|
sendResponse(res, { ok: true, message: "user deleted" });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
next(e);
|
next(e);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* signs out the user signed in
|
* signs out the user signed in
|
||||||
*/
|
*/
|
||||||
apiRouter.post("/sign-out", (req, res) => {
|
apiRouter.post("/sign-out", (req, res) => {
|
||||||
req.logOut();
|
req.logOut();
|
||||||
sendResponse(res, { ok: true, message: "signed out" });
|
sendResponse(res, { ok: true, message: "signed out" });
|
||||||
});
|
});
|
||||||
|
|
||||||
export default apiRouter;
|
export default apiRouter;
|
||||||
|
|
|
@ -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,
|
||||||
|
@ -169,17 +176,23 @@ const authRouter = (passport: PassportStatic) => {
|
||||||
try {
|
try {
|
||||||
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)
|
||||||
const user = await createNewUser({
|
return res.send({ ok: false, message: "User Already Exists" });
|
||||||
strategy: "local",
|
try {
|
||||||
email,
|
const user = await createNewUser({
|
||||||
passwordPlainText: password,
|
strategy: "local",
|
||||||
name,
|
email,
|
||||||
});
|
passwordPlainText: password,
|
||||||
req.logIn(user, (err) => {
|
name,
|
||||||
if (err) return next(err);
|
});
|
||||||
return res.send({ ok: true, user });
|
req.logIn(user, (err) => {
|
||||||
});
|
if (err) return next(err);
|
||||||
|
return res.send({ ok: true, user });
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return res.send({ ok: false, message: "Invalid E-mail" });
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
next(e);
|
next(e);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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">
|
||||||
|
@ -98,6 +98,7 @@
|
||||||
body: JSON.stringify({ email, password, name, token }),
|
body: JSON.stringify({ email, password, name, token }),
|
||||||
}).then((res) => res.json())
|
}).then((res) => res.json())
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
|
console.log({ res });
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
location.reload();
|
location.reload();
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue