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,
|
tokenHash: T.Hash,
|
||||||
requestedOn: T.TimeStamp,
|
requestedOn: T.TimeStamp,
|
||||||
},
|
},
|
||||||
|
} |
|
||||||
|
{
|
||||||
|
level: "student",
|
||||||
|
wordlistDbName: T.WordlistDbName,
|
||||||
|
userDbPassword: T.UserDbPassword,
|
||||||
}
|
}
|
||||||
): Promise<T.LingdocsUser> {
|
): Promise<T.LingdocsUser> {
|
||||||
const user = await getLingdocsUser("userId", uuid);
|
const user = await getLingdocsUser("userId", uuid);
|
||||||
|
@ -97,3 +102,46 @@ export async function updateLingdocsUser(uuid: T.UUID, toUpdate:
|
||||||
...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_TWITTER_CLIENT_SECRET",
|
||||||
"LINGDOCS_ACCOUNT_GITHUB_CLIENT_SECRET",
|
"LINGDOCS_ACCOUNT_GITHUB_CLIENT_SECRET",
|
||||||
"LINGDOCS_ACCOUNT_RECAPTCHA_SECRET",
|
"LINGDOCS_ACCOUNT_RECAPTCHA_SECRET",
|
||||||
|
"LINGDOCS_ACCOUNT_UPGRADE_PASSWORD",
|
||||||
];
|
];
|
||||||
|
|
||||||
const values = names.map((name) => ({
|
const values = names.map((name) => ({
|
||||||
|
@ -31,4 +32,5 @@ export default {
|
||||||
twitterClientSecret: values[6].value,
|
twitterClientSecret: values[6].value,
|
||||||
githubClientSecret: values[7].value,
|
githubClientSecret: values[7].value,
|
||||||
recaptchaSecret: values[8].value,
|
recaptchaSecret: values[8].value,
|
||||||
|
upgradePassword: values[9].value,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import express, { Response } from "express";
|
import express, { Response } from "express";
|
||||||
import {
|
import {
|
||||||
deleteLingdocsUser,
|
deleteLingdocsUser,
|
||||||
|
getLingdocsUser,
|
||||||
updateLingdocsUser,
|
updateLingdocsUser,
|
||||||
|
createWordlistDatabase,
|
||||||
} from "../lib/couch-db";
|
} from "../lib/couch-db";
|
||||||
import {
|
import {
|
||||||
getHash,
|
getHash,
|
||||||
|
@ -12,6 +14,8 @@ import {
|
||||||
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";
|
||||||
|
import * as FT from "../../../website/src/lib/functions-types";
|
||||||
|
import env from "../lib/env-vars";
|
||||||
|
|
||||||
function sendResponse(res: Response, payload: T.APIResponse) {
|
function sendResponse(res: Response, payload: T.APIResponse) {
|
||||||
return res.send(payload);
|
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
|
* 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 UUID = string & { __brand: "Random Unique UID" };
|
||||||
export type TimeStamp = number & { __brand: "UNIX Timestamp in milliseconds" };
|
export type TimeStamp = number & { __brand: "UNIX Timestamp in milliseconds" };
|
||||||
export type UserDbPassword = string & { __brand: "password for an individual user couchdb" };
|
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 URLToken = string & { __brand: "Base 64 URL Token" };
|
||||||
export type EmailVerified = true | Hash | false;
|
export type EmailVerified = true | Hash | false;
|
||||||
export type ActionComplete = { ok: true, message: string };
|
export type ActionComplete = { ok: true, message: string };
|
||||||
|
@ -33,5 +34,20 @@ export type LingdocsUser = {
|
||||||
tests: [],
|
tests: [],
|
||||||
lastLogin: TimeStamp,
|
lastLogin: TimeStamp,
|
||||||
lastActive: 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/";
|
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, {
|
const response = await fetch(accountBaseUrl + url, {
|
||||||
method,
|
method,
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
|
...body ? {
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
} : {},
|
||||||
});
|
});
|
||||||
return await response.json() as AT.APIResponse;
|
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> {
|
export async function upgradeAccount(password: string): Promise<AT.UpgradeUserResponse> {
|
||||||
return {
|
const response = await accountApiFetch(accountBaseUrl + "user/upgrade", "PUT", { password });
|
||||||
ok: false,
|
return response as AT.UpgradeUserResponse;
|
||||||
error: "incorrect password",
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function postSubmissions(submissions: FT.SubmissionsRequest): Promise<FT.SubmissionsResponse> {
|
export async function postSubmissions(submissions: FT.SubmissionsRequest): Promise<FT.SubmissionsResponse> {
|
||||||
|
|
|
@ -72,12 +72,3 @@ export type SubmissionsResponse = {
|
||||||
message: string,
|
message: string,
|
||||||
submissions: Submission[],
|
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) {
|
function initializeLocalDb(type: LocalDbType, refresh: () => void, user: AT.LingdocsUser) {
|
||||||
const name = type === "wordlist"
|
if (type !== "submissions" && "wordlistDb" in user) return
|
||||||
? `userdb-${stringToHex(user.userId)}`
|
const name = type === "reviewTasks"
|
||||||
|
? "review-tasks"
|
||||||
: type === "submissions"
|
: type === "submissions"
|
||||||
? "submissions"
|
? "submissions"
|
||||||
: "review-tasks";
|
: (type === "wordlist" && "wordlistDbName" in user)
|
||||||
|
? user.wordlistDbName
|
||||||
|
: "";
|
||||||
|
const password = "userDbPassword" in user ? user.userDbPassword : "";
|
||||||
const db = dbs[type];
|
const db = dbs[type];
|
||||||
// only initialize the db if it doesn't exist or if it has a different name
|
// only initialize the db if it doesn't exist or if it has a different name
|
||||||
if ((!db) || (db.db?.name !== name)) {
|
if ((!db) || (db.db?.name !== name)) {
|
||||||
|
@ -87,12 +91,11 @@ function initializeLocalDb(type: LocalDbType, refresh: () => void, user: AT.Ling
|
||||||
} else {
|
} else {
|
||||||
dbs[type]?.sync.cancel();
|
dbs[type]?.sync.cancel();
|
||||||
const db = new PouchDB(name);
|
const db = new PouchDB(name);
|
||||||
const pass = "userDbPassword" in user ? user.userDbPassword : "";
|
|
||||||
dbs[type] = {
|
dbs[type] = {
|
||||||
db,
|
db,
|
||||||
refresh,
|
refresh,
|
||||||
sync: db.sync(
|
sync: db.sync(
|
||||||
`https://${user.userId}:${pass}@couch.lingdocs.com/${name}`,
|
`https://${user.userId}:${password}@couch.lingdocs.com/${name}`,
|
||||||
{ live: true, retry: true },
|
{ live: true, retry: true },
|
||||||
).on("change", (info) => {
|
).on("change", (info) => {
|
||||||
if (info.direction === "pull") {
|
if (info.direction === "pull") {
|
||||||
|
|
Loading…
Reference in New Issue