diff --git a/account/src/lib/couch-db.ts b/account/src/lib/couch-db.ts index ffd80e0..058e0e7 100644 --- a/account/src/lib/couch-db.ts +++ b/account/src/lib/couch-db.ts @@ -2,25 +2,26 @@ import Nano from "nano"; import { DocumentInsertResponse } from "nano"; import { getTimestamp } from "./time-utils"; import env from "./env-vars"; +import * as T from "../../../website/src/lib/account-types"; const nano = Nano(env.couchDbURL); const usersDb = nano.db.use("test-users"); -export function updateLastActive(user: LingdocsUser): LingdocsUser { +export function updateLastActive(user: T.LingdocsUser): T.LingdocsUser { return { ...user, lastActive: getTimestamp(), }; } -export function updateLastLogin(user: LingdocsUser): LingdocsUser { +export function updateLastLogin(user: T.LingdocsUser): T.LingdocsUser { return { ...user, 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; return { ...user, @@ -29,7 +30,7 @@ function processAPIResponse(user: LingdocsUser, response: DocumentInsertResponse }; } -export async function getLingdocsUser(field: "email" | "userId" | "githubId" | "googleId" | "twitterId", value: string): Promise { +export async function getLingdocsUser(field: "email" | "userId" | "githubId" | "googleId" | "twitterId", value: string): Promise { const user = await usersDb.find({ selector: field === "githubId" ? { github: { id: value }} @@ -42,10 +43,10 @@ export async function getLingdocsUser(field: "email" | "userId" | "githubId" | " if (!user.docs.length) { return undefined; } - return user.docs[0] as LingdocsUser; + return user.docs[0] as T.LingdocsUser; } -export async function insertLingdocsUser(user: LingdocsUser): Promise { +export async function insertLingdocsUser(user: T.LingdocsUser): Promise { const res = await usersDb.insert(user); const newUser = processAPIResponse(user, res); if (!newUser) { @@ -54,7 +55,7 @@ export async function insertLingdocsUser(user: LingdocsUser): Promise { +export async function deleteLingdocsUser(uuid: T.UUID): Promise { const user = await getLingdocsUser("userId", uuid); if (!user) return; // TODO: cleanup userdbs etc @@ -64,24 +65,24 @@ export async function deleteLingdocsUser(uuid: UUID): Promise { // 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 -export async function updateLingdocsUser(uuid: UUID, toUpdate: +export async function updateLingdocsUser(uuid: T.UUID, toUpdate: // TODO: OR USE REDUCER?? { name: string } | - { name?: string, email: string, emailVerified: Hash } | + { name?: string, email: string, emailVerified: T.Hash } | { email: string, emailVerified: true } | - { emailVerified: Hash } | + { emailVerified: T.Hash } | { emailVerified: true } | - { password: Hash } | - { google: GoogleProfile | undefined } | - { github: GitHubProfile | undefined } | - { twitter: TwitterProfile | undefined } | + { password: T.Hash } | + { google: T.GoogleProfile | undefined } | + { github: T.GitHubProfile | undefined } | + { twitter: T.TwitterProfile | undefined } | { passwordReset: { - tokenHash: Hash, - requestedOn: TimeStamp, + tokenHash: T.Hash, + requestedOn: T.TimeStamp, }, } -): Promise { +): Promise { const user = await getLingdocsUser("userId", uuid); if (!user) throw new Error("unable to update - user not found " + uuid); if ("password" in toUpdate) { diff --git a/account/src/lib/mail-utils.ts b/account/src/lib/mail-utils.ts index 5f5033c..09711fc 100644 --- a/account/src/lib/mail-utils.ts +++ b/account/src/lib/mail-utils.ts @@ -1,6 +1,7 @@ import nodemailer from "nodemailer"; import inProd from "./inProd"; import env from "./env-vars"; +import * as T from "../../../website/src/lib/account-types"; type Address = string | { name: string, address: string }; @@ -9,7 +10,7 @@ const from: Address = { address: "admin@lingdocs.com", }; -function getAddress(user: LingdocsUser): Address { +function getAddress(user: T.LingdocsUser): Address { // TODO: Guard against "" if (!user.name) return user.email || ""; return { @@ -40,7 +41,7 @@ async function sendEmail(to: Address, subject: string, text: string) { // TODO: MAKE THIS 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}, 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); } -export async function sendPasswordResetEmail(user: LingdocsUser, token: URLToken) { +export async function sendPasswordResetEmail(user: T.LingdocsUser, token: T.URLToken) { const content = `Hello ${user.name}, Please visit this link to reset your password: ${baseURL}/password-reset/${user.userId}/${token} diff --git a/account/src/lib/password-utils.ts b/account/src/lib/password-utils.ts index beac0b7..4611265 100644 --- a/account/src/lib/password-utils.ts +++ b/account/src/lib/password-utils.ts @@ -1,23 +1,24 @@ import { hash, compare } from "bcryptjs"; import { randomBytes } from "crypto"; import base64url from "base64url"; +import * as T from "../../../website/src/lib/account-types"; const tokenSize = 24; -export async function getHash(p: string): Promise { - return await hash(p, 10) as Hash; +export async function getHash(p: string): Promise { + 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 h = await getHash(token); return { token, hash: h }; } -export function getURLToken(): URLToken { - return base64url(randomBytes(tokenSize)) as URLToken; +export function getURLToken(): T.URLToken { + return base64url(randomBytes(tokenSize)) as T.URLToken; } -export function compareToHash(s: string, hash: Hash): Promise { +export function compareToHash(s: string, hash: T.Hash): Promise { return compare(s, hash); } diff --git a/account/src/lib/time-utils.ts b/account/src/lib/time-utils.ts index 0dbc6a9..c9647d3 100644 --- a/account/src/lib/time-utils.ts +++ b/account/src/lib/time-utils.ts @@ -1,3 +1,5 @@ -export function getTimestamp(): TimeStamp { - return Date.now() as TimeStamp; +import * as T from "../../../website/src/lib/account-types"; + +export function getTimestamp(): T.TimeStamp { + return Date.now() as T.TimeStamp; } \ No newline at end of file diff --git a/account/src/lib/user-utils.ts b/account/src/lib/user-utils.ts index 09f2cca..6b94d74 100644 --- a/account/src/lib/user-utils.ts +++ b/account/src/lib/user-utils.ts @@ -7,12 +7,13 @@ import { import { getTimestamp } from "../lib/time-utils"; import { sendVerificationEmail } from "../lib/mail-utils"; import { outsideProviders } from "../middleware/setup-passport"; +import * as T from "../../../website/src/lib/account-types"; -function getUUID(): UUID { - return uuidv4() as UUID; +function getUUID(): T.UUID { + return uuidv4() as T.UUID; } -export function canRemoveOneOutsideProvider(user: LingdocsUser): boolean { +export function canRemoveOneOutsideProvider(user: T.LingdocsUser): boolean { if (user.email && user.password) { return true; } @@ -20,7 +21,7 @@ export function canRemoveOneOutsideProvider(user: LingdocsUser): boolean { return providersPresent.length > 1; } -export function getVerifiedEmail({ emails }: ProviderProfile): string | false { +export function getVerifiedEmail({ emails }: T.ProviderProfile): string | false { return ( emails && emails.length @@ -29,7 +30,7 @@ export function getVerifiedEmail({ emails }: ProviderProfile): string | 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) { return { email: undefined, verified: false }; } @@ -49,20 +50,20 @@ export async function createNewUser(input: { passwordPlainText: string, } | { strategy: "github", - profile: GitHubProfile, + profile: T.GitHubProfile, } | { strategy: "google", - profile: GoogleProfile, + profile: T.GoogleProfile, } | { strategy: "twitter", - profile: TwitterProfile, -}): Promise { + profile: T.TwitterProfile, +}): Promise { const userId = getUUID(); const now = getTimestamp(); if (input.strategy === "local") { const email = await getEmailTokenAndHash(); const password = await getHash(input.passwordPlainText); - const newUser: LingdocsUser = { + const newUser: T.LingdocsUser = { _id: userId, userId, email: input.email, @@ -80,7 +81,7 @@ export async function createNewUser(input: { } // GitHub || Twitter if (input.strategy === "github" || input.strategy === "twitter") { - const newUser: LingdocsUser = { + const newUser: T.LingdocsUser = { _id: userId, userId, emailVerified: false, @@ -99,7 +100,7 @@ export async function createNewUser(input: { const { email, verified } = getEmailFromGoogleProfile(input.profile); if (email && !verified) { const em = await getEmailTokenAndHash(); - const newUser: LingdocsUser = { + const newUser: T.LingdocsUser = { _id: userId, userId, email, @@ -115,7 +116,7 @@ export async function createNewUser(input: { sendVerificationEmail(user, em.token); return user; } - const newUser: LingdocsUser = { + const newUser: T.LingdocsUser = { _id: userId, userId, email, @@ -129,4 +130,4 @@ export async function createNewUser(input: { } const user = await insertLingdocsUser(newUser); return user; -} \ No newline at end of file +} diff --git a/account/src/middleware/setup-passport.ts b/account/src/middleware/setup-passport.ts index 7e357ae..6a9cd77 100644 --- a/account/src/middleware/setup-passport.ts +++ b/account/src/middleware/setup-passport.ts @@ -16,6 +16,7 @@ import { getVerifiedEmail, } from "../lib/user-utils"; import env from "../lib/env-vars"; +import * as T from "../../../website/src/lib/account-types"; 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) { // not getting refresh token const { _json, _raw, ...profile } = profileRaw; - const ghProfile: GitHubProfile = { ...profile, accessToken }; + const ghProfile: T.GitHubProfile = { ...profile, accessToken }; try { if (req.isAuthenticated()) { if (!req.user) done(new Error("user lost")); @@ -142,7 +143,7 @@ function setupPassport(passport: PassportStatic) { cb(null, user.userId); }); - passport.deserializeUser(async (userId: UUID, cb) => { + passport.deserializeUser(async (userId: T.UUID, cb) => { try { const user = await getLingdocsUser("userId", userId); if (!user) { diff --git a/account/src/routers/api-router.ts b/account/src/routers/api-router.ts index f62af7e..ad4ccb5 100644 --- a/account/src/routers/api-router.ts +++ b/account/src/routers/api-router.ts @@ -5,15 +5,15 @@ import { } from "../lib/couch-db"; import { getHash, - getURLToken, compareToHash, getEmailTokenAndHash, } from "../lib/password-utils"; import { sendVerificationEmail, } 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); } @@ -24,7 +24,7 @@ apiRouter.use((req, res, next) => { if (req.isAuthenticated()) { 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); }); diff --git a/account/src/routers/auth-router.ts b/account/src/routers/auth-router.ts index dcba12d..d1ce8a4 100644 --- a/account/src/routers/auth-router.ts +++ b/account/src/routers/auth-router.ts @@ -21,6 +21,7 @@ import { } from "../lib/mail-utils"; import { outsideProviders } from "../middleware/setup-passport"; import inProd from "../lib/inProd"; +import * as T from "../../../website/src/lib/account-types"; const authRouter = (passport: PassportStatic) => { const router = Router(); @@ -71,7 +72,7 @@ const authRouter = (passport: PassportStatic) => { 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 (!user && info.message === "email not found") { return res.send({ ok: false, newSignup: true }); diff --git a/account/src/types.d.ts b/account/src/types.d.ts deleted file mode 100644 index 77a8325..0000000 --- a/account/src/types.d.ts +++ /dev/null @@ -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 = Omit; - -type GoogleProfile = WoutRJ & { refreshToken: string, accessToken: string }; -type GitHubProfile = WoutRJ & { accessToken: string }; -type TwitterProfile = WoutRJ & { 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; diff --git a/website/src/lib/account-types.ts b/website/src/lib/account-types.ts new file mode 100644 index 0000000..826096d --- /dev/null +++ b/website/src/lib/account-types.ts @@ -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 = Omit; + +export type GoogleProfile = WoutRJ & { refreshToken: string, accessToken: string }; +export type GitHubProfile = WoutRJ & { accessToken: string }; +export type TwitterProfile = WoutRJ & { 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;