with upgrade
This commit is contained in:
parent
44312e6f0c
commit
9436a4e077
|
@ -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;
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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
|
||||
*/
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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)}`;
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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",
|
||||
...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> {
|
||||
|
|
|
@ -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",
|
||||
};
|
||||
|
||||
|
|
|
@ -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") {
|
||||
|
|
Loading…
Reference in New Issue