store twilio stuff in TwilioAccount table and remodel session data

This commit is contained in:
m5r
2022-05-21 21:33:23 +02:00
parent 19a35bac92
commit 6684dcc0e5
23 changed files with 411 additions and 365 deletions

View File

@ -1,26 +1,31 @@
import { redirect, type Session } from "@remix-run/node";
import type { FormStrategyVerifyParams } from "remix-auth-form";
import SecurePassword from "secure-password";
import type { MembershipRole, Organization, PhoneNumber, User } from "@prisma/client";
import type { MembershipRole, Organization, PhoneNumber, TwilioAccount, User } from "@prisma/client";
import db from "./db.server";
import logger from "./logger.server";
import authenticator from "./authenticator.server";
import { AuthenticationError } from "./errors";
import { AuthenticationError, NotFoundError } from "./errors";
import { commitSession, destroySession, getSession } from "./session.server";
export type SessionOrganization = Pick<Organization, "id" | "twilioSubAccountSid" | "twilioAccountSid"> & {
role: MembershipRole;
type SessionTwilioAccount = Pick<
TwilioAccount,
"accountSid" | "accountAuthToken" | "subAccountSid" | "subAccountAuthToken" | "twimlAppSid"
>;
type SessionOrganization = Pick<Organization, "id"> & { role: MembershipRole };
type SessionPhoneNumber = Pick<PhoneNumber, "id" | "number">;
export type SessionUser = Pick<User, "id" | "role" | "email" | "fullName">;
export type SessionData = {
user: SessionUser;
organization: SessionOrganization;
phoneNumber: SessionPhoneNumber | null;
twilioAccount: SessionTwilioAccount | null;
};
export type SessionPhoneNumber = Pick<PhoneNumber, "id" | "number">;
export type SessionUser = Omit<User, "hashedPassword"> & {
organizations: SessionOrganization[];
};
export type SessionData = SessionUser & { currentOrganization: SessionOrganization; currentPhoneNumber: SessionPhoneNumber };
const SP = new SecurePassword();
export async function login({ form }: FormStrategyVerifyParams): Promise<SessionUser> {
export async function login({ form }: FormStrategyVerifyParams): Promise<SessionData> {
const email = form.get("email");
const password = form.get("password");
const isEmailValid = typeof email === "string" && email.length > 0;
@ -36,21 +41,8 @@ export async function login({ form }: FormStrategyVerifyParams): Promise<Session
throw new AuthenticationError("Password is required");
}
const user = await db.user.findUnique({
where: { email: email.toLowerCase() },
include: {
memberships: {
select: {
organization: {
select: { id: true, twilioSubAccountSid: true, twilioAccountSid: true },
},
role: true,
},
},
},
});
const user = await db.user.findUnique({ where: { email: email.toLowerCase() } });
if (!user || !user.hashedPassword) {
logger.warn(`User with email=${email.toLowerCase()} not found`);
throw new AuthenticationError("Incorrect password");
}
@ -67,16 +59,15 @@ export async function login({ form }: FormStrategyVerifyParams): Promise<Session
throw new AuthenticationError("Incorrect password");
}
const { hashedPassword, memberships, ...rest } = user;
const organizations = memberships.map((membership) => ({
...membership.organization,
role: membership.role,
}));
try {
return await buildSessionData(user.id);
} catch (error: any) {
if (error instanceof AuthenticationError) {
throw error;
}
return {
...rest,
organizations,
};
throw new AuthenticationError("Incorrect password");
}
}
export async function verifyPassword(hashedPassword: string, password: string) {
@ -114,9 +105,10 @@ export async function authenticate({
method: "post",
headers: request.headers,
});
const user = await authenticator.authenticate("email-password", signInRequest, { failureRedirect });
const sessionData = await authenticator.authenticate("email-password", signInRequest, { failureRedirect });
console.log("sessionKey", authenticator.sessionKey);
const session = await getSession(request);
session.set(authenticator.sessionKey, user);
session.set(authenticator.sessionKey, sessionData);
const redirectTo = successRedirect ?? "/messages";
return redirect(redirectTo, {
headers: { "Set-Cookie": await commitSession(session) },
@ -161,23 +153,50 @@ function buildRedirectTo(url: URL) {
}
export async function refreshSessionData(request: Request) {
const { id } = await requireLoggedIn(request);
const {
user: { id },
} = await requireLoggedIn(request);
const user = await db.user.findUnique({ where: { id } });
if (!user || !user.hashedPassword) {
logger.warn(`User with id=${id} not found`);
throw new AuthenticationError("Could not refresh session, user does not exist");
}
const sessionData = await buildSessionData(id);
const session = await getSession(request);
session.set(authenticator.sessionKey, sessionData);
return { session, sessionData: sessionData };
}
async function buildSessionData(id: string): Promise<SessionData> {
const user = await db.user.findUnique({
where: { id },
include: {
memberships: {
select: {
organization: {
select: { id: true, twilioSubAccountSid: true, twilioAccountSid: true },
select: {
id: true,
twilioAccount: {
select: {
accountSid: true,
accountAuthToken: true,
subAccountSid: true,
subAccountAuthToken: true,
twimlAppSid: true,
},
},
},
},
role: true,
},
},
},
});
if (!user || !user.hashedPassword) {
if (!user) {
logger.warn(`User with id=${id} not found`);
throw new AuthenticationError("Could not refresh session, user does not exist");
throw new NotFoundError(`User with id=${id} not found`);
}
const { hashedPassword, memberships, ...rest } = user;
@ -185,12 +204,14 @@ export async function refreshSessionData(request: Request) {
...membership.organization,
role: membership.role,
}));
const sessionUser: SessionUser = {
...rest,
organizations,
const { twilioAccount, ...organization } = organizations[0];
const phoneNumber = await db.phoneNumber.findUnique({
where: { organizationId_isCurrent: { organizationId: organization.id, isCurrent: true } },
});
return {
user: rest,
organization,
phoneNumber,
twilioAccount,
};
const session = await getSession(request);
session.set(authenticator.sessionKey, sessionUser);
return { session, user: sessionUser };
}

View File

@ -2,9 +2,9 @@ import { Authenticator } from "remix-auth";
import { FormStrategy } from "remix-auth-form";
import { sessionStorage } from "./session.server";
import { type SessionUser, login } from "./auth.server";
import { type SessionData, login } from "./auth.server";
const authenticator = new Authenticator<SessionUser>(sessionStorage);
const authenticator = new Authenticator<SessionData>(sessionStorage);
authenticator.use(new FormStrategy(login), "email-password");

View File

@ -3,6 +3,8 @@ import { type Session, type SessionIdStorageStrategy, createSessionStorage } fro
import serverConfig from "~/config/config.server";
import db from "./db.server";
import logger from "./logger.server";
import authenticator from "~/utils/authenticator.server";
import type { SessionData } from "~/utils/auth.server";
const SECOND = 1;
const MINUTE = 60 * SECOND;
@ -32,8 +34,9 @@ function createDatabaseSessionStorage({ cookie }: Pick<SessionIdStorageStrategy,
cookie,
async createData(sessionData, expiresAt) {
let user;
if (sessionData.user) {
user = { connect: { id: sessionData.user.id } };
const sessionAuthData: SessionData = sessionData[authenticator.sessionKey];
if (sessionAuthData) {
user = { connect: { id: sessionAuthData.user.id } };
}
const { id } = await db.session.create({
data: {

View File

@ -1,22 +1,40 @@
import twilio from "twilio";
import type { MessageInstance } from "twilio/lib/rest/api/v2010/account/message";
import type { CallInstance } from "twilio/lib/rest/api/v2010/account/call";
import { type Organization, CallStatus, Direction, MessageStatus } from "@prisma/client";
import { type TwilioAccount, CallStatus, Direction, MessageStatus } from "@prisma/client";
import serverConfig from "~/config/config.server";
type MinimalOrganization = Pick<Organization, "twilioSubAccountSid" | "twilioAccountSid">;
export default function getTwilioClient({ twilioAccountSid, twilioSubAccountSid }: MinimalOrganization): twilio.Twilio {
if (!twilioSubAccountSid || !twilioAccountSid) {
export default function getTwilioClient({
accountSid,
subAccountSid,
subAccountAuthToken,
}: Pick<TwilioAccount, "accountSid" | "subAccountSid"> &
Partial<Pick<TwilioAccount, "subAccountAuthToken">>): twilio.Twilio {
if (!subAccountSid || !accountSid) {
throw new Error("unreachable");
}
return twilio(twilioSubAccountSid, serverConfig.twilio.authToken, {
accountSid: twilioAccountSid,
return twilio(subAccountSid, serverConfig.twilio.authToken, {
accountSid,
});
}
export const smsUrl = `https://${serverConfig.app.baseUrl}/webhook/message`;
export const voiceUrl = `https://${serverConfig.app.baseUrl}/webhook/call`;
export function getTwiMLName() {
switch (serverConfig.app.baseUrl) {
case "local.shellphone.app":
return "Shellphone LOCAL";
case "dev.shellphone.app":
return "Shellphone DEV";
case "www.shellphone.app":
return "Shellphone";
}
}
export function translateMessageStatus(status: MessageInstance["status"]): MessageStatus {
switch (status) {
case "accepted":