From a396c772526697e76e343392a7bd59123235a590 Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Sat, 11 Jan 2025 13:09:26 -0500 Subject: [PATCH 01/30] List Users Finaaalllyy, list users again in a new branch --- frontend/src/admin/use-users.ts | 61 +++++++++ frontend/src/admin/user-form.tsx | 127 ++++++++++++++++++ frontend/src/admin/users.tsx | 58 ++++++++ frontend/src/components/zero-header.tsx | 4 + frontend/src/router.tsx | 5 + .../orm/companysurvey/SurveyRepository.kt | 2 +- .../main/kotlin/com/zenmo/orm/user/User.kt | 11 -- .../com/zenmo/orm/user/UserRepository.kt | 19 ++- .../com/zenmo/ztor/plugins/Databases.kt | 65 ++++++++- zummon/src/commonMain/kotlin/User.kt | 15 ++- 10 files changed, 351 insertions(+), 16 deletions(-) create mode 100644 frontend/src/admin/use-users.ts create mode 100644 frontend/src/admin/user-form.tsx create mode 100644 frontend/src/admin/users.tsx delete mode 100644 zorm/src/main/kotlin/com/zenmo/orm/user/User.kt diff --git a/frontend/src/admin/use-users.ts b/frontend/src/admin/use-users.ts new file mode 100644 index 00000000..5f610843 --- /dev/null +++ b/frontend/src/admin/use-users.ts @@ -0,0 +1,61 @@ +import {useState} from "react"; +import {useOnce} from "../hooks/use-once"; +import {User, usersFromJson } from "zero-zummon" +import {useNavigate} from "react-router-dom"; + +type UseUserReturn = { + loadingUsers: boolean, + users: User[], + changeUser: (newUser: User) => void, + removeUser: (userId: string) => void, +} + +type UseUserData = { + loadingUser: boolean, + user: User, +} + +export const useUsers = (): UseUserReturn => { + const [loadingUsers, setLoading] = useState(true) + const [users, setUsers] = useState([]) + + const changeUser = (newUser: User) => { + setUsers(users.map(user => user.id.toString() === newUser.id.toString() ? newUser : user)) + } + + useOnce(async () => { + try { + const response = await fetch(import.meta.env.VITE_ZTOR_URL + '/users', { + credentials: 'include', + }) + if (response.status === 401) { + redirectToLogin() + return + } + if (response.status === 500) { + return + } + + setUsers(usersFromJson(await response.text())) + } catch (error) { + alert((error as Error).message) + } finally { + setLoading(false) + } + }) + + const removeUser = (userId: any) => { + setUsers(users.filter(user => user.id.toString() !== userId.toString())) + } + + return { + loadingUsers, + users, + changeUser, + removeUser, + } +} + +export const redirectToLogin = () => { + window.location.href = import.meta.env.VITE_ZTOR_URL + '/login?redirectUrl=' + encodeURIComponent(window.location.href) +} diff --git a/frontend/src/admin/user-form.tsx b/frontend/src/admin/user-form.tsx new file mode 100644 index 00000000..f4c5cb6b --- /dev/null +++ b/frontend/src/admin/user-form.tsx @@ -0,0 +1,127 @@ +import React, { FormEvent, FunctionComponent, useEffect, useState } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import { PrimeReactProvider } from "primereact/api"; +import { InputText } from "primereact/inputtext"; +import { Button } from "primereact/button"; +import { User } from "zero-zummon"; +import { redirectToLogin } from "./use-users"; + +export const UserForm: FunctionComponent = () => { + const {userId} = useParams<{ userId: string }>(); + const [user, setUser] = useState(null); + const [originalData, setOriginalData] = useState(null); + + const [loading, setLoading] = useState(false); + const [isEditing, setIsEditing] = useState(false); + const navigate = useNavigate(); + + const handleCancel = () => { + if (originalData) { + setUser(originalData); // Revert to original data + } + setIsEditing(false); + }; + + const handleEditToggle = () => { + setIsEditing(true); + }; + + const handleInputChange =(e: React.ChangeEvent) => { + const { name, value } = e.target; + setUser((prev) => ({ ...prev, [name]: value } as User)); + }; + + useEffect(() => { + if (userId) { + const fetchUser = async () => { + setLoading(true); + try { + const response = await fetch(`${import.meta.env.VITE_ZTOR_URL}/users/${userId}`, { + credentials: "include", + }); + if (response.status === 401) { + redirectToLogin(); + return; + } + if (response.ok) { + const userData = await response.json(); + setUser(userData); + setOriginalData(userData); + } else { + alert(`Error fetching user: ${response.statusText}`); + } + } catch (error) { + alert((error as Error).message); + } finally { + setLoading(false); + } + }; + fetchUser(); + } else { + setIsEditing(true); + } + }, [userId]); + + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + setLoading(true); + try { + const method = userId ? "PUT" : "POST"; + const url = `${import.meta.env.VITE_ZTOR_URL}/users` + const response = await fetch(url, { + method, + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + body: JSON.stringify(user), + }); + if (response.status === 401) { + redirectToLogin(); + return; + } + + if (response.ok) { + navigate(`/users`); + } else { + const errorData = await response.json(); + alert(`Error: ${errorData.message}`); + } + } finally { + setIsEditing(false); + setLoading(false); + } + }; + + return ( + +
+

{userId ? "Edit User" : "Add User"}

+
+ + + +
+ {isEditing ? ( + <> +
+ +
+
+ ); +}; diff --git a/frontend/src/admin/users.tsx b/frontend/src/admin/users.tsx new file mode 100644 index 00000000..b22fb9ff --- /dev/null +++ b/frontend/src/admin/users.tsx @@ -0,0 +1,58 @@ +import React, {FunctionComponent} from "react"; +import { DataTable } from 'primereact/datatable'; +import { Column } from 'primereact/column'; +import {useUsers} from "./use-users"; +import {PrimeReactProvider} from "primereact/api"; +import {User} from "zero-zummon" + +import "primereact/resources/themes/lara-light-cyan/theme.css" +import 'primeicons/primeicons.css' +import {DeleteButton} from "./delete-button"; +import {EditButton} from "./edit-button"; +import {Button} from "primereact/button"; +import {useNavigate} from "react-router-dom" + +export const Users: FunctionComponent = () => { + const {loadingUsers, users, changeUser, removeUser} = useUsers() + const navigate = useNavigate(); + + return ( + +
+

Users List

+
+ + + + ( +
*': { + margin: `${1 / 6}rem` + }, + }}> + + +
+ )}/> +
+
+ ) +} \ No newline at end of file diff --git a/frontend/src/components/zero-header.tsx b/frontend/src/components/zero-header.tsx index a9772f9d..eee5ae20 100644 --- a/frontend/src/components/zero-header.tsx +++ b/frontend/src/components/zero-header.tsx @@ -72,6 +72,10 @@ export const ZeroHeader: FunctionComponent = () => { Projects + loadContent('/users')} css={buttonStyle}> + + Users + loadContent('/simulation')} css={buttonStyle}> Simulation diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index 24b7bc29..ea8760d1 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -7,7 +7,9 @@ import {LoginWidget} from "./user/login"; import {BedrijvenFormV1} from "./components/bedrijven-form-v1"; import {Surveys} from "./admin/surveys"; import {Projects} from "./admin/projects"; +import {Users} from "./admin/users"; import {ProjectForm} from "./admin/project-form"; +import {UserForm} from "./admin/user-form"; import {fetchSurveyById, SurveyById, SurveyByIdLoaderData} from "./components/company-survey-v2/survey-by-id" import {Intro} from "./components/intro" import {ExcelImport} from "./excel-import/excel-import" @@ -26,6 +28,9 @@ export const router = createBrowserRouter([ {path: "/projects", element: }, {path: "/projects/new-project", element: }, {path: "/projects/:projectId", element: }, + {path: "/users", element: }, + {path: "/users/new-user", element: }, + {path: "/users/:userId", element: }, {path: "/simulation", element: }, ], }, diff --git a/zorm/src/main/kotlin/com/zenmo/orm/companysurvey/SurveyRepository.kt b/zorm/src/main/kotlin/com/zenmo/orm/companysurvey/SurveyRepository.kt index 642dc0ef..cda4bec6 100644 --- a/zorm/src/main/kotlin/com/zenmo/orm/companysurvey/SurveyRepository.kt +++ b/zorm/src/main/kotlin/com/zenmo/orm/companysurvey/SurveyRepository.kt @@ -267,7 +267,7 @@ class SurveyRepository( val userId = row[CompanySurveyTable.createdById] ?: return null return com.zenmo.zummon.User( - row[UserTable.id].toKotlinUuid(), + row[UserTable.id], row[UserTable.note], ) } diff --git a/zorm/src/main/kotlin/com/zenmo/orm/user/User.kt b/zorm/src/main/kotlin/com/zenmo/orm/user/User.kt deleted file mode 100644 index 918dc2a7..00000000 --- a/zorm/src/main/kotlin/com/zenmo/orm/user/User.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.zenmo.orm.user - -import com.zenmo.zummon.companysurvey.Project -import java.util.UUID - -data class User( - // Keycloak id - val id: UUID, - val projects: List = emptyList(), - val note: String, -) diff --git a/zorm/src/main/kotlin/com/zenmo/orm/user/UserRepository.kt b/zorm/src/main/kotlin/com/zenmo/orm/user/UserRepository.kt index 42d25f73..444659f9 100644 --- a/zorm/src/main/kotlin/com/zenmo/orm/user/UserRepository.kt +++ b/zorm/src/main/kotlin/com/zenmo/orm/user/UserRepository.kt @@ -1,5 +1,6 @@ package com.zenmo.orm.user +import com.zenmo.zummon.User import com.zenmo.orm.companysurvey.ProjectRepository import com.zenmo.orm.user.table.UserProjectTable import com.zenmo.orm.user.table.UserTable @@ -53,10 +54,24 @@ class UserRepository( } } - fun getUserById(id: UUID): User? { + fun getUserById(id: UUID): User { return getUsers( (UserTable.id eq id) - ).firstOrNull() + ).first() + } + + @OptIn(ExperimentalUuidApi::class) + fun save( + user: User, + ) { + transaction(db) { + UserTable.upsertReturning() { + it[id] = user.id + it[UserTable.note] = user.note + }.map { + hydrateUser(it) + }.first() + } } fun saveUser( diff --git a/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt b/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt index 0a437bc8..cce4239e 100644 --- a/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt +++ b/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt @@ -10,6 +10,8 @@ import com.zenmo.ztor.errorMessageToJson import com.zenmo.ztor.user.getUserId import com.zenmo.zummon.companysurvey.Survey import com.zenmo.zummon.companysurvey.Project +import com.zenmo.zummon.User + import io.ktor.http.* import io.ktor.serialization.* import io.ktor.server.application.* @@ -28,6 +30,68 @@ fun Application.configureDatabases(): Database { val deeplinkService = DeeplinkService(DeeplinkRepository(db)) routing { + // List users for current user + get("/users") { + call.respond(HttpStatusCode.OK, userRepository.getUsers()) + } + + // Get one user that belongs to the user + get("/users/{userId}") { + val userId = UUID.fromString(call.parameters["userId"]) + + val adminUserId = call.getUserId() + if (adminUserId == null) { + call.respond(HttpStatusCode.Unauthorized) + return@get + } + + call.respond(HttpStatusCode.OK, userRepository.getUserById(userId)) + } + + // Create + post("/users") { + val user: User? + try { + user = call.receive() + } catch (e: BadRequestException) { + if (e.cause is JsonConvertException) { + call.respond(HttpStatusCode.BadRequest, errorMessageToJson(e.cause?.message)) + return@post + } + call.respond(HttpStatusCode.BadRequest, errorMessageToJson(e.message)) + return@post + } + + val userId = call.getUserId() + if (userId == null) { + call.respond(HttpStatusCode.Unauthorized) + return@post + } + + val newUser = userRepository.save(user) + + call.respond(HttpStatusCode.Created, newUser) + } + + // Update + put("/users") { + val user: User? + try { + user = call.receive() + } catch (e: BadRequestException) { + if (e.cause is JsonConvertException) { + call.respond(HttpStatusCode.BadRequest, errorMessageToJson(e.cause?.message)) + return@put + } + call.respond(HttpStatusCode.BadRequest, errorMessageToJson(e.message)) + return@put + } + + val newUser = userRepository.save(user) + + call.respond(HttpStatusCode.OK, newUser) + } + // List projects for current user get("/projects") { val userId = call.getUserId() @@ -68,7 +132,6 @@ fun Application.configureDatabases(): Database { } val userId = call.getUserId() - println("User ID: $userId") if (userId == null) { call.respond(HttpStatusCode.Unauthorized) return@post diff --git a/zummon/src/commonMain/kotlin/User.kt b/zummon/src/commonMain/kotlin/User.kt index cc882379..fb259e94 100644 --- a/zummon/src/commonMain/kotlin/User.kt +++ b/zummon/src/commonMain/kotlin/User.kt @@ -1,9 +1,12 @@ package com.zenmo.zummon + +import com.benasher44.uuid.Uuid +import com.benasher44.uuid.uuid4 +import com.zenmo.zummon.BenasherUuidSerializer import com.zenmo.zummon.companysurvey.Project import kotlinx.serialization.Serializable import kotlin.js.JsExport -import kotlin.uuid.Uuid /** * This object is intended to be enriched with Keycloak data. @@ -16,3 +19,13 @@ data class User( val note: String, val projects: List = emptyList() ) + +@JsExport +fun usersFromJson(json: String): Array { + return kotlinx.serialization.json.Json.decodeFromString>(json) +} + +@JsExport +fun userFromJson(json: String): User { + return kotlinx.serialization.json.Json.decodeFromString(json) +} From ef4dc3a111a778d361d4fab27ec7bef789b7386c Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Sat, 11 Jan 2025 16:53:15 -0500 Subject: [PATCH 02/30] Add Admin flag on User Admin flag to determine if the user has admin privileges. --- frontend/src/admin/user-form.tsx | 19 ++++++++++++++-- frontend/src/admin/users.tsx | 14 +++++++++++- migrations/V33__admin_flag_to_user.sql | 1 + .../com/zenmo/orm/user/UserRepository.kt | 7 ++++++ .../kotlin/com/zenmo/orm/user/table/User.kt | 5 +++++ .../com/zenmo/ztor/plugins/Databases.kt | 22 ++++++++++++++++++- zummon/src/commonMain/kotlin/User.kt | 4 ++-- 7 files changed, 66 insertions(+), 6 deletions(-) create mode 100644 migrations/V33__admin_flag_to_user.sql diff --git a/frontend/src/admin/user-form.tsx b/frontend/src/admin/user-form.tsx index f4c5cb6b..2a788d88 100644 --- a/frontend/src/admin/user-form.tsx +++ b/frontend/src/admin/user-form.tsx @@ -27,8 +27,10 @@ export const UserForm: FunctionComponent = () => { }; const handleInputChange =(e: React.ChangeEvent) => { - const { name, value } = e.target; - setUser((prev) => ({ ...prev, [name]: value } as User)); + const { name, value, type, checked } = e.target; + setUser((prev) => ({...prev, + [name]: type === "checkbox" ? checked : value, + } as User)); }; useEffect(() => { @@ -109,6 +111,19 @@ export const UserForm: FunctionComponent = () => { onChange={handleInputChange} disabled={!isEditing} /> +
+ +
{isEditing ? ( diff --git a/frontend/src/admin/users.tsx b/frontend/src/admin/users.tsx index b22fb9ff..e19b1a58 100644 --- a/frontend/src/admin/users.tsx +++ b/frontend/src/admin/users.tsx @@ -40,7 +40,19 @@ export const Users: FunctionComponent = () => { filterDisplay="row" > - + ( +
+ {user.isAdmin ? ( + + ) : ( + + )} +
+ )} + /> (
= emptyList() + val projects: List = emptyList(), + val isAdmin: Boolean = false ) @JsExport From 0902e76378e366812b358263005837a0f0667e12 Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Mon, 13 Jan 2025 10:41:10 -0500 Subject: [PATCH 03/30] hide users in menu --- frontend/src/admin/use-users.ts | 6 ----- frontend/src/components/zero-header.tsx | 4 +-- .../com/zenmo/ztor/plugins/Databases.kt | 26 +++++++------------ 3 files changed, 11 insertions(+), 25 deletions(-) diff --git a/frontend/src/admin/use-users.ts b/frontend/src/admin/use-users.ts index 5f610843..02744110 100644 --- a/frontend/src/admin/use-users.ts +++ b/frontend/src/admin/use-users.ts @@ -1,7 +1,6 @@ import {useState} from "react"; import {useOnce} from "../hooks/use-once"; import {User, usersFromJson } from "zero-zummon" -import {useNavigate} from "react-router-dom"; type UseUserReturn = { loadingUsers: boolean, @@ -10,11 +9,6 @@ type UseUserReturn = { removeUser: (userId: string) => void, } -type UseUserData = { - loadingUser: boolean, - user: User, -} - export const useUsers = (): UseUserReturn => { const [loadingUsers, setLoading] = useState(true) const [users, setUsers] = useState([]) diff --git a/frontend/src/components/zero-header.tsx b/frontend/src/components/zero-header.tsx index eee5ae20..2f7dc351 100644 --- a/frontend/src/components/zero-header.tsx +++ b/frontend/src/components/zero-header.tsx @@ -72,10 +72,10 @@ export const ZeroHeader: FunctionComponent = () => { Projects - loadContent('/users')} css={buttonStyle}> + {/* loadContent('/users')} css={buttonStyle}> Users - + */} loadContent('/simulation')} css={buttonStyle}> Simulation diff --git a/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt b/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt index 24308388..1e3308a1 100644 --- a/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt +++ b/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt @@ -35,23 +35,18 @@ fun Application.configureDatabases(): Database { get("/users") { val userId = call.getUserId() - println("userId " + userId) - - // if (userId == null) { - // call.respond(HttpStatusCode.Unauthorized) - // return@get - // } - - // val isAdmin = userRepository.isAdmin(userId) - // println("isAdmin $isAdmin") + if (userId == null) { + call.respond(HttpStatusCode.Unauthorized) + return@get + } + val isAdmin = userRepository.isAdmin(userId) - // if (!isAdmin) { - // call.respond(HttpStatusCode.Forbidden, "Access denied") - // return@get - // } + if (!isAdmin) { + call.respond(HttpStatusCode.Forbidden, "Access denied") + return@get + } val users = userRepository.getUsers() - println("All users " + users) call.respond(HttpStatusCode.OK, users) } @@ -128,7 +123,6 @@ fun Application.configureDatabases(): Database { val projectId = UUID.fromString(call.parameters["projectId"]) val userId = call.getUserId() - println("User ID: $userId") if (userId == null) { call.respond(HttpStatusCode.Unauthorized) return@get @@ -265,7 +259,6 @@ fun Application.configureDatabases(): Database { delete("/company-surveys/{surveyId}") { val userId = call.getUserId() - println("User ID: $userId") if (userId == null) { call.respond(HttpStatusCode.Unauthorized) return@delete @@ -303,7 +296,6 @@ fun Application.configureDatabases(): Database { // set active state put("/company-surveys/{surveyId}/include-in-simulation") { val userId = call.getUserId() - println("User ID: $userId") if (userId == null) { call.respond(HttpStatusCode.Unauthorized) return@put From ac2a8493989326d247a51501febb402ba9c63946 Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Wed, 15 Jan 2025 10:22:14 -0500 Subject: [PATCH 04/30] Update Databases.kt check if user and admin on each end point --- .../com/zenmo/ztor/plugins/Databases.kt | 61 ++++++++++++------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt b/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt index 1e3308a1..87af1399 100644 --- a/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt +++ b/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt @@ -23,6 +23,8 @@ import io.ktor.server.routing.* import org.jetbrains.exposed.sql.Database import java.util.* + + fun Application.configureDatabases(): Database { val db: Database = connectToPostgres() val userRepository = UserRepository(db) @@ -30,41 +32,47 @@ fun Application.configureDatabases(): Database { val projectRepository = ProjectRepository(db) val deeplinkService = DeeplinkService(DeeplinkRepository(db)) + fun authenticateAndAuthorize(call: ApplicationCall, userRepository: UserRepository): Boolean { + val userId = call.getUserId() + if (userId == null) { + call.respond(HttpStatusCode.Unauthorized, "User not authenticated") + return false + } + + val isAdmin = userRepository.isAdmin(userId) + if (!isAdmin) { + call.respond(HttpStatusCode.Forbidden, "Access denied") + return false + } + + return true + } + routing { // List users for current user - get("/users") { - val userId = call.getUserId() + get("/users") {\ + if (!authenticateAndAuthorize(call, userRepository)) return@get - if (userId == null) { - call.respond(HttpStatusCode.Unauthorized) - return@get - } - val isAdmin = userRepository.isAdmin(userId) - - if (!isAdmin) { - call.respond(HttpStatusCode.Forbidden, "Access denied") - return@get + try { + val users = userRepository.getUsers() + call.respond(HttpStatusCode.OK, users) + } catch (e: Exception) { + call.respond(HttpStatusCode.InternalServerError, "Failed to fetch users: ${e.message}") } - - val users = userRepository.getUsers() - call.respond(HttpStatusCode.OK, users) } // Get one user that belongs to the user get("/users/{userId}") { - val userId = UUID.fromString(call.parameters["userId"]) - - val adminUserId = call.getUserId() - if (adminUserId == null) { - call.respond(HttpStatusCode.Unauthorized) - return@get - } + if (!authenticateAndAuthorize(call, userRepository)) return@get - call.respond(HttpStatusCode.OK, userRepository.getUserById(userId)) + val user = userRepository.getUserById(userId) + call.respond(HttpStatusCode.OK, user) } // Create post("/users") { + if (!authenticateAndAuthorize(call, userRepository)) return@get + val user: User? try { user = call.receive() @@ -82,6 +90,12 @@ fun Application.configureDatabases(): Database { call.respond(HttpStatusCode.Unauthorized) return@post } + val isAdmin = userRepository.isAdmin(userId) + + if (!isAdmin) { + call.respond(HttpStatusCode.Forbidden, "Access denied") + return@get + } val newUser = userRepository.save(user) @@ -90,6 +104,8 @@ fun Application.configureDatabases(): Database { // Update put("/users") { + if (!authenticateAndAuthorize(call, userRepository)) return@get + val user: User? try { user = call.receive() @@ -308,6 +324,5 @@ fun Application.configureDatabases(): Database { call.respond(HttpStatusCode.OK) } } - return db } From 37454732ce5802237d6706577b7a5d2ab7e2181c Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Mon, 20 Jan 2025 11:49:59 -0500 Subject: [PATCH 05/30] authenticate and authorize all the user endpoints handle general response Delete User --- frontend/src/admin/use-users.ts | 4 +-- .../com/zenmo/ztor/plugins/Databases.kt | 33 +++++++++---------- zummon/src/commonMain/kotlin/User.kt | 6 ++-- 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/frontend/src/admin/use-users.ts b/frontend/src/admin/use-users.ts index 02744110..ac7ab132 100644 --- a/frontend/src/admin/use-users.ts +++ b/frontend/src/admin/use-users.ts @@ -26,8 +26,8 @@ export const useUsers = (): UseUserReturn => { redirectToLogin() return } - if (response.status === 500) { - return + if (!response.ok) { + throw new Error(`Could not load users: ${response.status} ${response.statusText}`) } setUsers(usersFromJson(await response.text())) diff --git a/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt b/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt index 87af1399..1027a2d2 100644 --- a/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt +++ b/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt @@ -32,7 +32,7 @@ fun Application.configureDatabases(): Database { val projectRepository = ProjectRepository(db) val deeplinkService = DeeplinkService(DeeplinkRepository(db)) - fun authenticateAndAuthorize(call: ApplicationCall, userRepository: UserRepository): Boolean { + suspend fun authenticateAndAuthorize(call: ApplicationCall, userRepository: UserRepository): Boolean { val userId = call.getUserId() if (userId == null) { call.respond(HttpStatusCode.Unauthorized, "User not authenticated") @@ -47,10 +47,10 @@ fun Application.configureDatabases(): Database { return true } - + routing { // List users for current user - get("/users") {\ + get("/users") { if (!authenticateAndAuthorize(call, userRepository)) return@get try { @@ -64,14 +64,14 @@ fun Application.configureDatabases(): Database { // Get one user that belongs to the user get("/users/{userId}") { if (!authenticateAndAuthorize(call, userRepository)) return@get - + val userId = UUID.fromString(call.parameters["userId"]) val user = userRepository.getUserById(userId) call.respond(HttpStatusCode.OK, user) } // Create post("/users") { - if (!authenticateAndAuthorize(call, userRepository)) return@get + if (!authenticateAndAuthorize(call, userRepository)) return@post val user: User? try { @@ -85,18 +85,6 @@ fun Application.configureDatabases(): Database { return@post } - val userId = call.getUserId() - if (userId == null) { - call.respond(HttpStatusCode.Unauthorized) - return@post - } - val isAdmin = userRepository.isAdmin(userId) - - if (!isAdmin) { - call.respond(HttpStatusCode.Forbidden, "Access denied") - return@get - } - val newUser = userRepository.save(user) call.respond(HttpStatusCode.Created, newUser) @@ -104,7 +92,7 @@ fun Application.configureDatabases(): Database { // Update put("/users") { - if (!authenticateAndAuthorize(call, userRepository)) return@get + if (!authenticateAndAuthorize(call, userRepository)) return@put val user: User? try { @@ -117,12 +105,21 @@ fun Application.configureDatabases(): Database { call.respond(HttpStatusCode.BadRequest, errorMessageToJson(e.message)) return@put } + println(user) val newUser = userRepository.save(user) + println(newUser) call.respond(HttpStatusCode.OK, newUser) } + delete("/users/{userId}") { + if (!authenticateAndAuthorize(call, userRepository)) return@delete + val userId = UUID.fromString(call.parameters["userId"]) + userRepository.deleteUserById(userId) + call.respond(HttpStatusCode.OK) + } + // List projects for current user get("/projects") { val userId = call.getUserId() diff --git a/zummon/src/commonMain/kotlin/User.kt b/zummon/src/commonMain/kotlin/User.kt index 278faf23..6966d06d 100644 --- a/zummon/src/commonMain/kotlin/User.kt +++ b/zummon/src/commonMain/kotlin/User.kt @@ -13,8 +13,10 @@ import kotlin.js.JsExport */ @Serializable @JsExport -data class User( - val id: Uuid = Uuid.random(), +data class User +constructor( + @Serializable(with = BenasherUuidSerializer::class) + val id: Uuid = uuid4(), val note: String, val projects: List = emptyList(), val isAdmin: Boolean = false From d62832198fac37eae1fb050e935c8b8cba8e33f6 Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Sat, 11 Jan 2025 13:09:26 -0500 Subject: [PATCH 06/30] List Users Finaaalllyy, list users again in a new branch --- frontend/src/admin/use-users.ts | 61 +++++++++ frontend/src/admin/user-form.tsx | 127 ++++++++++++++++++ frontend/src/admin/users.tsx | 58 ++++++++ frontend/src/components/zero-header.tsx | 4 + frontend/src/router.tsx | 5 + .../orm/companysurvey/SurveyRepository.kt | 2 +- .../main/kotlin/com/zenmo/orm/user/User.kt | 11 -- .../com/zenmo/orm/user/UserRepository.kt | 19 ++- .../com/zenmo/ztor/plugins/Databases.kt | 65 ++++++++- zummon/src/commonMain/kotlin/User.kt | 21 ++- 10 files changed, 355 insertions(+), 18 deletions(-) create mode 100644 frontend/src/admin/use-users.ts create mode 100644 frontend/src/admin/user-form.tsx create mode 100644 frontend/src/admin/users.tsx delete mode 100644 zorm/src/main/kotlin/com/zenmo/orm/user/User.kt diff --git a/frontend/src/admin/use-users.ts b/frontend/src/admin/use-users.ts new file mode 100644 index 00000000..5f610843 --- /dev/null +++ b/frontend/src/admin/use-users.ts @@ -0,0 +1,61 @@ +import {useState} from "react"; +import {useOnce} from "../hooks/use-once"; +import {User, usersFromJson } from "zero-zummon" +import {useNavigate} from "react-router-dom"; + +type UseUserReturn = { + loadingUsers: boolean, + users: User[], + changeUser: (newUser: User) => void, + removeUser: (userId: string) => void, +} + +type UseUserData = { + loadingUser: boolean, + user: User, +} + +export const useUsers = (): UseUserReturn => { + const [loadingUsers, setLoading] = useState(true) + const [users, setUsers] = useState([]) + + const changeUser = (newUser: User) => { + setUsers(users.map(user => user.id.toString() === newUser.id.toString() ? newUser : user)) + } + + useOnce(async () => { + try { + const response = await fetch(import.meta.env.VITE_ZTOR_URL + '/users', { + credentials: 'include', + }) + if (response.status === 401) { + redirectToLogin() + return + } + if (response.status === 500) { + return + } + + setUsers(usersFromJson(await response.text())) + } catch (error) { + alert((error as Error).message) + } finally { + setLoading(false) + } + }) + + const removeUser = (userId: any) => { + setUsers(users.filter(user => user.id.toString() !== userId.toString())) + } + + return { + loadingUsers, + users, + changeUser, + removeUser, + } +} + +export const redirectToLogin = () => { + window.location.href = import.meta.env.VITE_ZTOR_URL + '/login?redirectUrl=' + encodeURIComponent(window.location.href) +} diff --git a/frontend/src/admin/user-form.tsx b/frontend/src/admin/user-form.tsx new file mode 100644 index 00000000..f4c5cb6b --- /dev/null +++ b/frontend/src/admin/user-form.tsx @@ -0,0 +1,127 @@ +import React, { FormEvent, FunctionComponent, useEffect, useState } from "react"; +import { useParams, useNavigate } from "react-router-dom"; +import { PrimeReactProvider } from "primereact/api"; +import { InputText } from "primereact/inputtext"; +import { Button } from "primereact/button"; +import { User } from "zero-zummon"; +import { redirectToLogin } from "./use-users"; + +export const UserForm: FunctionComponent = () => { + const {userId} = useParams<{ userId: string }>(); + const [user, setUser] = useState(null); + const [originalData, setOriginalData] = useState(null); + + const [loading, setLoading] = useState(false); + const [isEditing, setIsEditing] = useState(false); + const navigate = useNavigate(); + + const handleCancel = () => { + if (originalData) { + setUser(originalData); // Revert to original data + } + setIsEditing(false); + }; + + const handleEditToggle = () => { + setIsEditing(true); + }; + + const handleInputChange =(e: React.ChangeEvent) => { + const { name, value } = e.target; + setUser((prev) => ({ ...prev, [name]: value } as User)); + }; + + useEffect(() => { + if (userId) { + const fetchUser = async () => { + setLoading(true); + try { + const response = await fetch(`${import.meta.env.VITE_ZTOR_URL}/users/${userId}`, { + credentials: "include", + }); + if (response.status === 401) { + redirectToLogin(); + return; + } + if (response.ok) { + const userData = await response.json(); + setUser(userData); + setOriginalData(userData); + } else { + alert(`Error fetching user: ${response.statusText}`); + } + } catch (error) { + alert((error as Error).message); + } finally { + setLoading(false); + } + }; + fetchUser(); + } else { + setIsEditing(true); + } + }, [userId]); + + const handleSubmit = async (event: FormEvent) => { + event.preventDefault(); + setLoading(true); + try { + const method = userId ? "PUT" : "POST"; + const url = `${import.meta.env.VITE_ZTOR_URL}/users` + const response = await fetch(url, { + method, + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + body: JSON.stringify(user), + }); + if (response.status === 401) { + redirectToLogin(); + return; + } + + if (response.ok) { + navigate(`/users`); + } else { + const errorData = await response.json(); + alert(`Error: ${errorData.message}`); + } + } finally { + setIsEditing(false); + setLoading(false); + } + }; + + return ( + +
+

{userId ? "Edit User" : "Add User"}

+
+ + + +
+ {isEditing ? ( + <> +
+ +
+
+ ); +}; diff --git a/frontend/src/admin/users.tsx b/frontend/src/admin/users.tsx new file mode 100644 index 00000000..b22fb9ff --- /dev/null +++ b/frontend/src/admin/users.tsx @@ -0,0 +1,58 @@ +import React, {FunctionComponent} from "react"; +import { DataTable } from 'primereact/datatable'; +import { Column } from 'primereact/column'; +import {useUsers} from "./use-users"; +import {PrimeReactProvider} from "primereact/api"; +import {User} from "zero-zummon" + +import "primereact/resources/themes/lara-light-cyan/theme.css" +import 'primeicons/primeicons.css' +import {DeleteButton} from "./delete-button"; +import {EditButton} from "./edit-button"; +import {Button} from "primereact/button"; +import {useNavigate} from "react-router-dom" + +export const Users: FunctionComponent = () => { + const {loadingUsers, users, changeUser, removeUser} = useUsers() + const navigate = useNavigate(); + + return ( + +
+

Users List

+
+ + + + ( +
*': { + margin: `${1 / 6}rem` + }, + }}> + + +
+ )}/> +
+
+ ) +} \ No newline at end of file diff --git a/frontend/src/components/zero-header.tsx b/frontend/src/components/zero-header.tsx index a9772f9d..eee5ae20 100644 --- a/frontend/src/components/zero-header.tsx +++ b/frontend/src/components/zero-header.tsx @@ -72,6 +72,10 @@ export const ZeroHeader: FunctionComponent = () => { Projects
+ loadContent('/users')} css={buttonStyle}> + + Users + loadContent('/simulation')} css={buttonStyle}> Simulation diff --git a/frontend/src/router.tsx b/frontend/src/router.tsx index 24b7bc29..ea8760d1 100644 --- a/frontend/src/router.tsx +++ b/frontend/src/router.tsx @@ -7,7 +7,9 @@ import {LoginWidget} from "./user/login"; import {BedrijvenFormV1} from "./components/bedrijven-form-v1"; import {Surveys} from "./admin/surveys"; import {Projects} from "./admin/projects"; +import {Users} from "./admin/users"; import {ProjectForm} from "./admin/project-form"; +import {UserForm} from "./admin/user-form"; import {fetchSurveyById, SurveyById, SurveyByIdLoaderData} from "./components/company-survey-v2/survey-by-id" import {Intro} from "./components/intro" import {ExcelImport} from "./excel-import/excel-import" @@ -26,6 +28,9 @@ export const router = createBrowserRouter([ {path: "/projects", element: }, {path: "/projects/new-project", element: }, {path: "/projects/:projectId", element: }, + {path: "/users", element: }, + {path: "/users/new-user", element: }, + {path: "/users/:userId", element: }, {path: "/simulation", element: }, ], }, diff --git a/zorm/src/main/kotlin/com/zenmo/orm/companysurvey/SurveyRepository.kt b/zorm/src/main/kotlin/com/zenmo/orm/companysurvey/SurveyRepository.kt index 642dc0ef..cda4bec6 100644 --- a/zorm/src/main/kotlin/com/zenmo/orm/companysurvey/SurveyRepository.kt +++ b/zorm/src/main/kotlin/com/zenmo/orm/companysurvey/SurveyRepository.kt @@ -267,7 +267,7 @@ class SurveyRepository( val userId = row[CompanySurveyTable.createdById] ?: return null return com.zenmo.zummon.User( - row[UserTable.id].toKotlinUuid(), + row[UserTable.id], row[UserTable.note], ) } diff --git a/zorm/src/main/kotlin/com/zenmo/orm/user/User.kt b/zorm/src/main/kotlin/com/zenmo/orm/user/User.kt deleted file mode 100644 index 918dc2a7..00000000 --- a/zorm/src/main/kotlin/com/zenmo/orm/user/User.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.zenmo.orm.user - -import com.zenmo.zummon.companysurvey.Project -import java.util.UUID - -data class User( - // Keycloak id - val id: UUID, - val projects: List = emptyList(), - val note: String, -) diff --git a/zorm/src/main/kotlin/com/zenmo/orm/user/UserRepository.kt b/zorm/src/main/kotlin/com/zenmo/orm/user/UserRepository.kt index 42d25f73..444659f9 100644 --- a/zorm/src/main/kotlin/com/zenmo/orm/user/UserRepository.kt +++ b/zorm/src/main/kotlin/com/zenmo/orm/user/UserRepository.kt @@ -1,5 +1,6 @@ package com.zenmo.orm.user +import com.zenmo.zummon.User import com.zenmo.orm.companysurvey.ProjectRepository import com.zenmo.orm.user.table.UserProjectTable import com.zenmo.orm.user.table.UserTable @@ -53,10 +54,24 @@ class UserRepository( } } - fun getUserById(id: UUID): User? { + fun getUserById(id: UUID): User { return getUsers( (UserTable.id eq id) - ).firstOrNull() + ).first() + } + + @OptIn(ExperimentalUuidApi::class) + fun save( + user: User, + ) { + transaction(db) { + UserTable.upsertReturning() { + it[id] = user.id + it[UserTable.note] = user.note + }.map { + hydrateUser(it) + }.first() + } } fun saveUser( diff --git a/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt b/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt index 0a437bc8..cce4239e 100644 --- a/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt +++ b/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt @@ -10,6 +10,8 @@ import com.zenmo.ztor.errorMessageToJson import com.zenmo.ztor.user.getUserId import com.zenmo.zummon.companysurvey.Survey import com.zenmo.zummon.companysurvey.Project +import com.zenmo.zummon.User + import io.ktor.http.* import io.ktor.serialization.* import io.ktor.server.application.* @@ -28,6 +30,68 @@ fun Application.configureDatabases(): Database { val deeplinkService = DeeplinkService(DeeplinkRepository(db)) routing { + // List users for current user + get("/users") { + call.respond(HttpStatusCode.OK, userRepository.getUsers()) + } + + // Get one user that belongs to the user + get("/users/{userId}") { + val userId = UUID.fromString(call.parameters["userId"]) + + val adminUserId = call.getUserId() + if (adminUserId == null) { + call.respond(HttpStatusCode.Unauthorized) + return@get + } + + call.respond(HttpStatusCode.OK, userRepository.getUserById(userId)) + } + + // Create + post("/users") { + val user: User? + try { + user = call.receive() + } catch (e: BadRequestException) { + if (e.cause is JsonConvertException) { + call.respond(HttpStatusCode.BadRequest, errorMessageToJson(e.cause?.message)) + return@post + } + call.respond(HttpStatusCode.BadRequest, errorMessageToJson(e.message)) + return@post + } + + val userId = call.getUserId() + if (userId == null) { + call.respond(HttpStatusCode.Unauthorized) + return@post + } + + val newUser = userRepository.save(user) + + call.respond(HttpStatusCode.Created, newUser) + } + + // Update + put("/users") { + val user: User? + try { + user = call.receive() + } catch (e: BadRequestException) { + if (e.cause is JsonConvertException) { + call.respond(HttpStatusCode.BadRequest, errorMessageToJson(e.cause?.message)) + return@put + } + call.respond(HttpStatusCode.BadRequest, errorMessageToJson(e.message)) + return@put + } + + val newUser = userRepository.save(user) + + call.respond(HttpStatusCode.OK, newUser) + } + // List projects for current user get("/projects") { val userId = call.getUserId() @@ -68,7 +132,6 @@ fun Application.configureDatabases(): Database { } val userId = call.getUserId() - println("User ID: $userId") if (userId == null) { call.respond(HttpStatusCode.Unauthorized) return@post diff --git a/zummon/src/commonMain/kotlin/User.kt b/zummon/src/commonMain/kotlin/User.kt index cc882379..0ac1339f 100644 --- a/zummon/src/commonMain/kotlin/User.kt +++ b/zummon/src/commonMain/kotlin/User.kt @@ -1,9 +1,12 @@ package com.zenmo.zummon + +import com.benasher44.uuid.Uuid +import com.benasher44.uuid.uuid4 +import com.zenmo.zummon.BenasherUuidSerializer import com.zenmo.zummon.companysurvey.Project import kotlinx.serialization.Serializable import kotlin.js.JsExport -import kotlin.uuid.Uuid /** * This object is intended to be enriched with Keycloak data. @@ -11,8 +14,20 @@ import kotlin.uuid.Uuid */ @Serializable @JsExport -data class User( - val id: Uuid = Uuid.random(), +data class User +constructor( + @Serializable(with = BenasherUuidSerializer::class) + val id: Uuid = uuid4(), val note: String, val projects: List = emptyList() ) + +@JsExport +fun usersFromJson(json: String): Array { + return kotlinx.serialization.json.Json.decodeFromString>(json) +} + +@JsExport +fun userFromJson(json: String): User { + return kotlinx.serialization.json.Json.decodeFromString(json) +} From 35cc0bf69a18107295c9beab6a3af4064868b473 Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Sat, 11 Jan 2025 16:53:15 -0500 Subject: [PATCH 07/30] Add Admin flag on User Admin flag to determine if the user has admin privileges. --- frontend/src/admin/user-form.tsx | 19 ++++++++++++++-- frontend/src/admin/users.tsx | 14 +++++++++++- migrations/V33__admin_flag_to_user.sql | 1 + .../com/zenmo/orm/user/UserRepository.kt | 7 ++++++ .../kotlin/com/zenmo/orm/user/table/User.kt | 5 +++++ .../com/zenmo/ztor/plugins/Databases.kt | 22 ++++++++++++++++++- zummon/src/commonMain/kotlin/User.kt | 4 ++-- 7 files changed, 66 insertions(+), 6 deletions(-) create mode 100644 migrations/V33__admin_flag_to_user.sql diff --git a/frontend/src/admin/user-form.tsx b/frontend/src/admin/user-form.tsx index f4c5cb6b..2a788d88 100644 --- a/frontend/src/admin/user-form.tsx +++ b/frontend/src/admin/user-form.tsx @@ -27,8 +27,10 @@ export const UserForm: FunctionComponent = () => { }; const handleInputChange =(e: React.ChangeEvent) => { - const { name, value } = e.target; - setUser((prev) => ({ ...prev, [name]: value } as User)); + const { name, value, type, checked } = e.target; + setUser((prev) => ({...prev, + [name]: type === "checkbox" ? checked : value, + } as User)); }; useEffect(() => { @@ -109,6 +111,19 @@ export const UserForm: FunctionComponent = () => { onChange={handleInputChange} disabled={!isEditing} /> +
+ +
{isEditing ? ( diff --git a/frontend/src/admin/users.tsx b/frontend/src/admin/users.tsx index b22fb9ff..e19b1a58 100644 --- a/frontend/src/admin/users.tsx +++ b/frontend/src/admin/users.tsx @@ -40,7 +40,19 @@ export const Users: FunctionComponent = () => { filterDisplay="row" > - + ( +
+ {user.isAdmin ? ( + + ) : ( + + )} +
+ )} + /> (
= emptyList() + val projects: List = emptyList(), + val isAdmin: Boolean = false ) @JsExport From 19be1c799ccae2592d396cea65a46288f081141f Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Mon, 13 Jan 2025 10:41:10 -0500 Subject: [PATCH 08/30] hide users in menu --- frontend/src/admin/use-users.ts | 6 ----- frontend/src/components/zero-header.tsx | 4 +-- .../com/zenmo/ztor/plugins/Databases.kt | 26 +++++++------------ 3 files changed, 11 insertions(+), 25 deletions(-) diff --git a/frontend/src/admin/use-users.ts b/frontend/src/admin/use-users.ts index 5f610843..02744110 100644 --- a/frontend/src/admin/use-users.ts +++ b/frontend/src/admin/use-users.ts @@ -1,7 +1,6 @@ import {useState} from "react"; import {useOnce} from "../hooks/use-once"; import {User, usersFromJson } from "zero-zummon" -import {useNavigate} from "react-router-dom"; type UseUserReturn = { loadingUsers: boolean, @@ -10,11 +9,6 @@ type UseUserReturn = { removeUser: (userId: string) => void, } -type UseUserData = { - loadingUser: boolean, - user: User, -} - export const useUsers = (): UseUserReturn => { const [loadingUsers, setLoading] = useState(true) const [users, setUsers] = useState([]) diff --git a/frontend/src/components/zero-header.tsx b/frontend/src/components/zero-header.tsx index eee5ae20..2f7dc351 100644 --- a/frontend/src/components/zero-header.tsx +++ b/frontend/src/components/zero-header.tsx @@ -72,10 +72,10 @@ export const ZeroHeader: FunctionComponent = () => { Projects - loadContent('/users')} css={buttonStyle}> + {/* loadContent('/users')} css={buttonStyle}> Users - + */} loadContent('/simulation')} css={buttonStyle}> Simulation diff --git a/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt b/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt index 24308388..1e3308a1 100644 --- a/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt +++ b/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt @@ -35,23 +35,18 @@ fun Application.configureDatabases(): Database { get("/users") { val userId = call.getUserId() - println("userId " + userId) - - // if (userId == null) { - // call.respond(HttpStatusCode.Unauthorized) - // return@get - // } - - // val isAdmin = userRepository.isAdmin(userId) - // println("isAdmin $isAdmin") + if (userId == null) { + call.respond(HttpStatusCode.Unauthorized) + return@get + } + val isAdmin = userRepository.isAdmin(userId) - // if (!isAdmin) { - // call.respond(HttpStatusCode.Forbidden, "Access denied") - // return@get - // } + if (!isAdmin) { + call.respond(HttpStatusCode.Forbidden, "Access denied") + return@get + } val users = userRepository.getUsers() - println("All users " + users) call.respond(HttpStatusCode.OK, users) } @@ -128,7 +123,6 @@ fun Application.configureDatabases(): Database { val projectId = UUID.fromString(call.parameters["projectId"]) val userId = call.getUserId() - println("User ID: $userId") if (userId == null) { call.respond(HttpStatusCode.Unauthorized) return@get @@ -265,7 +259,6 @@ fun Application.configureDatabases(): Database { delete("/company-surveys/{surveyId}") { val userId = call.getUserId() - println("User ID: $userId") if (userId == null) { call.respond(HttpStatusCode.Unauthorized) return@delete @@ -303,7 +296,6 @@ fun Application.configureDatabases(): Database { // set active state put("/company-surveys/{surveyId}/include-in-simulation") { val userId = call.getUserId() - println("User ID: $userId") if (userId == null) { call.respond(HttpStatusCode.Unauthorized) return@put From 3e4de10fff973e0534cd3ad03cfc1d56751c2b96 Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Tue, 14 Jan 2025 23:37:11 -0500 Subject: [PATCH 09/30] show is admin in menu --- frontend/src/components/zero-header.tsx | 13 +++-- frontend/src/user/use-user.ts | 65 +++++++++++++++++++++---- 2 files changed, 64 insertions(+), 14 deletions(-) diff --git a/frontend/src/components/zero-header.tsx b/frontend/src/components/zero-header.tsx index 2f7dc351..710bab37 100644 --- a/frontend/src/components/zero-header.tsx +++ b/frontend/src/components/zero-header.tsx @@ -3,6 +3,7 @@ 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"; const sidebarStyle = css({ width: '16rem', @@ -31,6 +32,8 @@ const buttonStyle = css({ }); export const ZeroHeader: FunctionComponent = () => { + const { isLoading, isLoggedIn, username, isAdmin } = useUser() + const [visible, setVisible] = useState(false); const navigate = useNavigate(); @@ -72,10 +75,12 @@ export const ZeroHeader: FunctionComponent = () => { Projects - {/* loadContent('/users')} css={buttonStyle}> - - Users - */} + {isAdmin && ( + loadContent('/users')} css={buttonStyle}> + + Users + + )} loadContent('/simulation')} css={buttonStyle}> Simulation diff --git a/frontend/src/user/use-user.ts b/frontend/src/user/use-user.ts index b8936023..5caa35e9 100644 --- a/frontend/src/user/use-user.ts +++ b/frontend/src/user/use-user.ts @@ -1,36 +1,77 @@ import {useOnce} from "../hooks/use-once"; -import {useState} from "react"; +import {useState, useEffect} from "react"; +import {User, userFromJson } from "zero-zummon" type UseUserReturn = { isLoading: boolean, isLoggedIn?: boolean, username?: string, + isAdmin?: boolean, } export const useUser = (): UseUserReturn => { + const [userId, setUserId] = useState() + const [state, setState] = useState({ isLoading: true, isLoggedIn: undefined, username: undefined, + isAdmin: false, }) + useEffect(() => { + if (userId) { + const fetchUser = async () => { + try { + const response = await fetch(`${import.meta.env.VITE_ZTOR_URL}/users/${userId}`, { + credentials: 'include', + }) + if (response.ok) { + const userData = await response.json(); + setState((prevState) => ({ + ...prevState, + isLoading: false, + isLoggedIn: true, + isAdmin: userData.isAdmin || false, + })); + } else { + alert(`Error fetching user: ${response.statusText}`); + } + } catch (error) { + alert((error as Error).message) + } finally { + setState({isLoading: false}) + } + }; + fetchUser(); + } + }, [userId]); + + 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.status === 401) { + redirectToLogin() + return + } + if (response.status === 500) { + return + } + + if (response.ok) { + const userInfo = await response.json(); + setUserId(userInfo.sub); + + setState((prevState) => ({ + ...prevState, isLoading: false, isLoggedIn: true, username: userInfo.preferred_username, - }) + })); } } catch (e) { console.error(e) @@ -43,4 +84,8 @@ export const useUser = (): UseUserReturn => { }) return state +} + +export const redirectToLogin = () => { + window.location.href = import.meta.env.VITE_ZTOR_URL + '/login?redirectUrl=' + encodeURIComponent(window.location.href) } \ No newline at end of file From c298179efbed9d500d5545a9bd8035a12b274195 Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Mon, 20 Jan 2025 16:50:14 -0500 Subject: [PATCH 10/30] Load is admin on user info --- compose.yaml | 4 +- frontend/src/user/use-user.ts | 38 +++---------------- .../com/zenmo/ztor/plugins/Authentication.kt | 18 ++++++++- .../kotlin/com/zenmo/ztor/user/userInfo.kt | 8 ++++ 4 files changed, 31 insertions(+), 37 deletions(-) create mode 100644 ztor/src/main/kotlin/com/zenmo/ztor/user/userInfo.kt diff --git a/compose.yaml b/compose.yaml index 5c561e69..baecf7fd 100644 --- a/compose.yaml +++ b/compose.yaml @@ -232,7 +232,7 @@ services: - 127.0.0.1:2015:2015 postgres: - image: postgres:17.2 + image: postgres:13.9 ports: - 127.0.0.1:5432:5432 volumes: @@ -242,7 +242,7 @@ services: - docker/local/postgres.env postgres-github-actions: - image: postgres:17.2 + image: postgres:13.9 environment: POSTGRES_PASSWORD: github-actions diff --git a/frontend/src/user/use-user.ts b/frontend/src/user/use-user.ts index 5caa35e9..c1319936 100644 --- a/frontend/src/user/use-user.ts +++ b/frontend/src/user/use-user.ts @@ -19,35 +19,6 @@ export const useUser = (): UseUserReturn => { isAdmin: false, }) - useEffect(() => { - if (userId) { - const fetchUser = async () => { - try { - const response = await fetch(`${import.meta.env.VITE_ZTOR_URL}/users/${userId}`, { - credentials: 'include', - }) - if (response.ok) { - const userData = await response.json(); - setState((prevState) => ({ - ...prevState, - isLoading: false, - isLoggedIn: true, - isAdmin: userData.isAdmin || false, - })); - } else { - alert(`Error fetching user: ${response.statusText}`); - } - } catch (error) { - alert((error as Error).message) - } finally { - setState({isLoading: false}) - } - }; - fetchUser(); - } - }, [userId]); - - useOnce(async () => { try { const response = await fetch(import.meta.env.VITE_ZTOR_URL + "/user-info", { @@ -58,19 +29,20 @@ export const useUser = (): UseUserReturn => { redirectToLogin() return } - if (response.status === 500) { - return + if (!response.ok) { + throw new Error(`Failed to get user: ${response.statusText}`) } if (response.ok) { const userInfo = await response.json(); - setUserId(userInfo.sub); + setUserId(userInfo.decodedAccessToken.sub); setState((prevState) => ({ ...prevState, isLoading: false, isLoggedIn: true, - username: userInfo.preferred_username, + username: userInfo.decodedAccessToken.preferred_username, + isAdmin: userInfo.isAdmin })); } } catch (e) { diff --git a/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Authentication.kt b/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Authentication.kt index a9260863..563dfadb 100644 --- a/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Authentication.kt +++ b/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Authentication.kt @@ -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.* @@ -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) { @@ -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) @@ -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") { diff --git a/ztor/src/main/kotlin/com/zenmo/ztor/user/userInfo.kt b/ztor/src/main/kotlin/com/zenmo/ztor/user/userInfo.kt new file mode 100644 index 00000000..5d44556d --- /dev/null +++ b/ztor/src/main/kotlin/com/zenmo/ztor/user/userInfo.kt @@ -0,0 +1,8 @@ +package com.zenmo.ztor.user +import kotlinx.serialization.Serializable + +@Serializable +data class UserInfo( + val isAdmin: Boolean, + val decodedAccessToken: AccessTokenPayload +) From 00a3c653b44a3eabf8be2dc1a8226688a69d5a35 Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Tue, 21 Jan 2025 13:53:10 -0500 Subject: [PATCH 11/30] fix type --- zorm/src/main/kotlin/com/zenmo/orm/user/UserRepository.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zorm/src/main/kotlin/com/zenmo/orm/user/UserRepository.kt b/zorm/src/main/kotlin/com/zenmo/orm/user/UserRepository.kt index ec1db042..2feae8a1 100644 --- a/zorm/src/main/kotlin/com/zenmo/orm/user/UserRepository.kt +++ b/zorm/src/main/kotlin/com/zenmo/orm/user/UserRepository.kt @@ -113,7 +113,7 @@ class UserRepository( protected fun hydrateUser(row: ResultRow): User { return User( - id = row[UserTable.id].toKotlinUuid(), + id = row[UserTable.id], note = row[UserTable.note], isAdmin = row[UserTable.isAdmin], projects = emptyList(), // data from different table From 35b638b28f293e7f77247d000248ce757bf6116e Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Tue, 21 Jan 2025 14:00:10 -0500 Subject: [PATCH 12/30] fix test --- .../com/zenmo/orm/companysurvey/SurveyRepositoryTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/zorm/src/test/kotlin/com/zenmo/orm/companysurvey/SurveyRepositoryTest.kt b/zorm/src/test/kotlin/com/zenmo/orm/companysurvey/SurveyRepositoryTest.kt index ed33ed65..893f0e8b 100644 --- a/zorm/src/test/kotlin/com/zenmo/orm/companysurvey/SurveyRepositoryTest.kt +++ b/zorm/src/test/kotlin/com/zenmo/orm/companysurvey/SurveyRepositoryTest.kt @@ -159,7 +159,7 @@ class SurveyRepositoryTest { val createdBy = surveyLoadedAfterCreate?.createdBy assertNotNull(createdBy) assertEquals("Jaap", createdBy.note) - assertEquals(jaapId.toKotlinUuid(), createdBy.id) + assertEquals(jaapId, createdBy.id) // edit survey surveyRepository.save(surveyLoadedAfterCreate, pietId) @@ -167,7 +167,7 @@ class SurveyRepositoryTest { val createdBy2 = surveyLoadedAfterEdit?.createdBy assertNotNull(createdBy2) assertEquals("Jaap", createdBy2.note) - assertEquals(jaapId.toKotlinUuid(), createdBy2.id) + assertEquals(jaapId, createdBy2.id) } private fun wipeSequence(survey: Survey) From e81255550842f87e2999afdbdf1a2acd48a74626 Mon Sep 17 00:00:00 2001 From: Erik van Velzen Date: Tue, 21 Jan 2025 22:13:10 +0100 Subject: [PATCH 13/30] Align uuid types --- .../com/zenmo/orm/companysurvey/SurveyRepository.kt | 2 +- .../main/kotlin/com/zenmo/orm/user/UserRepository.kt | 4 ++-- .../zenmo/orm/companysurvey/SurveyRepositoryTest.kt | 4 ++-- zummon/src/commonMain/kotlin/User.kt | 10 +++------- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/zorm/src/main/kotlin/com/zenmo/orm/companysurvey/SurveyRepository.kt b/zorm/src/main/kotlin/com/zenmo/orm/companysurvey/SurveyRepository.kt index cda4bec6..642dc0ef 100644 --- a/zorm/src/main/kotlin/com/zenmo/orm/companysurvey/SurveyRepository.kt +++ b/zorm/src/main/kotlin/com/zenmo/orm/companysurvey/SurveyRepository.kt @@ -267,7 +267,7 @@ class SurveyRepository( val userId = row[CompanySurveyTable.createdById] ?: return null return com.zenmo.zummon.User( - row[UserTable.id], + row[UserTable.id].toKotlinUuid(), row[UserTable.note], ) } diff --git a/zorm/src/main/kotlin/com/zenmo/orm/user/UserRepository.kt b/zorm/src/main/kotlin/com/zenmo/orm/user/UserRepository.kt index 2feae8a1..16bf213e 100644 --- a/zorm/src/main/kotlin/com/zenmo/orm/user/UserRepository.kt +++ b/zorm/src/main/kotlin/com/zenmo/orm/user/UserRepository.kt @@ -68,7 +68,7 @@ class UserRepository( ) { transaction(db) { UserTable.upsertReturning() { - it[id] = user.id + it[id] = user.id.toJavaUuid() it[UserTable.note] = user.note it[UserTable.isAdmin] = user.isAdmin }.map { @@ -113,7 +113,7 @@ class UserRepository( protected fun hydrateUser(row: ResultRow): User { return User( - id = row[UserTable.id], + id = row[UserTable.id].toKotlinUuid(), note = row[UserTable.note], isAdmin = row[UserTable.isAdmin], projects = emptyList(), // data from different table diff --git a/zorm/src/test/kotlin/com/zenmo/orm/companysurvey/SurveyRepositoryTest.kt b/zorm/src/test/kotlin/com/zenmo/orm/companysurvey/SurveyRepositoryTest.kt index 893f0e8b..ed33ed65 100644 --- a/zorm/src/test/kotlin/com/zenmo/orm/companysurvey/SurveyRepositoryTest.kt +++ b/zorm/src/test/kotlin/com/zenmo/orm/companysurvey/SurveyRepositoryTest.kt @@ -159,7 +159,7 @@ class SurveyRepositoryTest { val createdBy = surveyLoadedAfterCreate?.createdBy assertNotNull(createdBy) assertEquals("Jaap", createdBy.note) - assertEquals(jaapId, createdBy.id) + assertEquals(jaapId.toKotlinUuid(), createdBy.id) // edit survey surveyRepository.save(surveyLoadedAfterCreate, pietId) @@ -167,7 +167,7 @@ class SurveyRepositoryTest { val createdBy2 = surveyLoadedAfterEdit?.createdBy assertNotNull(createdBy2) assertEquals("Jaap", createdBy2.note) - assertEquals(jaapId, createdBy2.id) + assertEquals(jaapId.toKotlinUuid(), createdBy2.id) } private fun wipeSequence(survey: Survey) diff --git a/zummon/src/commonMain/kotlin/User.kt b/zummon/src/commonMain/kotlin/User.kt index 6966d06d..6bf576c8 100644 --- a/zummon/src/commonMain/kotlin/User.kt +++ b/zummon/src/commonMain/kotlin/User.kt @@ -1,11 +1,9 @@ package com.zenmo.zummon -import com.benasher44.uuid.Uuid -import com.benasher44.uuid.uuid4 -import com.zenmo.zummon.BenasherUuidSerializer import com.zenmo.zummon.companysurvey.Project import kotlinx.serialization.Serializable import kotlin.js.JsExport +import kotlin.uuid.Uuid /** * This object is intended to be enriched with Keycloak data. @@ -13,10 +11,8 @@ import kotlin.js.JsExport */ @Serializable @JsExport -data class User -constructor( - @Serializable(with = BenasherUuidSerializer::class) - val id: Uuid = uuid4(), +data class User( + val id: Uuid = Uuid.random(), val note: String, val projects: List = emptyList(), val isAdmin: Boolean = false From 4653fa2f5a084dfc6cbe6f266d15eed8643dd554 Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Wed, 22 Jan 2025 10:00:26 -0500 Subject: [PATCH 14/30] clean code --- frontend/src/user/use-user.ts | 22 ++++++++----------- .../com/zenmo/ztor/plugins/Databases.kt | 18 --------------- 2 files changed, 9 insertions(+), 31 deletions(-) diff --git a/frontend/src/user/use-user.ts b/frontend/src/user/use-user.ts index c1319936..47cbac9d 100644 --- a/frontend/src/user/use-user.ts +++ b/frontend/src/user/use-user.ts @@ -1,6 +1,6 @@ import {useOnce} from "../hooks/use-once"; -import {useState, useEffect} from "react"; -import {User, userFromJson } from "zero-zummon" +import {useState} from "react"; +import { redirectToLogin } from "../admin/use-users"; type UseUserReturn = { isLoading: boolean, @@ -10,8 +10,6 @@ type UseUserReturn = { } export const useUser = (): UseUserReturn => { - const [userId, setUserId] = useState() - const [state, setState] = useState({ isLoading: true, isLoggedIn: undefined, @@ -25,17 +23,19 @@ export const useUser = (): UseUserReturn => { credentials: "include", }) - if (response.status === 401) { - redirectToLogin() - return - } 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(); - setUserId(userInfo.decodedAccessToken.sub); setState((prevState) => ({ ...prevState, @@ -56,8 +56,4 @@ export const useUser = (): UseUserReturn => { }) return state -} - -export const redirectToLogin = () => { - window.location.href = import.meta.env.VITE_ZTOR_URL + '/login?redirectUrl=' + encodeURIComponent(window.location.href) } \ No newline at end of file diff --git a/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt b/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt index a0f30132..5848cd14 100644 --- a/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt +++ b/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt @@ -23,8 +23,6 @@ import io.ktor.server.routing.* import org.jetbrains.exposed.sql.Database import java.util.* - - fun Application.configureDatabases(): Database { val db: Database = connectToPostgres() val userRepository = UserRepository(db) @@ -32,22 +30,6 @@ fun Application.configureDatabases(): Database { val projectRepository = ProjectRepository(db) val deeplinkService = DeeplinkService(DeeplinkRepository(db)) - suspend fun authenticateAndAuthorize(call: ApplicationCall, userRepository: UserRepository): Boolean { - val userId = call.getUserId() - if (userId == null) { - call.respond(HttpStatusCode.Unauthorized, "User not authenticated") - return false - } - - val isAdmin = userRepository.isAdmin(userId) - if (!isAdmin) { - call.respond(HttpStatusCode.Forbidden, "Access denied") - return false - } - - return true - } - routing { suspend fun RoutingContext.asAdmin(body: suspend () -> Unit) { val userId = call.getUserId() From 46988bc4349a05cdd27588edd2649b0ae7ab03e0 Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Wed, 22 Jan 2025 10:34:54 -0500 Subject: [PATCH 15/30] Add log in buttom --- frontend/src/components/zero-header.tsx | 32 ++++++++++++++++++------- frontend/src/user/use-user.ts | 3 +-- 2 files changed, 25 insertions(+), 10 deletions(-) diff --git a/frontend/src/components/zero-header.tsx b/frontend/src/components/zero-header.tsx index 710bab37..9f9d8267 100644 --- a/frontend/src/components/zero-header.tsx +++ b/frontend/src/components/zero-header.tsx @@ -4,6 +4,7 @@ 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', @@ -43,6 +44,7 @@ export const ZeroHeader: FunctionComponent = () => { } return ( setVisible(false)} css={sidebarStyle}> diff --git a/frontend/src/user/use-user.ts b/frontend/src/user/use-user.ts index 47cbac9d..45cb2604 100644 --- a/frontend/src/user/use-user.ts +++ b/frontend/src/user/use-user.ts @@ -1,6 +1,5 @@ import {useOnce} from "../hooks/use-once"; import {useState} from "react"; -import { redirectToLogin } from "../admin/use-users"; type UseUserReturn = { isLoading: boolean, @@ -26,7 +25,7 @@ export const useUser = (): UseUserReturn => { if (!response.ok) { throw new Error(`Failed to get user: ${response.statusText}`) } - + if (response.status === 500) { setState(prevState => ({ ...prevState, From b255024652d6564fc8315f9e1a702ace9d31ce6f Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Wed, 22 Jan 2025 12:49:42 -0500 Subject: [PATCH 16/30] change version join --- compose.yaml | 4 ++-- frontend/src/admin/use-projects.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/compose.yaml b/compose.yaml index baecf7fd..5c561e69 100644 --- a/compose.yaml +++ b/compose.yaml @@ -232,7 +232,7 @@ services: - 127.0.0.1:2015:2015 postgres: - image: postgres:13.9 + image: postgres:17.2 ports: - 127.0.0.1:5432:5432 volumes: @@ -242,7 +242,7 @@ services: - docker/local/postgres.env postgres-github-actions: - image: postgres:13.9 + image: postgres:17.2 environment: POSTGRES_PASSWORD: github-actions diff --git a/frontend/src/admin/use-projects.ts b/frontend/src/admin/use-projects.ts index d45749c2..b1f1a6ee 100644 --- a/frontend/src/admin/use-projects.ts +++ b/frontend/src/admin/use-projects.ts @@ -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())) From 691f237a9e93db69fc7e536bf11bc826685ed988 Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Wed, 22 Jan 2025 13:06:05 -0500 Subject: [PATCH 17/30] List projects in the user page --- frontend/src/admin/user-form.tsx | 2 + frontend/src/admin/user-projects-list.tsx | 47 +++++++++++++++++++ .../com/zenmo/ztor/plugins/Databases.kt | 8 ++++ 3 files changed, 57 insertions(+) create mode 100644 frontend/src/admin/user-projects-list.tsx diff --git a/frontend/src/admin/user-form.tsx b/frontend/src/admin/user-form.tsx index 98672e58..b6369197 100644 --- a/frontend/src/admin/user-form.tsx +++ b/frontend/src/admin/user-form.tsx @@ -5,6 +5,7 @@ import { InputText } from "primereact/inputtext"; import { Button } from "primereact/button"; import { User } from "zero-zummon"; import { redirectToLogin } from "./use-users"; +import { UserProjectsList } from "./user-projects-list"; export const UserForm: FunctionComponent = () => { const {userId} = useParams<{ userId: string }>(); @@ -142,6 +143,7 @@ export const UserForm: FunctionComponent = () => { )}
+
); diff --git a/frontend/src/admin/user-projects-list.tsx b/frontend/src/admin/user-projects-list.tsx new file mode 100644 index 00000000..7551e14e --- /dev/null +++ b/frontend/src/admin/user-projects-list.tsx @@ -0,0 +1,47 @@ +import React, { useEffect, FunctionComponent, useState } from "react"; +import { Project, projectsFromJson } from "zero-zummon"; +import { DataTable } from "primereact/datatable"; +import { Column } from "primereact/column"; +import { EditButton } from "./edit-button"; + +type UserProjectsListProps = { + userId?: string; +}; + +export const UserProjectsList: FunctionComponent = ({ + userId, +}) => { + const [projects, setProjects] = useState([]); + + useEffect(() => { + const fetchProjects = async () => { + try { + const response = await fetch(`${import.meta.env.VITE_ZTOR_URL}/users/${userId}/projects`, { + credentials: "include", + }); + if (!response.ok) return + + setProjects(projectsFromJson(await response.text())) + } catch (error) { + console.error("Error fetching projects:", error); + } + }; + + fetchProjects(); + }); + + return ( +
+

Current Projects

+ + + ( + + )} + header="Actions" + /> + +
+ ); +}; diff --git a/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt b/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt index 5848cd14..f62b9ed3 100644 --- a/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt +++ b/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt @@ -62,6 +62,14 @@ fun Application.configureDatabases(): Database { } } + get("/users/{userId}/projects") { + asAdmin { + val userId = UUID.fromString(call.parameters["userId"]) + val user = projectRepository.getProjectsByUserId(userId) + call.respond(HttpStatusCode.OK, user) + } + } + // Create post("/users") { val user: User? From f6d407e8d43abf56deb8323702faa80e677443a1 Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Wed, 22 Jan 2025 13:18:18 -0500 Subject: [PATCH 18/30] add dropdown to select projects --- frontend/src/admin/projects-dropdown.tsx | 51 ++++++++++++++++++++++++ frontend/src/admin/user-form.tsx | 10 ++++- 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 frontend/src/admin/projects-dropdown.tsx diff --git a/frontend/src/admin/projects-dropdown.tsx b/frontend/src/admin/projects-dropdown.tsx new file mode 100644 index 00000000..307ae255 --- /dev/null +++ b/frontend/src/admin/projects-dropdown.tsx @@ -0,0 +1,51 @@ +import React, { useEffect, FunctionComponent, useState } from "react"; + +import { MultiSelect } from "primereact/multiselect"; +import { Project, projectsFromJson } from "zero-zummon"; + +type ProjectDropdownProps = { + selectedProjects: Project[]; + onChange: (selectedProjects: Project[]) => void; + disabled?: boolean; +}; + +export const ProjectsDropdown: FunctionComponent = ({ + selectedProjects, + onChange, + disabled, +}) => { + const [projects, setProjects] = useState([]); + + useEffect(() => { + const fetchProjects = async () => { + try { + const response = await fetch(`${import.meta.env.VITE_ZTOR_URL}/all-projects`, { + credentials: "include", + }); + if (!response.ok) return + + setProjects(projectsFromJson(await response.text())) + } catch (error) { + console.error("Error fetching projects:", error); + } + }; + + fetchProjects(); + }); + + return ( +
+ + ({ + label: project.name, + value: project, + }))} + value={selectedProjects} + onChange={(e) => onChange(e.value)} + disabled={disabled} + /> +
+ ); +}; diff --git a/frontend/src/admin/user-form.tsx b/frontend/src/admin/user-form.tsx index 98672e58..37ae5143 100644 --- a/frontend/src/admin/user-form.tsx +++ b/frontend/src/admin/user-form.tsx @@ -3,13 +3,15 @@ import { useParams, useNavigate } from "react-router-dom"; import { PrimeReactProvider } from "primereact/api"; import { InputText } from "primereact/inputtext"; import { Button } from "primereact/button"; -import { User } from "zero-zummon"; +import { User, Project } from "zero-zummon"; import { redirectToLogin } from "./use-users"; +import { ProjectsDropdown } from "./projects-dropdown"; export const UserForm: FunctionComponent = () => { const {userId} = useParams<{ userId: string }>(); const [user, setUser] = useState(null); const [originalData, setOriginalData] = useState(null); + const [assignedProjects, setAssignedProjects] = useState([]); const [loading, setLoading] = useState(false); const [isEditing, setIsEditing] = useState(false); @@ -131,6 +133,12 @@ export const UserForm: FunctionComponent = () => {
+ +
{isEditing ? ( <> From 182d418b3a81553db7f0e6d2db244f58d819275d Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Wed, 22 Jan 2025 14:55:11 -0500 Subject: [PATCH 19/30] Default values --- frontend/src/admin/user-form.tsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/frontend/src/admin/user-form.tsx b/frontend/src/admin/user-form.tsx index bdec90dd..ba1ba597 100644 --- a/frontend/src/admin/user-form.tsx +++ b/frontend/src/admin/user-form.tsx @@ -79,17 +79,25 @@ export const UserForm: FunctionComponent = () => { "Content-Type": "application/json", }, credentials: "include", - body: JSON.stringify(user), + body: JSON.stringify({ + ...user, + projectIds: assignedProjects.map((project) => project.id), // Pass project IDs + }), }); + if (response.status === 401) { - redirectToLogin(); return; } - if (!response.ok) { + + if (response.ok) { + const userData = await response.json(); + setUser(userData); + setOriginalData(userData); + setUserProjects(assignedProjects); + } else { alert(`Error: ${response.statusText}`); } - navigate(`/users`); } finally { setIsEditing(false); setLoading(false); @@ -135,7 +143,7 @@ export const UserForm: FunctionComponent = () => {
From 87a170b4c800a9c6f80bf3c65b72609d6599bf41 Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Wed, 22 Jan 2025 14:59:19 -0500 Subject: [PATCH 20/30] Pass projects instead --- frontend/src/admin/user-form.tsx | 25 +++++++++++++----- frontend/src/admin/user-projects-list.tsx | 31 +++++------------------ 2 files changed, 25 insertions(+), 31 deletions(-) diff --git a/frontend/src/admin/user-form.tsx b/frontend/src/admin/user-form.tsx index b6369197..22428fb5 100644 --- a/frontend/src/admin/user-form.tsx +++ b/frontend/src/admin/user-form.tsx @@ -1,9 +1,9 @@ import React, { FormEvent, FunctionComponent, useEffect, useState } from "react"; -import { useParams, useNavigate } from "react-router-dom"; +import { useParams } from "react-router-dom"; import { PrimeReactProvider } from "primereact/api"; import { InputText } from "primereact/inputtext"; import { Button } from "primereact/button"; -import { User } from "zero-zummon"; +import { User, Project, projectsFromJson } from "zero-zummon"; import { redirectToLogin } from "./use-users"; import { UserProjectsList } from "./user-projects-list"; @@ -11,10 +11,10 @@ export const UserForm: FunctionComponent = () => { const {userId} = useParams<{ userId: string }>(); const [user, setUser] = useState(null); const [originalData, setOriginalData] = useState(null); + const [userProjects, setUserProjects] = useState([]); const [loading, setLoading] = useState(false); const [isEditing, setIsEditing] = useState(false); - const navigate = useNavigate(); const handleCancel = () => { if (originalData) { @@ -59,6 +59,21 @@ export const UserForm: FunctionComponent = () => { setLoading(false); } }; + + const fetchProjects = async () => { + try { + const response = await fetch(`${import.meta.env.VITE_ZTOR_URL}/users/${userId}/projects`, { + credentials: "include", + }); + if (!response.ok) return + + setUserProjects(projectsFromJson(await response.text())) + } catch (error) { + console.error("Error fetching projects:", error); + } + }; + + fetchProjects(); fetchUser(); } else { setIsEditing(true); @@ -86,8 +101,6 @@ export const UserForm: FunctionComponent = () => { if (!response.ok) { alert(`Error: ${response.statusText}`); } - - navigate(`/users`); } finally { setIsEditing(false); setLoading(false); @@ -143,7 +156,7 @@ export const UserForm: FunctionComponent = () => { )}
- + ); diff --git a/frontend/src/admin/user-projects-list.tsx b/frontend/src/admin/user-projects-list.tsx index 7551e14e..de7c9e3d 100644 --- a/frontend/src/admin/user-projects-list.tsx +++ b/frontend/src/admin/user-projects-list.tsx @@ -1,35 +1,16 @@ -import React, { useEffect, FunctionComponent, useState } from "react"; -import { Project, projectsFromJson } from "zero-zummon"; -import { DataTable } from "primereact/datatable"; -import { Column } from "primereact/column"; +import { FunctionComponent } from "react"; +import { Project } from "zero-zummon"; +import { DataTable } from 'primereact/datatable'; +import { Column } from 'primereact/column'; import { EditButton } from "./edit-button"; type UserProjectsListProps = { - userId?: string; + projects?: Project[]; }; export const UserProjectsList: FunctionComponent = ({ - userId, + projects, }) => { - const [projects, setProjects] = useState([]); - - useEffect(() => { - const fetchProjects = async () => { - try { - const response = await fetch(`${import.meta.env.VITE_ZTOR_URL}/users/${userId}/projects`, { - credentials: "include", - }); - if (!response.ok) return - - setProjects(projectsFromJson(await response.text())) - } catch (error) { - console.error("Error fetching projects:", error); - } - }; - - fetchProjects(); - }); - return (

Current Projects

From aafc0a526f48550dfecc2b3a4992bec8fab6f6ac Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Thu, 23 Jan 2025 18:53:42 -0500 Subject: [PATCH 21/30] Pre select projects --- frontend/src/admin/projects-dropdown.tsx | 2 +- frontend/src/admin/user-form.tsx | 11 ++++++----- .../main/kotlin/com/zenmo/ztor/plugins/Databases.kt | 10 ++++++++++ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/frontend/src/admin/projects-dropdown.tsx b/frontend/src/admin/projects-dropdown.tsx index 307ae255..aca606dd 100644 --- a/frontend/src/admin/projects-dropdown.tsx +++ b/frontend/src/admin/projects-dropdown.tsx @@ -35,7 +35,7 @@ export const ProjectsDropdown: FunctionComponent = ({ return (
- + ({ diff --git a/frontend/src/admin/user-form.tsx b/frontend/src/admin/user-form.tsx index 4baa5e9f..d6d62ef7 100644 --- a/frontend/src/admin/user-form.tsx +++ b/frontend/src/admin/user-form.tsx @@ -12,7 +12,7 @@ export const UserForm: FunctionComponent = () => { const {userId} = useParams<{ userId: string }>(); const [user, setUser] = useState(null); const [originalData, setOriginalData] = useState(null); - const [assignedProjects, setAssignedProjects] = useState([]); + const [selectedProjects, setSelectedProjects] = useState([]); const [userProjects, setUserProjects] = useState([]); const [loading, setLoading] = useState(false); @@ -70,6 +70,7 @@ export const UserForm: FunctionComponent = () => { if (!response.ok) return setUserProjects(projectsFromJson(await response.text())) + setSelectedProjects(userProjects) } catch (error) { console.error("Error fetching projects:", error); } @@ -96,7 +97,7 @@ export const UserForm: FunctionComponent = () => { credentials: "include", body: JSON.stringify({ ...user, - projectIds: assignedProjects.map((project) => project.id), // Pass project IDs + projectIds: selectedProjects.map((project) => project.id), // Pass project IDs }), }); @@ -108,7 +109,7 @@ export const UserForm: FunctionComponent = () => { const userData = await response.json(); setUser(userData); setOriginalData(userData); - setUserProjects(assignedProjects); + setUserProjects(selectedProjects); } else { alert(`Error: ${response.statusText}`); } @@ -157,8 +158,8 @@ export const UserForm: FunctionComponent = () => {
diff --git a/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt b/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt index f62b9ed3..689bb695 100644 --- a/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt +++ b/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt @@ -116,6 +116,16 @@ fun Application.configureDatabases(): Database { } } + get("/all-projects") { + val userId = call.getUserId() + if (userId == null) { + call.respond(HttpStatusCode.Unauthorized) + return@get + } + + call.respond(HttpStatusCode.OK, projectRepository.getProjects()) + } + get("/projects") { val userId = call.getUserId() if (userId == null) { From e1fa0476ead78f2869d4f9185cd6ea1ea34a4cfc Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Thu, 23 Jan 2025 19:03:18 -0500 Subject: [PATCH 22/30] Cancel edit --- frontend/src/admin/user-form.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/frontend/src/admin/user-form.tsx b/frontend/src/admin/user-form.tsx index d6d62ef7..6fa914e2 100644 --- a/frontend/src/admin/user-form.tsx +++ b/frontend/src/admin/user-form.tsx @@ -19,8 +19,10 @@ export const UserForm: FunctionComponent = () => { const [isEditing, setIsEditing] = useState(false); const handleCancel = () => { - if (originalData) { - setUser(originalData); // Revert to original data + if (originalData) { // Revert to original data + setUser(originalData); + setSelectedProjects(userProjects) + } setIsEditing(false); }; @@ -68,9 +70,10 @@ export const UserForm: FunctionComponent = () => { credentials: "include", }); if (!response.ok) return - - setUserProjects(projectsFromJson(await response.text())) - setSelectedProjects(userProjects) + + const projectData = projectsFromJson(await response.text()) + setUserProjects(projectData) + setSelectedProjects(projectData) } catch (error) { console.error("Error fetching projects:", error); } From fab0fde3b685b88207acb410efc69e7c8fc805f7 Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Fri, 24 Jan 2025 13:27:11 -0500 Subject: [PATCH 23/30] Save Projects Work in projects in users. Projects deserialization is not working --- frontend/src/admin/projects-dropdown.tsx | 2 +- frontend/src/admin/user-form.tsx | 37 +++++++++++++++++------- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/frontend/src/admin/projects-dropdown.tsx b/frontend/src/admin/projects-dropdown.tsx index aca606dd..3833c4d2 100644 --- a/frontend/src/admin/projects-dropdown.tsx +++ b/frontend/src/admin/projects-dropdown.tsx @@ -31,7 +31,7 @@ export const ProjectsDropdown: FunctionComponent = ({ }; fetchProjects(); - }); + }, []); return (
diff --git a/frontend/src/admin/user-form.tsx b/frontend/src/admin/user-form.tsx index 6fa914e2..8484604e 100644 --- a/frontend/src/admin/user-form.tsx +++ b/frontend/src/admin/user-form.tsx @@ -1,4 +1,4 @@ -import React, { FormEvent, FunctionComponent, useEffect, useState } from "react"; +import React, { FormEvent, FunctionComponent, useEffect, useState, useRef } from "react"; import { useParams } from "react-router-dom"; import { PrimeReactProvider } from "primereact/api"; import { InputText } from "primereact/inputtext"; @@ -7,6 +7,8 @@ import { User, Project, projectsFromJson } from "zero-zummon"; import { redirectToLogin } from "./use-users"; import { ProjectsDropdown } from "./projects-dropdown"; import { UserProjectsList } from "./user-projects-list"; +import { Messages } from 'primereact/messages'; +import { Toast } from "primereact/toast"; export const UserForm: FunctionComponent = () => { const {userId} = useParams<{ userId: string }>(); @@ -14,6 +16,8 @@ export const UserForm: FunctionComponent = () => { const [originalData, setOriginalData] = useState(null); const [selectedProjects, setSelectedProjects] = useState([]); const [userProjects, setUserProjects] = useState([]); + // const msgs = useRef(null); + const msgs = useRef(null); const [loading, setLoading] = useState(false); const [isEditing, setIsEditing] = useState(false); @@ -90,6 +94,14 @@ export const UserForm: FunctionComponent = () => { event.preventDefault(); setLoading(true); try { + const sendUser = JSON.stringify({ + ...user, + projects: selectedProjects.map((project) => ({ + id: project.id.toString(), + name: project.name, + })), + }) + console.log("sendUser " + sendUser) const method = userId ? "PUT" : "POST"; const url = `${import.meta.env.VITE_ZTOR_URL}/users` const response = await fetch(url, { @@ -98,10 +110,7 @@ export const UserForm: FunctionComponent = () => { "Content-Type": "application/json", }, credentials: "include", - body: JSON.stringify({ - ...user, - projectIds: selectedProjects.map((project) => project.id), // Pass project IDs - }), + body: sendUser, }); if (response.status === 401) { @@ -109,12 +118,17 @@ export const UserForm: FunctionComponent = () => { } if (response.ok) { - const userData = await response.json(); - setUser(userData); - setOriginalData(userData); - setUserProjects(selectedProjects); + msgs.current?.show([ + { sticky: true, severity: "success", summary: "Success", detail: "User saved successfully.", closable: true }, + ]); + // const userData = await response.json(); + // setUser(userData); + // setOriginalData(userData); + // setUserProjects(selectedProjects); } else { - alert(`Error: ${response.statusText}`); + msgs.current?.show([ + {sticky: true, severity: 'error', summary: 'Error', detail: `Error: ${response.statusText}`, closable: false}, + ]); } } finally { setIsEditing(false); @@ -124,6 +138,7 @@ export const UserForm: FunctionComponent = () => { return ( +

{userId ? "Edit User" : "Add User"}

{ onChange={setSelectedProjects} disabled={!isEditing} /> +
{isEditing ? ( @@ -177,6 +193,7 @@ export const UserForm: FunctionComponent = () => { )}
+
From 629b54f1c19073524c6165e89a8b61a025c2bfb8 Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Wed, 22 Jan 2025 13:06:05 -0500 Subject: [PATCH 24/30] List projects in the user page --- frontend/src/admin/user-form.tsx | 2 + frontend/src/admin/user-projects-list.tsx | 47 +++++++++++++++++++ .../com/zenmo/ztor/plugins/Databases.kt | 8 ++++ 3 files changed, 57 insertions(+) create mode 100644 frontend/src/admin/user-projects-list.tsx diff --git a/frontend/src/admin/user-form.tsx b/frontend/src/admin/user-form.tsx index 98672e58..b6369197 100644 --- a/frontend/src/admin/user-form.tsx +++ b/frontend/src/admin/user-form.tsx @@ -5,6 +5,7 @@ import { InputText } from "primereact/inputtext"; import { Button } from "primereact/button"; import { User } from "zero-zummon"; import { redirectToLogin } from "./use-users"; +import { UserProjectsList } from "./user-projects-list"; export const UserForm: FunctionComponent = () => { const {userId} = useParams<{ userId: string }>(); @@ -142,6 +143,7 @@ export const UserForm: FunctionComponent = () => { )}
+
); diff --git a/frontend/src/admin/user-projects-list.tsx b/frontend/src/admin/user-projects-list.tsx new file mode 100644 index 00000000..7551e14e --- /dev/null +++ b/frontend/src/admin/user-projects-list.tsx @@ -0,0 +1,47 @@ +import React, { useEffect, FunctionComponent, useState } from "react"; +import { Project, projectsFromJson } from "zero-zummon"; +import { DataTable } from "primereact/datatable"; +import { Column } from "primereact/column"; +import { EditButton } from "./edit-button"; + +type UserProjectsListProps = { + userId?: string; +}; + +export const UserProjectsList: FunctionComponent = ({ + userId, +}) => { + const [projects, setProjects] = useState([]); + + useEffect(() => { + const fetchProjects = async () => { + try { + const response = await fetch(`${import.meta.env.VITE_ZTOR_URL}/users/${userId}/projects`, { + credentials: "include", + }); + if (!response.ok) return + + setProjects(projectsFromJson(await response.text())) + } catch (error) { + console.error("Error fetching projects:", error); + } + }; + + fetchProjects(); + }); + + return ( +
+

Current Projects

+ + + ( + + )} + header="Actions" + /> + +
+ ); +}; diff --git a/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt b/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt index 5848cd14..f62b9ed3 100644 --- a/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt +++ b/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt @@ -62,6 +62,14 @@ fun Application.configureDatabases(): Database { } } + get("/users/{userId}/projects") { + asAdmin { + val userId = UUID.fromString(call.parameters["userId"]) + val user = projectRepository.getProjectsByUserId(userId) + call.respond(HttpStatusCode.OK, user) + } + } + // Create post("/users") { val user: User? From 6f3091eed96f475b8556e7211a9c94f57cd91aaf Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Wed, 22 Jan 2025 14:59:19 -0500 Subject: [PATCH 25/30] Pass projects instead --- frontend/src/admin/user-form.tsx | 25 +++++++++++++----- frontend/src/admin/user-projects-list.tsx | 31 +++++------------------ 2 files changed, 25 insertions(+), 31 deletions(-) diff --git a/frontend/src/admin/user-form.tsx b/frontend/src/admin/user-form.tsx index b6369197..22428fb5 100644 --- a/frontend/src/admin/user-form.tsx +++ b/frontend/src/admin/user-form.tsx @@ -1,9 +1,9 @@ import React, { FormEvent, FunctionComponent, useEffect, useState } from "react"; -import { useParams, useNavigate } from "react-router-dom"; +import { useParams } from "react-router-dom"; import { PrimeReactProvider } from "primereact/api"; import { InputText } from "primereact/inputtext"; import { Button } from "primereact/button"; -import { User } from "zero-zummon"; +import { User, Project, projectsFromJson } from "zero-zummon"; import { redirectToLogin } from "./use-users"; import { UserProjectsList } from "./user-projects-list"; @@ -11,10 +11,10 @@ export const UserForm: FunctionComponent = () => { const {userId} = useParams<{ userId: string }>(); const [user, setUser] = useState(null); const [originalData, setOriginalData] = useState(null); + const [userProjects, setUserProjects] = useState([]); const [loading, setLoading] = useState(false); const [isEditing, setIsEditing] = useState(false); - const navigate = useNavigate(); const handleCancel = () => { if (originalData) { @@ -59,6 +59,21 @@ export const UserForm: FunctionComponent = () => { setLoading(false); } }; + + const fetchProjects = async () => { + try { + const response = await fetch(`${import.meta.env.VITE_ZTOR_URL}/users/${userId}/projects`, { + credentials: "include", + }); + if (!response.ok) return + + setUserProjects(projectsFromJson(await response.text())) + } catch (error) { + console.error("Error fetching projects:", error); + } + }; + + fetchProjects(); fetchUser(); } else { setIsEditing(true); @@ -86,8 +101,6 @@ export const UserForm: FunctionComponent = () => { if (!response.ok) { alert(`Error: ${response.statusText}`); } - - navigate(`/users`); } finally { setIsEditing(false); setLoading(false); @@ -143,7 +156,7 @@ export const UserForm: FunctionComponent = () => { )} - + ); diff --git a/frontend/src/admin/user-projects-list.tsx b/frontend/src/admin/user-projects-list.tsx index 7551e14e..de7c9e3d 100644 --- a/frontend/src/admin/user-projects-list.tsx +++ b/frontend/src/admin/user-projects-list.tsx @@ -1,35 +1,16 @@ -import React, { useEffect, FunctionComponent, useState } from "react"; -import { Project, projectsFromJson } from "zero-zummon"; -import { DataTable } from "primereact/datatable"; -import { Column } from "primereact/column"; +import { FunctionComponent } from "react"; +import { Project } from "zero-zummon"; +import { DataTable } from 'primereact/datatable'; +import { Column } from 'primereact/column'; import { EditButton } from "./edit-button"; type UserProjectsListProps = { - userId?: string; + projects?: Project[]; }; export const UserProjectsList: FunctionComponent = ({ - userId, + projects, }) => { - const [projects, setProjects] = useState([]); - - useEffect(() => { - const fetchProjects = async () => { - try { - const response = await fetch(`${import.meta.env.VITE_ZTOR_URL}/users/${userId}/projects`, { - credentials: "include", - }); - if (!response.ok) return - - setProjects(projectsFromJson(await response.text())) - } catch (error) { - console.error("Error fetching projects:", error); - } - }; - - fetchProjects(); - }); - return (

Current Projects

From a5306e187c13be02acff11569b53932cda3aca63 Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Mon, 27 Jan 2025 09:05:35 -0500 Subject: [PATCH 26/30] Get User and Projects in single call --- frontend/src/admin/user-form.tsx | 18 +++--------------- .../com/zenmo/orm/user/UserRepository.kt | 6 ++++++ .../kotlin/com/zenmo/ztor/plugins/Databases.kt | 2 +- 3 files changed, 10 insertions(+), 16 deletions(-) diff --git a/frontend/src/admin/user-form.tsx b/frontend/src/admin/user-form.tsx index 22428fb5..7ab59f23 100644 --- a/frontend/src/admin/user-form.tsx +++ b/frontend/src/admin/user-form.tsx @@ -39,7 +39,7 @@ export const UserForm: FunctionComponent = () => { const fetchUser = async () => { setLoading(true); try { - const response = await fetch(`${import.meta.env.VITE_ZTOR_URL}/users/${userId}`, { + const response = await fetch(`${import.meta.env.VITE_ZTOR_URL}/users/${userId}/projects`, { credentials: "include", }); if (response.status === 401) { @@ -50,6 +50,8 @@ export const UserForm: FunctionComponent = () => { const userData = await response.json(); setUser(userData); setOriginalData(userData); + setUserProjects(userData.projects) + } else { alert(`Error fetching user: ${response.statusText}`); } @@ -60,20 +62,6 @@ export const UserForm: FunctionComponent = () => { } }; - const fetchProjects = async () => { - try { - const response = await fetch(`${import.meta.env.VITE_ZTOR_URL}/users/${userId}/projects`, { - credentials: "include", - }); - if (!response.ok) return - - setUserProjects(projectsFromJson(await response.text())) - } catch (error) { - console.error("Error fetching projects:", error); - } - }; - - fetchProjects(); fetchUser(); } else { setIsEditing(true); diff --git a/zorm/src/main/kotlin/com/zenmo/orm/user/UserRepository.kt b/zorm/src/main/kotlin/com/zenmo/orm/user/UserRepository.kt index 4ae34b10..6953f494 100644 --- a/zorm/src/main/kotlin/com/zenmo/orm/user/UserRepository.kt +++ b/zorm/src/main/kotlin/com/zenmo/orm/user/UserRepository.kt @@ -28,6 +28,12 @@ class UserRepository( } } } + + fun getUserAndProjects(userId: UUID): User { + return getUsersAndProjects( + (UserTable.id eq userId) + ).first() + } fun getUsersAndProjects(filter: Op = Op.TRUE): List { return transaction(db) { diff --git a/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt b/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt index f62b9ed3..8ab3aa9f 100644 --- a/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt +++ b/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt @@ -65,7 +65,7 @@ fun Application.configureDatabases(): Database { get("/users/{userId}/projects") { asAdmin { val userId = UUID.fromString(call.parameters["userId"]) - val user = projectRepository.getProjectsByUserId(userId) + val user = userRepository.getUserAndProjects(userId) call.respond(HttpStatusCode.OK, user) } } From de0c4a1b048ed6df9bdcc2d2bae7fafa0262f260 Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Mon, 27 Jan 2025 11:36:13 -0500 Subject: [PATCH 27/30] Edit projects Add, delete projects for the user --- frontend/src/admin/user-form.tsx | 5 +-- .../com/zenmo/orm/user/UserRepository.kt | 41 ++++++++++++++++--- .../kotlin/companysurvey/Project.kt | 2 +- 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/frontend/src/admin/user-form.tsx b/frontend/src/admin/user-form.tsx index 8484604e..aeccff36 100644 --- a/frontend/src/admin/user-form.tsx +++ b/frontend/src/admin/user-form.tsx @@ -121,10 +121,7 @@ export const UserForm: FunctionComponent = () => { msgs.current?.show([ { sticky: true, severity: "success", summary: "Success", detail: "User saved successfully.", closable: true }, ]); - // const userData = await response.json(); - // setUser(userData); - // setOriginalData(userData); - // setUserProjects(selectedProjects); + setUserProjects(selectedProjects); } else { msgs.current?.show([ {sticky: true, severity: 'error', summary: 'Error', detail: `Error: ${response.statusText}`, closable: false}, diff --git a/zorm/src/main/kotlin/com/zenmo/orm/user/UserRepository.kt b/zorm/src/main/kotlin/com/zenmo/orm/user/UserRepository.kt index 16bf213e..1c338633 100644 --- a/zorm/src/main/kotlin/com/zenmo/orm/user/UserRepository.kt +++ b/zorm/src/main/kotlin/com/zenmo/orm/user/UserRepository.kt @@ -9,6 +9,7 @@ import com.zenmo.zummon.companysurvey.Project import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.transactions.transaction +import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList import java.util.UUID import kotlin.uuid.ExperimentalUuidApi import kotlin.uuid.toJavaUuid @@ -63,17 +64,45 @@ class UserRepository( } @OptIn(ExperimentalUuidApi::class) - fun save( - user: User, - ) { + fun save(user: User) { transaction(db) { - UserTable.upsertReturning() { + UserTable.upsertReturning { it[id] = user.id.toJavaUuid() - it[UserTable.note] = user.note - it[UserTable.isAdmin] = user.isAdmin + it[note] = user.note + it[isAdmin] = user.isAdmin }.map { hydrateUser(it) }.first() + + // db project ids + val currentProjectIds = UserProjectTable + .selectAll() + .where { UserProjectTable.userId eq user.id.toJavaUuid() } + .map { it[UserProjectTable.projectId] } + .toSet() + + // coming project ids + val newProjectIds = user.projects.map { it.id }.toSet() + + // add and remove projects + val projectsToAdd = newProjectIds - currentProjectIds + val projectsToRemove = currentProjectIds - newProjectIds + + // insert new + if (projectsToAdd.isNotEmpty()) { + UserProjectTable.batchInsert(projectsToAdd) { projectId -> + this[UserProjectTable.projectId] = projectId + this[UserProjectTable.userId] = user.id.toJavaUuid() + } + } + + // remove old + if (projectsToRemove.isNotEmpty()) { + UserProjectTable.deleteWhere { + (UserProjectTable.userId eq user.id.toJavaUuid()) and + (UserProjectTable.projectId inList projectsToRemove) + } + } } } diff --git a/zummon/src/commonMain/kotlin/companysurvey/Project.kt b/zummon/src/commonMain/kotlin/companysurvey/Project.kt index edaf18db..b092f73e 100644 --- a/zummon/src/commonMain/kotlin/companysurvey/Project.kt +++ b/zummon/src/commonMain/kotlin/companysurvey/Project.kt @@ -15,7 +15,7 @@ constructor( val id: Uuid = uuid4(), val name: String = "", // Project ID aka Energy Hub ID of Energieke Regio. - val energiekeRegioId: Int?, + val energiekeRegioId: Int? = null, val buurtCodes: List = emptyList(), ) From 7b5adf3549c751bdd0180d757d8633e6b67e9bc5 Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Mon, 27 Jan 2025 15:57:37 -0500 Subject: [PATCH 28/30] Use one query to load user and projects --- frontend/src/admin/user-form.tsx | 25 ++++++++----------------- 1 file changed, 8 insertions(+), 17 deletions(-) diff --git a/frontend/src/admin/user-form.tsx b/frontend/src/admin/user-form.tsx index a2b596c6..80554a8b 100644 --- a/frontend/src/admin/user-form.tsx +++ b/frontend/src/admin/user-form.tsx @@ -40,6 +40,11 @@ export const UserForm: FunctionComponent = () => { } as User)); }; + const transformProjects = (projects: any[]): Project[] => { + const jsonString = JSON.stringify(projects); + return projectsFromJson(jsonString); + }; + useEffect(() => { if (userId) { const fetchUser = async () => { @@ -57,6 +62,8 @@ export const UserForm: FunctionComponent = () => { setUser(userData); setOriginalData(userData); setUserProjects(userData.projects) + const formattedProjects = transformProjects(userData.projects) + setSelectedProjects(formattedProjects) } else { alert(`Error fetching user: ${response.statusText}`); @@ -67,23 +74,7 @@ export const UserForm: FunctionComponent = () => { setLoading(false); } }; - - const fetchProjects = async () => { - try { - const response = await fetch(`${import.meta.env.VITE_ZTOR_URL}/users/${userId}/projects`, { - credentials: "include", - }); - if (!response.ok) return - - const projectData = projectsFromJson(await response.text()) - setUserProjects(projectData) - setSelectedProjects(projectData) - } catch (error) { - console.error("Error fetching projects:", error); - } - }; - - fetchProjects(); + fetchUser(); } else { setIsEditing(true); From fea5737921f29680143b3d6ce34743caf71b8984 Mon Sep 17 00:00:00 2001 From: Maria Cano Date: Mon, 27 Jan 2025 18:26:35 -0500 Subject: [PATCH 29/30] Quick show projects in users list --- frontend/src/admin/use-users.ts | 4 ++-- frontend/src/admin/users.tsx | 13 ++++++++++++- .../main/kotlin/com/zenmo/ztor/plugins/Databases.kt | 2 +- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/frontend/src/admin/use-users.ts b/frontend/src/admin/use-users.ts index ac7ab132..ce856dee 100644 --- a/frontend/src/admin/use-users.ts +++ b/frontend/src/admin/use-users.ts @@ -29,8 +29,8 @@ export const useUsers = (): UseUserReturn => { if (!response.ok) { throw new Error(`Could not load users: ${response.status} ${response.statusText}`) } - - setUsers(usersFromJson(await response.text())) + const userData = await response.json() + setUsers(userData) } catch (error) { alert((error as Error).message) } finally { diff --git a/frontend/src/admin/users.tsx b/frontend/src/admin/users.tsx index e19b1a58..396383ba 100644 --- a/frontend/src/admin/users.tsx +++ b/frontend/src/admin/users.tsx @@ -3,7 +3,7 @@ import { DataTable } from 'primereact/datatable'; import { Column } from 'primereact/column'; import {useUsers} from "./use-users"; import {PrimeReactProvider} from "primereact/api"; -import {User} from "zero-zummon" +import {User, Project} from "zero-zummon" import "primereact/resources/themes/lara-light-cyan/theme.css" import 'primeicons/primeicons.css' @@ -53,6 +53,17 @@ export const Users: FunctionComponent = () => {
)} /> + ( +
    + {Array.from(user.projects).map((project: Project) => ( +
  • {project.name}
  • + ))} +
+ )} + /> (
Date: Tue, 28 Jan 2025 09:49:41 -0500 Subject: [PATCH 30/30] Check admin for all Projects endpoints Clean code from Code review --- frontend/src/admin/projects-dropdown.tsx | 4 +++- frontend/src/admin/user-form.tsx | 1 - ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt | 8 ++------ 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/frontend/src/admin/projects-dropdown.tsx b/frontend/src/admin/projects-dropdown.tsx index 3833c4d2..9d72caa0 100644 --- a/frontend/src/admin/projects-dropdown.tsx +++ b/frontend/src/admin/projects-dropdown.tsx @@ -22,7 +22,9 @@ export const ProjectsDropdown: FunctionComponent = ({ const response = await fetch(`${import.meta.env.VITE_ZTOR_URL}/all-projects`, { credentials: "include", }); - if (!response.ok) return + if (!response.ok) { + throw new Error(`Failed: ${response.statusText}`) + } setProjects(projectsFromJson(await response.text())) } catch (error) { diff --git a/frontend/src/admin/user-form.tsx b/frontend/src/admin/user-form.tsx index 80554a8b..fa23cf3d 100644 --- a/frontend/src/admin/user-form.tsx +++ b/frontend/src/admin/user-form.tsx @@ -92,7 +92,6 @@ export const UserForm: FunctionComponent = () => { name: project.name, })), }) - console.log("sendUser " + sendUser) const method = userId ? "PUT" : "POST"; const url = `${import.meta.env.VITE_ZTOR_URL}/users` const response = await fetch(url, { diff --git a/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt b/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt index ac79a559..1ec8cae6 100644 --- a/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt +++ b/ztor/src/main/kotlin/com/zenmo/ztor/plugins/Databases.kt @@ -117,13 +117,9 @@ fun Application.configureDatabases(): Database { } get("/all-projects") { - val userId = call.getUserId() - if (userId == null) { - call.respond(HttpStatusCode.Unauthorized) - return@get + asAdmin { + call.respond(HttpStatusCode.OK, projectRepository.getProjects()) } - - call.respond(HttpStatusCode.OK, projectRepository.getProjects()) } get("/projects") {