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: