diff --git a/README.md b/README.md index dc0d753..69b364d 100644 --- a/README.md +++ b/README.md @@ -203,7 +203,7 @@ server { When a user upgrades their account level to `student` or `editor`: 1. A doc in the `_users` db is created with their Firebase Authentication info, account level, and a password they can use for syncing their personal wordlistdb -2. A user database is created (by the firebase functions - *not* by the couchdb_peruser) which they use to sync their personal wordlist. +2. A user database is created (automatically by `couchdb_peruser`) which they use to sync their personal wordlist. There is also a `review-tasks` database which is used to store all the review tasks for editors and syncs with the review tasks in the app for the editor(s). diff --git a/account/src/lib/couch-db.ts b/account/src/lib/couch-db.ts index 014e016..0c082a3 100644 --- a/account/src/lib/couch-db.ts +++ b/account/src/lib/couch-db.ts @@ -95,8 +95,10 @@ export async function updateLingdocsUser(uuid: T.UUID, toUpdate: level: "student", wordlistDbName: T.WordlistDbName, couchDbPassword: T.UserDbPassword, + requestedUpgradeToStudent: undefined, } | - { userTextOptionsRecord: T.UserTextOptionsRecord } + { userTextOptionsRecord: T.UserTextOptionsRecord } | + { requestedUpgradeToStudent: true } ): Promise { const user = await getLingdocsUser("userId", uuid); if (!user) throw new Error("unable to update - user not found " + uuid); diff --git a/account/src/lib/mail-utils.ts b/account/src/lib/mail-utils.ts index 09711fc..8267eca 100644 --- a/account/src/lib/mail-utils.ts +++ b/account/src/lib/mail-utils.ts @@ -5,7 +5,7 @@ import * as T from "../../../website/src/lib/account-types"; type Address = string | { name: string, address: string }; -const from: Address = { +const adminAddress: Address = { name: "LingDocs Admin", address: "admin@lingdocs.com", }; @@ -31,31 +31,53 @@ const transporter = nodemailer.createTransport({ async function sendEmail(to: Address, subject: string, text: string) { await transporter.sendMail({ - from, + from: adminAddress, to, subject, text, }); } -// TODO: MAKE THIS +// TODO: MAKE THIS A URL ACROSS PROJECT const baseURL = inProd ? "https://account.lingdocs.com" : "http://localhost:4000"; export async function sendVerificationEmail(user: T.LingdocsUser, token: T.URLToken) { + const subject = "Please Verify Your E-mail"; const content = `Hello ${user.name}, Please verify your email by visiting this link: ${baseURL}/email-verification/${user.userId}/${token} LingDocs Admin`; - await sendEmail(getAddress(user), "Please Verify Your E-mail", content); + await sendEmail(getAddress(user), subject, content); } export async function sendPasswordResetEmail(user: T.LingdocsUser, 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} LingDocs Admin`; - await sendEmail(getAddress(user), "Reset Your Password", content); + await sendEmail(getAddress(user), subject, content); +} + +export async function sendAccountUpgradeMessage(user: T.LingdocsUser) { + const subject = "You've Upgraded to Student"; + const content = `Hello ${user.name}, + +Congratulations on your upgrade to a LingDocs Student account! 👨‍🎓 + +Now you can start using your wordlist in the dictionary. It will automatically sync across any devices you're signed in to. + +LingDocs Admin`; + + await sendEmail(getAddress(user), subject, content); +} + +export async function sendUpgradeRequestToAdmin(user: T.LingdocsUser) { + const subject = "Account Upgrade Request"; + const content = `${user.name} - ${user.email} - ${user.userId} is requesting to upgrade to student.`; + + await sendEmail(adminAddress, subject, content); } \ No newline at end of file diff --git a/account/src/routers/api-router.ts b/account/src/routers/api-router.ts index fe68c3b..e3c38a2 100644 --- a/account/src/routers/api-router.ts +++ b/account/src/routers/api-router.ts @@ -12,6 +12,8 @@ import { getEmailTokenAndHash, } from "../lib/password-utils"; import { + sendAccountUpgradeMessage, + sendUpgradeRequestToAdmin, sendVerificationEmail, } from "../lib/mail-utils"; import * as T from "../../../website/src/lib/account-types"; @@ -103,7 +105,7 @@ apiRouter.put("/user/userTextOptionsRecord", async (req, res, next) => { } catch (e) { next(e); } -}) +}); apiRouter.put("/user/upgrade", async (req, res, next) => { if (!req.user) throw new Error("user not found"); @@ -133,9 +135,14 @@ apiRouter.put("/user/upgrade", async (req, res, next) => { // add user to couchdb authentication db const { password, userDbName } = await addCouchDbAuthUser(userId); // // create user db - // const { name } = await createWordlistDatabase(userId, password); // update LingdocsUser - const u = await updateLingdocsUser(userId, { level: "student", wordlistDbName: userDbName, couchDbPassword: password }); + const u = await updateLingdocsUser(userId, { + level: "student", + wordlistDbName: userDbName, + couchDbPassword: password, + requestedUpgradeToStudent: undefined, + }); + sendAccountUpgradeMessage(u).catch(console.error); const upgraded: T.UpgradeUserResponse = { ok: true, message: "user upgraded to student", @@ -147,6 +154,20 @@ apiRouter.put("/user/upgrade", async (req, res, next) => { } }); +apiRouter.post("/user/upgradeToStudentRequest", async (req, res, next) => { + if (!req.user) throw new Error("user not found"); + try { + if (req.user.level === "student" || req.user.level === "editor") { + res.send({ ok: true, message: "user already upgraded "}); + return; + } + sendUpgradeRequestToAdmin(req.user).catch(console.error); + await updateLingdocsUser(req.user.userId, { requestedUpgradeToStudent: true }); + } catch (e) { + next(e); + } +}); + /** * deletes a users own account */ diff --git a/website/src/lib/account-types.ts b/website/src/lib/account-types.ts index 90a5854..2c599c8 100644 --- a/website/src/lib/account-types.ts +++ b/website/src/lib/account-types.ts @@ -38,6 +38,7 @@ export type LingdocsUser = { tokenHash: Hash, requestedOn: TimeStamp, }, + requestedUpgradeToStudent?: boolean, tests: [], lastLogin: TimeStamp, lastActive: TimeStamp, diff --git a/website/src/lib/backend-calls.ts b/website/src/lib/backend-calls.ts index e2cc46b..e9fdf5a 100644 --- a/website/src/lib/backend-calls.ts +++ b/website/src/lib/backend-calls.ts @@ -23,6 +23,10 @@ export async function upgradeAccount(password: string): Promise { + return await myFetch("account", "upgradeToStudentRequest", "POST") as AT.APIResponse; +} + export async function updateUserTextOptionsRecord(userTextOptionsRecord: AT.UserTextOptionsRecord): Promise { const response = await myFetch("account", "user/userTextOptionsRecord", "PUT", { userTextOptionsRecord }) as AT.UpdateUserTextOptionsRecordResponse; return response; diff --git a/website/src/screens/Account.tsx b/website/src/screens/Account.tsx index 127c451..99a58bd 100644 --- a/website/src/screens/Account.tsx +++ b/website/src/screens/Account.tsx @@ -7,6 +7,7 @@ import { upgradeAccount, signOut, publishDictionary, + upgradeToStudentRequest, } from "../lib/backend-calls"; import LoadingElipses from "../components/LoadingElipses"; import { Helmet } from "react-helmet"; @@ -51,6 +52,22 @@ const Account = ({ user, loadUser }: { user: AT.LingdocsUser | undefined, loadUs setUpgradePassword(""); setUpgradeError(""); } + async function handleUpgradeRequest() { + setUpgradeError(""); + setWaiting(true); + upgradeToStudentRequest().then((res) => { + setWaiting(false); + if (res.ok) { + loadUser(); + closeUpgrade(); + } else { + setUpgradeError("Error requesting upgrade"); + } + }).catch((err) => { + setWaiting(false); + setUpgradeError(err.message); + }); + } async function handleUpgrade() { setUpgradeError(""); setWaiting(true); @@ -136,7 +153,7 @@ const Account = ({ user, loadUser }: { user: AT.LingdocsUser | undefined, loadUs } -
  • Account Level: {capitalize(user.level)}
  • +
  • Account Level: {capitalize(user.level)} {user.requestedUpgradeToStudent ? "(Upgrade Requested)" : ""}
  • @@ -167,7 +184,7 @@ const Account = ({ user, loadUser }: { user: AT.LingdocsUser | undefined, loadUs Upgrade Account - Enter the secret upgrade password to upgrade your account. + Enter the secret upgrade password to upgrade your account or request an upgrade.