Skip to content

Commit

Permalink
Merge pull request #659 from haoming29/web-ui-logut
Browse files Browse the repository at this point in the history
Add option to Logout for server Web UI
  • Loading branch information
haoming29 authored Jan 17, 2024
2 parents 8b68603 + f12f8a8 commit 39ea7aa
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 16 deletions.
18 changes: 10 additions & 8 deletions web_ui/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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{}
Expand Down Expand Up @@ -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
Expand Down
83 changes: 83 additions & 0 deletions web_ui/authentication_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
61 changes: 53 additions & 8 deletions web_ui/frontend/components/layout/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLElement>) => {
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 (
<Box>
<Snackbar
open={error!=""}
autoHideDuration={6000}
onClose={() => {setError("")}}
anchorOrigin={{vertical: "top", horizontal: "center"}}
>
<Alert onClose={() => {setError("")}} severity="error" sx={{ width: '100%' }}>
{error}
</Alert>
</Snackbar>
<div className={styles.header} style={{display: "flex", flexDirection: "column", justifyContent:"space-between", padding:"1rem", top:0, position:"fixed", zIndex:"1", overflow: "hidden", height: "100vh"}}>
<div style={{display:"flex", flexDirection: "column"}}>
{children}
</div>
<Box display={"flex"} justifyContent={"center"}>
<Box display={"flex"} flexDirection={"column"} justifyContent={"center"} textAlign={"center"}>
<Tooltip title="Logout" placement="right" arrow>
<a aria-label='logout' onClick={handleLogout} style={{marginBottom: 10}}>
<LogoutIcon/>
</a>
</Tooltip>
<a href={"https://github.com/PelicanPlatform"}>
<Image
src={GithubIcon}
Expand Down

0 comments on commit 39ea7aa

Please sign in to comment.