mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-06-03 20:13:00 +00:00
feat(core): consolidate auth service into backend and implement stripe billing
This commit performs a major architectural refactor by migrating the standalone `auth-service` into the main `backend` application, enabling a unified codebase and simplified deployment. It also introduces comprehensive Stripe billing support and a new administrative dashboard.
Key changes:
- **Architecture**: Deleted `apps/auth-service` and integrated its functionality (JWT, magic links, OAuth, user management) into `apps/backend`.
- **Billing**: Added Stripe integration to `backend`, supporting both monthly and yearly subscription cycles with automatic plan entitlement enforcement (e.g., location limits).
- **Admin Dashboard**: Implemented a new administrative service and API endpoints to manage tenants, users, and view platform-wide statistics.
- **Frontend**:
- Added a new pricing page with monthly/yearly toggle and comparison table.
- Integrated Stripe and Sentry for payments and error tracking.
- Improved dashboard UX/UI and added i18n support for new features.
- Enhanced the public booking flow with better validation and contact form integration.
- **Database**: Added migrations for users, magic links, password resets, OAuth states, admin audit logs, and refresh tokens.
- **DevOps**: Updated environment configurations for Railway and Vercel, and streamlined the project's `package.json` scripts.
This commit is contained in:
@@ -10,40 +10,146 @@ import (
|
||||
type EmailType string
|
||||
|
||||
const (
|
||||
EmailTypeConfirmation EmailType = "confirmation"
|
||||
EmailTypeConfirmation EmailType = "confirmation"
|
||||
EmailTypeReminder EmailType = "reminder"
|
||||
EmailTypeReschedule EmailType = "reschedule"
|
||||
EmailTypeCancellation EmailType = "cancellation"
|
||||
EmailTypeBusinessNotify EmailType = "business_notify"
|
||||
EmailTypeUsageWarning EmailType = "usage_warning"
|
||||
EmailTypeTrialEnding EmailType = "trial_ending"
|
||||
)
|
||||
|
||||
type BookingEmailData struct {
|
||||
Type EmailType
|
||||
TenantName string
|
||||
TenantSlug string
|
||||
BusinessEmail string
|
||||
BusinessPhone string
|
||||
BusinessAddress string
|
||||
BrandColor string
|
||||
CustomerName string
|
||||
CustomerEmail string
|
||||
Service string
|
||||
Location string
|
||||
Reference string
|
||||
StartsAt time.Time
|
||||
EndsAt time.Time
|
||||
Timezone string
|
||||
Locale string
|
||||
Notes string
|
||||
ManagementURL string
|
||||
AddToCalendarURL string
|
||||
}
|
||||
|
||||
type UsageNotificationData struct {
|
||||
Type EmailType
|
||||
TenantName string
|
||||
TenantSlug string
|
||||
BusinessEmail string
|
||||
BusinessPhone string
|
||||
BusinessAddress string
|
||||
BrandColor string
|
||||
CustomerName string
|
||||
CustomerEmail string
|
||||
Service string
|
||||
Location string
|
||||
Reference string
|
||||
StartsAt time.Time
|
||||
EndsAt time.Time
|
||||
Timezone string
|
||||
AdminEmail string
|
||||
Locale string
|
||||
Notes string
|
||||
ManagementURL string
|
||||
AddToCalendarURL string
|
||||
PlanCode string
|
||||
LocationCount int
|
||||
LocationLimit int
|
||||
UsagePercent int
|
||||
UpgradeURL string
|
||||
DashboardURL string
|
||||
}
|
||||
|
||||
func RenderUsageNotificationEmail(data UsageNotificationData) EmailMessage {
|
||||
subject := renderUsageSubject(data)
|
||||
htmlBody := renderUsageHTML(data)
|
||||
textBody := renderUsageText(data)
|
||||
|
||||
return EmailMessage{
|
||||
From: data.BusinessEmail,
|
||||
To: data.AdminEmail,
|
||||
Subject: subject,
|
||||
Text: textBody,
|
||||
HTML: htmlBody,
|
||||
}
|
||||
}
|
||||
|
||||
func renderUsageSubject(data UsageNotificationData) string {
|
||||
if data.Locale == "cs" {
|
||||
switch data.Type {
|
||||
case EmailTypeUsageWarning:
|
||||
return "⚠️ Blížíte se limitu lokací - Upgrade na vyšší plán"
|
||||
case EmailTypeTrialEnding:
|
||||
return "⏰ Vaše zkušební období končí - Pokračujte s Bookra"
|
||||
}
|
||||
}
|
||||
switch data.Type {
|
||||
case EmailTypeUsageWarning:
|
||||
return "⚠️ You're nearing your location limit - Upgrade your plan"
|
||||
case EmailTypeTrialEnding:
|
||||
return "⏰ Your trial period is ending - Continue with Bookra"
|
||||
}
|
||||
return "Bookra notification"
|
||||
}
|
||||
|
||||
func renderUsageHTML(data UsageNotificationData) string {
|
||||
cs := data.Locale == "cs"
|
||||
upgradeBtn := `<a href="` + data.UpgradeURL + `" style="display:inline-block;background:#4f46e5;color:#fff;padding:12px 24px;text-decoration:none;border-radius:8px;font-weight:600;margin:8px 0;">` + map[bool]string{true: "Upgradeovat", false: "Upgrade"}[cs] + `</a>`
|
||||
dashboardBtn := `<a href="` + data.DashboardURL + `" style="display:inline-block;background:#f3f4f6;color:#374151;padding:12px 24px;text-decoration:none;border-radius:8px;font-weight:600;margin:8px 0;">` + map[bool]string{true: "Otevřít dashboard", false: "Open dashboard"}[cs] + `</a>`
|
||||
|
||||
if data.Type == EmailTypeUsageWarning {
|
||||
var msg string
|
||||
if cs {
|
||||
msg = fmt.Sprintf("Váš plán %s umožňuje pouze %d lokací. Aktuálně používáte %d (%d%%).", data.PlanCode, data.LocationLimit, data.LocationCount, data.UsagePercent)
|
||||
} else {
|
||||
msg = fmt.Sprintf("Your %s plan allows only %d locations. You're currently using %d (%d%%).", data.PlanCode, data.LocationLimit, data.LocationCount, data.UsagePercent)
|
||||
}
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
|
||||
<body style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:600px;margin:0 auto;padding:20px;color:#1f2937;">
|
||||
<div style="text-align:center;margin-bottom:24px;"><span style="font-size:32px;">📍</span></div>
|
||||
<h2 style="text-align:center;color:#1f2937;margin-bottom:16px;">` + map[bool]string{true: "Blížíte se limitu lokací", false: "You're nearing your location limit"}[cs] + `</h2>
|
||||
<p style="color:#6b7280;text-align:center;margin-bottom:24px;">` + msg + `</p>
|
||||
<div style="text-align:center;margin-bottom:24px;">` + upgradeBtn + `</div>
|
||||
<p style="color:#9ca3af;font-size:14px;text-align:center;">` + map[bool]string{true: "Přidejte další lokace s vyšším plánem", false: "Add more locations with a higher plan"}[cs] + `</p>
|
||||
<hr style="border:none;border-top:1px solid #e5e7eb;margin:24px 0;">
|
||||
<p style="color:#9ca3af;font-size:12px;text-align:center;">Powered by <a href="https://bookra.eu" style="color:#4f46e5;">Bookra</a></p>
|
||||
</body></html>`
|
||||
}
|
||||
|
||||
// Trial ending email
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"></head>
|
||||
<body style="font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;max-width:600px;margin:0 auto;padding:20px;color:#1f2937;">
|
||||
<div style="text-align:center;margin-bottom:24px;"><span style="font-size:32px;">🎉</span></div>
|
||||
<h2 style="text-align:center;color:#1f2937;margin-bottom:16px;">` + map[bool]string{true: "Děkujeme, že používáte Bookra!", false: "Thank you for using Bookra!"}[cs] + `</h2>
|
||||
<p style="color:#6b7280;text-align:center;margin-bottom:24px;">` + map[bool]string{true: "Vaše zkušební období brzy končí. Pokud se vám naše služba líbí, můžete pokračovat s vybraným plánem.", false: "Your trial period is ending soon. If you like our service, you can continue with your chosen plan."}[cs] + `</p>
|
||||
<div style="text-align:center;margin-bottom:24px;">` + upgradeBtn + `</div>
|
||||
<p style="color:#6b7280;text-align:center;margin-bottom:16px;">` + map[bool]string{true: "Pokud se vám služba nelíbí, můžete ji kdykoliv zrušit. Nechceme vám brát peníze, pokud nejste spokojeni.", false: "If you don't like our service, you can cancel anytime. We don't want to take your money if you're not happy."}[cs] + `</p>
|
||||
<p style="color:#9ca3af;text-align:center;margin-bottom:24px;">` + map[bool]string{true: "Zrušit můžete zde:", false: "Cancel here:"}[cs] + ` ` + dashboardBtn + `</p>
|
||||
<hr style="border:none;border-top:1px solid #e5e7eb;margin:24px 0;">
|
||||
<p style="color:#9ca3af;font-size:12px;text-align:center;">Powered by <a href="https://bookra.eu" style="color:#4f46e5;">Bookra</a></p>
|
||||
</body></html>`
|
||||
}
|
||||
|
||||
func renderUsageText(data UsageNotificationData) string {
|
||||
cs := data.Locale == "cs"
|
||||
if data.Type == EmailTypeUsageWarning {
|
||||
if cs {
|
||||
return fmt.Sprintf("Blížíte se limitu lokací! Váš plán %s umožňuje pouze %d lokací. Aktuálně používáte %d (%d%%). Upgradeujte na vyšší plán: %s", data.PlanCode, data.LocationLimit, data.LocationCount, data.UsagePercent, data.UpgradeURL)
|
||||
}
|
||||
return fmt.Sprintf("You're nearing your location limit! Your %s plan allows only %d locations. You're currently using %d (%d%%). Upgrade: %s", data.PlanCode, data.LocationLimit, data.LocationCount, data.UsagePercent, data.UpgradeURL)
|
||||
}
|
||||
if cs {
|
||||
return "Vaše zkušební období končí. Pokud se vám služba líbí, můžete pokračovat. Pokud ne, můžete zrušit. Nechceme vám brát peníze, pokud nejste spokojeni. Dashboard: " + data.DashboardURL
|
||||
}
|
||||
return "Your trial period is ending. If you like our service, you can continue. If not, you can cancel - we don't want your money if you're not happy. Dashboard: " + data.DashboardURL
|
||||
}
|
||||
|
||||
func RenderEmailMessage(data BookingEmailData) EmailMessage {
|
||||
subject := renderSubject(data)
|
||||
htmlBody := renderHTMLBody(data)
|
||||
textBody := renderTextBody(data)
|
||||
|
||||
|
||||
return EmailMessage{
|
||||
From: data.BusinessEmail,
|
||||
To: data.CustomerEmail,
|
||||
@@ -55,7 +161,7 @@ func RenderEmailMessage(data BookingEmailData) EmailMessage {
|
||||
|
||||
func renderSubject(data BookingEmailData) string {
|
||||
localizedTime := formatLocalizedTime(data.StartsAt, data.Timezone, data.Locale)
|
||||
|
||||
|
||||
switch data.Type {
|
||||
case EmailTypeConfirmation:
|
||||
if data.Locale == "cs" {
|
||||
@@ -89,7 +195,7 @@ func renderSubject(data BookingEmailData) string {
|
||||
|
||||
func renderTextBody(data BookingEmailData) string {
|
||||
localizedTime := formatLocalizedDateTime(data.StartsAt, data.Timezone, data.Locale)
|
||||
|
||||
|
||||
switch data.Type {
|
||||
case EmailTypeConfirmation:
|
||||
if data.Locale == "cs" {
|
||||
@@ -124,7 +230,7 @@ Manage your booking at: %s
|
||||
Thank you,
|
||||
%s
|
||||
%s`, data.CustomerName, data.Service, localizedTime, data.Location, data.Reference, data.ManagementURL, data.TenantName, data.BusinessEmail)
|
||||
|
||||
|
||||
case EmailTypeReminder:
|
||||
if data.Locale == "cs" {
|
||||
return fmt.Sprintf(`Dobrý den %s,
|
||||
@@ -152,7 +258,7 @@ This is a reminder for your booking tomorrow.
|
||||
Manage booking: %s
|
||||
|
||||
%s`, data.CustomerName, data.Service, localizedTime, data.Location, data.Reference, data.ManagementURL, data.TenantName)
|
||||
|
||||
|
||||
case EmailTypeReschedule:
|
||||
if data.Locale == "cs" {
|
||||
return fmt.Sprintf(`Dobrý den %s,
|
||||
@@ -182,7 +288,7 @@ New details:
|
||||
Manage booking: %s
|
||||
|
||||
%s`, data.CustomerName, data.Service, localizedTime, data.Location, data.Reference, data.ManagementURL, data.TenantName)
|
||||
|
||||
|
||||
case EmailTypeCancellation:
|
||||
if data.Locale == "cs" {
|
||||
return fmt.Sprintf(`Dobrý den %s,
|
||||
@@ -210,7 +316,7 @@ Cancelled booking:
|
||||
If you didn't cancel this, please contact us: %s
|
||||
|
||||
%s`, data.CustomerName, data.Service, localizedTime, data.Reference, data.BusinessEmail, data.TenantName)
|
||||
|
||||
|
||||
case EmailTypeBusinessNotify:
|
||||
if data.Locale == "cs" {
|
||||
return fmt.Sprintf(`Nová rezervace od %s
|
||||
@@ -232,7 +338,7 @@ Details:
|
||||
- Email: %s
|
||||
|
||||
Manage in dashboard: https://bookra.eu/dashboard`, data.CustomerName, data.Service, localizedTime, data.Reference, data.CustomerEmail)
|
||||
|
||||
|
||||
default:
|
||||
return "Booking update"
|
||||
}
|
||||
@@ -245,7 +351,7 @@ func renderHTMLBody(data BookingEmailData) string {
|
||||
html := "<html><body style='font-family: Arial, sans-serif; line-height: 1.6; color: #333;'>"
|
||||
html += "<div style='max-width: 600px; margin: 0 auto; padding: 20px;'>"
|
||||
html += fmt.Sprintf("<h2 style='color: %s; margin-bottom: 20px;'>%s</h2>", data.BrandColor, data.TenantName)
|
||||
|
||||
|
||||
// Convert text to simple HTML
|
||||
paragraphs := splitParagraphs(textBody)
|
||||
for _, p := range paragraphs {
|
||||
@@ -253,12 +359,12 @@ func renderHTMLBody(data BookingEmailData) string {
|
||||
html += fmt.Sprintf("<p style='margin-bottom: 10px;'>%s</p>", p)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Add management button
|
||||
if data.ManagementURL != "" {
|
||||
html += fmt.Sprintf("<div style='margin-top: 30px;'><a href='%s' style='display: inline-block; background: %s; color: white; padding: 12px 24px; text-decoration: none; border-radius: 6px;'>Manage Booking</a></div>", data.ManagementURL, data.BrandColor)
|
||||
}
|
||||
|
||||
|
||||
html += "</div></body></html>"
|
||||
return html
|
||||
}
|
||||
@@ -347,7 +453,7 @@ func RenderReminderEmail(from string, job db.ReminderJobRecord) EmailMessage {
|
||||
StartsAt: job.StartsAt,
|
||||
Timezone: job.Timezone,
|
||||
Locale: job.Locale,
|
||||
Service: "Service", // Legacy
|
||||
Service: "Service", // Legacy
|
||||
Location: "Location", // Legacy
|
||||
}
|
||||
return RenderEmailMessage(data)
|
||||
|
||||
Reference in New Issue
Block a user