feedback view
This commit is contained in:
parent
af5a731e43
commit
60d024abb6
|
@ -25,7 +25,10 @@ export function updateLastLogin(user: T.LingdocsUser): T.LingdocsUser {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function processAPIResponse(user: T.LingdocsUser, response: DocumentInsertResponse): T.LingdocsUser | undefined {
|
function processAPIResponse(
|
||||||
|
user: T.LingdocsUser,
|
||||||
|
response: DocumentInsertResponse
|
||||||
|
): T.LingdocsUser | undefined {
|
||||||
if (response.ok !== true) return undefined;
|
if (response.ok !== true) return undefined;
|
||||||
return {
|
return {
|
||||||
...user,
|
...user,
|
||||||
|
@ -34,31 +37,44 @@ function processAPIResponse(user: T.LingdocsUser, response: DocumentInsertRespon
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLingdocsUser(field: "email" | "userId" | "githubId" | "googleId" | "twitterId", value: string): Promise<undefined | T.LingdocsUser> {
|
export async function getLingdocsUser(
|
||||||
const user = await usersDb.find({
|
field: "email" | "userId" | "githubId" | "googleId" | "twitterId",
|
||||||
selector: field === "githubId"
|
value: string
|
||||||
? { github: { id: value }}
|
): Promise<undefined | T.LingdocsUser> {
|
||||||
|
const user = await usersDb.find({
|
||||||
|
selector:
|
||||||
|
field === "githubId"
|
||||||
|
? { github: { id: value } }
|
||||||
: field === "googleId"
|
: field === "googleId"
|
||||||
? { google: { id: value }}
|
? { google: { id: value } }
|
||||||
: field === "twitterId"
|
: field === "twitterId"
|
||||||
? { twitter: { id: value }}
|
? { twitter: { id: value } }
|
||||||
: { [field]: value },
|
: { [field]: value },
|
||||||
});
|
});
|
||||||
if (!user.docs.length) {
|
if (!user.docs.length) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return user.docs[0] as T.LingdocsUser;
|
return user.docs[0] as T.LingdocsUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAllLingdocsUsers(): Promise<T.LingdocsUser[]> {
|
export async function getAllLingdocsUsers(): Promise<T.LingdocsUser[]> {
|
||||||
const users = await usersDb.find({
|
const users = await usersDb.find({
|
||||||
selector: { userId: { $exists: true }},
|
selector: { userId: { $exists: true } },
|
||||||
limit: 5000,
|
limit: 5000,
|
||||||
});
|
});
|
||||||
return users.docs as T.LingdocsUser[];
|
return users.docs as T.LingdocsUser[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function insertLingdocsUser(user: T.LingdocsUser): Promise<T.LingdocsUser> {
|
export async function getAllFeedback(): Promise<any[]> {
|
||||||
|
const res = await feedbackDb.find({
|
||||||
|
selector: { doc: { $exists: true } },
|
||||||
|
});
|
||||||
|
return res.docs as any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function insertLingdocsUser(
|
||||||
|
user: T.LingdocsUser
|
||||||
|
): Promise<T.LingdocsUser> {
|
||||||
try {
|
try {
|
||||||
const res = await usersDb.insert(user);
|
const res = await usersDb.insert(user);
|
||||||
const newUser = processAPIResponse(user, res);
|
const newUser = processAPIResponse(user, res);
|
||||||
|
@ -66,7 +82,7 @@ export async function insertLingdocsUser(user: T.LingdocsUser): Promise<T.Lingdo
|
||||||
throw new Error("error inserting user");
|
throw new Error("error inserting user");
|
||||||
}
|
}
|
||||||
return newUser;
|
return newUser;
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
console.log("ERROR on insertLingdocsUser", user);
|
console.log("ERROR on insertLingdocsUser", user);
|
||||||
throw new Error("error inserting user - on update");
|
throw new Error("error inserting user - on update");
|
||||||
}
|
}
|
||||||
|
@ -83,46 +99,47 @@ export async function deleteLingdocsUser(uuid: T.UUID): Promise<void> {
|
||||||
|
|
||||||
export async function deleteCouchDbAuthUser(uuid: T.UUID): Promise<void> {
|
export async function deleteCouchDbAuthUser(uuid: T.UUID): Promise<void> {
|
||||||
const authUsers = nano.db.use("_users");
|
const authUsers = nano.db.use("_users");
|
||||||
const user = await authUsers.find({ selector: { name: uuid }});
|
const user = await authUsers.find({ selector: { name: uuid } });
|
||||||
if (!user.docs.length) return;
|
if (!user.docs.length) return;
|
||||||
const u = user.docs[0];
|
const u = user.docs[0];
|
||||||
await authUsers.destroy(u._id, u._rev);
|
await authUsers.destroy(u._id, u._rev);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateLingdocsUser(uuid: T.UUID, toUpdate:
|
export async function updateLingdocsUser(
|
||||||
// TODO: OR USE REDUCER??
|
uuid: T.UUID,
|
||||||
{ name: string } |
|
toUpdate: // TODO: OR USE REDUCER??
|
||||||
{ name?: string, email: string, emailVerified: T.Hash } |
|
| { name: string }
|
||||||
{ email: string, emailVerified: true } |
|
| { name?: string; email: string; emailVerified: T.Hash }
|
||||||
{ emailVerified: T.Hash } |
|
| { email: string; emailVerified: true }
|
||||||
{ emailVerified: true } |
|
| { emailVerified: T.Hash }
|
||||||
{ password: T.Hash } |
|
| { emailVerified: true }
|
||||||
{ google: T.GoogleProfile | undefined } |
|
| { password: T.Hash }
|
||||||
{ github: T.GitHubProfile | undefined } |
|
| { google: T.GoogleProfile | undefined }
|
||||||
{ twitter: T.TwitterProfile | undefined } |
|
| { github: T.GitHubProfile | undefined }
|
||||||
{
|
| { twitter: T.TwitterProfile | undefined }
|
||||||
passwordReset: {
|
| {
|
||||||
tokenHash: T.Hash,
|
passwordReset: {
|
||||||
requestedOn: T.TimeStamp,
|
tokenHash: T.Hash;
|
||||||
},
|
requestedOn: T.TimeStamp;
|
||||||
} |
|
};
|
||||||
{
|
}
|
||||||
level: "student",
|
| {
|
||||||
wordlistDbName: T.WordlistDbName,
|
level: "student";
|
||||||
couchDbPassword: T.UserDbPassword,
|
wordlistDbName: T.WordlistDbName;
|
||||||
upgradeToStudentRequest: undefined,
|
couchDbPassword: T.UserDbPassword;
|
||||||
subscription?: T.StripeSubscription,
|
upgradeToStudentRequest: undefined;
|
||||||
} |
|
subscription?: T.StripeSubscription;
|
||||||
{
|
}
|
||||||
level: "basic",
|
| {
|
||||||
wordlistDbName: undefined,
|
level: "basic";
|
||||||
couchDbPassword: undefined,
|
wordlistDbName: undefined;
|
||||||
upgradeToStudentRequest: undefined,
|
couchDbPassword: undefined;
|
||||||
subscription: undefined,
|
upgradeToStudentRequest: undefined;
|
||||||
} |
|
subscription: undefined;
|
||||||
{ upgradeToStudentRequest: "waiting" } |
|
}
|
||||||
{ upgradeToStudentRequest: "denied" } |
|
| { upgradeToStudentRequest: "waiting" }
|
||||||
{ tests: T.TestResult[] }
|
| { upgradeToStudentRequest: "denied" }
|
||||||
|
| { tests: T.TestResult[] }
|
||||||
): 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);
|
||||||
|
@ -145,7 +162,9 @@ export async function updateLingdocsUser(uuid: T.UUID, toUpdate:
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addCouchDbAuthUser(uuid: T.UUID): Promise<{ password: T.UserDbPassword, userDbName: T.WordlistDbName }> {
|
export async function addCouchDbAuthUser(
|
||||||
|
uuid: T.UUID
|
||||||
|
): Promise<{ password: T.UserDbPassword; userDbName: T.WordlistDbName }> {
|
||||||
const password = generateWordlistDbPassword();
|
const password = generateWordlistDbPassword();
|
||||||
const userDbName = getWordlistDbName(uuid);
|
const userDbName = getWordlistDbName(uuid);
|
||||||
const usersDb = nano.db.use("_users");
|
const usersDb = nano.db.use("_users");
|
||||||
|
@ -199,21 +218,21 @@ export function getWordlistDbName(uid: T.UUID): T.WordlistDbName {
|
||||||
|
|
||||||
function generateWordlistDbPassword(): T.UserDbPassword {
|
function generateWordlistDbPassword(): T.UserDbPassword {
|
||||||
function makeChunk(): string {
|
function makeChunk(): string {
|
||||||
return Math.random().toString(36).slice(2)
|
return Math.random().toString(36).slice(2);
|
||||||
}
|
}
|
||||||
const password = new Array(4).fill(0).reduce((acc: string): string => (
|
const password = new Array(4)
|
||||||
acc + makeChunk()
|
.fill(0)
|
||||||
), "");
|
.reduce((acc: string): string => acc + makeChunk(), "");
|
||||||
return password as T.UserDbPassword;
|
return password as T.UserDbPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
function stringToHex(str: string) {
|
function stringToHex(str: string) {
|
||||||
const arr1 = [];
|
const arr1 = [];
|
||||||
for (let n = 0, l = str.length; n < l; n ++) {
|
for (let n = 0, l = str.length; n < l; n++) {
|
||||||
const hex = Number(str.charCodeAt(n)).toString(16);
|
const hex = Number(str.charCodeAt(n)).toString(16);
|
||||||
arr1.push(hex);
|
arr1.push(hex);
|
||||||
}
|
}
|
||||||
return arr1.join('');
|
return arr1.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -224,15 +243,19 @@ function stringToHex(str: string) {
|
||||||
* @param newResults - the tests to be added to a users record
|
* @param newResults - the tests to be added to a users record
|
||||||
* @param amountToKeep - the amount of repeat tests to keep (defaults to 2)
|
* @param amountToKeep - the amount of repeat tests to keep (defaults to 2)
|
||||||
*/
|
*/
|
||||||
function addNewTests(existing: Readonly<T.TestResult[]>, toAdd: T.TestResult[], amountToKeep = 2): T.TestResult[] {
|
function addNewTests(
|
||||||
|
existing: Readonly<T.TestResult[]>,
|
||||||
|
toAdd: T.TestResult[],
|
||||||
|
amountToKeep = 2
|
||||||
|
): T.TestResult[] {
|
||||||
const tests = [...existing];
|
const tests = [...existing];
|
||||||
// check to make sure that we're only adding test results that are not already added
|
// check to make sure that we're only adding test results that are not already added
|
||||||
const newTests = toAdd.filter((t) => !tests.some(x => x.time === t.time));
|
const newTests = toAdd.filter((t) => !tests.some((x) => x.time === t.time));
|
||||||
newTests.forEach((nt) => {
|
newTests.forEach((nt) => {
|
||||||
const repeats = tests.filter(x => ((x.id === nt.id)) && (x.done === nt.done));
|
const repeats = tests.filter((x) => x.id === nt.id && x.done === nt.done);
|
||||||
if (repeats.length > (amountToKeep - 1)) {
|
if (repeats.length > amountToKeep - 1) {
|
||||||
// already have enough repeat passes saved, remove the oldest one
|
// already have enough repeat passes saved, remove the oldest one
|
||||||
const i = tests.findIndex(x => x.time === repeats[0].time);
|
const i = tests.findIndex((x) => x.time === repeats[0].time);
|
||||||
if (i > -1) tests.splice(i, 1);
|
if (i > -1) tests.splice(i, 1);
|
||||||
}
|
}
|
||||||
tests.push(nt);
|
tests.push(nt);
|
||||||
|
|
|
@ -2,25 +2,25 @@ import { Router } from "express";
|
||||||
import { PassportStatic } from "passport";
|
import { PassportStatic } from "passport";
|
||||||
import {
|
import {
|
||||||
deleteLingdocsUser,
|
deleteLingdocsUser,
|
||||||
|
getAllFeedback,
|
||||||
getAllLingdocsUsers,
|
getAllLingdocsUsers,
|
||||||
getLingdocsUser,
|
getLingdocsUser,
|
||||||
updateLingdocsUser,
|
updateLingdocsUser,
|
||||||
} from "../lib/couch-db";
|
} from "../lib/couch-db";
|
||||||
import { createNewUser, canRemoveOneOutsideProvider, downgradeUser } from "../lib/user-utils";
|
import {
|
||||||
|
createNewUser,
|
||||||
|
canRemoveOneOutsideProvider,
|
||||||
|
downgradeUser,
|
||||||
|
} from "../lib/user-utils";
|
||||||
import {
|
import {
|
||||||
getHash,
|
getHash,
|
||||||
getURLToken,
|
getURLToken,
|
||||||
compareToHash,
|
compareToHash,
|
||||||
getEmailTokenAndHash,
|
getEmailTokenAndHash,
|
||||||
} from "../lib/password-utils";
|
} from "../lib/password-utils";
|
||||||
import {
|
import { upgradeUser, denyUserUpgradeRequest } from "../lib/user-utils";
|
||||||
upgradeUser,
|
|
||||||
denyUserUpgradeRequest,
|
|
||||||
} from "../lib/user-utils";
|
|
||||||
import { validateReCaptcha } from "../lib/recaptcha";
|
import { validateReCaptcha } from "../lib/recaptcha";
|
||||||
import {
|
import { getTimestamp } from "../lib/time-utils";
|
||||||
getTimestamp,
|
|
||||||
} from "../lib/time-utils";
|
|
||||||
import {
|
import {
|
||||||
sendPasswordResetEmail,
|
sendPasswordResetEmail,
|
||||||
sendVerificationEmail,
|
sendVerificationEmail,
|
||||||
|
@ -56,10 +56,16 @@ const authRouter = (passport: PassportStatic) => {
|
||||||
const name = req.body.name as string;
|
const name = req.body.name as string;
|
||||||
const email = req.body.email as string;
|
const email = req.body.email as string;
|
||||||
if (email !== req.user.email) {
|
if (email !== req.user.email) {
|
||||||
if (name !== req.user.name) await updateLingdocsUser(req.user.userId, { name });
|
if (name !== req.user.name)
|
||||||
const withSameEmail = (email !== "") && await getLingdocsUser("email", email);
|
await updateLingdocsUser(req.user.userId, { name });
|
||||||
|
const withSameEmail =
|
||||||
|
email !== "" && (await getLingdocsUser("email", email));
|
||||||
if (withSameEmail) {
|
if (withSameEmail) {
|
||||||
return res.render(page, { user: { ...req.user, email }, error: "email taken", removeProviderOption: canRemoveOneOutsideProvider(req.user) });
|
return res.render(page, {
|
||||||
|
user: { ...req.user, email },
|
||||||
|
error: "email taken",
|
||||||
|
removeProviderOption: canRemoveOneOutsideProvider(req.user),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
// TODO: ABSTRACT THE PROCESS OF GETTING A NEW EMAIL TOKEN AND MAILING!
|
// TODO: ABSTRACT THE PROCESS OF GETTING A NEW EMAIL TOKEN AND MAILING!
|
||||||
const { token, hash } = await getEmailTokenAndHash();
|
const { token, hash } = await getEmailTokenAndHash();
|
||||||
|
@ -69,11 +75,19 @@ const authRouter = (passport: PassportStatic) => {
|
||||||
emailVerified: hash,
|
emailVerified: hash,
|
||||||
});
|
});
|
||||||
sendVerificationEmail(updated, token).catch(console.error);
|
sendVerificationEmail(updated, token).catch(console.error);
|
||||||
return res.render(page, { user: updated, error: null, removeProviderOption: canRemoveOneOutsideProvider(req.user) });
|
return res.render(page, {
|
||||||
|
user: updated,
|
||||||
|
error: null,
|
||||||
|
removeProviderOption: canRemoveOneOutsideProvider(req.user),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
const updated = await updateLingdocsUser(req.user.userId, { name });
|
const updated = await updateLingdocsUser(req.user.userId, { name });
|
||||||
// need to do this because sometimes the update seems slow?
|
// need to do this because sometimes the update seems slow?
|
||||||
return res.render(page, { user: updated, error: null, removeProviderOption: canRemoveOneOutsideProvider(req.user) });
|
return res.render(page, {
|
||||||
|
user: updated,
|
||||||
|
error: null,
|
||||||
|
removeProviderOption: canRemoveOneOutsideProvider(req.user),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/login", async (req, res, next) => {
|
router.post("/login", async (req, res, next) => {
|
||||||
|
@ -83,22 +97,26 @@ const authRouter = (passport: PassportStatic) => {
|
||||||
return res.render("login", { recaptcha: "fail", inProd });
|
return res.render("login", { recaptcha: "fail", inProd });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
passport.authenticate("local", (err, user: T.LingdocsUser | undefined, info) => {
|
passport.authenticate(
|
||||||
if (err) throw err;
|
"local",
|
||||||
if (!user && info.message === "email not found") {
|
(err, user: T.LingdocsUser | undefined, info) => {
|
||||||
return res.send({ ok: false, newSignup: true });
|
if (err) throw err;
|
||||||
|
if (!user && info.message === "email not found") {
|
||||||
|
return res.send({ ok: false, newSignup: true });
|
||||||
|
}
|
||||||
|
if (!user)
|
||||||
|
res.send({
|
||||||
|
ok: false,
|
||||||
|
message: "Incorrect password",
|
||||||
|
});
|
||||||
|
else {
|
||||||
|
req.logIn(user, (err) => {
|
||||||
|
if (err) return next(err);
|
||||||
|
res.send({ ok: true, user });
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!user) res.send({
|
)(req, res, next);
|
||||||
ok: false,
|
|
||||||
message: "Incorrect password",
|
|
||||||
});
|
|
||||||
else {
|
|
||||||
req.logIn(user, (err) => {
|
|
||||||
if (err) return next(err);
|
|
||||||
res.send({ ok: true, user });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
})(req, res, next);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
|
@ -106,31 +124,42 @@ const authRouter = (passport: PassportStatic) => {
|
||||||
passport.authenticate("google", {
|
passport.authenticate("google", {
|
||||||
// @ts-ignore - needed for getting refreshToken]
|
// @ts-ignore - needed for getting refreshToken]
|
||||||
accessType: "offline",
|
accessType: "offline",
|
||||||
scope: ["openid", "https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"],
|
scope: [
|
||||||
|
"openid",
|
||||||
|
"https://www.googleapis.com/auth/userinfo.email",
|
||||||
|
"https://www.googleapis.com/auth/userinfo.profile",
|
||||||
|
],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
router.get('/github', passport.authenticate("github", {
|
router.get(
|
||||||
scope: ["read:user", "user:email"],
|
"/github",
|
||||||
}));
|
passport.authenticate("github", {
|
||||||
router.get('/twitter', passport.authenticate("twitter"));
|
scope: ["read:user", "user:email"],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
router.get("/twitter", passport.authenticate("twitter"));
|
||||||
|
|
||||||
// all callback and remove routes/functions are the same for each provider
|
// all callback and remove routes/functions are the same for each provider
|
||||||
outsideProviders.forEach((provider) => {
|
outsideProviders.forEach((provider) => {
|
||||||
router.get(
|
router.get(
|
||||||
`/${provider}/callback`,
|
`/${provider}/callback`,
|
||||||
passport.authenticate(provider, { successRedirect: '/user', failureRedirect: '/' }),
|
passport.authenticate(provider, {
|
||||||
|
successRedirect: "/user",
|
||||||
|
failureRedirect: "/",
|
||||||
|
})
|
||||||
);
|
);
|
||||||
router.post(`/${provider}/remove`, async (req, res, next) => {
|
router.post(`/${provider}/remove`, async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
if (!req.user) return next("user not found");
|
if (!req.user) return next("user not found");
|
||||||
if (!canRemoveOneOutsideProvider(req.user)) return res.redirect("/user");
|
if (!canRemoveOneOutsideProvider(req.user))
|
||||||
|
return res.redirect("/user");
|
||||||
await updateLingdocsUser(
|
await updateLingdocsUser(
|
||||||
req.user.userId,
|
req.user.userId,
|
||||||
// @ts-ignore - shouldn't need this
|
// @ts-ignore - shouldn't need this
|
||||||
{ [provider]: undefined }
|
{ [provider]: undefined }
|
||||||
);
|
);
|
||||||
return res.redirect("/user");
|
return res.redirect("/user");
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
next(e);
|
next(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -141,12 +170,17 @@ const authRouter = (passport: PassportStatic) => {
|
||||||
const { email, password, name } = req.body;
|
const { email, password, name } = req.body;
|
||||||
const existingUser = await getLingdocsUser("email", email);
|
const existingUser = await getLingdocsUser("email", email);
|
||||||
if (existingUser) return res.send("User Already Exists");
|
if (existingUser) return res.send("User Already Exists");
|
||||||
const user = await createNewUser({ strategy: "local", email, passwordPlainText: password, name });
|
const user = await createNewUser({
|
||||||
|
strategy: "local",
|
||||||
|
email,
|
||||||
|
passwordPlainText: password,
|
||||||
|
name,
|
||||||
|
});
|
||||||
req.logIn(user, (err) => {
|
req.logIn(user, (err) => {
|
||||||
if (err) return next(err);
|
if (err) return next(err);
|
||||||
return res.send({ ok: true, user });
|
return res.send({ ok: true, user });
|
||||||
});
|
});
|
||||||
} catch(e) {
|
} catch (e) {
|
||||||
next(e);
|
next(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -156,16 +190,30 @@ const authRouter = (passport: PassportStatic) => {
|
||||||
if (!req.user || !req.user.admin) {
|
if (!req.user || !req.user.admin) {
|
||||||
return res.redirect("/");
|
return res.redirect("/");
|
||||||
}
|
}
|
||||||
const users = (await getAllLingdocsUsers()).sort((a, b) => (
|
const users = (await getAllLingdocsUsers()).sort(
|
||||||
(a.accountCreated || 0) - (b.accountCreated || 0)
|
(a, b) => (a.accountCreated || 0) - (b.accountCreated || 0)
|
||||||
));
|
);
|
||||||
const tests = getTestCompletionSummary(users)
|
const tests = getTestCompletionSummary(users);
|
||||||
res.render("admin", { users, tests });
|
res.render("admin", { users, tests });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
next(e);
|
next(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.get("/grammar-feedback", async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
if (!req.user || !req.user.admin) {
|
||||||
|
return res.redirect("/");
|
||||||
|
}
|
||||||
|
const docs = await getAllFeedback();
|
||||||
|
console.log("one doc");
|
||||||
|
console.log(docs[0]);
|
||||||
|
res.render("grammar-feedback", { docs });
|
||||||
|
} catch (e) {
|
||||||
|
next(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
router.get("/privacy", (req, res) => {
|
router.get("/privacy", (req, res) => {
|
||||||
res.render("privacy");
|
res.render("privacy");
|
||||||
});
|
});
|
||||||
|
@ -173,36 +221,43 @@ const authRouter = (passport: PassportStatic) => {
|
||||||
/**
|
/**
|
||||||
* Grant request for upgrade to student
|
* Grant request for upgrade to student
|
||||||
*/
|
*/
|
||||||
router.post("/admin/upgradeToStudent/:userId/:grantOrDeny", async (req, res, next) => {
|
router.post(
|
||||||
try {
|
"/admin/upgradeToStudent/:userId/:grantOrDeny",
|
||||||
if (!req.user || !req.user.admin) {
|
async (req, res, next) => {
|
||||||
return res.redirect("/");
|
try {
|
||||||
|
if (!req.user || !req.user.admin) {
|
||||||
|
return res.redirect("/");
|
||||||
|
}
|
||||||
|
const userId = req.params.userId as T.UUID;
|
||||||
|
const grantOrDeny = req.params.grantOrDeny as "grant" | "deny";
|
||||||
|
if (grantOrDeny === "grant") {
|
||||||
|
await upgradeUser(userId);
|
||||||
|
} else {
|
||||||
|
await denyUserUpgradeRequest(userId);
|
||||||
|
}
|
||||||
|
res.redirect("/admin");
|
||||||
|
} catch (e) {
|
||||||
|
next(e);
|
||||||
}
|
}
|
||||||
const userId = req.params.userId as T.UUID;
|
|
||||||
const grantOrDeny = req.params.grantOrDeny as "grant" | "deny";
|
|
||||||
if (grantOrDeny === "grant") {
|
|
||||||
await upgradeUser(userId);
|
|
||||||
} else {
|
|
||||||
await denyUserUpgradeRequest(userId);
|
|
||||||
}
|
|
||||||
res.redirect("/admin");
|
|
||||||
} catch (e) {
|
|
||||||
next(e);
|
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
router.post("/downgradeToBasic", async (req, res, next) => {
|
router.post("/downgradeToBasic", async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
if (!req.user) {
|
if (!req.user) {
|
||||||
return res.send({ ok: false, error: "user not logged in" });
|
return res.send({ ok: false, error: "user not logged in" });
|
||||||
}
|
}
|
||||||
const subscription = "subscription" in req.user ? req.user.subscription : undefined;
|
const subscription =
|
||||||
await downgradeUser(req.user.userId, subscription
|
"subscription" in req.user ? req.user.subscription : undefined;
|
||||||
? subscription.id
|
await downgradeUser(
|
||||||
: undefined);
|
req.user.userId,
|
||||||
|
subscription ? subscription.id : undefined
|
||||||
|
);
|
||||||
res.send({
|
res.send({
|
||||||
ok: true,
|
ok: true,
|
||||||
message: `account downgraded to basic${subscription ? " and subscription cancelled" : ""}`,
|
message: `account downgraded to basic${
|
||||||
|
subscription ? " and subscription cancelled" : ""
|
||||||
|
}`,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
next(e);
|
next(e);
|
||||||
|
@ -250,11 +305,11 @@ const authRouter = (passport: PassportStatic) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get("/password-reset", (req, res) => {
|
router.get("/password-reset", (req, res) => {
|
||||||
const email = req.query.email || ""
|
const email = req.query.email || "";
|
||||||
res.render("password-reset-request", { email, done: false });
|
res.render("password-reset-request", { email, done: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post("/password-reset", async(req, res, next) => {
|
router.post("/password-reset", async (req, res, next) => {
|
||||||
const page = "password-reset-request";
|
const page = "password-reset-request";
|
||||||
const email = req.body.email || "";
|
const email = req.body.email || "";
|
||||||
try {
|
try {
|
||||||
|
@ -275,10 +330,9 @@ const authRouter = (passport: PassportStatic) => {
|
||||||
}
|
}
|
||||||
const token = getURLToken();
|
const token = getURLToken();
|
||||||
const tokenHash = await getHash(token);
|
const tokenHash = await getHash(token);
|
||||||
const u = await updateLingdocsUser(
|
const u = await updateLingdocsUser(user.userId, {
|
||||||
user.userId,
|
passwordReset: { tokenHash, requestedOn: getTimestamp() },
|
||||||
{ passwordReset: { tokenHash, requestedOn: getTimestamp() }},
|
});
|
||||||
);
|
|
||||||
await sendPasswordResetEmail(u, token);
|
await sendPasswordResetEmail(u, token);
|
||||||
return res.render(page, { email, done: true });
|
return res.render(page, { email, done: true });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -311,14 +365,23 @@ const authRouter = (passport: PassportStatic) => {
|
||||||
return res.render(page, { ok: false, message: "not found" });
|
return res.render(page, { ok: false, message: "not found" });
|
||||||
}
|
}
|
||||||
const result = await compareToHash(token, user.passwordReset.tokenHash);
|
const result = await compareToHash(token, user.passwordReset.tokenHash);
|
||||||
if (!result) return res.render(page, { ok: false, user: null, message: "invalid token" });
|
if (!result)
|
||||||
|
return res.render(page, {
|
||||||
|
ok: false,
|
||||||
|
user: null,
|
||||||
|
message: "invalid token",
|
||||||
|
});
|
||||||
const passwordsMatch = password === passwordConfirmed;
|
const passwordsMatch = password === passwordConfirmed;
|
||||||
if (passwordsMatch) {
|
if (passwordsMatch) {
|
||||||
const hash = await getHash(password);
|
const hash = await getHash(password);
|
||||||
await updateLingdocsUser(user.userId, { password: hash });
|
await updateLingdocsUser(user.userId, { password: hash });
|
||||||
return res.render(page, { ok: true, user, message: "password reset" });
|
return res.render(page, { ok: true, user, message: "password reset" });
|
||||||
} else {
|
} else {
|
||||||
return res.render(page, { ok: false, user, message: "passwords don't match" });
|
return res.render(page, {
|
||||||
|
ok: false,
|
||||||
|
user,
|
||||||
|
message: "passwords don't match",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -328,33 +391,35 @@ const authRouter = (passport: PassportStatic) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
};
|
||||||
|
|
||||||
function getTestCompletionSummary(users: T.LingdocsUser[]) {
|
function getTestCompletionSummary(users: T.LingdocsUser[]) {
|
||||||
const tests: { id: string, passes: number, fails: number }[] = [];
|
const tests: { id: string; passes: number; fails: number }[] = [];
|
||||||
users.forEach(u => {
|
users.forEach((u) => {
|
||||||
const usersTests = removeDuplicateTests(u.tests)
|
const usersTests = removeDuplicateTests(u.tests);
|
||||||
usersTests.forEach(ut => {
|
usersTests.forEach((ut) => {
|
||||||
const ti = tests.findIndex(x => x.id === ut.id);
|
const ti = tests.findIndex((x) => x.id === ut.id);
|
||||||
if (ti === -1) {
|
if (ti === -1) {
|
||||||
tests.push({
|
tests.push({
|
||||||
id: ut.id,
|
id: ut.id,
|
||||||
...ut.done ? { passes: 1, fails: 0 } : { passes: 0, fails: 1 },
|
...(ut.done ? { passes: 1, fails: 0 } : { passes: 0, fails: 1 }),
|
||||||
});
|
});
|
||||||
}
|
} else tests[ti][ut.done ? "passes" : "fails"]++;
|
||||||
else tests[ti][ut.done ? "passes" : "fails"]++;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return tests;
|
return tests;
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeDuplicateTests(tests: T.TestResult[]): T.TestResult[] {
|
function removeDuplicateTests(tests: T.TestResult[]): T.TestResult[] {
|
||||||
return tests.reduceRight((acc, curr) => {
|
return tests.reduceRight(
|
||||||
const redundant = acc.filter(x => ((x.id === curr.id) && (x.done === curr.done)));
|
(acc, curr) => {
|
||||||
return redundant.length
|
const redundant = acc.filter(
|
||||||
? acc
|
(x) => x.id === curr.id && x.done === curr.done
|
||||||
: [...acc, curr];
|
);
|
||||||
}, [...tests]);
|
return redundant.length ? acc : [...acc, curr];
|
||||||
|
},
|
||||||
|
[...tests]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// function getTestCompletionSummary(users: T.LingdocsUser[]) {
|
// function getTestCompletionSummary(users: T.LingdocsUser[]) {
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<meta name="description" content="LingDocs Signin">
|
||||||
|
<title>Grammar Feedback · LingDocs</title>
|
||||||
|
<link href="/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.15.4/css/all.css" integrity="sha384-DyZ88mC6Up2uqS4h/KRgHuoeGwBcD4Ng9SiP4dIRy0EXTlnuz47vAwmeGwVChigm" crossorigin="anonymous">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1 class="my-4">LingDocs Grammar Feedback</h1>
|
||||||
|
<p><%= docs.length %> pieces of feedback</p>
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Date</th>
|
||||||
|
<th scope="col">User</th>
|
||||||
|
<th scope="col">Chapter</th>
|
||||||
|
<th scope="col">Feedback</th>
|
||||||
|
<th scope="col">Rating</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<% for(var i=0; i < docs.length; i++) { %>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<%= new Date(docs[i].feedback.ts).toDateString() %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<%= docs[i].feedback.user %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<%= docs[i].feedback.chapter %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<%= docs[i].feedback.feedback %>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<%= docs[i].feedback.rating %>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<% } %>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue