From 9436a4e077a8f704f63230fe70d509de87a7e0f9 Mon Sep 17 00:00:00 2001 From: lingdocs <71590811+lingdocs@users.noreply.github.com> Date: Sat, 21 Aug 2021 23:01:35 +0400 Subject: [PATCH] with upgrade --- account/src/lib/couch-db.ts | 48 ++++++++++++++++++++++++++++++ account/src/lib/env-vars.ts | 2 ++ account/src/routers/api-router.ts | 37 +++++++++++++++++++++++ functions/src/generate-password.ts | 9 ------ functions/src/lib/userDbName.ts | 12 -------- website/src/lib/account-types.ts | 20 +++++++++++-- website/src/lib/backend-calls.ts | 16 +++++----- website/src/lib/functions-types.ts | 9 ------ website/src/lib/pouch-dbs.ts | 13 ++++---- 9 files changed, 122 insertions(+), 44 deletions(-) delete mode 100644 functions/src/generate-password.ts delete mode 100644 functions/src/lib/userDbName.ts diff --git a/account/src/lib/couch-db.ts b/account/src/lib/couch-db.ts index 058e0e7..02bfb79 100644 --- a/account/src/lib/couch-db.ts +++ b/account/src/lib/couch-db.ts @@ -81,6 +81,11 @@ export async function updateLingdocsUser(uuid: T.UUID, toUpdate: tokenHash: T.Hash, requestedOn: T.TimeStamp, }, + } | + { + level: "student", + wordlistDbName: T.WordlistDbName, + userDbPassword: T.UserDbPassword, } ): Promise { const user = await getLingdocsUser("userId", uuid); @@ -97,3 +102,46 @@ export async function updateLingdocsUser(uuid: T.UUID, toUpdate: ...toUpdate, }); } + +export async function createWordlistDatabase(uuid: T.UUID): Promise<{ name: T.WordlistDbName, password: T.UserDbPassword }> { + const password = generateWordlistDbPassword(); + const name = getWordlistDbName(uuid); + // create wordlist database for user + await nano.db.create(name); + const securityInfo = { + admins: { + names: [uuid], + roles: ["_admin"] + }, + members: { + names: [uuid], + roles: ["_admin"], + }, + }; + const userDb = nano.db.use(name); + await userDb.insert(securityInfo as any, "_security"); + return { password, name }; +} + +function generateWordlistDbPassword(): T.UserDbPassword { + function makeChunk(): string { + return Math.random().toString(36).slice(2) + } + const password = new Array(4).fill(0).reduce((acc: string): string => ( + acc + makeChunk() + ), ""); + return password as T.UserDbPassword; +} + +function stringToHex(str: string) { + const arr1 = []; + for (let n = 0, l = str.length; n < l; n ++) { + const hex = Number(str.charCodeAt(n)).toString(16); + arr1.push(hex); + } + return arr1.join(''); +} + +export function getWordlistDbName(uid: T.UUID): T.WordlistDbName { + return `wordlist-${stringToHex(uid)}` as T.WordlistDbName; +} \ No newline at end of file diff --git a/account/src/lib/env-vars.ts b/account/src/lib/env-vars.ts index b56e93e..4169fe3 100644 --- a/account/src/lib/env-vars.ts +++ b/account/src/lib/env-vars.ts @@ -8,6 +8,7 @@ const names = [ "LINGDOCS_ACCOUNT_TWITTER_CLIENT_SECRET", "LINGDOCS_ACCOUNT_GITHUB_CLIENT_SECRET", "LINGDOCS_ACCOUNT_RECAPTCHA_SECRET", + "LINGDOCS_ACCOUNT_UPGRADE_PASSWORD", ]; const values = names.map((name) => ({ @@ -31,4 +32,5 @@ export default { twitterClientSecret: values[6].value, githubClientSecret: values[7].value, recaptchaSecret: values[8].value, + upgradePassword: values[9].value, }; diff --git a/account/src/routers/api-router.ts b/account/src/routers/api-router.ts index ad4ccb5..393cb19 100644 --- a/account/src/routers/api-router.ts +++ b/account/src/routers/api-router.ts @@ -1,7 +1,9 @@ import express, { Response } from "express"; import { deleteLingdocsUser, + getLingdocsUser, updateLingdocsUser, + createWordlistDatabase, } from "../lib/couch-db"; import { getHash, @@ -12,6 +14,8 @@ import { sendVerificationEmail, } from "../lib/mail-utils"; import * as T from "../../../website/src/lib/account-types"; +import * as FT from "../../../website/src/lib/functions-types"; +import env from "../lib/env-vars"; function sendResponse(res: Response, payload: T.APIResponse) { return res.send(payload); @@ -87,6 +91,39 @@ apiRouter.put("/email-verification", async (req, res, next) => { } }); +apiRouter.post("/user/upgrade", async (req, res, next) => { + if (!req.user) throw new Error("user not found"); + const givenPassword = (req.body.password || "") as string; + const studentPassword = env.upgradePassword; + if (givenPassword.toLowerCase().trim() !== studentPassword.toLowerCase()) { + const wrongPass: T.UpgradeUserResponse = { + ok: false, + error: "incorrect password", + }; + res.send(wrongPass); + return; + } + const { userId } = req.user; + const user = await getLingdocsUser("userId", userId); + if (user) { + const alreadyUpgraded: T.UpgradeUserResponse = { + ok: true, + message: "user already upgraded", + user, + }; + res.send(alreadyUpgraded); + return; + } + const { name, password } = await createWordlistDatabase(userId); + const u = await updateLingdocsUser(userId, { level: "student", wordlistDbName: name, userDbPassword: password }); + const upgraded: T.UpgradeUserResponse = { + ok: true, + message: "user upgraded to student", + user: u, + }; + res.send(upgraded); +}); + /** * deletes a users own account */ diff --git a/functions/src/generate-password.ts b/functions/src/generate-password.ts deleted file mode 100644 index 8316696..0000000 --- a/functions/src/generate-password.ts +++ /dev/null @@ -1,9 +0,0 @@ -export default function generatePassword(): string { - function makeChunk(): string { - return Math.random().toString(36).slice(2) - } - const password = new Array(4).fill(0).reduce((acc: string): string => ( - acc + makeChunk() - ), ""); - return password; -} \ No newline at end of file diff --git a/functions/src/lib/userDbName.ts b/functions/src/lib/userDbName.ts deleted file mode 100644 index 9800e08..0000000 --- a/functions/src/lib/userDbName.ts +++ /dev/null @@ -1,12 +0,0 @@ -function stringToHex(str: string) { - const arr1 = []; - for (let n = 0, l = str.length; n < l; n ++) { - const hex = Number(str.charCodeAt(n)).toString(16); - arr1.push(hex); - } - return arr1.join(''); -} - -export function getUserDbName(uid: string): string { - return `userdb-${stringToHex(uid)}`; -} \ No newline at end of file diff --git a/website/src/lib/account-types.ts b/website/src/lib/account-types.ts index 826096d..1a5e8d4 100644 --- a/website/src/lib/account-types.ts +++ b/website/src/lib/account-types.ts @@ -2,6 +2,7 @@ export type Hash = string & { __brand: "Hashed String" }; export type UUID = string & { __brand: "Random Unique UID" }; export type TimeStamp = number & { __brand: "UNIX Timestamp in milliseconds" }; export type UserDbPassword = string & { __brand: "password for an individual user couchdb" }; +export type WordlistDbName = string & { __brand: "name for an individual user couchdb" }; export type URLToken = string & { __brand: "Base 64 URL Token" }; export type EmailVerified = true | Hash | false; export type ActionComplete = { ok: true, message: string }; @@ -33,5 +34,20 @@ export type LingdocsUser = { tests: [], lastLogin: TimeStamp, lastActive: TimeStamp, -} & ({ level: "basic"} | { level: "student" | "editor", userDbPassword: UserDbPassword }) -& import("nano").MaybeDocument; +} & ( + { level: "basic"} | + { + level: "student" | "editor", + userDbPassword: UserDbPassword, + wordlistDbName: WordlistDbName, + } +) & import("nano").MaybeDocument; + +export type UpgradeUserResponse = { + ok: false, + error: "incorrect password", +} | { + ok: true, + message: "user already upgraded" | "user upgraded to student", + user: LingdocsUser, +}; diff --git a/website/src/lib/backend-calls.ts b/website/src/lib/backend-calls.ts index ddc9daa..5b91772 100644 --- a/website/src/lib/backend-calls.ts +++ b/website/src/lib/backend-calls.ts @@ -7,10 +7,14 @@ import * as AT from "./account-types"; const accountBaseUrl = "https://account.lingdocs.com/api/"; -async function accountApiFetch(url: string, method: "GET" | "POST" | "PUT" | "DELETE" = "GET"): Promise { +// TODO: TYPE BODY +async function accountApiFetch(url: string, method: "GET" | "POST" | "PUT" | "DELETE" = "GET", body?: any): Promise { const response = await fetch(accountBaseUrl + url, { method, - credentials: "include", + credentials: "include", + ...body ? { + body: JSON.stringify(body), + } : {}, }); return await response.json() as AT.APIResponse; } @@ -23,11 +27,9 @@ export async function publishDictionary(): Promise }; } -export async function upgradeAccount(password: string): Promise { - return { - ok: false, - error: "incorrect password", - }; +export async function upgradeAccount(password: string): Promise { + const response = await accountApiFetch(accountBaseUrl + "user/upgrade", "PUT", { password }); + return response as AT.UpgradeUserResponse; } export async function postSubmissions(submissions: FT.SubmissionsRequest): Promise { diff --git a/website/src/lib/functions-types.ts b/website/src/lib/functions-types.ts index 7f02789..8ca80c9 100644 --- a/website/src/lib/functions-types.ts +++ b/website/src/lib/functions-types.ts @@ -72,12 +72,3 @@ export type SubmissionsResponse = { message: string, submissions: Submission[], }; - -export type UpgradeUserResponse = { - ok: false, - error: "incorrect password", -} | { - ok: true, - message: "user already upgraded" | "user upgraded to student", -}; - diff --git a/website/src/lib/pouch-dbs.ts b/website/src/lib/pouch-dbs.ts index 311d6d0..4ab0ebb 100644 --- a/website/src/lib/pouch-dbs.ts +++ b/website/src/lib/pouch-dbs.ts @@ -71,11 +71,15 @@ export function stopLocalDbs() { } function initializeLocalDb(type: LocalDbType, refresh: () => void, user: AT.LingdocsUser) { - const name = type === "wordlist" - ? `userdb-${stringToHex(user.userId)}` + if (type !== "submissions" && "wordlistDb" in user) return + const name = type === "reviewTasks" + ? "review-tasks" : type === "submissions" ? "submissions" - : "review-tasks"; + : (type === "wordlist" && "wordlistDbName" in user) + ? user.wordlistDbName + : ""; + const password = "userDbPassword" in user ? user.userDbPassword : ""; const db = dbs[type]; // only initialize the db if it doesn't exist or if it has a different name if ((!db) || (db.db?.name !== name)) { @@ -87,12 +91,11 @@ function initializeLocalDb(type: LocalDbType, refresh: () => void, user: AT.Ling } else { dbs[type]?.sync.cancel(); const db = new PouchDB(name); - const pass = "userDbPassword" in user ? user.userDbPassword : ""; dbs[type] = { db, refresh, sync: db.sync( - `https://${user.userId}:${pass}@couch.lingdocs.com/${name}`, + `https://${user.userId}:${password}@couch.lingdocs.com/${name}`, { live: true, retry: true }, ).on("change", (info) => { if (info.direction === "pull") {