From fe542f2ce2f676ddf0bbb423af95b069c4580a28 Mon Sep 17 00:00:00 2001
From: Rahul Agrawal <12agrawalrahul@gmail.com>
Date: Tue, 24 Mar 2026 17:01:22 +0530
Subject: [PATCH 1/6] feat: include login/signup modal plan
---
plans/Current/login-modal.md | 182 +++++++++++++++++++++++++++++++++++
1 file changed, 182 insertions(+)
create mode 100644 plans/Current/login-modal.md
diff --git a/plans/Current/login-modal.md b/plans/Current/login-modal.md
new file mode 100644
index 0000000..dbcc3db
--- /dev/null
+++ b/plans/Current/login-modal.md
@@ -0,0 +1,182 @@
+# Plan: In-App Login Modal
+
+## Goal
+Replace the redirect to Frappe's `/login` page with an in-app modal dialog that supports all login features. Users stay on the current page (booking, custom form, etc.) and authenticate without leaving context.
+
+## Frappe APIs (all `allow_guest=True`)
+
+| Feature | Endpoint | Params |
+|---------|----------|--------|
+| Login | `POST /api/v2/method/login` | `usr`, `pwd` |
+| Sign up | `POST /api/v2/method/frappe.core.doctype.user.user.sign_up` | `email`, `full_name`, `redirect_to` |
+| Forgot password | `POST /api/v2/method/frappe.core.doctype.user.user.reset_password` | `user` (email) |
+| Email link login | `POST /api/v2/method/frappe.www.login.send_login_link` | `email` |
+| Social login providers | `GET /api/v2/method/frappe.client.get_list` on `Social Login Key` | `filters`, `fields` |
+| OAuth authorize URL | Not whitelisted — need a custom wrapper API |
+
+## Architecture
+
+### New Files
+1. **`dashboard/src/components/LoginDialog.vue`** — Main modal component with multi-view state machine
+2. **`dashboard/src/composables/useLoginDialog.ts`** — Shared composable to open/close the dialog from anywhere
+3. **`buzz/api/auth.py`** — New whitelisted API `get_login_context` (returns Google OAuth URL, settings flags)
+
+### Modified Files
+1. **`dashboard/src/components/LoginRequired.vue`** — Change "Log In" button to open modal instead of redirect
+2. **`dashboard/src/data/session.ts`** — Add resources for signup, forgot password, email link login
+3. **`dashboard/src/router.ts`** — Replace `window.location.href` redirect with showing `LoginRequired` state (no more redirect to Frappe's `/login`)
+4. **`dashboard/src/App.vue`** — Mount `LoginDialog` at app root level so it's available everywhere
+5. **`buzz/buzz/doctype/buzz_settings/buzz_settings.json`** — Add new "Login" tab with `login_banner` Markdown Editor field
+
+## Implementation Steps
+
+### Step 1: Backend API — `get_login_context`
+
+Create new file `buzz/api/auth.py`:
+
+```python
+@frappe.whitelist(allow_guest=True)
+def get_login_context():
+ """Return login configuration for the frontend modal."""
+ from frappe.utils.oauth import get_oauth2_authorize_url, get_oauth_keys
+
+ context = {
+ "disable_signup": cint(frappe.get_website_settings("disable_signup")),
+ "disable_user_pass_login": cint(frappe.get_system_settings("disable_user_pass_login")),
+ "login_with_email_link": cint(frappe.get_system_settings("login_with_email_link")),
+ "login_banner": frappe.db.get_single_value("Buzz Settings", "login_banner") or "",
+ "google_login": None,
+ }
+
+ # Only fetch Google provider
+ google = frappe.db.get_value(
+ "Social Login Key",
+ {"enable_social_login": 1, "provider_name": "Google"},
+ ["name", "client_id", "base_url"],
+ as_dict=True,
+ )
+
+ if google and google.client_id and google.base_url and get_oauth_keys(google.name):
+ redirect_to = frappe.utils.get_url("/dashboard")
+ context["google_login"] = {
+ "auth_url": get_oauth2_authorize_url(google.name, redirect_to),
+ }
+
+ return context
+```
+
+### Step 2: Composable — `useLoginDialog.ts`
+
+Simple reactive state to control the modal from anywhere:
+
+```typescript
+import { ref } from "vue"
+
+const isOpen = ref(false)
+const onSuccessCallback = ref<(() => void) | null>(null)
+
+export function useLoginDialog() {
+ function open(onSuccess?: () => void) {
+ onSuccessCallback.value = onSuccess || null
+ isOpen.value = true
+ }
+
+ function close() {
+ isOpen.value = false
+ onSuccessCallback.value = null
+ }
+
+ return { isOpen, open, close, onSuccessCallback }
+}
+```
+
+### Step 3: `LoginDialog.vue` — Multi-View Modal
+
+Uses frappe-ui `Dialog` component. Internal state machine with 4 views:
+
+**Views:**
+1. **`login`** — Email + password fields, Login button, "Forgot Password?" link, social login buttons, "Login with Email Link" button, "Don't have an account? Sign up" link
+2. **`signup`** — Full name + email fields, Sign up button, "Have an account? Login" link
+3. **`forgot-password`** — Email field, "Reset Password" button, "Back to Login" link
+4. **`email-link`** — Email field, "Send Login Link" button, "Back to Login" link
+
+Each view shows a success/error message area. All use frappe-ui components (`FormControl`, `Button`, `Dialog`).
+
+**Component structure:**
+```
+
+```
+
+**API calls:**
+- Login: `POST login` with `usr`/`pwd` → on success, reload user resource, close dialog, call `onSuccessCallback`
+- Sign up: `POST frappe.core.doctype.user.user.sign_up` → show success message ("Check your email")
+- Forgot password: `POST frappe.core.doctype.user.user.reset_password` → show success message
+- Email link: `POST frappe.www.login.send_login_link` → show success message
+- Social login: `window.location.href = provider.auth_url` (OAuth requires full redirect)
+
+### Step 4: Mount in App.vue
+
+Add `` to `App.vue` so it's available globally without each page importing it.
+
+### Step 5: Update LoginRequired.vue
+
+Replace `redirectToLogin()` with `useLoginDialog().open()`. After successful login, the page re-renders with authenticated state.
+
+## Design Details
+
+- Dialog size: `sm` (matches Frappe's login card width)
+- Use Tailwind CSS variables: `bg-surface-modal`, `text-ink-gray-8`, `border-outline-gray-2`
+- Form spacing: `space-y-4` between fields
+- Social login buttons: outline style with provider icon
+- Error messages: red text below the form using `text-red-600`
+- Success messages: green text using `text-green-600`
+- All text uses `__()` for translation
+- No Frappe logo/branding in the modal — clean, minimal
+- **Banner persistence**: Hash the banner content, store in `localStorage` as `login_banner_seen`. Only show banner if stored hash doesn't match current content hash. When admin updates the banner, hash changes → banner re-appears for all users
+
+## Notes
+
+1. **Social login redirect**: OAuth requires a full page redirect to Google. After auth, Frappe redirects back to `/dashboard`. This is how all OAuth works — no way around it.
+
+2. **Router guard behavior**: Protected routes still show `LoginRequired.vue` with the "Login Required" message + button. Clicking the button opens the login modal instead of redirecting. On successful login, the page re-renders with authenticated state.
From d1a8b9f5fffbc96be1a59f9fd934e2e001a5dc51 Mon Sep 17 00:00:00 2001
From: Rahul Agrawal <12agrawalrahul@gmail.com>
Date: Mon, 30 Mar 2026 00:54:48 +0530
Subject: [PATCH 2/6] feat: completed login flow
---
.github/workflows/ui-tests.yml | 20 +-
buzz/api/auth.py | 38 ++
.../doctype/buzz_settings/buzz_settings.json | 21 +-
.../doctype/buzz_settings/buzz_settings.py | 3 +-
dashboard/components.d.ts | 1 +
dashboard/src/App.vue | 2 +
dashboard/src/components/BookingForm.vue | 56 ++-
dashboard/src/components/LoginDialog.vue | 419 ++++++++++++++++++
dashboard/src/components/LoginRequired.vue | 10 +-
dashboard/src/components/Navbar.vue | 5 +-
.../src/composables/useBookingFormStorage.ts | 4 +-
dashboard/src/composables/useCustomFields.ts | 5 +-
dashboard/src/composables/useLoginDialog.ts | 18 +
dashboard/src/data/session.ts | 11 +-
dashboard/src/data/user.ts | 6 -
dashboard/src/layouts/Layout.vue | 14 +-
dashboard/src/pages/BookTickets.vue | 9 -
dashboard/src/router.ts | 20 +-
dashboard/src/utils/index.ts | 8 -
e2e/tests/login-modal.spec.ts | 35 ++
plans/Completed/login-modal.md | 60 +++
plans/Current/login-modal.md | 182 --------
playwright.config.ts | 11 +-
23 files changed, 692 insertions(+), 266 deletions(-)
create mode 100644 buzz/api/auth.py
create mode 100644 dashboard/src/components/LoginDialog.vue
create mode 100644 dashboard/src/composables/useLoginDialog.ts
create mode 100644 e2e/tests/login-modal.spec.ts
create mode 100644 plans/Completed/login-modal.md
delete mode 100644 plans/Current/login-modal.md
diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml
index 68ab395..525906d 100644
--- a/.github/workflows/ui-tests.yml
+++ b/.github/workflows/ui-tests.yml
@@ -111,6 +111,22 @@ jobs:
bench --site buzz.test set-config allow_tests true
bench --site buzz.test set-config host_name "http://buzz.test:8000"
+ - name: Create Test User
+ working-directory: /home/runner/frappe-bench
+ run: |
+ bench --site buzz.test execute "
+ import frappe
+ frappe.get_doc({
+ 'doctype': 'User',
+ 'email': 'testuser@buzz.test',
+ 'first_name': 'Test',
+ 'send_welcome_email': 0,
+ 'new_password': 'Test@123',
+ 'roles': [{'role': 'System Manager'}]
+ }).insert(ignore_permissions=True)
+ frappe.db.commit()
+ "
+
- name: Start Frappe Server
working-directory: /home/runner/frappe-bench
run: |
@@ -135,8 +151,8 @@ jobs:
run: npx playwright test
env:
BASE_URL: http://buzz.test:8000
- FRAPPE_USER: Administrator
- FRAPPE_PASSWORD: admin
+ FRAPPE_USER: testuser@buzz.test
+ FRAPPE_PASSWORD: Test@123
- name: Upload Playwright Report
uses: actions/upload-artifact@v4
diff --git a/buzz/api/auth.py b/buzz/api/auth.py
new file mode 100644
index 0000000..f062b18
--- /dev/null
+++ b/buzz/api/auth.py
@@ -0,0 +1,38 @@
+import frappe
+from frappe.utils import cint, md_to_html
+from frappe.utils.oauth import get_oauth2_authorize_url, get_oauth_keys
+
+
+@frappe.whitelist(allow_guest=True) # nosemgrep: frappe-semgrep-rules.rules.security.guest-whitelisted-method
+def get_login_context(redirect_to=None):
+ context = {
+ "disable_signup": frappe.get_website_settings("disable_signup"),
+ "disable_user_pass_login": frappe.get_system_settings("disable_user_pass_login"),
+ "login_with_email_link": frappe.get_system_settings("login_with_email_link"),
+ "login_banner": md_to_html(raw_banner)
+ if (raw_banner := frappe.db.get_single_value("Buzz Settings", "login_banner"))
+ else None,
+ "provider_logins": [],
+ }
+
+ if not redirect_to:
+ redirect_to = frappe.utils.get_url("/dashboard")
+
+ social_login_keys = frappe.get_all(
+ "Social Login Key",
+ filters={"enable_social_login": 1},
+ fields=["name", "provider_name", "icon", "client_id", "base_url"],
+ )
+
+ for provider in social_login_keys:
+ if provider.client_id and provider.base_url and get_oauth_keys(provider.name):
+ context["provider_logins"].append(
+ {
+ "name": provider.name,
+ "provider_name": provider.provider_name,
+ "icon": provider.icon or "",
+ "auth_url": get_oauth2_authorize_url(provider.name, redirect_to),
+ }
+ )
+
+ return context
diff --git a/buzz/events/doctype/buzz_settings/buzz_settings.json b/buzz/events/doctype/buzz_settings/buzz_settings.json
index 83cdc04..f690cac 100644
--- a/buzz/events/doctype/buzz_settings/buzz_settings.json
+++ b/buzz/events/doctype/buzz_settings/buzz_settings.json
@@ -13,6 +13,9 @@
"allow_add_ons_change_before_event_start_days",
"column_break_hagy",
"allow_ticket_cancellation_request_before_event_start_days",
+ "login_tab",
+ "login_banner_section",
+ "login_banner",
"communications_tab",
"ticketing_emails_section",
"default_ticket_email_template",
@@ -73,6 +76,22 @@
"label": "Support Email",
"options": "Email"
},
+ {
+ "fieldname": "login_tab",
+ "fieldtype": "Tab Break",
+ "label": "Login"
+ },
+ {
+ "fieldname": "login_banner_section",
+ "fieldtype": "Section Break",
+ "label": "Login Banner Config"
+ },
+ {
+ "description": "Promotional message shown in the login/signup modal. Supports Markdown.",
+ "fieldname": "login_banner",
+ "fieldtype": "Markdown Editor",
+ "label": "Login Banner"
+ },
{
"fieldname": "communications_tab",
"fieldtype": "Tab Break",
@@ -148,7 +167,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2026-01-13 22:34:24.493305",
+ "modified": "2026-03-26 17:39:40.326309",
"modified_by": "Administrator",
"module": "Events",
"name": "Buzz Settings",
diff --git a/buzz/events/doctype/buzz_settings/buzz_settings.py b/buzz/events/doctype/buzz_settings/buzz_settings.py
index d5031a0..3ab416c 100644
--- a/buzz/events/doctype/buzz_settings/buzz_settings.py
+++ b/buzz/events/doctype/buzz_settings/buzz_settings.py
@@ -21,8 +21,9 @@ class BuzzSettings(Document):
auto_send_pitch_deck: DF.Check
default_sponsor_deck_cc: DF.SmallText | None
default_sponsor_deck_email_template: DF.Link | None
- default_sponsor_deck_reply_to: DF.Data
+ default_sponsor_deck_reply_to: DF.Data | None
default_ticket_email_template: DF.Link | None
+ login_banner: DF.MarkdownEditor | None
support_email: DF.Data | None
# end: auto-generated types
diff --git a/dashboard/components.d.ts b/dashboard/components.d.ts
index 916445e..d8d060c 100644
--- a/dashboard/components.d.ts
+++ b/dashboard/components.d.ts
@@ -26,6 +26,7 @@ declare module 'vue' {
EventSelector: typeof import('./src/components/EventSelector.vue')['default']
EventSponsorForm: typeof import('./src/components/EventSponsorForm.vue')['default']
LanguageSwitcher: typeof import('./src/components/LanguageSwitcher.vue')['default']
+ LoginDialog: typeof import('./src/components/LoginDialog.vue')['default']
LoginRequired: typeof import('./src/components/LoginRequired.vue')['default']
Navbar: typeof import('./src/components/Navbar.vue')['default']
OfflinePaymentDialog: typeof import('./src/components/OfflinePaymentDialog.vue')['default']
diff --git a/dashboard/src/App.vue b/dashboard/src/App.vue
index 13fb793..6987cad 100644
--- a/dashboard/src/App.vue
+++ b/dashboard/src/App.vue
@@ -1,4 +1,5 @@
diff --git a/dashboard/src/components/LoginRequired.vue b/dashboard/src/components/LoginRequired.vue
index 3945410..0197695 100644
--- a/dashboard/src/components/LoginRequired.vue
+++ b/dashboard/src/components/LoginRequired.vue
@@ -5,21 +5,23 @@
{{ __("Login Required") }}
- {{ message }}
+ {{ __(message) }}
-
+
diff --git a/dashboard/src/components/Navbar.vue b/dashboard/src/components/Navbar.vue
index d9043ff..64a0d21 100644
--- a/dashboard/src/components/Navbar.vue
+++ b/dashboard/src/components/Navbar.vue
@@ -27,7 +27,7 @@
-