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 @@ -