This is an automated email from the ASF dual-hosted git repository.

msyavuz pushed a commit to branch msyavuz/feat/login-view-to-react
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 22c7e8a9b0f43634f6a0b13f05bc988aa66ae234
Author: Mehmet Salih Yavuz <salih.ya...@proton.me>
AuthorDate: Wed Apr 23 22:46:59 2025 +0300

    chore: initial commit
---
 superset-frontend/src/pages/Login/index.tsx | 118 ++++++++++++++++++++++++++++
 superset-frontend/src/views/routes.tsx      |   8 ++
 superset/initialization/__init__.py         |   2 +
 superset/security/auth.py                   |  35 +++++++++
 superset/security/manager.py                |   4 +
 superset/views/auth.py                      |  39 +++++++++
 superset/views/core.py                      |   5 +-
 superset/views/error_handling.py            |   4 +-
 8 files changed, 210 insertions(+), 5 deletions(-)

diff --git a/superset-frontend/src/pages/Login/index.tsx 
b/superset-frontend/src/pages/Login/index.tsx
new file mode 100644
index 0000000000..e85dbe0b1a
--- /dev/null
+++ b/superset-frontend/src/pages/Login/index.tsx
@@ -0,0 +1,118 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { css } from '@emotion/react';
+import { SupersetClient, styled, t } from '@superset-ui/core';
+import { Button, Card, Flex, Form, Input } from 'src/components';
+import { Icons } from 'src/components/Icons';
+import Typography from 'src/components/Typography';
+import { useState } from 'react';
+import { useLocation } from 'react-router-dom';
+
+type LoginType = {
+  username: string;
+  password: string;
+};
+
+const LoginContainer = styled(Flex)`
+  width: 100%;
+`;
+
+const StyledCard = styled(Card)`
+  ${({ theme }) => css`
+    width: 40%;
+    margin-top: ${theme.marginXL}px;
+    background: ${theme.colorBgBase};
+    .antd5-form-item-label label {
+      color: ${theme.colorPrimary};
+    }
+  `}
+`;
+
+const StyledLabel = styled(Typography.Text)`
+  ${({ theme }) => css`
+    font-size: ${theme.fontSizeSM}px;
+  `}
+`;
+
+interface LoginForm {
+  username: string;
+  password: string;
+}
+
+export default function Login() {
+  const [form] = Form.useForm<LoginForm>();
+  const [loading, setLoading] = useState(false);
+  const location = useLocation();
+
+  // Parse the query string to get the 'next' parameter
+  const queryParams = new URLSearchParams(location.search);
+  const nextUrl = queryParams.get('next') || '/superset/welcome/';
+
+  const onFinish = (values: LoginType) => {
+    setLoading(true);
+    SupersetClient.postForm('/login/', values, '').then(response => {
+      setLoading(false);
+      console.log('Login response:', response);
+      window.location.href = nextUrl;
+    });
+  };
+
+  return (
+    <LoginContainer justify="center">
+      <StyledCard title={t('Sign in')} padded variant="borderless">
+        <Flex justify="center" vertical gap="middle">
+          <Typography.Text type="secondary">
+            {t('Enter your login and password below:')}
+          </Typography.Text>
+          <Form
+            layout="vertical"
+            requiredMark="optional"
+            form={form}
+            onFinish={onFinish}
+          >
+            <Form.Item<LoginType>
+              label={<StyledLabel>{t('Username:')}</StyledLabel>}
+              name="username"
+              rules={[
+                { required: true, message: t('Please enter your username') },
+              ]}
+            >
+              <Input prefix={<Icons.UserOutlined size={1} />} />
+            </Form.Item>
+            <Form.Item<LoginType>
+              label={<StyledLabel>{t('Password:')}</StyledLabel>}
+              name="password"
+              rules={[
+                { required: true, message: t('Please enter your password') },
+              ]}
+            >
+              <Input.Password prefix={<Icons.KeyOutlined size={1} />} />
+            </Form.Item>
+            <Form.Item label={null}>
+              <Button block type="primary" htmlType="submit" loading={loading}>
+                {t('Sign in')}
+              </Button>
+            </Form.Item>
+          </Form>
+        </Flex>
+      </StyledCard>
+    </LoginContainer>
+  );
+}
diff --git a/superset-frontend/src/views/routes.tsx 
b/superset-frontend/src/views/routes.tsx
index 521bbee0dd..2e4dbff755 100644
--- a/superset-frontend/src/views/routes.tsx
+++ b/superset-frontend/src/views/routes.tsx
@@ -138,6 +138,10 @@ const UsersList: LazyExoticComponent<any> = lazy(
   () => import(/* webpackChunkName: "UsersList" */ 'src/pages/UsersList'),
 );
 
+const Login = lazy(
+  () => import(/* webpackChunkName: "Login" */ 'src/pages/Login'),
+);
+
 type Routes = {
   path: string;
   Component: ComponentType;
@@ -146,6 +150,10 @@ type Routes = {
 }[];
 
 export const routes: Routes = [
+  {
+    path: '/login/',
+    Component: Login,
+  },
   {
     path: '/superset/welcome/',
     Component: Home,
diff --git a/superset/initialization/__init__.py 
b/superset/initialization/__init__.py
index 7da7a23537..d24bf229f9 100644
--- a/superset/initialization/__init__.py
+++ b/superset/initialization/__init__.py
@@ -163,6 +163,7 @@ class SupersetAppInitializer:  # pylint: 
disable=too-many-public-methods
         from superset.views.all_entities import TaggedObjectsModelView
         from superset.views.annotations import AnnotationLayerView
         from superset.views.api import Api
+        from superset.views.auth import SupersetAuthView
         from superset.views.chart.views import SliceModelView
         from superset.views.core import Superset
         from superset.views.css_templates import CssTemplateModelView
@@ -331,6 +332,7 @@ class SupersetAppInitializer:  # pylint: 
disable=too-many-public-methods
         appbuilder.add_view_no_menu(TagView)
         appbuilder.add_view_no_menu(ReportView)
         appbuilder.add_view_no_menu(RoleRestAPI)
+        appbuilder.add_view_no_menu(SupersetAuthView)
 
         #
         # Add links
diff --git a/superset/security/auth.py b/superset/security/auth.py
new file mode 100644
index 0000000000..21354d45a9
--- /dev/null
+++ b/superset/security/auth.py
@@ -0,0 +1,35 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from flask import (
+    abort,
+)
+from flask_appbuilder import expose
+from flask_appbuilder.security.decorators import no_cache
+from flask_appbuilder.security.views import AuthView
+
+
+class NoLoginView(AuthView):
+    @expose("/login/", methods=["GET"])
+    @no_cache
+    def login(self):
+        abort(404)
+
+    @expose("/logout/", methods=["GET"])
+    @no_cache
+    def logout(self):
+        abort(404)
diff --git a/superset/security/manager.py b/superset/security/manager.py
index 962e0d9c5c..8c6858574e 100644
--- a/superset/security/manager.py
+++ b/superset/security/manager.py
@@ -60,6 +60,7 @@ from superset.exceptions import (
     DatasetInvalidPermissionEvaluationException,
     SupersetSecurityException,
 )
+from superset.security.auth import NoLoginView
 from superset.security.guest_token import (
     GuestToken,
     GuestTokenResources,
@@ -246,6 +247,9 @@ class SupersetSecurityManager(  # pylint: 
disable=too-many-public-methods
     role_api = SupersetRoleApi
     user_api = SupersetUserApi
 
+    auth_view = NoLoginView
+    authdbview = NoLoginView
+
     USER_MODEL_VIEWS = {
         "RegisterUserModelView",
         "UserDBModelView",
diff --git a/superset/views/auth.py b/superset/views/auth.py
new file mode 100644
index 0000000000..7a9c962513
--- /dev/null
+++ b/superset/views/auth.py
@@ -0,0 +1,39 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from flask_appbuilder import expose
+from flask_appbuilder.security.views import no_cache
+
+from superset.views.base import BaseSupersetView
+
+
+class SupersetAuthView(BaseSupersetView):
+    """
+    This class is used to override the default authentication view in Flask 
AppBuilder.
+    It is used to customize the login and logout views.
+    """
+
+    route_base = "/"
+
+    @expose("/login/", methods=["GET"])
+    @no_cache
+    def login(self) -> str:
+        """
+        Override the default login view to return a 404 error.
+        This is used to disable the login view in Flask AppBuilder.
+        """
+        return super().render_app_template()
diff --git a/superset/views/core.py b/superset/views/core.py
index 54dead027b..ea59dc74b7 100755
--- a/superset/views/core.py
+++ b/superset/views/core.py
@@ -35,7 +35,6 @@ from sqlalchemy.exc import SQLAlchemyError
 
 from superset import (
     app,
-    appbuilder,
     conf,
     db,
     event_logger,
@@ -794,7 +793,7 @@ class Superset(BaseSupersetView):
         except SupersetSecurityException as ex:
             # anonymous users should get the login screen, others should go to 
dashboard list  # noqa: E501
             if g.user is None or g.user.is_anonymous:
-                redirect_url = 
f"{appbuilder.get_url_for_login}?next={request.url}"
+                redirect_url = 
f"{url_for('SupersetAuthView.login')}?next={request.url}"
                 warn_msg = "Users must be logged in to view this dashboard."
             else:
                 redirect_url = url_for("DashboardModelView.list")
@@ -899,7 +898,7 @@ class Superset(BaseSupersetView):
         if not g.user or not get_user_id():
             if conf["PUBLIC_ROLE_LIKE"]:
                 return self.render_template("superset/public_welcome.html")
-            return redirect(appbuilder.get_url_for_login)
+            return redirect(url_for("SupersetAuthView.login"))
 
         if welcome_dashboard_id := (
             db.session.query(UserAttribute.welcome_dashboard_id)
diff --git a/superset/views/error_handling.py b/superset/views/error_handling.py
index 946142b0fc..dd7a808993 100644
--- a/superset/views/error_handling.py
+++ b/superset/views/error_handling.py
@@ -29,12 +29,12 @@ from flask import (
     request,
     Response,
     send_file,
+    url_for,
 )
 from flask_wtf.csrf import CSRFError
 from sqlalchemy import exc
 from werkzeug.exceptions import HTTPException
 
-from superset import appbuilder
 from superset.commands.exceptions import CommandException, CommandInvalidError
 from superset.errors import ErrorLevel, SupersetError, SupersetErrorType
 from superset.exceptions import (
@@ -153,7 +153,7 @@ def set_app_error_handlers(app: Flask) -> None:  # noqa: 
C901
         if request.is_json:
             return show_http_exception(ex)
 
-        return redirect(appbuilder.get_url_for_login)
+        return redirect(url_for("SupersetAuthView.login"))
 
     @app.errorhandler(HTTPException)
     def show_http_exception(ex: HTTPException) -> FlaskResponse:

Reply via email to