@main12/auth-login 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +218 -0
  2. package/dist/auth/application/hooks/useForgotPasswordFlow.d.ts +10 -0
  3. package/dist/auth/application/hooks/useForgotPasswordFlow.js +45 -0
  4. package/dist/auth/application/hooks/useLoginFlow.d.ts +30 -0
  5. package/dist/auth/application/hooks/useLoginFlow.js +105 -0
  6. package/dist/auth/application/hooks/useSetPasswordFlow.d.ts +18 -0
  7. package/dist/auth/application/hooks/useSetPasswordFlow.js +59 -0
  8. package/dist/auth/application/hooks/useVerifyOtpFlow.d.ts +18 -0
  9. package/dist/auth/application/hooks/useVerifyOtpFlow.js +84 -0
  10. package/dist/auth/application/services/authService.d.ts +26 -0
  11. package/dist/auth/application/services/authService.js +90 -0
  12. package/dist/auth/domain/otp.d.ts +25 -0
  13. package/dist/auth/domain/otp.js +41 -0
  14. package/dist/auth/domain/passwordRules.d.ts +12 -0
  15. package/dist/auth/domain/passwordRules.js +34 -0
  16. package/dist/auth/domain/types.d.ts +35 -0
  17. package/dist/auth/domain/types.js +2 -0
  18. package/dist/components/AuthLayout.d.ts +20 -0
  19. package/dist/components/AuthLayout.js +49 -0
  20. package/dist/components/PoweredBy.d.ts +10 -0
  21. package/dist/components/PoweredBy.js +50 -0
  22. package/dist/components/email/baseTemplate.d.ts +13 -0
  23. package/dist/components/email/baseTemplate.js +69 -0
  24. package/dist/components/email/constants.d.ts +26 -0
  25. package/dist/components/email/constants.js +30 -0
  26. package/dist/components/email/index.d.ts +7 -0
  27. package/dist/components/email/index.js +7 -0
  28. package/dist/components/email/templates/otp.d.ts +16 -0
  29. package/dist/components/email/templates/otp.js +38 -0
  30. package/dist/components/email/templates/passwordChanged.d.ts +15 -0
  31. package/dist/components/email/templates/passwordChanged.js +33 -0
  32. package/dist/components/email/templates/passwordReset.d.ts +15 -0
  33. package/dist/components/email/templates/passwordReset.js +36 -0
  34. package/dist/components/email/templates/welcome.d.ts +16 -0
  35. package/dist/components/email/templates/welcome.js +38 -0
  36. package/dist/components/email/translations.d.ts +45 -0
  37. package/dist/components/email/translations.js +88 -0
  38. package/dist/components/pages/ForgotPasswordPage.d.ts +5 -0
  39. package/dist/components/pages/ForgotPasswordPage.js +45 -0
  40. package/dist/components/pages/LoginPage.d.ts +11 -0
  41. package/dist/components/pages/LoginPage.js +222 -0
  42. package/dist/components/pages/SetPasswordPage.d.ts +5 -0
  43. package/dist/components/pages/SetPasswordPage.js +74 -0
  44. package/dist/components/pages/SignupPage.d.ts +10 -0
  45. package/dist/components/pages/SignupPage.js +129 -0
  46. package/dist/components/pages/VerifyOtpPage.d.ts +5 -0
  47. package/dist/components/pages/VerifyOtpPage.js +87 -0
  48. package/dist/components/ui/index.d.ts +57 -0
  49. package/dist/components/ui/index.js +121 -0
  50. package/dist/css.d.js +0 -0
  51. package/dist/endpoints/authEndpoints.d.ts +22 -0
  52. package/dist/endpoints/authEndpoints.js +422 -0
  53. package/dist/exports/client.d.ts +24 -0
  54. package/dist/exports/client.js +22 -0
  55. package/dist/exports/rsc.d.ts +6 -0
  56. package/dist/exports/rsc.js +5 -0
  57. package/dist/index.d.ts +12 -0
  58. package/dist/index.js +16 -0
  59. package/package.json +115 -0
@@ -0,0 +1,90 @@
1
+ const API_PREFIX = '/api/auth';
2
+ /**
3
+ * Check if a user exists and whether they have a password set.
4
+ * Used in the two-step login flow.
5
+ */ export async function checkEmail(email) {
6
+ const response = await fetch(`${API_PREFIX}/check-email`, {
7
+ method: 'POST',
8
+ headers: {
9
+ 'Content-Type': 'application/json'
10
+ },
11
+ body: JSON.stringify({
12
+ email: email.trim()
13
+ })
14
+ });
15
+ return response.json();
16
+ }
17
+ /**
18
+ * Send an OTP verification code to the user's email.
19
+ */ export async function sendOtp(email, purpose = 'login') {
20
+ const response = await fetch(`${API_PREFIX}/otp/send`, {
21
+ method: 'POST',
22
+ headers: {
23
+ 'Content-Type': 'application/json'
24
+ },
25
+ body: JSON.stringify({
26
+ email: email.trim(),
27
+ purpose
28
+ })
29
+ });
30
+ return response.json();
31
+ }
32
+ /**
33
+ * Verify an OTP code and receive an auth token.
34
+ */ export async function verifyOtp(email, otp) {
35
+ const response = await fetch(`${API_PREFIX}/otp/verify`, {
36
+ method: 'POST',
37
+ headers: {
38
+ 'Content-Type': 'application/json'
39
+ },
40
+ body: JSON.stringify({
41
+ email,
42
+ otp
43
+ })
44
+ });
45
+ return response.json();
46
+ }
47
+ /**
48
+ * Set a new user password (requires valid auth session).
49
+ */ export async function setUserPassword(password, confirmPassword) {
50
+ const response = await fetch(`${API_PREFIX}/set-password`, {
51
+ method: 'POST',
52
+ headers: {
53
+ 'Content-Type': 'application/json'
54
+ },
55
+ body: JSON.stringify({
56
+ password,
57
+ confirmPassword
58
+ })
59
+ });
60
+ return response.json();
61
+ }
62
+ /**
63
+ * Create a new user account.
64
+ */ export async function signup(name, email) {
65
+ const response = await fetch(`${API_PREFIX}/signup`, {
66
+ method: 'POST',
67
+ headers: {
68
+ 'Content-Type': 'application/json'
69
+ },
70
+ body: JSON.stringify({
71
+ name: name.trim(),
72
+ email: email.trim()
73
+ })
74
+ });
75
+ if (!response.ok) {
76
+ const error = await response.json();
77
+ throw new Error(error.message || 'Signup failed');
78
+ }
79
+ return response.json();
80
+ }
81
+ /**
82
+ * Redirect the browser to the Google OAuth login endpoint.
83
+ */ export function initiateGoogleLogin(redirectTo = '/') {
84
+ const params = new URLSearchParams();
85
+ if (redirectTo !== '/') {
86
+ params.set('redirect', redirectTo);
87
+ }
88
+ const qs = params.toString();
89
+ window.location.href = `/api/users/oauth/google${qs ? `?${qs}` : ''}`;
90
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Generates a cryptographically secure 6-digit OTP code.
3
+ */
4
+ export declare function generateOtp(): string;
5
+ /**
6
+ * Hashes an OTP code with SHA-256 for secure storage.
7
+ */
8
+ export declare function hashOtp(otp: string): string;
9
+ /**
10
+ * Constant-time comparison of an OTP against its hash.
11
+ * Uses crypto.timingSafeEqual to prevent timing attacks.
12
+ */
13
+ export declare function verifyOtp(otp: string, hashedOtp: string): boolean;
14
+ /**
15
+ * Returns expiry date (default: 10 minutes from now).
16
+ */
17
+ export declare function getOtpExpiry(minutes?: number): Date;
18
+ /**
19
+ * Checks if an OTP has expired.
20
+ */
21
+ export declare function isOtpExpired(expiryDate: Date | string): boolean;
22
+ /**
23
+ * Max OTP verification attempts before code is invalidated.
24
+ */
25
+ export declare function getMaxOtpAttempts(): number;
@@ -0,0 +1,41 @@
1
+ import crypto from 'crypto';
2
+ /**
3
+ * Generates a cryptographically secure 6-digit OTP code.
4
+ */ export function generateOtp() {
5
+ return crypto.randomInt(100000, 999999).toString();
6
+ }
7
+ /**
8
+ * Hashes an OTP code with SHA-256 for secure storage.
9
+ */ export function hashOtp(otp) {
10
+ return crypto.createHash('sha256').update(otp).digest('hex');
11
+ }
12
+ /**
13
+ * Constant-time comparison of an OTP against its hash.
14
+ * Uses crypto.timingSafeEqual to prevent timing attacks.
15
+ */ export function verifyOtp(otp, hashedOtp) {
16
+ const inputHash = hashOtp(otp);
17
+ if (inputHash.length !== hashedOtp.length) {
18
+ return false;
19
+ }
20
+ try {
21
+ return crypto.timingSafeEqual(Buffer.from(inputHash), Buffer.from(hashedOtp));
22
+ } catch {
23
+ return false;
24
+ }
25
+ }
26
+ /**
27
+ * Returns expiry date (default: 10 minutes from now).
28
+ */ export function getOtpExpiry(minutes = 10) {
29
+ return new Date(Date.now() + minutes * 60 * 1000);
30
+ }
31
+ /**
32
+ * Checks if an OTP has expired.
33
+ */ export function isOtpExpired(expiryDate) {
34
+ const expiry = typeof expiryDate === 'string' ? new Date(expiryDate) : expiryDate;
35
+ return new Date() > expiry;
36
+ }
37
+ /**
38
+ * Max OTP verification attempts before code is invalidated.
39
+ */ export function getMaxOtpAttempts() {
40
+ return parseInt(process.env.OTP_MAX_ATTEMPTS || '3', 10);
41
+ }
@@ -0,0 +1,12 @@
1
+ import type { PasswordStrengthResult } from './types.js';
2
+ export declare const MIN_PASSWORD_LENGTH = 8;
3
+ export declare const MIN_CRITERIA_COUNT = 3;
4
+ /**
5
+ * Evaluate password strength against standard criteria.
6
+ * Returns a score 0-5 and individual flag checks.
7
+ */
8
+ export declare function evaluatePasswordStrength(password: string): PasswordStrengthResult;
9
+ /**
10
+ * Quick check: does password meet minimum requirements?
11
+ */
12
+ export declare function isPasswordValid(password: string): boolean;
@@ -0,0 +1,34 @@
1
+ export const MIN_PASSWORD_LENGTH = 8;
2
+ export const MIN_CRITERIA_COUNT = 3;
3
+ /**
4
+ * Evaluate password strength against standard criteria.
5
+ * Returns a score 0-5 and individual flag checks.
6
+ */ export function evaluatePasswordStrength(password) {
7
+ const hasMinLength = password.length >= MIN_PASSWORD_LENGTH;
8
+ const hasUppercase = /[A-Z]/.test(password);
9
+ const hasLowercase = /[a-z]/.test(password);
10
+ const hasNumber = /[0-9]/.test(password);
11
+ const hasSpecial = /[^A-Za-z0-9]/.test(password);
12
+ const criteria = [
13
+ hasMinLength,
14
+ hasUppercase,
15
+ hasLowercase,
16
+ hasNumber,
17
+ hasSpecial
18
+ ];
19
+ const met = criteria.filter(Boolean).length;
20
+ return {
21
+ score: met,
22
+ hasMinLength,
23
+ hasUppercase,
24
+ hasLowercase,
25
+ hasNumber,
26
+ hasSpecial,
27
+ isValid: met >= MIN_CRITERIA_COUNT
28
+ };
29
+ }
30
+ /**
31
+ * Quick check: does password meet minimum requirements?
32
+ */ export function isPasswordValid(password) {
33
+ return evaluatePasswordStrength(password).isValid;
34
+ }
@@ -0,0 +1,35 @@
1
+ export type LoginStep = 'email' | 'password' | 'otp-prompt';
2
+ export type OTPPurpose = 'login' | 'signup' | 'password-reset';
3
+ export interface CheckEmailResponse {
4
+ exists: boolean;
5
+ hasPassword: boolean;
6
+ authProvider: string | null;
7
+ }
8
+ export interface SendOtpResponse {
9
+ success: boolean;
10
+ message?: string;
11
+ }
12
+ export interface VerifyOtpResponse {
13
+ success: boolean;
14
+ token?: string;
15
+ error?: string;
16
+ isNewUser?: boolean;
17
+ }
18
+ export interface SetPasswordResponse {
19
+ success: boolean;
20
+ message?: string;
21
+ }
22
+ export interface SignupResponse {
23
+ success: boolean;
24
+ message?: string;
25
+ userId?: string;
26
+ }
27
+ export interface PasswordStrengthResult {
28
+ score: number;
29
+ hasMinLength: boolean;
30
+ hasUppercase: boolean;
31
+ hasLowercase: boolean;
32
+ hasNumber: boolean;
33
+ hasSpecial: boolean;
34
+ isValid: boolean;
35
+ }
@@ -0,0 +1,2 @@
1
+ // Auth domain types shared across the plugin
2
+ export { };
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ export interface AuthLayoutConfig {
3
+ logo: React.ReactNode;
4
+ title?: string;
5
+ subtitle?: string;
6
+ poweredBy?: {
7
+ enabled?: boolean;
8
+ logoUrl?: string;
9
+ linkUrl?: string;
10
+ width?: number;
11
+ height?: number;
12
+ };
13
+ cardClassName?: string;
14
+ backgroundClass?: string;
15
+ }
16
+ export interface AuthLayoutProps extends AuthLayoutConfig {
17
+ children: React.ReactNode;
18
+ footer?: React.ReactNode;
19
+ }
20
+ export declare const AuthLayout: React.FC<AuthLayoutProps>;
@@ -0,0 +1,49 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import React from 'react';
4
+ import { Card, CardContent, CardFooter } from './ui/index.js';
5
+ import { PoweredBy } from './PoweredBy.js';
6
+ export const AuthLayout = ({ children, logo, title, subtitle, footer, poweredBy, cardClassName = '', backgroundClass = 'bg-white md:bg-[#191919]' })=>{
7
+ return /*#__PURE__*/ _jsx("main", {
8
+ className: `flex flex-col min-h-screen ${backgroundClass}`,
9
+ children: /*#__PURE__*/ _jsx("div", {
10
+ className: "min-h-screen flex items-center justify-center px-4 py-12",
11
+ children: /*#__PURE__*/ _jsxs("div", {
12
+ className: "w-full max-w-[400px] animate-[fadeIn_0.5s_ease-out]",
13
+ children: [
14
+ /*#__PURE__*/ _jsxs(Card, {
15
+ className: cardClassName,
16
+ children: [
17
+ (logo || title) && /*#__PURE__*/ _jsxs("div", {
18
+ className: "flex flex-col items-center gap-2 pt-6 pb-2 px-6",
19
+ children: [
20
+ logo && /*#__PURE__*/ _jsx("div", {
21
+ className: "flex justify-center mb-2",
22
+ children: logo
23
+ }),
24
+ title && /*#__PURE__*/ _jsx("h1", {
25
+ className: "text-xl font-semibold text-gray-900 text-center",
26
+ children: title
27
+ }),
28
+ subtitle && /*#__PURE__*/ _jsx("p", {
29
+ className: "text-gray-600 text-sm text-center",
30
+ children: subtitle
31
+ })
32
+ ]
33
+ }),
34
+ /*#__PURE__*/ _jsx(CardContent, {
35
+ children: children
36
+ }),
37
+ footer && /*#__PURE__*/ _jsx(CardFooter, {
38
+ children: footer
39
+ })
40
+ ]
41
+ }),
42
+ /*#__PURE__*/ _jsx(PoweredBy, {
43
+ ...poweredBy
44
+ })
45
+ ]
46
+ })
47
+ })
48
+ });
49
+ };
@@ -0,0 +1,10 @@
1
+ import React from 'react';
2
+ export interface PoweredByProps {
3
+ enabled?: boolean;
4
+ logoUrl?: string;
5
+ linkUrl?: string;
6
+ width?: number;
7
+ height?: number;
8
+ className?: string;
9
+ }
10
+ export declare const PoweredBy: React.FC<PoweredByProps>;
@@ -0,0 +1,50 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import React from 'react';
4
+ const Main12LogoSVG = ({ width = 28, height = 28 })=>/*#__PURE__*/ _jsxs("svg", {
5
+ width: width,
6
+ height: height,
7
+ viewBox: "0 0 100 100",
8
+ fill: "none",
9
+ xmlns: "http://www.w3.org/2000/svg",
10
+ children: [
11
+ /*#__PURE__*/ _jsx("rect", {
12
+ width: "100",
13
+ height: "100",
14
+ rx: "20",
15
+ fill: "#D5E855"
16
+ }),
17
+ /*#__PURE__*/ _jsx("text", {
18
+ x: "50",
19
+ y: "68",
20
+ textAnchor: "middle",
21
+ fontSize: "52",
22
+ fontWeight: "700",
23
+ fill: "#1d1d1f",
24
+ fontFamily: "system-ui, sans-serif",
25
+ children: "12"
26
+ })
27
+ ]
28
+ });
29
+ export const PoweredBy = ({ enabled = true, logoUrl, linkUrl = 'https://main12.com', width = 28, height = 28, className = '' })=>{
30
+ if (!enabled) return null;
31
+ return /*#__PURE__*/ _jsx("div", {
32
+ className: `mt-4 w-full flex justify-center transition-all duration-300 ${className}`,
33
+ children: /*#__PURE__*/ _jsx("a", {
34
+ href: linkUrl,
35
+ target: "_blank",
36
+ rel: "noopener noreferrer",
37
+ className: "flex items-center justify-center hover:opacity-80",
38
+ children: logoUrl ? /*#__PURE__*/ _jsx("img", {
39
+ src: logoUrl,
40
+ alt: "Powered by",
41
+ width: width,
42
+ height: height,
43
+ className: "object-contain"
44
+ }) : /*#__PURE__*/ _jsx(Main12LogoSVG, {
45
+ width: width,
46
+ height: height
47
+ })
48
+ })
49
+ });
50
+ };
@@ -0,0 +1,13 @@
1
+ import { type EmailColors, type SocialLink } from './constants.js';
2
+ import { type SupportedLanguage } from './translations.js';
3
+ export interface BaseTemplateOptions {
4
+ logoUrl?: string;
5
+ projectName?: string;
6
+ domain?: string;
7
+ colors?: Partial<EmailColors>;
8
+ preheader?: string;
9
+ language?: SupportedLanguage;
10
+ socialLinks?: SocialLink[];
11
+ userEmail?: string;
12
+ }
13
+ export declare function wrapInBaseTemplate(content: string, options?: BaseTemplateOptions): string;
@@ -0,0 +1,69 @@
1
+ import { DEFAULT_COLORS, SOCIAL_ICONS } from './constants.js';
2
+ import { getEmailTranslations } from './translations.js';
3
+ function getEmailHeader(logoUrl, projectName = '') {
4
+ const logoSrc = logoUrl || '';
5
+ const logoHtml = logoSrc ? `<img src="${logoSrc}" alt="${projectName}" width="200" style="max-width:200px;height:auto;display:block;margin:0 auto;">` : `<span style="font-size:28px;font-weight:bold;color:${DEFAULT_COLORS.primary};">${projectName}</span>`;
6
+ return `
7
+ <tr>
8
+ <td style="padding:24px 8px;text-align:center;">
9
+ ${logoHtml}
10
+ </td>
11
+ </tr>`;
12
+ }
13
+ function getSocialSection(socialLinks) {
14
+ if (!socialLinks?.length) return '';
15
+ const iconsHtml = socialLinks.map(({ platform, url })=>{
16
+ const icon = SOCIAL_ICONS[platform];
17
+ if (!icon) return '';
18
+ return `<a href="${url}" target="_blank" rel="noopener" style="display:inline-block;width:40px;height:40px;margin:0 6px;color:${DEFAULT_COLORS.textTertiary};text-decoration:none;">${icon}</a>`;
19
+ }).filter(Boolean).join('');
20
+ return `<tr><td style="padding:0 0 24px;text-align:center;">${iconsHtml}</td></tr>`;
21
+ }
22
+ function getEmailFooter(language = 'en', socialLinks, userEmail, contactEmail, projectName) {
23
+ const t = getEmailTranslations(language);
24
+ const year = new Date().getFullYear();
25
+ const emailLine = userEmail ? `<p style="margin:16px 0 0;font-size:14px;color:${DEFAULT_COLORS.textSecondary};line-height:1.5;">${t.footerEmailSentTo} <strong>${userEmail}</strong>. ${t.footerSecurityNotice}</p>` : '';
26
+ return `
27
+ <tr>
28
+ <td style="padding:16px;text-align:center;color:${DEFAULT_COLORS.textSecondary};font-size:14px;line-height:1.5;">
29
+ <p style="margin:0;">${t.footerContactMessage} <a href="mailto:${contactEmail || ''}" style="color:${DEFAULT_COLORS.text};text-decoration:none;font-weight:bold;">${contactEmail || ''}</a></p>
30
+ ${emailLine}
31
+ </td>
32
+ </tr>
33
+ ${getSocialSection(socialLinks || [])}
34
+ <tr>
35
+ <td style="padding:16px 0 32px;text-align:center;">
36
+ <p style="margin:0 0 8px;font-size:12px;color:${DEFAULT_COLORS.textTertiary};">© ${year} ${projectName || ''}. ${t.footerCopyright}</p>
37
+ <p style="margin:0;font-size:12px;color:${DEFAULT_COLORS.textTertiary};">${t.footerTagline}</p>
38
+ </td>
39
+ </tr>`;
40
+ }
41
+ export function wrapInBaseTemplate(content, options = {}) {
42
+ const { logoUrl, projectName = '', domain = '', preheader, language = 'en', socialLinks, userEmail } = options;
43
+ const contactEmail = options.colors ? undefined : undefined // will use env
44
+ ;
45
+ const preheaderHtml = preheader ? `<span style="display:none;font-size:1px;color:#fff;line-height:1px;max-height:0;max-width:0;opacity:0;overflow:hidden;">${preheader}</span>` : '';
46
+ return `<!DOCTYPE html>
47
+ <html lang="${language}">
48
+ <head>
49
+ <meta charset="utf-8">
50
+ <meta name="viewport" content="width=device-width,initial-scale=1.0">
51
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
52
+ <title>${projectName}</title>
53
+ </head>
54
+ <body style="margin:0;padding:0;font-family:-apple-system,BlinkMacSystemFont,'SF Pro Text','Segoe UI',Roboto,'Helvetica Neue',Arial,sans-serif;background-color:${DEFAULT_COLORS.background};-webkit-font-smoothing:antialiased;">
55
+ ${preheaderHtml}
56
+ <table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="background-color:${DEFAULT_COLORS.background};">
57
+ <tr>
58
+ <td style="padding:0 16px;">
59
+ <table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width:600px;margin:0 auto;">
60
+ ${getEmailHeader(logoUrl, projectName)}
61
+ ${content}
62
+ ${getEmailFooter(language, socialLinks, userEmail, undefined, projectName)}
63
+ </table>
64
+ </td>
65
+ </tr>
66
+ </table>
67
+ </body>
68
+ </html>`.trim();
69
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Email constants — configurable per project via plugin options.
3
+ * These are the defaults; overridden by the host project's options.
4
+ */
5
+ export interface EmailColors {
6
+ primary: string;
7
+ primaryText: string;
8
+ accent: string;
9
+ text: string;
10
+ textSecondary: string;
11
+ textTertiary: string;
12
+ background: string;
13
+ backgroundSecondary: string;
14
+ border: string;
15
+ }
16
+ export declare const DEFAULT_COLORS: EmailColors;
17
+ export type SocialPlatform = 'facebook' | 'twitter' | 'instagram' | 'linkedin' | 'youtube' | 'tiktok' | 'github' | 'discord';
18
+ export interface SocialLink {
19
+ platform: SocialPlatform;
20
+ url: string;
21
+ }
22
+ /** Minimal Material Design SVG icons for social platforms */
23
+ export declare const SOCIAL_ICONS: Record<SocialPlatform, string>;
24
+ /** Helper to build base URL from environment */
25
+ export declare function getBaseUrl(): string;
26
+ export declare function getSenderEmail(): string;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Email constants — configurable per project via plugin options.
3
+ * These are the defaults; overridden by the host project's options.
4
+ */ export const DEFAULT_COLORS = {
5
+ primary: '#D5E855',
6
+ primaryText: '#1d1d1f',
7
+ accent: '#0071e3',
8
+ text: '#1d1d1f',
9
+ textSecondary: '#6e6e73',
10
+ textTertiary: '#86868b',
11
+ background: '#ffffff',
12
+ backgroundSecondary: '#f5f5f7',
13
+ border: '#d2d2d7'
14
+ };
15
+ /** Minimal Material Design SVG icons for social platforms */ export const SOCIAL_ICONS = {
16
+ facebook: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M22 12c0-5.52-4.48-10-10-10S2 6.48 2 12c0 4.84 3.44 8.87 8 9.8V15H8v-3h2V9.5C10 7.57 11.57 6 13.5 6H16v3h-2c-.55 0-1 .45-1 1v2h3v3h-3v6.95c5.05-.5 9-4.76 9-9.95z"/></svg>',
17
+ twitter: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>',
18
+ instagram: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M7.8 2h8.4C19.4 2 22 4.6 22 7.8v8.4a5.8 5.8 0 0 1-5.8 5.8H7.8C4.6 22 2 19.4 2 16.2V7.8A5.8 5.8 0 0 1 7.8 2m-.2 2A3.6 3.6 0 0 0 4 7.6v8.8C4 18.39 5.61 20 7.6 20h8.8a3.6 3.6 0 0 0 3.6-3.6V7.6C20 5.61 18.39 4 16.4 4H7.6m9.65 1.5a1.25 1.25 0 0 1 1.25 1.25A1.25 1.25 0 0 1 17.25 8 1.25 1.25 0 0 1 16 6.75a1.25 1.25 0 0 1 1.25-1.25M12 7a5 5 0 0 1 5 5 5 5 0 0 1-5 5 5 5 0 0 1-5-5 5 5 0 0 1 5-5m0 2a3 3 0 0 0-3 3 3 3 0 0 0 3 3 3 3 0 0 0 3-3 3 3 0 0 0-3-3z"/></svg>',
19
+ linkedin: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M19 3a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h14m-.5 15.5v-5.3a3.26 3.26 0 0 0-3.26-3.26c-.85 0-1.84.52-2.32 1.3v-1.11h-2.79v8.37h2.79v-4.93c0-.77.62-1.4 1.39-1.4a1.4 1.4 0 0 1 1.4 1.4v4.93h2.79M6.88 8.56a1.68 1.68 0 0 0 1.68-1.68c0-.93-.75-1.69-1.68-1.69a1.69 1.69 0 0 0-1.69 1.69c0 .93.76 1.68 1.69 1.68m1.39 9.94v-8.37H5.5v8.37h2.77z"/></svg>',
20
+ youtube: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M10 15l5.19-3L10 9v6m11.56-7.83c.13.47.22 1.1.28 1.9.07.8.1 1.49.1 2.09L22 12c0 2.19-.16 3.8-.44 4.83-.25.9-.83 1.48-1.73 1.73-.47.13-1.33.22-2.65.28-1.3.07-2.49.1-3.59.1L12 19c-4.19 0-6.8-.16-7.83-.44-.9-.25-1.48-.83-1.73-1.73-.13-.47-.22-1.1-.28-1.9-.07-.8-.1-1.49-.1-2.09L2 12c0-2.19.16-3.8.44-4.83.25-.9.83-1.48 1.73-1.73.47-.13 1.33-.22 2.65-.28 1.3-.07 2.49-.1 3.59-.1L12 5c4.19 0 6.8.16 7.83.44.9.25 1.48.83 1.73 1.73z"/></svg>',
21
+ tiktok: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M16.6 5.82s.51.5 0 0A4.278 4.278 0 0 1 15.54 3h-3.09v12.4a2.592 2.592 0 0 1-2.59 2.5c-1.42 0-2.6-1.16-2.6-2.6 0-1.72 1.66-3.01 3.37-2.48V9.66c-3.45-.46-6.47 2.22-6.47 5.64 0 3.33 2.76 5.7 5.69 5.7 3.14 0 5.69-2.55 5.69-5.7V9.01a7.35 7.35 0 0 0 4.3 1.38V7.3s-1.88.09-3.24-1.48z"/></svg>',
22
+ github: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34-.46-1.16-1.11-1.47-1.11-1.47-.91-.62.07-.6.07-.6 1 .07 1.53 1.03 1.53 1.03.87 1.52 2.34 1.07 2.91.83.09-.65.35-1.09.63-1.34-2.22-.25-4.55-1.11-4.55-4.92 0-1.11.38-2 1.03-2.71-.1-.25-.45-1.29.1-2.64 0 0 .84-.27 2.75 1.02.79-.22 1.65-.33 2.5-.33.85 0 1.71.11 2.5.33 1.91-1.29 2.75-1.02 2.75-1.02.55 1.35.2 2.39.1 2.64.65.71 1.03 1.6 1.03 2.71 0 3.82-2.34 4.66-4.57 4.91.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2z"/></svg>',
23
+ discord: '<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M19.27 5.33C17.94 4.71 16.5 4.26 15 4a.09.09 0 0 0-.07.03c-.18.33-.39.76-.53 1.09a16.09 16.09 0 0 0-4.8 0c-.14-.34-.35-.76-.54-1.09-.01-.02-.04-.03-.07-.03-1.5.26-2.93.71-4.27 1.33-.01 0-.02.01-.03.02-2.72 4.07-3.47 8.03-3.1 11.95 0 .02.01.04.03.05 1.8 1.32 3.53 2.12 5.24 2.65.03.01.06 0 .07-.02.4-.55.76-1.13 1.07-1.74.02-.04 0-.08-.04-.09-.57-.22-1.11-.48-1.64-.78-.04-.02-.04-.08-.01-.11.11-.08.22-.17.33-.25.02-.02.05-.02.07-.01 3.44 1.57 7.15 1.57 10.55 0 .02-.01.05-.01.07.01.11.09.22.17.33.26.04.03.04.09-.01.11-.52.31-1.07.56-1.64.78-.04.01-.05.06-.04.09.32.61.68 1.19 1.07 1.74.03.01.06.02.09.01 1.72-.53 3.45-1.33 5.25-2.65.02-.01.03-.03.03-.05.44-4.53-.73-8.46-3.1-11.95-.01-.01-.02-.02-.04-.02zM8.52 14.91c-1.03 0-1.89-.95-1.89-2.12s.84-2.12 1.89-2.12c1.06 0 1.9.96 1.89 2.12 0 1.17-.84 2.12-1.89 2.12zm6.97 0c-1.03 0-1.89-.95-1.89-2.12s.84-2.12 1.89-2.12c1.06 0 1.9.96 1.89 2.12 0 1.17-.83 2.12-1.89 2.12z"/></svg>'
24
+ };
25
+ /** Helper to build base URL from environment */ export function getBaseUrl() {
26
+ return process.env.NEXT_PUBLIC_SERVER_URL || 'http://localhost:3000';
27
+ }
28
+ export function getSenderEmail() {
29
+ return process.env.BREVO_SENDER_EMAIL || process.env.MAILGUN_SENDER_EMAIL || 'noreply@example.com';
30
+ }
@@ -0,0 +1,7 @@
1
+ export { wrapInBaseTemplate, type BaseTemplateOptions } from './baseTemplate.js';
2
+ export { DEFAULT_COLORS, SOCIAL_ICONS, getBaseUrl, getSenderEmail, type EmailColors, type SocialLink, type SocialPlatform } from './constants.js';
3
+ export { getEmailTranslations, type SupportedLanguage, type EmailTranslations } from './translations.js';
4
+ export { generateOtpEmail, type OtpEmailParams, type OtpEmailResult } from './templates/otp.js';
5
+ export { generateWelcomeEmail, type WelcomeEmailParams, type WelcomeEmailResult } from './templates/welcome.js';
6
+ export { generatePasswordResetEmail, type PasswordResetEmailParams, type PasswordResetEmailResult } from './templates/passwordReset.js';
7
+ export { generatePasswordChangedEmail, type PasswordChangedEmailParams, type PasswordChangedEmailResult } from './templates/passwordChanged.js';
@@ -0,0 +1,7 @@
1
+ export { wrapInBaseTemplate } from './baseTemplate.js';
2
+ export { DEFAULT_COLORS, SOCIAL_ICONS, getBaseUrl, getSenderEmail } from './constants.js';
3
+ export { getEmailTranslations } from './translations.js';
4
+ export { generateOtpEmail } from './templates/otp.js';
5
+ export { generateWelcomeEmail } from './templates/welcome.js';
6
+ export { generatePasswordResetEmail } from './templates/passwordReset.js';
7
+ export { generatePasswordChangedEmail } from './templates/passwordChanged.js';
@@ -0,0 +1,16 @@
1
+ import { type SocialLink } from '../constants.js';
2
+ import { type BaseTemplateOptions } from '../baseTemplate.js';
3
+ import { type SupportedLanguage } from '../translations.js';
4
+ export interface OtpEmailParams {
5
+ userName: string;
6
+ otp: string;
7
+ purpose?: 'login' | 'password-reset';
8
+ language?: SupportedLanguage;
9
+ socialLinks?: SocialLink[];
10
+ baseOptions?: BaseTemplateOptions;
11
+ }
12
+ export interface OtpEmailResult {
13
+ subject: string;
14
+ html: string;
15
+ }
16
+ export declare function generateOtpEmail(params: OtpEmailParams): OtpEmailResult;
@@ -0,0 +1,38 @@
1
+ import { DEFAULT_COLORS } from '../constants.js';
2
+ import { wrapInBaseTemplate } from '../baseTemplate.js';
3
+ import { getEmailTranslations } from '../translations.js';
4
+ export function generateOtpEmail(params) {
5
+ const { userName, otp, purpose = 'login', language = 'en', socialLinks, baseOptions } = params;
6
+ const t = getEmailTranslations(language);
7
+ const purposeText = purpose === 'password-reset' ? t.otp.purposePasswordReset : t.otp.purposeLogin;
8
+ const subject = purpose === 'password-reset' ? `${t.otp.subjectPasswordReset}: ${otp}` : `${t.otp.subjectLogin}: ${otp}`;
9
+ const content = `
10
+ <tr>
11
+ <td style="text-align:center;padding-bottom:32px;">
12
+ <h1 style="margin:0 0 12px;font-size:28px;font-weight:600;color:${DEFAULT_COLORS.text};letter-spacing:-0.02em;">${t.greeting} ${userName}</h1>
13
+ <p style="margin:0;font-size:17px;color:${DEFAULT_COLORS.textSecondary};line-height:1.5;">${purposeText}</p>
14
+ </td>
15
+ </tr>
16
+ <tr>
17
+ <td style="text-align:center;padding-bottom:32px;">
18
+ <div style="display:inline-block;background-color:${DEFAULT_COLORS.backgroundSecondary};border-radius:12px;padding:20px 32px;">
19
+ <span style="font-size:32px;font-weight:600;letter-spacing:6px;color:${DEFAULT_COLORS.text};font-family:'SF Mono',SFMono-Regular,ui-monospace,Menlo,monospace;">${otp}</span>
20
+ </div>
21
+ </td>
22
+ </tr>
23
+ <tr>
24
+ <td style="text-align:center;">
25
+ <p style="margin:0 0 8px;font-size:14px;color:${DEFAULT_COLORS.textTertiary};">${t.otp.expiresIn} <span style="color:${DEFAULT_COLORS.textSecondary};">10 ${language === 'es' ? 'minutos' : 'minutes'}</span></p>
26
+ <p style="margin:0;font-size:13px;color:${DEFAULT_COLORS.textTertiary};">${t.otp.ignoreMessage}</p>
27
+ </td>
28
+ </tr>`;
29
+ return {
30
+ subject,
31
+ html: wrapInBaseTemplate(content, {
32
+ ...baseOptions,
33
+ preheader: `${t.otp.preheader}: ${otp}`,
34
+ language,
35
+ socialLinks
36
+ })
37
+ };
38
+ }
@@ -0,0 +1,15 @@
1
+ import { type SocialLink } from '../constants.js';
2
+ import { type BaseTemplateOptions } from '../baseTemplate.js';
3
+ import { type SupportedLanguage } from '../translations.js';
4
+ export interface PasswordChangedEmailParams {
5
+ userName: string;
6
+ loginUrl?: string;
7
+ language?: SupportedLanguage;
8
+ socialLinks?: SocialLink[];
9
+ baseOptions?: BaseTemplateOptions;
10
+ }
11
+ export interface PasswordChangedEmailResult {
12
+ subject: string;
13
+ html: string;
14
+ }
15
+ export declare function generatePasswordChangedEmail(params: PasswordChangedEmailParams): PasswordChangedEmailResult;
@@ -0,0 +1,33 @@
1
+ import { DEFAULT_COLORS, getBaseUrl } from '../constants.js';
2
+ import { wrapInBaseTemplate } from '../baseTemplate.js';
3
+ import { getEmailTranslations } from '../translations.js';
4
+ export function generatePasswordChangedEmail(params) {
5
+ const { userName, loginUrl = `${getBaseUrl()}/login`, language = 'en', socialLinks, baseOptions } = params;
6
+ const t = getEmailTranslations(language);
7
+ const content = `
8
+ <tr>
9
+ <td style="text-align:center;padding-bottom:32px;">
10
+ <h1 style="margin:0 0 12px;font-size:28px;font-weight:600;color:${DEFAULT_COLORS.text};">${t.passwordChanged.title}</h1>
11
+ <p style="margin:0;font-size:17px;color:${DEFAULT_COLORS.textSecondary};line-height:1.5;">${t.greeting} ${userName}, ${t.passwordChanged.message}</p>
12
+ </td>
13
+ </tr>
14
+ <tr>
15
+ <td style="text-align:center;padding:16px;">
16
+ <a href="${loginUrl}" style="display:inline-block;background-color:${DEFAULT_COLORS.primary};color:${DEFAULT_COLORS.primaryText};font-weight:600;text-decoration:none;padding:14px 40px;border-radius:980px;font-size:16px;">${t.passwordChanged.ctaButton}</a>
17
+ </td>
18
+ </tr>
19
+ <tr>
20
+ <td style="text-align:center;padding:24px 16px 8px;">
21
+ <p style="margin:0;font-size:13px;color:${DEFAULT_COLORS.textTertiary};">${t.passwordChanged.warningMessage}</p>
22
+ </td>
23
+ </tr>`;
24
+ return {
25
+ subject: t.passwordChanged.subject,
26
+ html: wrapInBaseTemplate(content, {
27
+ ...baseOptions,
28
+ preheader: t.passwordChanged.preheader,
29
+ language,
30
+ socialLinks
31
+ })
32
+ };
33
+ }