def test_hooks(self): # TODO Replace these with mocks ... def pre_login_test(username): assert isinstance(username, str) async def pre_login_test_async(username): assert isinstance(username, str) def login_success_test(username, user_id): assert isinstance(username, str) assert isinstance(user_id, int) async def login_success_test_async(username, user_id): assert isinstance(username, str) assert isinstance(user_id, int) def login_failure_test(username): assert isinstance(username, str) def login_failure_test_async(username): assert isinstance(username, str) router = Router( routes=[ Route( "/login/", session_login( hooks=LoginHooks( pre_login=[pre_login_test, pre_login_test_async], login_success=[ login_success_test, login_success_test_async, ], login_failure=[ login_failure_test, login_failure_test_async, ], ) ), ), ] ) app = ExceptionMiddleware(router) BaseUser(**self.credentials, active=True).save().run_sync() client = TestClient(app) client.post("/login/", json=self.credentials)
def test_simple_custom_login_template(self): """ Make sure that a custom login template can be used. """ template_path = os.path.join( os.path.dirname(__file__), "templates", "simple_login_template", "login.html", ) router = Router(routes=[ Route( "/login/", session_login(template_path=template_path), ), ]) app = ExceptionMiddleware(router) client = TestClient(app) response = client.get("/login/") self.assertEqual(response.content, b"<p>Hello world</p>")
def test_styles(self): """ Make sure the custom styles are shown in the HTML output. """ custom_styles = Styles(background_color="black") app = Router(routes=[ Route( "/login/", session_login(styles=custom_styles), ), Route( "/logout/", session_logout(styles=custom_styles), ), Route( "/register/", register(styles=custom_styles), ), ]) client = TestClient(app) for url in ("/login/", "/logout/", "/register/"): response = client.get(url) self.assertTrue(b"--background_color: black;" in response.content)
def __init__( self, *tables: t.Type[Table], auth_table: t.Type[BaseUser] = BaseUser, session_table: t.Type[SessionsBase] = SessionsBase, session_expiry: timedelta = timedelta(hours=1), max_session_expiry: timedelta = timedelta(days=7), increase_expiry: t.Optional[timedelta] = timedelta(minutes=20), page_size: int = 15, read_only: bool = False, rate_limit_provider: t.Optional[RateLimitProvider] = None, production: bool = False, site_name: str = "Piccolo Admin", ) -> None: super().__init__(title=site_name, description="Piccolo API documentation") self.auth_table = auth_table self.site_name = site_name self.tables = tables with open(os.path.join(ASSET_PATH, "index.html")) as f: self.template = f.read() ####################################################################### api_app = FastAPI() for table in tables: FastAPIWrapper( root_url=f"/tables/{table._meta.tablename}/", fastapi_app=api_app, piccolo_crud=PiccoloCRUD(table=table, read_only=read_only, page_size=page_size), fastapi_kwargs=FastAPIKwargs(all_routes={ "tags": [f"{table._meta.tablename.capitalize()}"] }, ), ) api_app.add_api_route( path="/tables/", endpoint=self.get_table_list, methods=["GET"], response_model=t.List[str], tags=["Tables"], ) api_app.add_api_route( path="/meta/", endpoint=self.get_meta, methods=["GET"], tags=["Meta"], response_model=MetaResponseModel, ) api_app.add_api_route( path="/user/", endpoint=self.get_user, methods=["GET"], tags=["User"], response_model=UserResponseModel, ) ####################################################################### auth_app = FastAPI() if not rate_limit_provider: rate_limit_provider = InMemoryLimitProvider(limit=1000, timespan=300) auth_app.mount( path="/login/", app=RateLimitingMiddleware( app=session_login( auth_table=self.auth_table, session_table=session_table, session_expiry=session_expiry, max_session_expiry=max_session_expiry, redirect_to=None, production=production, ), provider=rate_limit_provider, ), ) auth_app.add_route( path="/logout/", route=session_logout(session_table=session_table), methods=["POST"], ) ####################################################################### self.router.add_route(path="/", endpoint=self.get_root, methods=["GET"]) self.mount( path="/css", app=StaticFiles(directory=os.path.join(ASSET_PATH, "css")), ) self.mount( path="/js", app=StaticFiles(directory=os.path.join(ASSET_PATH, "js")), ) auth_middleware = partial( AuthenticationMiddleware, backend=SessionsAuthBackend( auth_table=auth_table, session_table=session_table, admin_only=True, increase_expiry=increase_expiry, ), on_error=handle_auth_exception, ) self.mount(path="/api", app=auth_middleware(api_app)) self.mount(path="/auth", app=auth_app)
# The main app with public endpoints, which will be served by Uvicorn app = FastAPI( routes=[ # If we want to use Piccolo admin: Mount( "/admin/", create_admin( tables=[Task], # Required when running under HTTPS: # allowed_hosts=['my_site.com'] ), ), # Session Auth login: Mount( "/login/", session_login(redirect_to="/private/docs/"), ), ], docs_url=None, redoc_url=None, ) # The private app with SessionAuth protected endpoints private_app = FastAPI( routes=[ Route("/logout/", session_logout(redirect_to="/")), # We use a custom Swagger docs endpoint instead of the default FastAPI # one, because this one supports CSRF middleware: Mount("/docs/", swagger_ui(schema_url="/private/openapi.json")), ], middleware=[
def __init__( self, *tables: t.Type[Table], auth_table: t.Type[BaseUser] = BaseUser, session_table: t.Type[SessionsBase] = SessionsBase, session_expiry: timedelta = timedelta(hours=1), max_session_expiry: timedelta = timedelta(days=7), increase_expiry: t.Optional[timedelta] = timedelta(minutes=20), page_size: int = 15, read_only: bool = False, rate_limit_provider: t.Optional[RateLimitProvider] = None, production: bool = False, ) -> None: self.auth_table = auth_table with open(os.path.join(ASSET_PATH, "index.html")) as f: self.template = f.read() auth_middleware = partial( AuthenticationMiddleware, backend=SessionsAuthBackend( auth_table=auth_table, session_table=session_table, admin_only=True, increase_expiry=increase_expiry, ), on_error=handle_auth_exception, ) if not rate_limit_provider: rate_limit_provider = InMemoryLimitProvider(limit=1000, timespan=300) table_routes: t.List[BaseRoute] = [ Mount( path=f"/{table._meta.tablename}/", app=PiccoloCRUD(table, read_only=read_only, page_size=page_size), ) for table in tables ] table_routes += [ Route( path="/", endpoint=self.get_table_list, methods=["GET"], ) ] routes: t.List[BaseRoute] = [ Route(path="/", endpoint=self.get_root, methods=["GET"]), Mount( path="/css", app=StaticFiles(directory=os.path.join(ASSET_PATH, "css")), ), Mount( path="/js", app=StaticFiles(directory=os.path.join(ASSET_PATH, "js")), ), Mount( path="/api", app=Router([ Mount( path="/tables/", app=auth_middleware(Router(table_routes)), ), Route( path="/login/", endpoint=RateLimitingMiddleware( app=session_login( auth_table=self.auth_table, session_table=session_table, session_expiry=session_expiry, max_session_expiry=max_session_expiry, redirect_to=None, production=production, ), provider=rate_limit_provider, ), methods=["POST"], ), Route( path="/logout/", endpoint=session_logout(session_table=session_table), methods=["POST"], ), Mount( path="/user/", app=auth_middleware( Router([Route(path="/", endpoint=self.get_user)])), ), Mount( path="/meta/", app=auth_middleware( Router([Route(path="/", endpoint=self.get_meta)])), ), ]), ), ] self.tables = tables super().__init__(routes)
return PlainTextResponse(f"hello {session_user['username']}") else: return PlainTextResponse("hello world") class ProtectedEndpoint(HTTPEndpoint): @requires("authenticated", redirect="login") def get(self, request): return PlainTextResponse("top secret") ROUTER = Router(routes=[ Route("/", HomeEndpoint, name="home"), Route( "/login/", session_login(), name="login", ), Route("/logout/", session_logout(), name="login"), Mount( "/secret/", AuthenticationMiddleware( ProtectedEndpoint, SessionsAuthBackend( admin_only=True, superuser_only=True, active_only=True), ), ), ]) APP = ExceptionMiddleware(ROUTER) ###############################################################################
def __init__( self, *tables: t.Union[t.Type[Table], TableConfig], forms: t.List[FormConfig] = [], auth_table: t.Type[BaseUser] = BaseUser, session_table: t.Type[SessionsBase] = SessionsBase, session_expiry: timedelta = timedelta(hours=1), max_session_expiry: timedelta = timedelta(days=7), increase_expiry: t.Optional[timedelta] = timedelta(minutes=20), page_size: int = 15, read_only: bool = False, rate_limit_provider: t.Optional[RateLimitProvider] = None, production: bool = False, site_name: str = "Piccolo Admin", ) -> None: super().__init__( title=site_name, description="Piccolo API documentation" ) ####################################################################### # Convert any table arguments which are plain ``Table`` classes into # ``TableConfig`` instances. table_configs: t.List[TableConfig] = [] for table in tables: if isinstance(table, TableConfig): table_configs.append(table) else: table_configs.append(TableConfig(table_class=table)) self.table_configs = table_configs for table_config in table_configs: table_class = table_config.table_class for column in table_class._meta.columns: if column._meta.secret and column._meta.required: message = ( f"{table_class._meta.tablename}." f"{column._meta._name} is using `secret` and " f"`required` column args which are incompatible. " f"You may encounter unexpected behavior when using " f"this table within Piccolo Admin." ) colored_warning(message, level=Level.high) ####################################################################### self.auth_table = auth_table self.site_name = site_name self.forms = forms self.form_config_map = {form.slug: form for form in self.forms} with open(os.path.join(ASSET_PATH, "index.html")) as f: self.template = f.read() ####################################################################### api_app = FastAPI(docs_url=None) api_app.mount("/docs/", swagger_ui(schema_url="../openapi.json")) for table_config in table_configs: table_class = table_config.table_class visible_column_names = table_config.get_visible_column_names() visible_filter_names = table_config.get_visible_filter_names() rich_text_columns_names = ( table_config.get_rich_text_columns_names() ) FastAPIWrapper( root_url=f"/tables/{table_class._meta.tablename}/", fastapi_app=api_app, piccolo_crud=PiccoloCRUD( table=table_class, read_only=read_only, page_size=page_size, schema_extra={ "visible_column_names": visible_column_names, "visible_filter_names": visible_filter_names, "rich_text_columns": rich_text_columns_names, }, ), fastapi_kwargs=FastAPIKwargs( all_routes={ "tags": [f"{table_class._meta.tablename.capitalize()}"] }, ), ) api_app.add_api_route( path="/tables/", endpoint=self.get_table_list, # type: ignore methods=["GET"], response_model=t.List[str], tags=["Tables"], ) api_app.add_api_route( path="/meta/", endpoint=self.get_meta, # type: ignore methods=["GET"], tags=["Meta"], response_model=MetaResponseModel, ) api_app.add_api_route( path="/forms/", endpoint=self.get_forms, # type: ignore methods=["GET"], tags=["Forms"], response_model=t.List[FormConfigResponseModel], ) api_app.add_api_route( path="/forms/{form_slug:str}/", endpoint=self.get_single_form, # type: ignore methods=["GET"], tags=["Forms"], ) api_app.add_api_route( path="/forms/{form_slug:str}/schema/", endpoint=self.get_single_form_schema, # type: ignore methods=["GET"], tags=["Forms"], ) api_app.add_api_route( path="/forms/{form_slug:str}/", endpoint=self.post_single_form, # type: ignore methods=["POST"], tags=["Forms"], ) api_app.add_api_route( path="/user/", endpoint=self.get_user, # type: ignore methods=["GET"], tags=["User"], response_model=UserResponseModel, ) ####################################################################### auth_app = FastAPI() if not rate_limit_provider: rate_limit_provider = InMemoryLimitProvider( limit=1000, timespan=300 ) auth_app.mount( path="/login/", app=RateLimitingMiddleware( app=session_login( auth_table=self.auth_table, session_table=session_table, session_expiry=session_expiry, max_session_expiry=max_session_expiry, redirect_to=None, production=production, ), provider=rate_limit_provider, ), ) auth_app.add_route( path="/logout/", route=session_logout(session_table=session_table), methods=["POST"], ) ####################################################################### self.router.add_route( path="/", endpoint=self.get_root, methods=["GET"] ) self.mount( path="/css", app=StaticFiles(directory=os.path.join(ASSET_PATH, "css")), ) self.mount( path="/js", app=StaticFiles(directory=os.path.join(ASSET_PATH, "js")), ) auth_middleware = partial( AuthenticationMiddleware, backend=SessionsAuthBackend( auth_table=auth_table, session_table=session_table, admin_only=True, increase_expiry=increase_expiry, ), on_error=handle_auth_exception, ) self.mount(path="/api", app=auth_middleware(api_app)) self.mount(path="/auth", app=auth_app) # We make the meta endpoint available without auth, because it contains # the site name. self.add_api_route("/meta/", endpoint=self.get_meta) # type: ignore
private_app = Starlette( routes=[ Route( "/change-password/", change_password(), ), ], middleware=[ Middleware( AuthenticationMiddleware, on_error=on_auth_error, backend=SessionsAuthBackend(admin_only=False), ), ], ) app = Starlette( routes=[ Route("/", HomeEndpoint), Route("/login/", session_login()), Route( "/register/", register(redirect_to="/login/", user_defaults={"active": True}), ), Mount("/private/", private_app), ], middleware=[ Middleware(CSRFMiddleware, allow_form_param=True), ], )