From f12f8a8296c3a5af84ee360ca77d124c53d22612 Mon Sep 17 00:00:00 2001 From: Haoming Meng Date: Fri, 12 Jan 2024 21:46:47 +0000 Subject: [PATCH] Add option to Logout --- web_ui/authentication.go | 18 ++-- web_ui/authentication_test.go | 83 +++++++++++++++++++ web_ui/frontend/components/layout/Sidebar.tsx | 61 ++++++++++++-- 3 files changed, 146 insertions(+), 16 deletions(-) diff --git a/web_ui/authentication.go b/web_ui/authentication.go index 40dfb274c..a303f1783 100644 --- a/web_ui/authentication.go +++ b/web_ui/authentication.go @@ -180,14 +180,8 @@ func setLoginCookie(ctx *gin.Context, user string) { return } - ctx.SetCookie("login", string(signed), 30*60, "/api/v1.0", - ctx.Request.URL.Host, true, true) - // Explicitly set Cookie for /metrics endpoint as they are in different paths - ctx.SetCookie("login", string(signed), 30*60, "/metrics", - ctx.Request.URL.Host, true, true) - // Explicitly set Cookie for /view endpoint as they are in different paths - ctx.SetCookie("login", string(signed), 30*60, "/view", - ctx.Request.URL.Host, true, true) + // One cookie should be used for all path + ctx.SetCookie("login", string(signed), 30*60, "/", ctx.Request.URL.Host, true, true) ctx.SetSameSite(http.SameSiteStrictMode) } @@ -324,6 +318,13 @@ func resetLoginHandler(ctx *gin.Context) { } } +func logoutHandler(ctx *gin.Context) { + ctx.SetCookie("login", "", -1, "/", ctx.Request.URL.Host, true, true) + ctx.SetSameSite(http.SameSiteStrictMode) + ctx.Set("User", "") + ctx.JSON(http.StatusOK, gin.H{"message": "Success"}) +} + // Returns the authentication status of the current user, including user id and role func whoamiHandler(ctx *gin.Context) { res := WhoAmIRes{} @@ -363,6 +364,7 @@ func configureAuthEndpoints(ctx context.Context, router *gin.Engine, egrp *errgr group := router.Group("/api/v1.0/auth") group.POST("/login", loginHandler) + group.POST("/logout", AuthHandler, logoutHandler) group.POST("/initLogin", initLoginHandler) group.POST("/resetLogin", AuthHandler, resetLoginHandler) // Pass csrfhanlder only to the whoami route to generate CSRF token diff --git a/web_ui/authentication_test.go b/web_ui/authentication_test.go index fc6f18ea0..b33c820e3 100644 --- a/web_ui/authentication_test.go +++ b/web_ui/authentication_test.go @@ -524,3 +524,86 @@ func TestAdminAuthHandler(t *testing.T) { }) } } + +func TestLogoutAPI(t *testing.T) { + ctx, cancel, egrp := test_utils.TestContext(context.Background(), t) + defer func() { require.NoError(t, egrp.Wait()) }() + defer cancel() + + dirName := t.TempDir() + viper.Reset() + config.InitConfig() + viper.Set("ConfigDir", dirName) + viper.Set("Server.UIPasswordFile", tempPasswdFile.Name()) + err := config.InitServer(ctx, config.OriginType) + require.NoError(t, err) + err = config.GeneratePrivateKey(param.IssuerKey.GetString(), elliptic.P256()) + require.NoError(t, err) + viper.Set("Server.UIPasswordFile", tempPasswdFile.Name()) + + ///////////////////////////SETUP/////////////////////////////////// + //Add an admin user to file to configure + content := "admin:password\n" + _, err = tempPasswdFile.WriteString(content) + assert.NoError(t, err, "Error writing to temp password file") + + //Configure UI + err = configureAuthDB() + assert.NoError(t, err) + + //Create a user for testing + err = WritePasswordEntry("user", "password") + assert.NoError(t, err, "error writing a user") + password := "password" + user := "user" + payload := fmt.Sprintf(`{"user": "%s", "password": "%s"}`, user, password) + + //Create a request + req, err := http.NewRequest("POST", "/api/v1.0/auth/login", strings.NewReader(payload)) + assert.NoError(t, err) + + req.Header.Set("Content-Type", "application/json") + + recorder := httptest.NewRecorder() + router.ServeHTTP(recorder, req) + //Check ok http reponse + assert.Equal(t, http.StatusOK, recorder.Code) + //Check that success message returned + assert.JSONEq(t, `{"msg":"Success"}`, recorder.Body.String()) + //Get the cookie to test 'logout' + loginCookie := recorder.Result().Cookies() + cookieValue := loginCookie[0].Value + + /////////////////////////////////////////////////////////////////// + + //Invoked with valid cookie, should return the username in the cookie + t.Run("With valid cookie", func(t *testing.T) { + req, err = http.NewRequest("POST", "/api/v1.0/auth/logout", nil) + assert.NoError(t, err) + + req.AddCookie(&http.Cookie{ + Name: "login", + Value: cookieValue, + }) + + recorder = httptest.NewRecorder() + router.ServeHTTP(recorder, req) + + //Check for http reponse code 200 + assert.Equal(t, 200, recorder.Code) + assert.Equal(t, 1, len(recorder.Result().Cookies())) + assert.Equal(t, "login", recorder.Result().Cookies()[0].Name) + assert.Greater(t, time.Now(), recorder.Result().Cookies()[0].Expires) + }) + //Invoked without valid cookie, should return there is no logged-in user + t.Run("Without a valid cookie", func(t *testing.T) { + req, err = http.NewRequest("POST", "/api/v1.0/auth/logout", nil) + assert.NoError(t, err) + + recorder = httptest.NewRecorder() + router.ServeHTTP(recorder, req) + + //Check for http reponse code 200 + assert.Equal(t, 401, recorder.Code) + }) +} diff --git a/web_ui/frontend/components/layout/Sidebar.tsx b/web_ui/frontend/components/layout/Sidebar.tsx index ecfce3cbc..e11b6c1c0 100644 --- a/web_ui/frontend/components/layout/Sidebar.tsx +++ b/web_ui/frontend/components/layout/Sidebar.tsx @@ -16,27 +16,72 @@ * ***************************************************************/ +"use client" + import Image from 'next/image' -import Link from 'next/link' -import {Typography, Box} from "@mui/material"; -import IconButton from "@mui/material/IconButton"; -import HomeIcon from '@mui/icons-material/Home'; -import BuildIcon from '@mui/icons-material/Build'; +import { useRouter } from 'next/navigation' +import {Typography, Box, Button, Snackbar, Alert, Tooltip} from "@mui/material"; +import LogoutIcon from '@mui/icons-material/Logout'; import styles from "../../app/page.module.css" -import PelicanLogo from "../../public/static/images/PelicanPlatformLogo_Icon.png" import GithubIcon from "../../public/static/images/github-mark.png" -import {ReactNode} from "react"; +import React, {ReactNode, useState} from "react"; export const Sidebar = ({children}: {children: ReactNode}) => { + const router = useRouter() + + const [error, setError] = useState("") + + const handleLogout = async (e: React.MouseEvent) => { + try { + let response = await fetch("/api/v1.0/auth/logout", { + method: "POST", + headers: { + "Content-Type": "application/json" + } + }) + + if(response.ok){ + router.push("/") + } else { + try { + let data = await response.json() + if (data?.error) { + setError(response.status + ": " + data['error']) + } else { + setError("Server error with status code " + response.status) + } + } catch { + setError("Server error with status code " + response.status) + } + } + } catch { + setError("Could not connect to server") + } + } return ( + {setError("")}} + anchorOrigin={{vertical: "top", horizontal: "center"}} + > + {setError("")}} severity="error" sx={{ width: '100%' }}> + {error} + +
{children}
- + + + + + +