move types

This commit is contained in:
lingdocs 2021-08-18 17:53:01 +04:00
parent a48d250564
commit e164964937
10 changed files with 93 additions and 85 deletions

View File

@ -2,25 +2,26 @@ import Nano from "nano";
import { DocumentInsertResponse } from "nano"; import { DocumentInsertResponse } from "nano";
import { getTimestamp } from "./time-utils"; import { getTimestamp } from "./time-utils";
import env from "./env-vars"; import env from "./env-vars";
import * as T from "../../../website/src/lib/account-types";
const nano = Nano(env.couchDbURL); const nano = Nano(env.couchDbURL);
const usersDb = nano.db.use("test-users"); const usersDb = nano.db.use("test-users");
export function updateLastActive(user: LingdocsUser): LingdocsUser { export function updateLastActive(user: T.LingdocsUser): T.LingdocsUser {
return { return {
...user, ...user,
lastActive: getTimestamp(), lastActive: getTimestamp(),
}; };
} }
export function updateLastLogin(user: LingdocsUser): LingdocsUser { export function updateLastLogin(user: T.LingdocsUser): T.LingdocsUser {
return { return {
...user, ...user,
lastLogin: getTimestamp(), lastLogin: getTimestamp(),
}; };
} }
function processAPIResponse(user: LingdocsUser, response: DocumentInsertResponse): 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,
@ -29,7 +30,7 @@ function processAPIResponse(user: LingdocsUser, response: DocumentInsertResponse
}; };
} }
export async function getLingdocsUser(field: "email" | "userId" | "githubId" | "googleId" | "twitterId", value: string): Promise<undefined | LingdocsUser> { export async function getLingdocsUser(field: "email" | "userId" | "githubId" | "googleId" | "twitterId", value: string): Promise<undefined | T.LingdocsUser> {
const user = await usersDb.find({ const user = await usersDb.find({
selector: field === "githubId" selector: field === "githubId"
? { github: { id: value }} ? { github: { id: value }}
@ -42,10 +43,10 @@ export async function getLingdocsUser(field: "email" | "userId" | "githubId" | "
if (!user.docs.length) { if (!user.docs.length) {
return undefined; return undefined;
} }
return user.docs[0] as LingdocsUser; return user.docs[0] as T.LingdocsUser;
} }
export async function insertLingdocsUser(user: LingdocsUser): Promise<LingdocsUser> { export async function insertLingdocsUser(user: T.LingdocsUser): Promise<T.LingdocsUser> {
const res = await usersDb.insert(user); const res = await usersDb.insert(user);
const newUser = processAPIResponse(user, res); const newUser = processAPIResponse(user, res);
if (!newUser) { if (!newUser) {
@ -54,7 +55,7 @@ export async function insertLingdocsUser(user: LingdocsUser): Promise<LingdocsUs
return newUser; return newUser;
} }
export async function deleteLingdocsUser(uuid: UUID): Promise<void> { export async function deleteLingdocsUser(uuid: T.UUID): Promise<void> {
const user = await getLingdocsUser("userId", uuid); const user = await getLingdocsUser("userId", uuid);
if (!user) return; if (!user) return;
// TODO: cleanup userdbs etc // TODO: cleanup userdbs etc
@ -64,24 +65,24 @@ export async function deleteLingdocsUser(uuid: UUID): Promise<void> {
// TODO: TO MAKE THIS SAFER, PASS IN JUST THE UPDATING FIELDS!! // TODO: TO MAKE THIS SAFER, PASS IN JUST THE UPDATING FIELDS!!
// TODO: take out the updated object - do just an ID, and then use the toUpdate safe thing // TODO: take out the updated object - do just an ID, and then use the toUpdate safe thing
export async function updateLingdocsUser(uuid: UUID, toUpdate: export async function updateLingdocsUser(uuid: T.UUID, toUpdate:
// TODO: OR USE REDUCER?? // TODO: OR USE REDUCER??
{ name: string } | { name: string } |
{ name?: string, email: string, emailVerified: Hash } | { name?: string, email: string, emailVerified: T.Hash } |
{ email: string, emailVerified: true } | { email: string, emailVerified: true } |
{ emailVerified: Hash } | { emailVerified: T.Hash } |
{ emailVerified: true } | { emailVerified: true } |
{ password: Hash } | { password: T.Hash } |
{ google: GoogleProfile | undefined } | { google: T.GoogleProfile | undefined } |
{ github: GitHubProfile | undefined } | { github: T.GitHubProfile | undefined } |
{ twitter: TwitterProfile | undefined } | { twitter: T.TwitterProfile | undefined } |
{ {
passwordReset: { passwordReset: {
tokenHash: Hash, tokenHash: T.Hash,
requestedOn: TimeStamp, requestedOn: T.TimeStamp,
}, },
} }
): Promise<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);
if ("password" in toUpdate) { if ("password" in toUpdate) {

View File

@ -1,6 +1,7 @@
import nodemailer from "nodemailer"; import nodemailer from "nodemailer";
import inProd from "./inProd"; import inProd from "./inProd";
import env from "./env-vars"; import env from "./env-vars";
import * as T from "../../../website/src/lib/account-types";
type Address = string | { name: string, address: string }; type Address = string | { name: string, address: string };
@ -9,7 +10,7 @@ const from: Address = {
address: "admin@lingdocs.com", address: "admin@lingdocs.com",
}; };
function getAddress(user: LingdocsUser): Address { function getAddress(user: T.LingdocsUser): Address {
// TODO: Guard against "" // TODO: Guard against ""
if (!user.name) return user.email || ""; if (!user.name) return user.email || "";
return { return {
@ -40,7 +41,7 @@ async function sendEmail(to: Address, subject: string, text: string) {
// TODO: MAKE THIS // TODO: MAKE THIS
const baseURL = inProd ? "https://account.lingdocs.com" : "http://localhost:4000"; const baseURL = inProd ? "https://account.lingdocs.com" : "http://localhost:4000";
export async function sendVerificationEmail(user: LingdocsUser, token: URLToken) { export async function sendVerificationEmail(user: T.LingdocsUser, token: T.URLToken) {
const content = `Hello ${user.name}, const content = `Hello ${user.name},
Please verify your email by visiting this link: ${baseURL}/email-verification/${user.userId}/${token} Please verify your email by visiting this link: ${baseURL}/email-verification/${user.userId}/${token}
@ -49,7 +50,7 @@ LingDocs Admin`;
await sendEmail(getAddress(user), "Please Verify Your E-mail", content); await sendEmail(getAddress(user), "Please Verify Your E-mail", content);
} }
export async function sendPasswordResetEmail(user: LingdocsUser, token: URLToken) { export async function sendPasswordResetEmail(user: T.LingdocsUser, token: T.URLToken) {
const content = `Hello ${user.name}, const content = `Hello ${user.name},
Please visit this link to reset your password: ${baseURL}/password-reset/${user.userId}/${token} Please visit this link to reset your password: ${baseURL}/password-reset/${user.userId}/${token}

View File

@ -1,23 +1,24 @@
import { hash, compare } from "bcryptjs"; import { hash, compare } from "bcryptjs";
import { randomBytes } from "crypto"; import { randomBytes } from "crypto";
import base64url from "base64url"; import base64url from "base64url";
import * as T from "../../../website/src/lib/account-types";
const tokenSize = 24; const tokenSize = 24;
export async function getHash(p: string): Promise<Hash> { export async function getHash(p: string): Promise<T.Hash> {
return await hash(p, 10) as Hash; return await hash(p, 10) as T.Hash;
} }
export async function getEmailTokenAndHash(): Promise<{ token: URLToken, hash: Hash }> { export async function getEmailTokenAndHash(): Promise<{ token: T.URLToken, hash: T.Hash }> {
const token = getURLToken(); const token = getURLToken();
const h = await getHash(token); const h = await getHash(token);
return { token, hash: h }; return { token, hash: h };
} }
export function getURLToken(): URLToken { export function getURLToken(): T.URLToken {
return base64url(randomBytes(tokenSize)) as URLToken; return base64url(randomBytes(tokenSize)) as T.URLToken;
} }
export function compareToHash(s: string, hash: Hash): Promise<boolean> { export function compareToHash(s: string, hash: T.Hash): Promise<boolean> {
return compare(s, hash); return compare(s, hash);
} }

View File

@ -1,3 +1,5 @@
export function getTimestamp(): TimeStamp { import * as T from "../../../website/src/lib/account-types";
return Date.now() as TimeStamp;
export function getTimestamp(): T.TimeStamp {
return Date.now() as T.TimeStamp;
} }

View File

@ -7,12 +7,13 @@ import {
import { getTimestamp } from "../lib/time-utils"; import { getTimestamp } from "../lib/time-utils";
import { sendVerificationEmail } from "../lib/mail-utils"; import { sendVerificationEmail } from "../lib/mail-utils";
import { outsideProviders } from "../middleware/setup-passport"; import { outsideProviders } from "../middleware/setup-passport";
import * as T from "../../../website/src/lib/account-types";
function getUUID(): UUID { function getUUID(): T.UUID {
return uuidv4() as UUID; return uuidv4() as T.UUID;
} }
export function canRemoveOneOutsideProvider(user: LingdocsUser): boolean { export function canRemoveOneOutsideProvider(user: T.LingdocsUser): boolean {
if (user.email && user.password) { if (user.email && user.password) {
return true; return true;
} }
@ -20,7 +21,7 @@ export function canRemoveOneOutsideProvider(user: LingdocsUser): boolean {
return providersPresent.length > 1; return providersPresent.length > 1;
} }
export function getVerifiedEmail({ emails }: ProviderProfile): string | false { export function getVerifiedEmail({ emails }: T.ProviderProfile): string | false {
return ( return (
emails emails
&& emails.length && emails.length
@ -29,7 +30,7 @@ export function getVerifiedEmail({ emails }: ProviderProfile): string | false {
) ? emails[0].value : false; ) ? emails[0].value : false;
} }
function getEmailFromGoogleProfile(profile: GoogleProfile): { email: string | undefined, verified: boolean } { function getEmailFromGoogleProfile(profile: T.GoogleProfile): { email: string | undefined, verified: boolean } {
if (!profile.emails || profile.emails.length === 0) { if (!profile.emails || profile.emails.length === 0) {
return { email: undefined, verified: false }; return { email: undefined, verified: false };
} }
@ -49,20 +50,20 @@ export async function createNewUser(input: {
passwordPlainText: string, passwordPlainText: string,
} | { } | {
strategy: "github", strategy: "github",
profile: GitHubProfile, profile: T.GitHubProfile,
} | { } | {
strategy: "google", strategy: "google",
profile: GoogleProfile, profile: T.GoogleProfile,
} | { } | {
strategy: "twitter", strategy: "twitter",
profile: TwitterProfile, profile: T.TwitterProfile,
}): Promise<LingdocsUser> { }): Promise<T.LingdocsUser> {
const userId = getUUID(); const userId = getUUID();
const now = getTimestamp(); const now = getTimestamp();
if (input.strategy === "local") { if (input.strategy === "local") {
const email = await getEmailTokenAndHash(); const email = await getEmailTokenAndHash();
const password = await getHash(input.passwordPlainText); const password = await getHash(input.passwordPlainText);
const newUser: LingdocsUser = { const newUser: T.LingdocsUser = {
_id: userId, _id: userId,
userId, userId,
email: input.email, email: input.email,
@ -80,7 +81,7 @@ export async function createNewUser(input: {
} }
// GitHub || Twitter // GitHub || Twitter
if (input.strategy === "github" || input.strategy === "twitter") { if (input.strategy === "github" || input.strategy === "twitter") {
const newUser: LingdocsUser = { const newUser: T.LingdocsUser = {
_id: userId, _id: userId,
userId, userId,
emailVerified: false, emailVerified: false,
@ -99,7 +100,7 @@ export async function createNewUser(input: {
const { email, verified } = getEmailFromGoogleProfile(input.profile); const { email, verified } = getEmailFromGoogleProfile(input.profile);
if (email && !verified) { if (email && !verified) {
const em = await getEmailTokenAndHash(); const em = await getEmailTokenAndHash();
const newUser: LingdocsUser = { const newUser: T.LingdocsUser = {
_id: userId, _id: userId,
userId, userId,
email, email,
@ -115,7 +116,7 @@ export async function createNewUser(input: {
sendVerificationEmail(user, em.token); sendVerificationEmail(user, em.token);
return user; return user;
} }
const newUser: LingdocsUser = { const newUser: T.LingdocsUser = {
_id: userId, _id: userId,
userId, userId,
email, email,
@ -129,4 +130,4 @@ export async function createNewUser(input: {
} }
const user = await insertLingdocsUser(newUser); const user = await insertLingdocsUser(newUser);
return user; return user;
} }

View File

@ -16,6 +16,7 @@ import {
getVerifiedEmail, getVerifiedEmail,
} from "../lib/user-utils"; } from "../lib/user-utils";
import env from "../lib/env-vars"; import env from "../lib/env-vars";
import * as T from "../../../website/src/lib/account-types";
export const outsideProviders: ("github" | "google" | "twitter")[] = ["github", "google", "twitter"]; export const outsideProviders: ("github" | "google" | "twitter")[] = ["github", "google", "twitter"];
@ -116,7 +117,7 @@ function setupPassport(passport: PassportStatic) {
async function(req: any, accessToken: any, refreshToken: any, profileRaw: any, done: any) { async function(req: any, accessToken: any, refreshToken: any, profileRaw: any, done: any) {
// not getting refresh token // not getting refresh token
const { _json, _raw, ...profile } = profileRaw; const { _json, _raw, ...profile } = profileRaw;
const ghProfile: GitHubProfile = { ...profile, accessToken }; const ghProfile: T.GitHubProfile = { ...profile, accessToken };
try { try {
if (req.isAuthenticated()) { if (req.isAuthenticated()) {
if (!req.user) done(new Error("user lost")); if (!req.user) done(new Error("user lost"));
@ -142,7 +143,7 @@ function setupPassport(passport: PassportStatic) {
cb(null, user.userId); cb(null, user.userId);
}); });
passport.deserializeUser(async (userId: UUID, cb) => { passport.deserializeUser(async (userId: T.UUID, cb) => {
try { try {
const user = await getLingdocsUser("userId", userId); const user = await getLingdocsUser("userId", userId);
if (!user) { if (!user) {

View File

@ -5,15 +5,15 @@ import {
} from "../lib/couch-db"; } from "../lib/couch-db";
import { import {
getHash, getHash,
getURLToken,
compareToHash, compareToHash,
getEmailTokenAndHash, getEmailTokenAndHash,
} from "../lib/password-utils"; } from "../lib/password-utils";
import { import {
sendVerificationEmail, sendVerificationEmail,
} from "../lib/mail-utils"; } from "../lib/mail-utils";
import * as T from "../../../website/src/lib/account-types";
function sendResponse(res: Response, payload: APIResponse) { function sendResponse(res: Response, payload: T.APIResponse) {
return res.send(payload); return res.send(payload);
} }
@ -24,7 +24,7 @@ apiRouter.use((req, res, next) => {
if (req.isAuthenticated()) { if (req.isAuthenticated()) {
return next(); return next();
} }
const r: APIResponse = { ok: false, error: "401 Unauthorized" }; const r: T.APIResponse = { ok: false, error: "401 Unauthorized" };
return res.status(401).send(r); return res.status(401).send(r);
}); });

View File

@ -21,6 +21,7 @@ import {
} from "../lib/mail-utils"; } from "../lib/mail-utils";
import { outsideProviders } from "../middleware/setup-passport"; import { outsideProviders } from "../middleware/setup-passport";
import inProd from "../lib/inProd"; import inProd from "../lib/inProd";
import * as T from "../../../website/src/lib/account-types";
const authRouter = (passport: PassportStatic) => { const authRouter = (passport: PassportStatic) => {
const router = Router(); const router = Router();
@ -71,7 +72,7 @@ const authRouter = (passport: PassportStatic) => {
return res.render("login", { recaptcha: "fail", inProd }); return res.render("login", { recaptcha: "fail", inProd });
} }
} }
passport.authenticate("local", (err, user: LingdocsUser | undefined, info) => { passport.authenticate("local", (err, user: T.LingdocsUser | undefined, info) => {
if (err) throw err; if (err) throw err;
if (!user && info.message === "email not found") { if (!user && info.message === "email not found") {
return res.send({ ok: false, newSignup: true }); return res.send({ ok: false, newSignup: true });

View File

@ -1,37 +0,0 @@
type Hash = string & { __brand: "Hashed String" };
type UUID = string & { __brand: "Random Unique UID" };
type TimeStamp = number & { __brand: "UNIX Timestamp in milliseconds" };
type UserDbPassword = string & { __brand: "password for an individual user couchdb" };
type URLToken = string & { __brand: "Base 64 URL Token" };
type EmailVerified = true | Hash | false;
type ActionComplete = { ok: true, message: string };
type ActionError = { ok: false, error: string };
type APIResponse = ActionComplete | ActionError | { ok: true, user: LingdocsUser };
type WoutRJ<T> = Omit<T, "_raw"|"_json">;
type GoogleProfile = WoutRJ<import("passport-google-oauth").Profile> & { refreshToken: string, accessToken: string };
type GitHubProfile = WoutRJ<import("passport-github2").Profile> & { accessToken: string };
type TwitterProfile = WoutRJ<import("passport-twitter").Profile> & { token: string, tokenSecret: string };
type ProviderProfile = GoogleProfile | GitHubProfile | TwitterProfile;
type UserLevel = "basic" | "student" | "editor";
// TODO: TYPE GUARDING SO WE NEVER HAVE A USER WITH NO Id or Password
type LingdocsUser = {
userId: UUID,
password?: Hash,
name: string,
email?: string,
emailVerified: EmailVerified,
github?: GitHubProfile,
google?: GoogleProfile,
twitter?: TwitterProfile,
passwordReset?: {
tokenHash: Hash,
requestedOn: TimeStamp,
},
tests: [],
lastLogin: TimeStamp,
lastActive: TimeStamp,
} & ({ level: "basic"} | { level: "student" | "editor", userDbPassword: UserDbPassword })
& import("nano").MaybeDocument;

View File

@ -0,0 +1,37 @@
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 URLToken = string & { __brand: "Base 64 URL Token" };
export type EmailVerified = true | Hash | false;
export type ActionComplete = { ok: true, message: string };
export type ActionError = { ok: false, error: string };
export type APIResponse = ActionComplete | ActionError | { ok: true, user: LingdocsUser };
export type WoutRJ<T> = Omit<T, "_raw"|"_json">;
export type GoogleProfile = WoutRJ<import("passport-google-oauth").Profile> & { refreshToken: string, accessToken: string };
export type GitHubProfile = WoutRJ<import("passport-github2").Profile> & { accessToken: string };
export type TwitterProfile = WoutRJ<import("passport-twitter").Profile> & { token: string, tokenSecret: string };
export type ProviderProfile = GoogleProfile | GitHubProfile | TwitterProfile;
export type UserLevel = "basic" | "student" | "editor";
// TODO: TYPE GUARDING SO WE NEVER HAVE A USER WITH NO Id or Password
export type LingdocsUser = {
userId: UUID,
password?: Hash,
name: string,
email?: string,
emailVerified: EmailVerified,
github?: GitHubProfile,
google?: GoogleProfile,
twitter?: TwitterProfile,
passwordReset?: {
tokenHash: Hash,
requestedOn: TimeStamp,
},
tests: [],
lastLogin: TimeStamp,
lastActive: TimeStamp,
} & ({ level: "basic"} | { level: "student" | "editor", userDbPassword: UserDbPassword })
& import("nano").MaybeDocument;