upgrade request function

This commit is contained in:
lingdocs 2021-08-25 12:06:37 +04:00
parent b4d6b14825
commit 3f798d5534
7 changed files with 79 additions and 12 deletions

View File

@ -203,7 +203,7 @@ server {
When a user upgrades their account level to `student` or `editor`: 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 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). 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).

View File

@ -95,8 +95,10 @@ export async function updateLingdocsUser(uuid: T.UUID, toUpdate:
level: "student", level: "student",
wordlistDbName: T.WordlistDbName, wordlistDbName: T.WordlistDbName,
couchDbPassword: T.UserDbPassword, couchDbPassword: T.UserDbPassword,
requestedUpgradeToStudent: undefined,
} | } |
{ userTextOptionsRecord: T.UserTextOptionsRecord } { userTextOptionsRecord: T.UserTextOptionsRecord } |
{ requestedUpgradeToStudent: true }
): Promise<T.LingdocsUser> { ): Promise<T.LingdocsUser> {
const user = await getLingdocsUser("userId", uuid); const user = await getLingdocsUser("userId", uuid);
if (!user) throw new Error("unable to update - user not found " + uuid); if (!user) throw new Error("unable to update - user not found " + uuid);

View File

@ -5,7 +5,7 @@ import * as T from "../../../website/src/lib/account-types";
type Address = string | { name: string, address: string }; type Address = string | { name: string, address: string };
const from: Address = { const adminAddress: Address = {
name: "LingDocs Admin", name: "LingDocs Admin",
address: "admin@lingdocs.com", address: "admin@lingdocs.com",
}; };
@ -31,31 +31,53 @@ const transporter = nodemailer.createTransport({
async function sendEmail(to: Address, subject: string, text: string) { async function sendEmail(to: Address, subject: string, text: string) {
await transporter.sendMail({ await transporter.sendMail({
from, from: adminAddress,
to, to,
subject, subject,
text, text,
}); });
} }
// TODO: MAKE THIS // 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(user: T.LingdocsUser, token: T.URLToken) {
const subject = "Please Verify Your E-mail";
const content = `Hello ${user.name}, const content = `Hello ${user.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/${user.userId}/${token}
LingDocs Admin`; 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) { export async function sendPasswordResetEmail(user: T.LingdocsUser, token: T.URLToken) {
const subject = "Reset Your Password";
const content = `Hello ${user.name}, 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), "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);
} }

View File

@ -12,6 +12,8 @@ import {
getEmailTokenAndHash, getEmailTokenAndHash,
} from "../lib/password-utils"; } from "../lib/password-utils";
import { import {
sendAccountUpgradeMessage,
sendUpgradeRequestToAdmin,
sendVerificationEmail, sendVerificationEmail,
} from "../lib/mail-utils"; } from "../lib/mail-utils";
import * as T from "../../../website/src/lib/account-types"; import * as T from "../../../website/src/lib/account-types";
@ -103,7 +105,7 @@ apiRouter.put("/user/userTextOptionsRecord", async (req, res, next) => {
} catch (e) { } catch (e) {
next(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");
@ -133,9 +135,14 @@ apiRouter.put("/user/upgrade", async (req, res, next) => {
// 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
// const { name } = await createWordlistDatabase(userId, password);
// update LingdocsUser // 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 = { const upgraded: T.UpgradeUserResponse = {
ok: true, ok: true,
message: "user upgraded to student", 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 * deletes a users own account
*/ */

View File

@ -38,6 +38,7 @@ export type LingdocsUser = {
tokenHash: Hash, tokenHash: Hash,
requestedOn: TimeStamp, requestedOn: TimeStamp,
}, },
requestedUpgradeToStudent?: boolean,
tests: [], tests: [],
lastLogin: TimeStamp, lastLogin: TimeStamp,
lastActive: TimeStamp, lastActive: TimeStamp,

View File

@ -23,6 +23,10 @@ export async function upgradeAccount(password: string): Promise<AT.UpgradeUserRe
return response as AT.UpgradeUserResponse; return response as AT.UpgradeUserResponse;
} }
export async function upgradeToStudentRequest(): Promise<AT.APIResponse> {
return await myFetch("account", "upgradeToStudentRequest", "POST") as AT.APIResponse;
}
export async function updateUserTextOptionsRecord(userTextOptionsRecord: AT.UserTextOptionsRecord): Promise<AT.UpdateUserTextOptionsRecordResponse> { export async function updateUserTextOptionsRecord(userTextOptionsRecord: AT.UserTextOptionsRecord): Promise<AT.UpdateUserTextOptionsRecordResponse> {
const response = await myFetch("account", "user/userTextOptionsRecord", "PUT", { userTextOptionsRecord }) as AT.UpdateUserTextOptionsRecordResponse; const response = await myFetch("account", "user/userTextOptionsRecord", "PUT", { userTextOptionsRecord }) as AT.UpdateUserTextOptionsRecordResponse;
return response; return response;

View File

@ -7,6 +7,7 @@ import {
upgradeAccount, upgradeAccount,
signOut, signOut,
publishDictionary, publishDictionary,
upgradeToStudentRequest,
} from "../lib/backend-calls"; } from "../lib/backend-calls";
import LoadingElipses from "../components/LoadingElipses"; import LoadingElipses from "../components/LoadingElipses";
import { Helmet } from "react-helmet"; import { Helmet } from "react-helmet";
@ -51,6 +52,22 @@ const Account = ({ user, loadUser }: { user: AT.LingdocsUser | undefined, loadUs
setUpgradePassword(""); setUpgradePassword("");
setUpgradeError(""); 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() { async function handleUpgrade() {
setUpgradeError(""); setUpgradeError("");
setWaiting(true); setWaiting(true);
@ -136,7 +153,7 @@ const Account = ({ user, loadUser }: { user: AT.LingdocsUser | undefined, loadUs
</div> </div>
</div> </div>
</li>} </li>}
<li className="list-group-item">Account Level: {capitalize(user.level)}</li> <li className="list-group-item">Account Level: {capitalize(user.level)} {user.requestedUpgradeToStudent ? "(Upgrade Requested)" : ""}</li>
</ul> </ul>
</div> </div>
</div> </div>
@ -167,7 +184,7 @@ const Account = ({ user, loadUser }: { user: AT.LingdocsUser | undefined, loadUs
<Modal.Header closeButton> <Modal.Header closeButton>
<Modal.Title>Upgrade Account</Modal.Title> <Modal.Title>Upgrade Account</Modal.Title>
</Modal.Header> </Modal.Header>
<Modal.Body>Enter the secret upgrade password to upgrade your account.</Modal.Body> <Modal.Body>Enter the secret upgrade password to upgrade your account or <a href="javascript:;" onClick={handleUpgradeRequest}>request an upgrade</a>.</Modal.Body>
<div className="form-group px-3"> <div className="form-group px-3">
<label htmlFor="upgradePasswordForm">Upgrade password:</label> <label htmlFor="upgradePasswordForm">Upgrade password:</label>
<input <input