init first commit, with login customize
This commit is contained in:
10
nexus/boot.py
Normal file
10
nexus/boot.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
def override_app_labels(bootinfo):
|
||||||
|
"""Override app titles in bootinfo before they reach the frontend"""
|
||||||
|
if bootinfo.get("app_data"):
|
||||||
|
for app in bootinfo.app_data:
|
||||||
|
if app.get("app_name") == "erpnext":
|
||||||
|
app["app_title"] = "Nexus"
|
||||||
|
elif app.get("app_name") == "frappe":
|
||||||
|
app["app_title"] = "CMA"
|
||||||
@@ -5,6 +5,17 @@ app_description = "ERPNexus"
|
|||||||
app_email = "admin@nexus.com"
|
app_email = "admin@nexus.com"
|
||||||
app_license = "mit"
|
app_license = "mit"
|
||||||
|
|
||||||
|
extend_bootinfo = ["nexus.boot.override_app_labels"]
|
||||||
|
|
||||||
|
fixtures = [
|
||||||
|
{"doctype": "Workspace", "filters": [["name", "=", "ERPNext"]]}
|
||||||
|
]
|
||||||
|
|
||||||
|
website_context = {
|
||||||
|
"favicon": "/assets/nexus/images/custom-favicon.svg",
|
||||||
|
"splash_image": "/assets/nexus/images/custom-splash.svg",
|
||||||
|
}
|
||||||
|
|
||||||
# Apps
|
# Apps
|
||||||
# ------------------
|
# ------------------
|
||||||
|
|
||||||
|
|||||||
321
nexus/public/css/login.css
Normal file
321
nexus/public/css/login.css
Normal file
@@ -0,0 +1,321 @@
|
|||||||
|
:root {
|
||||||
|
--bg-color: #ffffff;
|
||||||
|
--bg-secondary: #f9fafb; /* Light gray for the right side */
|
||||||
|
--text-primary: #111827;
|
||||||
|
--text-secondary: #6b7280;
|
||||||
|
--border-color: #e5e7eb;
|
||||||
|
--input-bg: #ffffff;
|
||||||
|
--accent-color: #000000;
|
||||||
|
--ring-color: rgba(0, 0, 0, 0.05);
|
||||||
|
--radius: 0.5rem; /* Modern, slightly rounded corners */
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||||
|
"Helvetica Neue", Arial, sans-serif;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1.5;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Main Layout: 50/50 Split --- */
|
||||||
|
.main-container {
|
||||||
|
display: flex;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Left Panel (Form) --- */
|
||||||
|
.left-panel {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2rem;
|
||||||
|
position: relative;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Brand Logo (Absolute Position Top Left) */
|
||||||
|
.brand {
|
||||||
|
position: absolute;
|
||||||
|
top: 2rem;
|
||||||
|
left: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-link {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-icon {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
color: #fff;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-icon svg {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form Wrapper */
|
||||||
|
.form-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 360px; /* Optimal width for readability */
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-header {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-header h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.2;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
letter-spacing: -0.025em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-header p {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Input Groups */
|
||||||
|
.input-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.375rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.625rem 0.75rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
background-color: var(--input-bg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field::-webkit-input-placeholder {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field::-moz-placeholder {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field:-ms-input-placeholder {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Field Icons - Position and style SVG icons */
|
||||||
|
.field-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 1rem;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
fill: currentColor; /* Inherit from text color for visibility */
|
||||||
|
pointer-events: none;
|
||||||
|
transition: fill 0.2s ease;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-field .field-icon,
|
||||||
|
.password-field .field-icon {
|
||||||
|
left: 0.75rem; /* Adjust position inside input fields */
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-field .input-field {
|
||||||
|
padding-left: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.email-field .field-icon {
|
||||||
|
top: 46%;
|
||||||
|
transform: translateY(50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-field .input-field {
|
||||||
|
padding-left: 2.5rem;
|
||||||
|
padding-right: 3.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-field {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-password {
|
||||||
|
padding: 0.1rem 0.5rem;
|
||||||
|
background: rgb(0 0 0 / 2%);
|
||||||
|
border: solid #0000001f;
|
||||||
|
border-radius: 20px;
|
||||||
|
position: absolute;
|
||||||
|
top: 21%;
|
||||||
|
right: 0;
|
||||||
|
transform: translateX(-10%);
|
||||||
|
z-index: 2;
|
||||||
|
font-size: small;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modern Focus State */
|
||||||
|
.input-field:focus {
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
box-shadow: 0 0 0 3px var(--ring-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field::-ms-input-placeholder {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-field::placeholder {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modern Focus State */
|
||||||
|
.input-field:focus {
|
||||||
|
border-color: var(--accent-color);
|
||||||
|
box-shadow: 0 0 0 3px var(--ring-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Actions Row (Forgot Password) */
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-btn {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link-btn:hover {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main Login Button */
|
||||||
|
.btn-primary {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.625rem 0.75rem;
|
||||||
|
background-color: var(--accent-color);
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Right Panel (Image) --- */
|
||||||
|
.right-panel {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.frame-panel {
|
||||||
|
border-radius: 2rem;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-placeholder {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
-o-object-fit: cover;
|
||||||
|
object-fit: cover;
|
||||||
|
/* Slight darkening filter to match modern aesthetics */
|
||||||
|
-webkit-filter: brightness(0.95);
|
||||||
|
filter: brightness(0.95);
|
||||||
|
border-radius: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Responsive --- */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.right-panel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
position: relative;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
align-self: flex-start; /* Align brand to start of form container */
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-panel {
|
||||||
|
justify-content: flex-start; /* Push content down on mobile */
|
||||||
|
padding-top: 4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Footer Styling Override --- */
|
||||||
|
|
||||||
|
/* Reduce footer padding */
|
||||||
|
.web-footer {
|
||||||
|
display: none;
|
||||||
|
padding: 0 !important; /* Was: 3rem 0 = 48px, now: 8px */
|
||||||
|
min-height: 0px !important; /* Was: 140px, now: 80px */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduce footer column padding */
|
||||||
|
.footer-col-left,
|
||||||
|
.footer-col-right {
|
||||||
|
padding-top: 0 !important; /* Was: 0.8rem = ~13px */
|
||||||
|
padding-bottom: 0 !important; /* Was: 1rem = 16px */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduce footer group margins */
|
||||||
|
.footer-group {
|
||||||
|
margin-top: 0 !important; /* Was: 2rem = 32px */
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduce footer logo height */
|
||||||
|
.footer-logo {
|
||||||
|
height: 0 !important; /* Was: 1.5rem */
|
||||||
|
}
|
||||||
BIN
nexus/public/images/bg.png
Normal file
BIN
nexus/public/images/bg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 851 KiB |
7
nexus/public/images/custom-favicon.svg
Normal file
7
nexus/public/images/custom-favicon.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="32" height="32" rx="6" fill="#067EFB"/>
|
||||||
|
<g transform="translate(8, 8) scale(0.5)">
|
||||||
|
<path d="m7 7 10 10" stroke="white" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M17 7v10H7" stroke="white" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 407 B |
7
nexus/public/images/custom-splash.svg
Normal file
7
nexus/public/images/custom-splash.svg
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<svg width="118" height="118" viewBox="0 0 118 118" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="118" height="118" rx="20" fill="#067EFB"/>
|
||||||
|
<g transform="translate(35, 35) scale(2)">
|
||||||
|
<path d="m7 7 10 10" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M17 7v10H7" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 414 B |
101
nexus/www/login.html
Normal file
101
nexus/www/login.html
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
{% extends "templates/web.html" %}
|
||||||
|
{% block navbar %}{% endblock %}
|
||||||
|
|
||||||
|
{% macro email_login_body() -%}
|
||||||
|
<div class="input-group">
|
||||||
|
<label class="input-label" for="login_email">{{ login_label or _("Email") }}</label>
|
||||||
|
<div class="email-field">
|
||||||
|
<input type="text" id="login_email" class="input-field"
|
||||||
|
placeholder="{% if login_name_placeholder %}{{ login_name_placeholder }}{% else %}{{ _('jane@example.com') }}{% endif %}"
|
||||||
|
required autofocus autocomplete="username">
|
||||||
|
|
||||||
|
<svg class="field-icon email-icon" width="16" height="16" viewBox="0 0 16 16" fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<use class="es-lock" href="#es-line-email"></use>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<label class="input-label" for="login_password">{{ _("Password") }}</label>
|
||||||
|
<div class="password-field">
|
||||||
|
<input type="password" id="login_password" class="input-field" placeholder="•••••"
|
||||||
|
autocomplete="current-password" required>
|
||||||
|
|
||||||
|
<svg class="field-icon password-icon" width="16" height="16" viewBox="0 0 16 16" fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<use class="es-lock" href="#es-line-lock"></use>
|
||||||
|
</svg>
|
||||||
|
<span toggle="#login_password" class="toggle-password">{{ _('Show') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if not disable_user_pass_login %}
|
||||||
|
<div class="form-actions">
|
||||||
|
<a href="#forgot" class="link-btn">{{ _("Forgot Password?") }}</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% block head_include %}
|
||||||
|
<link rel="stylesheet" href="/assets/nexus/css/login.css">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block page_content %}
|
||||||
|
<div class="main-container">
|
||||||
|
<noscript>
|
||||||
|
<div class="text-center my-5">
|
||||||
|
<h4>{{ _("Javascript is disabled on your browser") }}</h4>
|
||||||
|
<p class="text-muted">
|
||||||
|
{{ _("You need to enable JavaScript for your app to work.") }}<br>{{ _("To enable it follow the
|
||||||
|
instructions in the following link: {0}").format("<a
|
||||||
|
href='https://enable-javascript.com/'>enable-javascript.com</a></p>") }}
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
|
|
||||||
|
<!-- Left Side: Form -->
|
||||||
|
<div class="left-panel">
|
||||||
|
<div class="brand">
|
||||||
|
<a href="#" class="brand-link">
|
||||||
|
<div class="brand-icon">
|
||||||
|
<!-- Your Custom Icon -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||||
|
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="m7 7 10 10" />
|
||||||
|
<path d="M17 7v10H7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
Nexus
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Centered Form Area -->
|
||||||
|
<div class="form-container">
|
||||||
|
<div class="form-header">
|
||||||
|
<h1>Welcome back</h1>
|
||||||
|
<p>Please enter your credentials to access your account.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form class="form-signin form-login" role="form">
|
||||||
|
{{ email_login_body() }}
|
||||||
|
<button class="btn-primary btn-login" type="submit">
|
||||||
|
{{ _("Login") }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right Side: Image -->
|
||||||
|
<div class="right-panel">
|
||||||
|
<div class="frame-panel">
|
||||||
|
<img src="/assets/nexus/images/bg.png" alt="Abstract Placeholder" class="image-placeholder">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block script %}
|
||||||
|
<script>{% include "templates/includes/login/login.js" %}</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block sidebar %}{% endblock %}
|
||||||
224
nexus/www/login.py
Normal file
224
nexus/www/login.py
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# License: MIT. See LICENSE
|
||||||
|
|
||||||
|
|
||||||
|
from urllib.parse import urljoin, urlparse
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
import frappe.utils
|
||||||
|
from frappe import _
|
||||||
|
from frappe.apps import get_default_path
|
||||||
|
from frappe.auth import LoginManager
|
||||||
|
from frappe.core.doctype.navbar_settings.navbar_settings import get_app_logo
|
||||||
|
from frappe.rate_limiter import rate_limit
|
||||||
|
from frappe.utils import cint, get_url
|
||||||
|
from frappe.utils.data import escape_html
|
||||||
|
from frappe.utils.html_utils import get_icon_html
|
||||||
|
from frappe.utils.jinja import guess_is_path
|
||||||
|
from frappe.utils.oauth import get_oauth2_authorize_url, get_oauth_keys, redirect_post_login
|
||||||
|
from frappe.utils.password import get_decrypted_password
|
||||||
|
from frappe.website.utils import get_home_page
|
||||||
|
|
||||||
|
no_cache = True
|
||||||
|
|
||||||
|
|
||||||
|
def get_context(context):
|
||||||
|
from frappe.integrations.frappe_providers.frappecloud_billing import get_site_login_url
|
||||||
|
from frappe.utils.frappecloud import on_frappecloud
|
||||||
|
|
||||||
|
redirect_to = frappe.local.request.args.get("redirect-to")
|
||||||
|
redirect_to = sanitize_redirect(redirect_to)
|
||||||
|
|
||||||
|
if frappe.session.user != "Guest":
|
||||||
|
if not redirect_to:
|
||||||
|
if frappe.session.data.user_type == "Website User":
|
||||||
|
redirect_to = get_default_path() or get_home_page()
|
||||||
|
else:
|
||||||
|
redirect_to = get_default_path() or "/desk"
|
||||||
|
|
||||||
|
if redirect_to != "login":
|
||||||
|
frappe.local.flags.redirect_location = redirect_to
|
||||||
|
raise frappe.Redirect
|
||||||
|
|
||||||
|
context.no_header = True
|
||||||
|
context.for_test = "login.html"
|
||||||
|
context["title"] = "Login"
|
||||||
|
context["hide_login"] = True # dont show login link on login page again.
|
||||||
|
context["provider_logins"] = []
|
||||||
|
context["disable_signup"] = cint(frappe.get_website_settings("disable_signup"))
|
||||||
|
context["show_footer_on_login"] = cint(frappe.get_website_settings("show_footer_on_login"))
|
||||||
|
context["disable_user_pass_login"] = cint(frappe.get_system_settings("disable_user_pass_login"))
|
||||||
|
context["logo"] = get_app_logo()
|
||||||
|
context["app_name"] = (
|
||||||
|
frappe.get_website_settings("app_name") or frappe.get_system_settings("app_name") or _("Frappe")
|
||||||
|
)
|
||||||
|
|
||||||
|
signup_form_template = frappe.get_hooks("signup_form_template")
|
||||||
|
if signup_form_template and len(signup_form_template):
|
||||||
|
path = signup_form_template[-1]
|
||||||
|
if not guess_is_path(path):
|
||||||
|
path = frappe.get_attr(signup_form_template[-1])()
|
||||||
|
else:
|
||||||
|
path = "frappe/templates/signup.html"
|
||||||
|
|
||||||
|
if path:
|
||||||
|
context["signup_form_template"] = frappe.get_template(path).render()
|
||||||
|
|
||||||
|
providers = frappe.get_all(
|
||||||
|
"Social Login Key",
|
||||||
|
filters={"enable_social_login": 1},
|
||||||
|
fields=["name", "client_id", "base_url", "provider_name", "icon"],
|
||||||
|
order_by="name",
|
||||||
|
)
|
||||||
|
|
||||||
|
for provider in providers:
|
||||||
|
client_secret = get_decrypted_password(
|
||||||
|
"Social Login Key", provider.name, "client_secret", raise_exception=False
|
||||||
|
)
|
||||||
|
if not client_secret:
|
||||||
|
continue
|
||||||
|
|
||||||
|
icon = None
|
||||||
|
if provider.icon:
|
||||||
|
if provider.provider_name == "Custom":
|
||||||
|
icon = get_icon_html(provider.icon, small=True)
|
||||||
|
else:
|
||||||
|
icon = f"<img src={escape_html(provider.icon)!r} alt={escape_html(provider.provider_name)!r}>"
|
||||||
|
|
||||||
|
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,
|
||||||
|
"auth_url": get_oauth2_authorize_url(provider.name, redirect_to),
|
||||||
|
"icon": icon,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
context["social_login"] = True
|
||||||
|
|
||||||
|
if cint(frappe.db.get_value("LDAP Settings", "LDAP Settings", "enabled")):
|
||||||
|
from frappe.integrations.doctype.ldap_settings.ldap_settings import LDAPSettings
|
||||||
|
|
||||||
|
context["ldap_settings"] = LDAPSettings.get_ldap_client_settings()
|
||||||
|
|
||||||
|
login_label = [_("Email")]
|
||||||
|
|
||||||
|
if frappe.utils.cint(frappe.get_system_settings("allow_login_using_mobile_number")):
|
||||||
|
login_label.append(_("Mobile"))
|
||||||
|
|
||||||
|
if frappe.utils.cint(frappe.get_system_settings("allow_login_using_user_name")):
|
||||||
|
login_label.append(_("Username"))
|
||||||
|
|
||||||
|
context["login_label"] = f" {_('or')} ".join(login_label)
|
||||||
|
|
||||||
|
context["login_with_email_link"] = frappe.get_system_settings("login_with_email_link")
|
||||||
|
context["login_with_frappe_cloud_url"] = (
|
||||||
|
f"{get_site_login_url()}?site={frappe.local.site}"
|
||||||
|
if on_frappecloud() and frappe.conf.get("fc_communication_secret")
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
context["full_width"] = True
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=True)
|
||||||
|
def login_via_token(login_token: str):
|
||||||
|
sid = frappe.cache.get_value(f"login_token:{login_token}", expires=True)
|
||||||
|
if not sid:
|
||||||
|
frappe.respond_as_web_page(_("Invalid Request"), _("Invalid Login Token"), http_status_code=417)
|
||||||
|
return
|
||||||
|
|
||||||
|
frappe.local.form_dict.sid = sid
|
||||||
|
frappe.local.login_manager = LoginManager()
|
||||||
|
|
||||||
|
redirect_post_login(
|
||||||
|
desk_user=frappe.db.get_value("User", frappe.session.user, "user_type") == "System User"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_login_with_email_link_ratelimit() -> int:
|
||||||
|
return frappe.get_system_settings("rate_limit_email_link_login") or 5
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=True, methods=["POST"])
|
||||||
|
@rate_limit(limit=get_login_with_email_link_ratelimit, seconds=60 * 60)
|
||||||
|
def send_login_link(email: str):
|
||||||
|
if not frappe.get_system_settings("login_with_email_link"):
|
||||||
|
return
|
||||||
|
|
||||||
|
expiry = frappe.get_system_settings("login_with_email_link_expiry") or 10
|
||||||
|
link = _generate_temporary_login_link(email, expiry)
|
||||||
|
|
||||||
|
app_name = (
|
||||||
|
frappe.get_website_settings("app_name") or frappe.get_system_settings("app_name") or _("Frappe")
|
||||||
|
)
|
||||||
|
|
||||||
|
subject = _("Login To {0}").format(app_name)
|
||||||
|
|
||||||
|
frappe.sendmail(
|
||||||
|
subject=subject,
|
||||||
|
recipients=email,
|
||||||
|
template="login_with_email_link",
|
||||||
|
args={"link": link, "minutes": expiry, "app_name": app_name},
|
||||||
|
now=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _generate_temporary_login_link(email: str, expiry: int):
|
||||||
|
assert isinstance(email, str)
|
||||||
|
|
||||||
|
if not frappe.db.exists("User", email):
|
||||||
|
frappe.throw(_("User with email address {0} does not exist").format(email), frappe.DoesNotExistError)
|
||||||
|
key = frappe.generate_hash()
|
||||||
|
frappe.cache.set_value(f"one_time_login_key:{key}", email, expires_in_sec=expiry * 60)
|
||||||
|
|
||||||
|
return get_url(f"/api/method/frappe.www.login.login_via_key?key={key}")
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=True, methods=["GET"])
|
||||||
|
@rate_limit(limit=get_login_with_email_link_ratelimit, seconds=60 * 60)
|
||||||
|
def login_via_key(key: str):
|
||||||
|
cache_key = f"one_time_login_key:{key}"
|
||||||
|
email = frappe.cache.get_value(cache_key)
|
||||||
|
|
||||||
|
if email:
|
||||||
|
frappe.cache.delete_value(cache_key)
|
||||||
|
frappe.local.login_manager.login_as(email)
|
||||||
|
|
||||||
|
redirect_post_login(
|
||||||
|
desk_user=frappe.db.get_value("User", frappe.session.user, "user_type") == "System User"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
frappe.respond_as_web_page(
|
||||||
|
_("Not Permitted"),
|
||||||
|
_("The link you trying to login is invalid or expired."),
|
||||||
|
http_status_code=403,
|
||||||
|
indicator_color="red",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_redirect(redirect: str | None) -> str | None:
|
||||||
|
"""Only allow redirect on same domain.
|
||||||
|
|
||||||
|
Allowed redirects:
|
||||||
|
- Same host e.g. https://frappe.localhost/path
|
||||||
|
- Just path e.g. /app gets converted to https://frappe.localhost/app
|
||||||
|
"""
|
||||||
|
if not redirect:
|
||||||
|
return redirect
|
||||||
|
|
||||||
|
parsed_redirect = urlparse(redirect)
|
||||||
|
|
||||||
|
parsed_request_host = urlparse(frappe.local.request.url)
|
||||||
|
output_parsed_url = parsed_redirect._replace(
|
||||||
|
netloc=parsed_request_host.netloc, scheme=parsed_request_host.scheme
|
||||||
|
)
|
||||||
|
if parsed_redirect.netloc:
|
||||||
|
if parsed_request_host.netloc != parsed_redirect.netloc:
|
||||||
|
output_parsed_url = output_parsed_url._replace(path="/desk")
|
||||||
|
else:
|
||||||
|
output_parsed_url = output_parsed_url._replace(path=parsed_redirect.path)
|
||||||
|
|
||||||
|
return output_parsed_url.geturl()
|
||||||
Reference in New Issue
Block a user