with upgrade

This commit is contained in:
lingdocs 2021-08-21 23:01:35 +04:00
parent 44312e6f0c
commit 9436a4e077
9 changed files with 122 additions and 44 deletions

View File

@ -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<T.LingdocsUser> {
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;
}

View File

@ -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,
};

View File

@ -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
*/

View File

@ -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;
}

View File

@ -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)}`;
}

View File

@ -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,
};

View File

@ -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<AT.APIResponse> {
// TODO: TYPE BODY
async function accountApiFetch(url: string, method: "GET" | "POST" | "PUT" | "DELETE" = "GET", body?: any): Promise<AT.APIResponse> {
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<FT.PublishDictionaryResponse>
};
}
export async function upgradeAccount(password: string): Promise<FT.UpgradeUserResponse> {
return {
ok: false,
error: "incorrect password",
};
export async function upgradeAccount(password: string): Promise<AT.UpgradeUserResponse> {
const response = await accountApiFetch(accountBaseUrl + "user/upgrade", "PUT", { password });
return response as AT.UpgradeUserResponse;
}
export async function postSubmissions(submissions: FT.SubmissionsRequest): Promise<FT.SubmissionsResponse> {

View File

@ -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",
};

View File

@ -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") {