Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

158 admin menu #219

Merged
merged 20 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
macano marked this conversation as resolved.
Show resolved Hide resolved

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", {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I log in with a Keycloak user who is not in the database, /user-info gives a 500 error with java.util.NoSuchElementException.

Probably it should just have isAdmin: false

Copy link
Member

@Erikvv Erikvv Jan 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fix this in UserRepository.isAdmin() instead of in the frontend.

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
)
Loading