Skip to content

Commit

Permalink
158 admin menu (#219)
Browse files Browse the repository at this point in the history
* List Users

Finaaalllyy, list users again in a new branch

* Add Admin flag on User

Admin flag to determine if the user has admin privileges.

* hide users in menu

* Update Databases.kt

check if user and admin on each end point

* authenticate and authorize all the user endpoints

handle general response

Delete User

* List Users

Finaaalllyy, list users again in a new branch

* Add Admin flag on User

Admin flag to determine if the user has admin privileges.

* hide users in menu

* show is admin in menu

* Load is admin on user info

* fix type

* fix test

* Align uuid types

* clean code

* Add log in buttom

* change version

* change version

join

* NoSuchElementException

---------

Co-authored-by: Erik van Velzen <[email protected]>
  • Loading branch information
macano and Erikvv authored Jan 27, 2025
1 parent 5fb60eb commit 9db23bf
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 26 deletions.
2 changes: 1 addition & 1 deletion frontend/src/admin/use-projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const useProjects = (): UseProjectReturn => {
return
}
if (response.status === 500) {
return
throw new Error(`Failed: ${response.statusText}`)
}

setProjects(projectsFromJson(await response.text()))
Expand Down
45 changes: 33 additions & 12 deletions frontend/src/components/zero-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import {Button} from "primereact/button";
import {Sidebar} from "primereact/sidebar";
import {css} from "@emotion/react";
import {To, useNavigate} from "react-router-dom";
import {useUser} from "../user/use-user";
import { redirectToLogin } from "../admin/use-users";

const sidebarStyle = css({
width: '16rem',
Expand Down Expand Up @@ -31,6 +33,8 @@ const buttonStyle = css({
});

export const ZeroHeader: FunctionComponent<PropsWithChildren & {}> = () => {
const { isLoading, isLoggedIn, username, isAdmin } = useUser()

const [visible, setVisible] = useState(false);
const navigate = useNavigate();

Expand All @@ -40,6 +44,7 @@ export const ZeroHeader: FunctionComponent<PropsWithChildren & {}> = () => {
}
return (
<div className="app-header">

<div className="header" css={{
display: 'flex',
justifyContent: 'space-between',
Expand All @@ -49,14 +54,28 @@ export const ZeroHeader: FunctionComponent<PropsWithChildren & {}> = () => {
boxShadow: '1px solid #ddd'
}}>
<Button icon="pi pi-bars" onClick={() => setVisible(true)}/>
<a href="https://zenmo.com">
<img
alt="Zenmo logo"
src="https://zenmo.com/wp-content/uploads/elementor/thumbs/zenmo-logo-website-light-grey-square-o1piz2j6llwl7n0xd84ywkivuyf22xei68ewzwrvmc.png"
style={{height: "1.5em", verticalAlign: "sub"}}/>
&nbsp;
<b>Zenmo Zero</b>
</a>
<div style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}>
{!isLoggedIn && (
<Button
label="Log In"
className="p-button-text"
onClick={redirectToLogin}
css={{ marginLeft: "auto", fontSize: "0.9em", cursor: "pointer" }}
/>
)}
<a href="https://zenmo.com">
<img
alt="Zenmo logo"
src="https://zenmo.com/wp-content/uploads/elementor/thumbs/zenmo-logo-website-light-grey-square-o1piz2j6llwl7n0xd84ywkivuyf22xei68ewzwrvmc.png"
style={{height: "1.5em", verticalAlign: "sub"}}/>
&nbsp;
<b>Zenmo Zero</b>
</a>
</div>
</div>

<Sidebar visible={visible} position="left" onHide={() => setVisible(false)} css={sidebarStyle}>
Expand All @@ -72,10 +91,12 @@ export const ZeroHeader: FunctionComponent<PropsWithChildren & {}> = () => {
<i className="pi pi-fw pi-file" style={{marginRight: '0.5em'}}></i>
Projects
</a>
{/* <a onClick={() => loadContent('/users')} css={buttonStyle}>
<i className="pi pi-fw pi-file" style={{marginRight: '0.5em'}}></i>
Users
</a> */}
{isAdmin && (
<a onClick={() => loadContent('/users')} css={buttonStyle}>
<i className="pi pi-fw pi-file" style={{ marginRight: '0.5em' }}></i>
Users
</a>
)}
<a onClick={() => loadContent('/simulation')} css={buttonStyle}>
<i className="pi pi-fw pi-file" style={{marginRight: '0.5em'}}></i>
Simulation
Expand Down
32 changes: 22 additions & 10 deletions frontend/src/user/use-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,44 @@ type UseUserReturn = {
isLoading: boolean,
isLoggedIn?: boolean,
username?: string,
isAdmin?: boolean,
}

export const useUser = (): UseUserReturn => {
const [state, setState] = useState<UseUserReturn>({
isLoading: true,
isLoggedIn: undefined,
username: undefined,
isAdmin: false,
})

useOnce(async () => {
try {
const response = await fetch(import.meta.env.VITE_ZTOR_URL + "/user-info", {
credentials: "include",
})
if (response.status == 401) {
setState({
isLoading: false,
isLoggedIn: false,
})
} else {
const userInfo: any = await response.json()
setState({

if (!response.ok) {
throw new Error(`Failed to get user: ${response.statusText}`)
}

if (response.status === 500) {
setState(prevState => ({
...prevState,
isAdmin: false,
}))
}

if (response.ok) {
const userInfo = await response.json();

setState((prevState) => ({
...prevState,
isLoading: false,
isLoggedIn: true,
username: userInfo.preferred_username,
})
username: userInfo.decodedAccessToken.preferred_username,
isAdmin: userInfo.isAdmin
}));
}
} catch (e) {
console.error(e)
Expand Down
7 changes: 6 additions & 1 deletion zorm/src/main/kotlin/com/zenmo/orm/user/UserRepository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,12 @@ class UserRepository(
}

fun isAdmin(userId: UUID): Boolean {
val user = getUserById(userId)
val user = try {
getUserById(userId)
} catch (e: NoSuchElementException) {
null
}

return user?.isAdmin ?: false
}

Expand Down
18 changes: 16 additions & 2 deletions ztor/src/main/kotlin/com/zenmo/ztor/plugins/Authentication.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.zenmo.ztor.plugins

import com.zenmo.ztor.user.UserSession
import com.zenmo.ztor.user.UserInfo
import com.zenmo.ztor.user.decodeAccessToken
import io.ktor.client.*
import io.ktor.client.*
Expand All @@ -18,6 +19,9 @@ import io.ktor.server.plugins.forwardedheaders.*
import kotlinx.html.a
import kotlinx.html.body
import kotlinx.html.p
import com.zenmo.orm.connectToPostgres
import org.jetbrains.exposed.sql.Database
import com.zenmo.orm.user.UserRepository

val applicationHttpClient = HttpClient(CIO) {
install(ContentNegotiation) {
Expand All @@ -29,6 +33,9 @@ val applicationHttpClient = HttpClient(CIO) {
* After https://ktor.io/docs/server-oauth.html
*/
fun Application.configureAuthentication() {
val db: Database = connectToPostgres()
val userRepository = UserRepository(db)

// This reads the X-Forwarded-Proto header.
// This allows us to set the secure cookie below.
install(XForwardedHeaders)
Expand Down Expand Up @@ -95,11 +102,18 @@ fun Application.configureAuthentication() {
}
get("/user-info") {
val userSession: UserSession? = call.sessions.get()

if (userSession == null) {
// frontend can show login button
call.respondText("Not logged in", status=HttpStatusCode.Unauthorized)
} else {
call.respond(userSession.getDecodedAccessToken())
val userId = userSession.getUserId()
val isAdmin = userRepository.isAdmin(userId)

val response = UserInfo(
isAdmin = isAdmin,
decodedAccessToken = userSession.getDecodedAccessToken()
)
call.respond(response)
}
}
get("/home") {
Expand Down
8 changes: 8 additions & 0 deletions ztor/src/main/kotlin/com/zenmo/ztor/user/userInfo.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.zenmo.ztor.user
import kotlinx.serialization.Serializable

@Serializable
data class UserInfo(
val isAdmin: Boolean,
val decodedAccessToken: AccessTokenPayload
)

0 comments on commit 9db23bf

Please sign in to comment.