move types
This commit is contained in:
parent
a48d250564
commit
e164964937
|
@ -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<undefined | LingdocsUser> {
|
||||
export async function getLingdocsUser(field: "email" | "userId" | "githubId" | "googleId" | "twitterId", value: string): Promise<undefined | T.LingdocsUser> {
|
||||
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<LingdocsUser> {
|
||||
export async function insertLingdocsUser(user: T.LingdocsUser): Promise<T.LingdocsUser> {
|
||||
const res = await usersDb.insert(user);
|
||||
const newUser = processAPIResponse(user, res);
|
||||
if (!newUser) {
|
||||
|
@ -54,7 +55,7 @@ export async function insertLingdocsUser(user: LingdocsUser): Promise<LingdocsUs
|
|||
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);
|
||||
if (!user) return;
|
||||
// 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: 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<LingdocsUser> {
|
||||
): Promise<T.LingdocsUser> {
|
||||
const user = await getLingdocsUser("userId", uuid);
|
||||
if (!user) throw new Error("unable to update - user not found " + uuid);
|
||||
if ("password" in toUpdate) {
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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<Hash> {
|
||||
return await hash(p, 10) as Hash;
|
||||
export async function getHash(p: string): Promise<T.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 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<boolean> {
|
||||
export function compareToHash(s: string, hash: T.Hash): Promise<boolean> {
|
||||
return compare(s, hash);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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<LingdocsUser> {
|
||||
profile: T.TwitterProfile,
|
||||
}): Promise<T.LingdocsUser> {
|
||||
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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
Loading…
Reference in New Issue