@coursebuilder/core
Version:
Core package for Course Builder
1 lines • 16 kB
Source Map (JSON)
{"version":3,"sources":["../../../src/lib/actions/create-magic-link.ts","../../../src/lib/send-server-email.ts","../../../src/lib/send-verification-request.ts"],"sourcesContent":["import { NodemailerConfig } from '@auth/core/providers/nodemailer'\n\nimport { InternalOptions, RequestInternal, ResponseInternal } from '../../types'\nimport { createVerificationUrl } from '../send-server-email'\nimport { Cookie } from '../utils/cookie'\n\nexport async function createMagicLink(\n\trequest: RequestInternal,\n\tcookies: Cookie[],\n\toptions: InternalOptions,\n): Promise<ResponseInternal> {\n\tif (!options.adapter) throw new Error('Adapter not found')\n\tif (request.headers?.['x-skill-secret'] !== process.env.SKILL_SECRET) {\n\t\treturn { status: 401, body: 'unauthorized' }\n\t}\n\n\tconst { email } = request.body || {}\n\n\tif (!email) {\n\t\treturn { status: 400, body: 'email is required' }\n\t}\n\n\tconst emailProvider = options.providers.find((p) => p.type === 'email')\n\n\tif (!emailProvider) {\n\t\treturn { status: 400, body: 'email provider not found' }\n\t}\n\n\tconst expiresIn = request.body?.expiresIn || request.query?.expiresIn\n\n\tlet expiresAt: Date | undefined = undefined\n\n\tif (expiresIn) {\n\t\tconst durationInMilliseconds = expiresIn * 1000\n\t\texpiresAt = new Date(Date.now() + durationInMilliseconds)\n\t}\n\n\ttry {\n\t\tconst verificationDetails = await createVerificationUrl({\n\t\t\temail,\n\t\t\temailProvider: emailProvider as NodemailerConfig,\n\t\t\tauthOptions: options.authConfig,\n\t\t\tadapter: options.adapter,\n\t\t\tbaseUrl: options.baseUrl,\n\t\t\texpiresAt,\n\t\t})\n\t\tif (!verificationDetails?.url) {\n\t\t\treturn {\n\t\t\t\tstatus: 500,\n\t\t\t\tbody: JSON.stringify({\n\t\t\t\t\terror: 'Could not create verification url',\n\t\t\t\t}),\n\t\t\t}\n\t\t}\n\t\treturn {\n\t\t\tstatus: 200,\n\t\t\tbody: JSON.stringify({\n\t\t\t\turl: verificationDetails.url,\n\t\t\t}),\n\t\t}\n\t} catch (e) {\n\t\tconsole.log('error', e)\n\t\treturn {\n\t\t\tstatus: 500,\n\t\t\tbody: JSON.stringify({ error: (e as Error).message }),\n\t\t}\n\t}\n}\n","import { createHash } from 'crypto'\nimport { AuthConfig } from '@auth/core'\nimport type { EmailConfig } from '@auth/core/src/providers'\nimport { Theme } from '@auth/core/types'\nimport { v4 } from 'uuid'\n\nimport { CourseBuilderAdapter } from '../adapters'\nimport type {\n\tHTMLEmailParams,\n\tMagicLinkEmailType,\n} from './send-verification-request'\nimport { sendVerificationRequest } from './send-verification-request'\n\nfunction hashToken(token: string, options: any) {\n\tconst { provider, secret } = options\n\treturn (\n\t\tcreateHash('sha256')\n\t\t\t// Prefer provider specific secret, but use default secret if none specified!\n\t\t\t.update(`${token}${provider.secret ?? secret}`)\n\t\t\t.digest('hex')\n\t)\n}\n\nexport async function createVerificationUrl({\n\temail,\n\temailProvider,\n\tadapter,\n\tcallbackUrl,\n\texpiresAt,\n\tauthOptions,\n\tbaseUrl,\n}: {\n\temail: string\n\tauthOptions: AuthConfig\n\temailProvider: EmailConfig\n\tadapter: CourseBuilderAdapter\n\tcallbackUrl?: string\n\tbaseUrl: string\n\texpiresAt?: Date\n}) {\n\tif (!emailProvider) return\n\n\tcallbackUrl = (callbackUrl || baseUrl) as string\n\n\tconst token = (await emailProvider.generateVerificationToken?.()) ?? v4()\n\n\tconst ONE_DAY_IN_SECONDS = 86400\n\tconst durationInMilliseconds =\n\t\t(emailProvider.maxAge ?? ONE_DAY_IN_SECONDS) * 1000\n\tconst expires = expiresAt || new Date(Date.now() + durationInMilliseconds)\n\n\tawait adapter.createVerificationToken?.({\n\t\tidentifier: email,\n\t\ttoken: hashToken(token, {\n\t\t\tprovider: emailProvider,\n\t\t\tsecret: authOptions.secret,\n\t\t}),\n\t\texpires,\n\t})\n\n\tconst params = new URLSearchParams({ callbackUrl, token, email })\n\tconst verificationUrl = `${baseUrl}/api/auth/callback/${emailProvider.id}?${params}`\n\n\treturn { url: verificationUrl, token, expires }\n}\n\nexport async function sendServerEmail({\n\temail,\n\tcallbackUrl,\n\temailProvider,\n\ttype = 'login',\n\thtml,\n\ttext,\n\texpiresAt,\n\tauthOptions,\n\tadapter,\n\tbaseUrl,\n\tmerchantChargeId,\n}: {\n\tauthOptions: AuthConfig\n\temail: string\n\tcallbackUrl: string\n\temailProvider?: EmailConfig\n\ttype?: MagicLinkEmailType\n\thtml?: (options: HTMLEmailParams, theme?: Theme) => Promise<string>\n\ttext?: (options: HTMLEmailParams, theme?: Theme) => Promise<string>\n\texpiresAt?: Date | null\n\tadapter: CourseBuilderAdapter\n\tbaseUrl: string\n\tmerchantChargeId?: string | null\n}) {\n\tif (!emailProvider) return\n\ttry {\n\t\tconst verificationDetails = await createVerificationUrl({\n\t\t\temail,\n\t\t\tauthOptions,\n\t\t\tcallbackUrl,\n\t\t\temailProvider,\n\t\t\texpiresAt: expiresAt || undefined,\n\t\t\tadapter,\n\t\t\tbaseUrl,\n\t\t})\n\n\t\tif (!verificationDetails) return\n\n\t\tconst { url, token, expires } = verificationDetails\n\n\t\tawait sendVerificationRequest(\n\t\t\t{\n\t\t\t\tidentifier: email,\n\t\t\t\turl,\n\t\t\t\ttheme: { colorScheme: 'auto' },\n\t\t\t\tprovider: emailProvider,\n\t\t\t\ttoken: token as string,\n\t\t\t\texpires,\n\t\t\t\ttype,\n\t\t\t\thtml,\n\t\t\t\ttext,\n\t\t\t\tmerchantChargeId,\n\t\t\t},\n\t\t\tadapter,\n\t\t)\n\t} catch (error: any) {\n\t\tconsole.error(error)\n\t\tthrow new Error('Unable to sendVerificationRequest')\n\t}\n}\n","import { Theme } from '@auth/core/types'\nimport { render } from '@react-email/components'\n\nimport { NewMemberEmail } from '@coursebuilder/email-templates/emails/new-member'\nimport { PostPurchaseLoginEmail } from '@coursebuilder/email-templates/emails/post-purchase-login'\n\nimport { CourseBuilderAdapter } from '../adapters'\n\nexport type MagicLinkEmailType =\n\t| 'login'\n\t| 'signup'\n\t| 'reset'\n\t| 'purchase'\n\t| 'upgrade'\n\t| 'transfer'\n\nexport type HTMLEmailParams = Record<'url' | 'host' | 'email', string> & {\n\texpires?: Date\n\tmerchantChargeId?: string | null\n}\n\nfunction isValidateEmailServerConfig(server: any) {\n\treturn Boolean(\n\t\tserver &&\n\t\t\tserver.host &&\n\t\t\tserver.port &&\n\t\t\tserver.auth?.user &&\n\t\t\tserver.auth?.pass,\n\t)\n}\n\nexport interface SendVerificationRequestParams {\n\tidentifier: string\n\tname?: string\n\turl: string\n\texpires: Date\n\tprovider: any\n\ttoken: string\n\ttheme?: Theme\n}\n\nexport const sendVerificationRequest = async (\n\tparams: SendVerificationRequestParams & {\n\t\ttype?: MagicLinkEmailType\n\t\tmerchantChargeId?: string | null\n\t\thtml?: (options: HTMLEmailParams, theme?: Theme) => Promise<string>\n\t\ttext?: (options: HTMLEmailParams, theme?: Theme) => Promise<string>\n\t},\n\tadapter: CourseBuilderAdapter,\n) => {\n\tconst {\n\t\tidentifier: email,\n\t\tname,\n\t\turl,\n\t\tprovider,\n\t\ttheme,\n\t\texpires,\n\t\tmerchantChargeId,\n\t\ttype = 'login',\n\t} = params\n\n\tconst { host } = new URL(url)\n\tconsole.log(\n\t\t`[sendVerificationRequest] Initiated. Type: ${type}, Email: ${email}, Host: ${host}${merchantChargeId ? `, MerchantChargeId: ${merchantChargeId}` : ''}`,\n\t)\n\n\tlet text = params.text || defaultText\n\tlet html = params.html || defaultHtml\n\n\tconst { server, from } = provider.options ? provider.options : provider\n\n\tconst { getUserByEmail, findOrCreateUser } = adapter\n\n\tlet subject\n\n\tswitch (type) {\n\t\tcase 'purchase':\n\t\t\tsubject = `Thank you for Purchasing ${\n\t\t\t\tprocess.env.NEXT_PUBLIC_PRODUCT_NAME ||\n\t\t\t\tprocess.env.NEXT_PUBLIC_SITE_TITLE\n\t\t\t} (${host})`\n\t\t\tbreak\n\t\tcase 'transfer':\n\t\t\tsubject = `Accept Your Seat for ${\n\t\t\t\tprocess.env.NEXT_PUBLIC_PRODUCT_NAME ||\n\t\t\t\tprocess.env.NEXT_PUBLIC_SITE_TITLE\n\t\t\t} (${host})`\n\t\t\tbreak\n\t\tcase 'signup':\n\t\t\tsubject = `Welcome to ${\n\t\t\t\tprocess.env.NEXT_PUBLIC_PRODUCT_NAME ||\n\t\t\t\tprocess.env.NEXT_PUBLIC_SITE_TITLE\n\t\t\t} (${host})`\n\t\t\thtml = signUpHtml\n\t\t\ttext = signUpText\n\t\t\tbreak\n\t\tdefault:\n\t\t\tsubject = `Log in to ${\n\t\t\t\tprocess.env.NEXT_PUBLIC_PRODUCT_NAME ||\n\t\t\t\tprocess.env.NEXT_PUBLIC_SITE_TITLE\n\t\t\t} (${host})`\n\t}\n\n\tconsole.log(\n\t\t`[sendVerificationRequest] Determined email subject: \"${subject}\"`,\n\t)\n\n\tlet user: any\n\ttry {\n\t\tuser =\n\t\t\tprocess.env.CREATE_USER_ON_LOGIN !== 'false'\n\t\t\t\t? await findOrCreateUser(email, name)\n\t\t\t\t: await getUserByEmail?.(email)\n\n\t\tif (!user) {\n\t\t\tconsole.warn(\n\t\t\t\t`[sendVerificationRequest] User not found and creation disabled/failed for email: ${email}. Aborting.`,\n\t\t\t)\n\t\t\treturn\n\t\t}\n\t\tconsole.log(\n\t\t\t`[sendVerificationRequest] User found or created for email: ${email}, ID: ${user.id || user?.user?.id || 'unknown'}`,\n\t\t)\n\t} catch (error: any) {\n\t\tconsole.error(\n\t\t\t`[sendVerificationRequest] Error during user lookup/creation for email: ${email}`,\n\t\t\terror,\n\t\t)\n\t\tthrow error\n\t}\n\n\tif (process.env.LOG_VERIFICATION_URL) {\n\t\tconsole.log(`\n👋 MAGIC LINK URL ******************\n`)\n\t\tconsole.log(url)\n\t\tconsole.log(`\n************************************\n`)\n\t}\n\n\tif (process.env.SKIP_EMAIL === 'true') {\n\t\tconsole.warn(\n\t\t\t`[sendVerificationRequest] 🚫 Email sending is disabled via SKIP_EMAIL.`,\n\t\t)\n\t\treturn\n\t}\n\n\tif (!process.env.POSTMARK_API_TOKEN && !process.env.POSTMARK_KEY) {\n\t\tconsole.error(\n\t\t\t'[sendVerificationRequest] 🚫 Missing Postmark API Key (POSTMARK_API_TOKEN or POSTMARK_KEY). Cannot send email.',\n\t\t)\n\t\tthrow new Error('Missing Postmark API Key')\n\t}\n\n\ttry {\n\t\tconst textBody = await text(\n\t\t\t{ url, host, email, expires, merchantChargeId },\n\t\t\ttheme,\n\t\t)\n\t\tconst htmlBody = await html(\n\t\t\t{ url, host, email, expires, merchantChargeId },\n\t\t\ttheme,\n\t\t)\n\n\t\tconsole.log(\n\t\t\t`[sendVerificationRequest] Attempting to send email via Postmark to ${email} from ${from}`,\n\t\t)\n\n\t\tconst res = await fetch('https://api.postmarkapp.com/email', {\n\t\t\tmethod: 'POST',\n\t\t\theaders: {\n\t\t\t\tAccept: 'application/json',\n\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t'X-Postmark-Server-Token': (process.env.POSTMARK_API_TOKEN ||\n\t\t\t\t\tprocess.env.POSTMARK_KEY) as string,\n\t\t\t},\n\t\t\tbody: JSON.stringify({\n\t\t\t\tFrom: from,\n\t\t\t\tTo: email,\n\t\t\t\tSubject: subject,\n\t\t\t\tTextBody: textBody,\n\t\t\t\tHtmlBody: htmlBody,\n\t\t\t\tMessageStream: 'outbound',\n\t\t\t}),\n\t\t})\n\n\t\tif (!res.ok) {\n\t\t\tconst errorBody = await res.json()\n\t\t\tconsole.error(\n\t\t\t\t`[sendVerificationRequest] Postmark error sending email to ${email}. Status: ${res.status}`,\n\t\t\t\terrorBody,\n\t\t\t)\n\t\t\tthrow new Error(\n\t\t\t\t`Postmark error: ${res.status} ${JSON.stringify(errorBody)}`,\n\t\t\t)\n\t\t}\n\n\t\tconsole.log(\n\t\t\t`[sendVerificationRequest] ✅ Email successfully sent to ${email} via Postmark. Status: ${res.status}`,\n\t\t)\n\t} catch (error: any) {\n\t\tconsole.error(\n\t\t\t`[sendVerificationRequest] 💥 Failed to send email to ${email}. Error:`,\n\t\t\terror,\n\t\t)\n\t\tthrow error\n\t}\n}\n\nfunction defaultHtml(\n\t{ url, host, email, merchantChargeId }: HTMLEmailParams,\n\ttheme?: Theme,\n) {\n\treturn render(\n\t\tPostPurchaseLoginEmail(\n\t\t\t{\n\t\t\t\turl,\n\t\t\t\thost,\n\t\t\t\temail,\n\t\t\t\tsiteName:\n\t\t\t\t\tprocess.env.NEXT_PUBLIC_PRODUCT_NAME ||\n\t\t\t\t\tprocess.env.NEXT_PUBLIC_SITE_TITLE ||\n\t\t\t\t\t'',\n\t\t\t\t...(merchantChargeId && {\n\t\t\t\t\tinvoiceUrl: `${process.env.COURSEBUILDER_URL}/invoices/${merchantChargeId}`,\n\t\t\t\t}),\n\t\t\t\tpreviewText:\n\t\t\t\t\tprocess.env.NEXT_PUBLIC_PRODUCT_NAME ||\n\t\t\t\t\tprocess.env.NEXT_PUBLIC_SITE_TITLE ||\n\t\t\t\t\t'login link',\n\t\t\t},\n\t\t\ttheme,\n\t\t),\n\t)\n}\n\n// Email Text body (fallback for email clients that don't render HTML, e.g. feature phones)\nasync function defaultText(\n\t{ url, host, email, merchantChargeId }: HTMLEmailParams,\n\ttheme?: Theme,\n) {\n\treturn await render(\n\t\tPostPurchaseLoginEmail(\n\t\t\t{\n\t\t\t\turl,\n\t\t\t\thost,\n\t\t\t\temail,\n\t\t\t\tsiteName:\n\t\t\t\t\tprocess.env.NEXT_PUBLIC_PRODUCT_NAME ||\n\t\t\t\t\tprocess.env.NEXT_PUBLIC_SITE_TITLE ||\n\t\t\t\t\t'',\n\t\t\t\t...(merchantChargeId && {\n\t\t\t\t\tinvoiceUrl: `${process.env.COURSEBUILDER_URL}/invoices/${merchantChargeId}`,\n\t\t\t\t}),\n\t\t\t\tpreviewText:\n\t\t\t\t\tprocess.env.NEXT_PUBLIC_PRODUCT_NAME ||\n\t\t\t\t\tprocess.env.NEXT_PUBLIC_SITE_TITLE ||\n\t\t\t\t\t'login link',\n\t\t\t},\n\t\t\ttheme,\n\t\t),\n\t\t{\n\t\t\tplainText: true,\n\t\t},\n\t)\n}\n\nasync function signUpHtml(\n\t{ url, host, email }: HTMLEmailParams,\n\ttheme?: Theme,\n) {\n\treturn await render(\n\t\tNewMemberEmail({\n\t\t\turl,\n\t\t\thost,\n\t\t\temail,\n\t\t\tsiteName:\n\t\t\t\tprocess.env.NEXT_PUBLIC_PRODUCT_NAME ||\n\t\t\t\tprocess.env.NEXT_PUBLIC_SITE_TITLE ||\n\t\t\t\t'',\n\t\t}),\n\t)\n}\n\n// Email Text body (fallback for email clients that don't render HTML, e.g. feature phones)\nasync function signUpText(\n\t{ url, host, email }: HTMLEmailParams,\n\ttheme?: Theme,\n) {\n\treturn await render(\n\t\tNewMemberEmail({\n\t\t\turl,\n\t\t\thost,\n\t\t\temail,\n\t\t\tsiteName:\n\t\t\t\tprocess.env.NEXT_PUBLIC_PRODUCT_NAME ||\n\t\t\t\tprocess.env.NEXT_PUBLIC_SITE_TITLE ||\n\t\t\t\t'',\n\t\t}),\n\t\t{\n\t\t\tplainText: true,\n\t\t},\n\t)\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAGA;;;;;;;ACHA,oBAA2B;AAI3B,kBAAmB;;;ACHnB,wBAAuB;AAEvB,wBAA+B;AAC/B,iCAAuC;;;ADSvC,SAASA,UAAUC,OAAeC,SAAY;AAC7C,QAAM,EAAEC,UAAUC,OAAM,IAAKF;AAC7B,aACCG,0BAAW,QAAA,EAETC,OAAO,GAAGL,KAAAA,GAAQE,SAASC,UAAUA,MAAAA,EAAQ,EAC7CG,OAAO,KAAA;AAEX;AARSP;AAUT,eAAsBQ,sBAAsB,EAC3CC,OACAC,eACAC,SACAC,aACAC,WACAC,aACAC,QAAO,GASP;AACA,MAAI,CAACL;AAAe;AAEpBE,gBAAeA,eAAeG;AAE9B,QAAMd,QAAS,MAAMS,cAAcM,4BAAyB,SAASC,gBAAAA;AAErE,QAAMC,qBAAqB;AAC3B,QAAMC,0BACJT,cAAcU,UAAUF,sBAAsB;AAChD,QAAMG,UAAUR,aAAa,IAAIS,KAAKA,KAAKC,IAAG,IAAKJ,sBAAAA;AAEnD,QAAMR,QAAQa,0BAA0B;IACvCC,YAAYhB;IACZR,OAAOD,UAAUC,OAAO;MACvBE,UAAUO;MACVN,QAAQU,YAAYV;IACrB,CAAA;IACAiB;EACD,CAAA;AAEA,QAAMK,SAAS,IAAIC,gBAAgB;IAAEf;IAAaX;IAAOQ;EAAM,CAAA;AAC/D,QAAMmB,kBAAkB,GAAGb,OAAAA,sBAA6BL,cAAcmB,EAAE,IAAIH,MAAAA;AAE5E,SAAO;IAAEI,KAAKF;IAAiB3B;IAAOoB;EAAQ;AAC/C;AAzCsBb;;;ADjBtB,eAAsBuB,gBACrBC,SACAC,SACAC,SAAwB;AAExB,MAAI,CAACA,QAAQC;AAAS,UAAM,IAAIC,MAAM,mBAAA;AACtC,MAAIJ,QAAQK,UAAU,gBAAA,MAAsBC,QAAQC,IAAIC,cAAc;AACrE,WAAO;MAAEC,QAAQ;MAAKC,MAAM;IAAe;EAC5C;AAEA,QAAM,EAAEC,MAAK,IAAKX,QAAQU,QAAQ,CAAC;AAEnC,MAAI,CAACC,OAAO;AACX,WAAO;MAAEF,QAAQ;MAAKC,MAAM;IAAoB;EACjD;AAEA,QAAME,gBAAgBV,QAAQW,UAAUC,KAAK,CAACC,MAAMA,EAAEC,SAAS,OAAA;AAE/D,MAAI,CAACJ,eAAe;AACnB,WAAO;MAAEH,QAAQ;MAAKC,MAAM;IAA2B;EACxD;AAEA,QAAMO,YAAYjB,QAAQU,MAAMO,aAAajB,QAAQkB,OAAOD;AAE5D,MAAIE,YAA8BC;AAElC,MAAIH,WAAW;AACd,UAAMI,yBAAyBJ,YAAY;AAC3CE,gBAAY,IAAIG,KAAKA,KAAKC,IAAG,IAAKF,sBAAAA;EACnC;AAEA,MAAI;AACH,UAAMG,sBAAsB,MAAMC,sBAAsB;MACvDd;MACAC;MACAc,aAAaxB,QAAQyB;MACrBxB,SAASD,QAAQC;MACjByB,SAAS1B,QAAQ0B;MACjBT;IACD,CAAA;AACA,QAAI,CAACK,qBAAqBK,KAAK;AAC9B,aAAO;QACNpB,QAAQ;QACRC,MAAMoB,KAAKC,UAAU;UACpBC,OAAO;QACR,CAAA;MACD;IACD;AACA,WAAO;MACNvB,QAAQ;MACRC,MAAMoB,KAAKC,UAAU;QACpBF,KAAKL,oBAAoBK;MAC1B,CAAA;IACD;EACD,SAASI,GAAG;AACXC,YAAQC,IAAI,SAASF,CAAAA;AACrB,WAAO;MACNxB,QAAQ;MACRC,MAAMoB,KAAKC,UAAU;QAAEC,OAAQC,EAAYG;MAAQ,CAAA;IACpD;EACD;AACD;AA7DsBrC;","names":["hashToken","token","options","provider","secret","createHash","update","digest","createVerificationUrl","email","emailProvider","adapter","callbackUrl","expiresAt","authOptions","baseUrl","generateVerificationToken","v4","ONE_DAY_IN_SECONDS","durationInMilliseconds","maxAge","expires","Date","now","createVerificationToken","identifier","params","URLSearchParams","verificationUrl","id","url","createMagicLink","request","cookies","options","adapter","Error","headers","process","env","SKILL_SECRET","status","body","email","emailProvider","providers","find","p","type","expiresIn","query","expiresAt","undefined","durationInMilliseconds","Date","now","verificationDetails","createVerificationUrl","authOptions","authConfig","baseUrl","url","JSON","stringify","error","e","console","log","message"]}