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 { 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) {

View File

@ -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}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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,
@ -129,4 +130,4 @@ export async function createNewUser(input: {
}
const user = await insertLingdocsUser(newUser);
return user;
}
}

View File

@ -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) {

View File

@ -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);
});

View File

@ -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 });

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;