diff --git a/backend/Makefile b/backend/Makefile index 0ffc557..039340c 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -34,7 +34,6 @@ clean: rm -f plantuml.jar rm -f erd.png rm -f config.toml - rm -f database.txt # Test target test: db.sqlite3 @@ -106,7 +105,6 @@ docs: swag init -outputTypes go api: ./docs/swagger.json - rm ../frontend/src/API/GenApi.ts npx swagger-typescript-api \ --api-class-name GenApi \ --path ./docs/swagger.json \ diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 7a08b0e..0009c17 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -21,21 +21,21 @@ const docTemplate = `{ "paths": { "/login": { "post": { - "description": "Logs in a user and returns a JWT token", + "description": "logs the user in and returns a jwt token", "consumes": [ "application/json" ], "produces": [ - "application/json" + "text/plain" ], "tags": [ - "Auth" + "User" ], - "summary": "Login", + "summary": "login", "parameters": [ { - "description": "User credentials", - "name": "body", + "description": "login info", + "name": "NewUser", "in": "body", "required": true, "schema": { @@ -45,9 +45,9 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "JWT token", + "description": "Successfully signed token for user", "schema": { - "$ref": "#/definitions/types.Token" + "type": "Token" } }, "400": { @@ -71,26 +71,29 @@ const docTemplate = `{ } } }, - "/loginrenew": { + "/loginerenew": { "post": { "security": [ { - "JWT": [] + "bererToken": [] } ], - "description": "Renews the users token.", - "produces": [ + "description": "renews the users token", + "consumes": [ "application/json" ], + "produces": [ + "text/plain" + ], "tags": [ - "Auth" + "User" ], "summary": "LoginRenews", "responses": { "200": { "description": "Successfully signed token for user", "schema": { - "$ref": "#/definitions/types.Token" + "type": "Token" } }, "401": { @@ -110,12 +113,7 @@ const docTemplate = `{ }, "/promoteToAdmin": { "post": { - "security": [ - { - "JWT": [] - } - ], - "description": "Promote chosen user to site admin", + "description": "promote chosen user to admin", "consumes": [ "application/json" ], @@ -141,7 +139,7 @@ const docTemplate = `{ "200": { "description": "Successfully promoted user", "schema": { - "$ref": "#/definitions/types.Token" + "type": "json" } }, "400": { @@ -175,7 +173,7 @@ const docTemplate = `{ "text/plain" ], "tags": [ - "Auth" + "User" ], "summary": "Register", "parameters": [ @@ -213,11 +211,6 @@ const docTemplate = `{ }, "/userdelete/{username}": { "delete": { - "security": [ - { - "JWT": [] - } - ], "description": "UserDelete deletes a user from the database", "consumes": [ "application/json" @@ -259,27 +252,22 @@ const docTemplate = `{ }, "/users/all": { "get": { - "security": [ - { - "JWT": [] - } - ], "description": "lists all users", - "produces": [ + "consumes": [ "application/json" ], + "produces": [ + "text/plain" + ], "tags": [ "User" ], "summary": "ListsAllUsers", "responses": { "200": { - "description": "Successfully returned all users", + "description": "Successfully signed token for user", "schema": { - "type": "array", - "items": { - "type": "string" - } + "type": "json" } }, "401": { @@ -303,27 +291,16 @@ const docTemplate = `{ "type": "object", "properties": { "password": { - "type": "string", - "example": "password123" + "type": "string" }, "username": { - "type": "string", - "example": "username123" - } - } - }, - "types.Token": { - "type": "object", - "properties": { - "token": { "type": "string" } } } }, "securityDefinitions": { - "JWT": { - "description": "Use the JWT token provided by the login endpoint to authenticate requests. **Prefix the token with \"Bearer \".**", + "bererToken": { "type": "apiKey", "name": "Authorization", "in": "header" diff --git a/backend/internal/handlers/projects/GetUserProject.go b/backend/internal/handlers/projects/GetUserProject.go index 6c80515..99ed63b 100644 --- a/backend/internal/handlers/projects/GetUserProject.go +++ b/backend/internal/handlers/projects/GetUserProject.go @@ -4,16 +4,15 @@ import ( db "ttime/internal/database" "github.com/gofiber/fiber/v2" - "github.com/gofiber/fiber/v2/log" + "github.com/golang-jwt/jwt/v5" ) // GetUserProjects returns all projects that the user is a member of func GetUserProjects(c *fiber.Ctx) error { - username := c.Params("username") - if username == "" { - log.Info("No username provided") - return c.Status(400).SendString("No username provided") - } + // First we get the username from the token + user := c.Locals("user").(*jwt.Token) + claims := user.Claims.(jwt.MapClaims) + username := claims["name"].(string) // Then dip into the database to get the projects projects, err := db.GetDb(c).GetProjectsForUser(username) diff --git a/backend/internal/handlers/projects/ProjectRoleChange.go b/backend/internal/handlers/projects/ProjectRoleChange.go index 6c5d455..266127d 100644 --- a/backend/internal/handlers/projects/ProjectRoleChange.go +++ b/backend/internal/handlers/projects/ProjectRoleChange.go @@ -24,13 +24,7 @@ func ProjectRoleChange(c *fiber.Ctx) error { return c.Status(400).SendString(err.Error()) } - // Check if user is trying to change its own role - if username == data.UserName { - log.Info("Can't change your own role") - return c.Status(403).SendString("Can't change your own role") - } - - log.Info("Changing role for user: ", data.UserName, " in project: ", data.Projectname, " to: ", data.Role) + log.Info("Changing role for user: ", username, " in project: ", data.Projectname, " to: ", data.Role) // Dubble diping and checcking if current user is if ismanager, err := db.GetDb(c).IsProjectManager(username, data.Projectname); err != nil { @@ -42,7 +36,7 @@ func ProjectRoleChange(c *fiber.Ctx) error { } // Change the user's role within the project in the database - if err := db.GetDb(c).ChangeUserRole(data.UserName, data.Projectname, data.Role); err != nil { + if err := db.GetDb(c).ChangeUserRole(username, data.Projectname, data.Role); err != nil { return c.Status(500).SendString(err.Error()) } diff --git a/backend/internal/handlers/users/ListAllUsers.go b/backend/internal/handlers/users/ListAllUsers.go index 5ac5df0..1cae76c 100644 --- a/backend/internal/handlers/users/ListAllUsers.go +++ b/backend/internal/handlers/users/ListAllUsers.go @@ -7,17 +7,16 @@ import ( "github.com/gofiber/fiber/v2/log" ) -// @Summary ListsAllUsers -// @Description lists all users -// @Tags User -// @Produce json -// @Security JWT -// @Success 200 {array} string "Successfully returned all users" -// @Failure 401 {string} string "Unauthorized" -// @Failure 500 {string} string "Internal server error" -// @Router /users/all [get] -// -// ListAllUsers returns a list of all users in the application database +// ListAllUsers is a handler that returns a list of all users in the application database +// @Summary ListsAllUsers +// @Description lists all users +// @Tags User +// @Accept json +// @Produce plain +// @Success 200 {json} json "Successfully signed token for user" +// @Failure 401 {string} string "Unauthorized" +// @Failure 500 {string} string "Internal server error" +// @Router /users/all [get] func ListAllUsers(c *fiber.Ctx) error { // Get all users from the database users, err := db.GetDb(c).GetAllUsersApplication() diff --git a/backend/internal/handlers/users/Login.go b/backend/internal/handlers/users/Login.go index 42c52a5..c4d6c60 100644 --- a/backend/internal/handlers/users/Login.go +++ b/backend/internal/handlers/users/Login.go @@ -10,19 +10,18 @@ import ( "github.com/golang-jwt/jwt/v5" ) -// @Summary Login -// @Description Logs in a user and returns a JWT token -// @Tags Auth -// @Accept json -// @Produce json -// @Param body body types.NewUser true "User credentials" -// @Success 200 {object} types.Token "JWT token" -// @Failure 400 {string} string "Bad request" -// @Failure 401 {string} string "Unauthorized" -// @Failure 500 {string} string "Internal server error" -// @Router /login [post] -// -// Login logs in a user and returns a JWT token +// Login is a simple login handler that returns a JWT token +// @Summary login +// @Description logs the user in and returns a jwt token +// @Tags User +// @Accept json +// @Param NewUser body types.NewUser true "login info" +// @Produce plain +// @Success 200 Token types.Token "Successfully signed token for user" +// @Failure 400 {string} string "Bad request" +// @Failure 401 {string} string "Unauthorized" +// @Failure 500 {string} string "Internal server error" +// @Router /login [post] func Login(c *fiber.Ctx) error { // The body type is identical to a NewUser diff --git a/backend/internal/handlers/users/LoginRenew.go b/backend/internal/handlers/users/LoginRenew.go index 3926ce4..78eadfd 100644 --- a/backend/internal/handlers/users/LoginRenew.go +++ b/backend/internal/handlers/users/LoginRenew.go @@ -9,40 +9,34 @@ import ( "github.com/golang-jwt/jwt/v5" ) -// @Summary LoginRenews -// @Description Renews the users token. -// @Tags Auth -// @Produce json -// @Security JWT -// @Success 200 {object} types.Token "Successfully signed token for user" -// @Failure 401 {string} string "Unauthorized" -// @Failure 500 {string} string "Internal server error" -// @Router /loginrenew [post] -// -// LoginRenew renews the users token +// LoginRenew is a simple handler that renews the token +// @Summary LoginRenews +// @Description renews the users token +// @Security bererToken +// @Tags User +// @Accept json +// @Produce plain +// @Success 200 Token types.Token "Successfully signed token for user" +// @Failure 401 {string} string "Unauthorized" +// @Failure 500 {string} string "Internal server error" +// @Router /loginerenew [post] func LoginRenew(c *fiber.Ctx) error { user := c.Locals("user").(*jwt.Token) log.Info("Renewing token for user:", user.Claims.(jwt.MapClaims)["name"]) - // Renewing the token means we trust whatever is already in the token claims := user.Claims.(jwt.MapClaims) - - // 72 hour expiration time claims["exp"] = time.Now().Add(time.Hour * 72).Unix() - - // Create token with old claims, but new expiration time - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + renewed := jwt.MapClaims{ "name": claims["name"], "admin": claims["admin"], "exp": claims["exp"], - }) - - // Sign it with top secret key + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, renewed) t, err := token.SignedString([]byte("secret")) if err != nil { log.Warn("Error signing token") - return c.SendStatus(fiber.StatusInternalServerError) // 500 + return c.SendStatus(fiber.StatusInternalServerError) } log.Info("Successfully renewed token for user:", user.Claims.(jwt.MapClaims)["name"]) diff --git a/backend/internal/handlers/users/PromoteToAdmin.go b/backend/internal/handlers/users/PromoteToAdmin.go index 3f0a6d3..4a21758 100644 --- a/backend/internal/handlers/users/PromoteToAdmin.go +++ b/backend/internal/handlers/users/PromoteToAdmin.go @@ -8,20 +8,17 @@ import ( "github.com/gofiber/fiber/v2/log" ) -// @Summary PromoteToAdmin -// @Description Promote chosen user to site admin -// @Tags User -// @Accept json -// @Produce plain -// @Security JWT -// @Param NewUser body types.NewUser true "user info" -// @Success 200 {object} types.Token "Successfully promoted user" -// @Failure 400 {string} string "Bad request" -// @Failure 401 {string} string "Unauthorized" -// @Failure 500 {string} string "Internal server error" -// @Router /promoteToAdmin [post] -// -// PromoteToAdmin promotes a user to a site admin +// @Summary PromoteToAdmin +// @Description promote chosen user to admin +// @Tags User +// @Accept json +// @Produce plain +// @Param NewUser body types.NewUser true "user info" +// @Success 200 {json} json "Successfully promoted user" +// @Failure 400 {string} string "Bad request" +// @Failure 401 {string} string "Unauthorized" +// @Failure 500 {string} string "Internal server error" +// @Router /promoteToAdmin [post] func PromoteToAdmin(c *fiber.Ctx) error { // Extract the username from the request body var newUser types.NewUser diff --git a/backend/internal/handlers/users/Register.go b/backend/internal/handlers/users/Register.go index b9e0c78..9977246 100644 --- a/backend/internal/handlers/users/Register.go +++ b/backend/internal/handlers/users/Register.go @@ -8,9 +8,11 @@ import ( "github.com/gofiber/fiber/v2/log" ) +// Register is a simple handler that registers a new user +// // @Summary Register // @Description Register a new user -// @Tags Auth +// @Tags User // @Accept json // @Produce plain // @Param NewUser body types.NewUser true "User to register" @@ -18,8 +20,6 @@ import ( // @Failure 400 {string} string "Bad request" // @Failure 500 {string} string "Internal server error" // @Router /register [post] -// -// Register is a simple handler that registers a new user func Register(c *fiber.Ctx) error { u := new(types.NewUser) if err := c.BodyParser(u); err != nil { diff --git a/backend/internal/handlers/users/UserDelete.go b/backend/internal/handlers/users/UserDelete.go index 491a1b3..5957c2d 100644 --- a/backend/internal/handlers/users/UserDelete.go +++ b/backend/internal/handlers/users/UserDelete.go @@ -8,19 +8,19 @@ import ( "github.com/golang-jwt/jwt/v5" ) +// This path should obviously be protected in the future +// UserDelete deletes a user from the database +// // @Summary UserDelete // @Description UserDelete deletes a user from the database // @Tags User // @Accept json // @Produce plain -// @Security JWT // @Success 200 {string} string "User deleted" // @Failure 403 {string} string "You can only delete yourself" // @Failure 500 {string} string "Internal server error" // @Failure 401 {string} string "Unauthorized" // @Router /userdelete/{username} [delete] -// -// UserDelete deletes a user from the database func UserDelete(c *fiber.Ctx) error { // Read from path parameters username := c.Params("username") diff --git a/backend/internal/types/users.go b/backend/internal/types/users.go index 37cc8c2..88b4f06 100644 --- a/backend/internal/types/users.go +++ b/backend/internal/types/users.go @@ -18,8 +18,8 @@ func (u *User) ToPublicUser() (*PublicUser, error) { // Should be used when registering, for example type NewUser struct { - Username string `json:"username" example:"username123"` - Password string `json:"password" example:"password123"` + Username string `json:"username"` + Password string `json:"password"` } // PublicUser represents a user that is safe to send over the API (no password) diff --git a/backend/main.go b/backend/main.go index 6e65386..4c2056e 100644 --- a/backend/main.go +++ b/backend/main.go @@ -25,16 +25,15 @@ import ( // @license.name AGPL // @license.url https://www.gnu.org/licenses/agpl-3.0.html -// @securityDefinitions.apikey JWT -// @in header -// @name Authorization -// @description Use the JWT token provided by the login endpoint to authenticate requests. **Prefix the token with "Bearer ".** +//@securityDefinitions.apikey bererToken +//@in header +//@name Authorization // @host localhost:8080 // @BasePath /api -// @externalDocs.description OpenAPI -// @externalDocs.url https://swagger.io/resources/open-api/ +// @externalDocs.description OpenAPI +// @externalDocs.url https://swagger.io/resources/open-api/ /** Main function for starting the server and initializing configurations. @@ -112,8 +111,7 @@ func main() { // All project related routes // projectGroup := api.Group("/project") // Not currently in use - api.Get("/getProjectTimes/:projectName", projects.GetProjectTimesHandler) - api.Get("/getUserProjects/:username", projects.GetUserProjects) + api.Get("/getUserProjects", projects.GetUserProjects) api.Get("/project/:projectId", projects.GetProject) api.Get("/checkIfProjectManager/:projectName", projects.IsProjectManagerHandler) api.Get("/getUsersProject/:projectName", projects.ListAllUsersProject) diff --git a/frontend/.prettierignore b/frontend/.prettierignore deleted file mode 100644 index c49d006..0000000 --- a/frontend/.prettierignore +++ /dev/null @@ -1,2 +0,0 @@ -goTypes.ts -GenApi.ts \ No newline at end of file diff --git a/frontend/src/API/API.ts b/frontend/src/API/API.ts index c1480fb..0a85e70 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -1,16 +1,13 @@ -import { NewProjMember } from "../Components/AddMember"; -import { ProjectRoleChange } from "../Components/ChangeRole"; -import { projectTimes } from "../Components/GetProjectTimes"; -import { ProjectMember } from "../Components/GetUsersInProject"; import { - UpdateWeeklyReport, NewWeeklyReport, NewUser, User, Project, NewProject, + UserProjectMember, WeeklyReport, StrNameChange, + NewProjMember, } from "../Types/goTypes"; /** @@ -76,7 +73,10 @@ interface API { * @param {string} token The authentication token. * @returns {Promise>} A promise resolving to an API response with the created project. */ - createProject(project: NewProject, token: string): Promise>; + createProject( + project: NewProject, + token: string, + ): Promise>; /** Submits a weekly report * @param {NewWeeklyReport} weeklyReport The weekly report object. @@ -88,17 +88,6 @@ interface API { token: string, ): Promise>; - /** - * Updates a weekly report. - * @param {UpdateWeeklyReport} weeklyReport The updated weekly report object. - * @param {string} token The authentication token. - * @returns {Promise>} A promise containing the API response with the updated report. - */ - updateWeeklyReport( - weeklyReport: UpdateWeeklyReport, - token: string, - ): Promise>; - /** Gets a weekly report for a specific user, project and week * @param {string} projectName The name of the project. * @param {string} week The week number. @@ -124,14 +113,10 @@ interface API { ): Promise>; /** Gets all the projects of a user - * @param {string} username - The authentication token. * @param {string} token - The authentication token. * @returns {Promise>} A promise containing the API response with the user's projects. */ - getUserProjects( - username: string, - token: string, - ): Promise>; + getUserProjects(token: string): Promise>; /** Gets a project by its id. * @param {number} id The id of the project to retrieve. @@ -139,16 +124,6 @@ interface API { */ getProject(id: number): Promise>; - /** Gets a projects reported time - * @param {string} projectName The name of the project. - * @param {string} token The usertoken. - * @returns {Promise>} A promise resolving to an API response containing the project times. - */ - getProjectTimes( - projectName: string, - token: string, - ): Promise>; - /** Gets a list of all users. * @param {string} token The authentication token of the requesting user. * @returns {Promise>} A promise resolving to an API response containing the list of users. @@ -158,18 +133,7 @@ interface API { getAllUsersProject( projectName: string, token: string, - ): Promise>; - - /** Gets all unsigned reports in a project. - * @param {string} projectName The name of the project. - * @param {string} token The authentication token. - * @returns {Promise>} A promise resolving to an API response containing the list of unsigned reports. - */ - getUnsignedReportsInProject( - projectName: string, - token: string, - ): Promise>; - + ): Promise>; /** * Changes the username of a user in the database. * @param {StrNameChange} data The object containing the previous and new username. @@ -180,21 +144,10 @@ interface API { data: StrNameChange, token: string, ): Promise>; - /** - * Changes the role of a user in the database. - * @param {RoleChange} roleInfo The object containing the previous and new username. - * @param {string} token The authentication token. - * @returns {Promise>} A promise resolving to an API response. - */ - changeUserRole( - roleInfo: ProjectRoleChange, - token: string, - ): Promise>; - addUserToProject( user: NewProjMember, token: string, - ): Promise>; + ): Promise>; removeProject( projectName: string, @@ -296,7 +249,7 @@ export const api: API = { async createProject( project: NewProject, token: string, - ): Promise> { + ): Promise> { try { const response = await fetch("/api/project", { method: "POST", @@ -310,17 +263,18 @@ export const api: API = { if (!response.ok) { return { success: false, message: "Failed to create project" }; } else { - return { success: true }; + const data = (await response.json()) as Project; + return { success: true, data }; } } catch (e) { - return { success: false, message: "Failed to create project!" }; + return { success: false, message: "Failed to create project" }; } }, async addUserToProject( user: NewProjMember, token: string, - ): Promise> { + ): Promise> { try { const response = await fetch("/api/addUserToProject", { method: "PUT", @@ -362,39 +316,9 @@ export const api: API = { } }, - async changeUserRole( - roleInfo: ProjectRoleChange, - token: string, - ): Promise> { + async getUserProjects(token: string): Promise> { try { - const response = await fetch("/api/ProjectRoleChange", { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer " + token, - }, - body: JSON.stringify(roleInfo), - }); - - if (!response.ok) { - if (response.status === 403) { - return { success: false, message: "Cannot change your own role" }; - } - return { success: false, message: "Could not change role" }; - } else { - return { success: true }; - } - } catch (e) { - return { success: false, message: "Could not change role" }; - } - }, - - async getUserProjects( - username: string, - token: string, - ): Promise> { - try { - const response = await fetch(`/api/getUserProjects/${username}`, { + const response = await fetch("/api/getUserProjects", { method: "GET", headers: { "Content-Type": "application/json", @@ -419,37 +343,6 @@ export const api: API = { } }, - async getProjectTimes( - projectName: string, - token: string, - ): Promise> { - try { - const response = await fetch(`/api/getProjectTimes/${projectName}`, { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer " + token, - }, - }); - - if (!response.ok) { - return Promise.resolve({ - success: false, - message: - "Fetch error: " + response.status + ", failed to get project times", - }); - } else { - const data = (await response.json()) as projectTimes; - return Promise.resolve({ success: true, data }); - } - } catch (e) { - return Promise.resolve({ - success: false, - message: "API error! Could not get times.", - }); - } - }, - async submitWeeklyReport( weeklyReport: NewWeeklyReport, token: string, @@ -481,37 +374,6 @@ export const api: API = { } }, - async updateWeeklyReport( - weeklyReport: UpdateWeeklyReport, - token: string, - ): Promise> { - try { - const response = await fetch("/api/updateWeeklyReport", { - method: "PUT", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer " + token, - }, - body: JSON.stringify(weeklyReport), - }); - - if (!response.ok) { - return { - success: false, - message: "Failed to update weekly report", - }; - } - - const data = await response.text(); - return { success: true, message: data }; - } catch (e) { - return { - success: false, - message: "Failed to update weekly report", - }; - } - }, - async getWeeklyReport( projectName: string, week: string, @@ -648,7 +510,7 @@ export const api: API = { async getAllUsersProject( projectName: string, token: string, - ): Promise> { + ): Promise> { try { const response = await fetch(`/api/getUsersProject/${projectName}`, { method: "GET", @@ -664,7 +526,7 @@ export const api: API = { message: "Failed to get users", }); } else { - const data = (await response.json()) as ProjectMember[]; + const data = (await response.json()) as UserProjectMember[]; return Promise.resolve({ success: true, data }); } } catch (e) { @@ -675,38 +537,6 @@ export const api: API = { } }, - async getUnsignedReportsInProject( - projectName: string, - token: string, - ): Promise> { - try { - const response = await fetch(`/api/getUnsignedReports/${projectName}`, { - method: "GET", - headers: { - "Content-Type": "application/json", - Authorization: "Bearer " + token, - }, - }); - - if (!response.ok) { - return { - success: false, - message: - "Failed to get unsigned reports for project: Response code " + - response.status, - }; - } else { - const data = (await response.json()) as WeeklyReport[]; - return { success: true, data }; - } - } catch (e) { - return { - success: false, - message: "Failed to get unsigned reports for project, unknown error", - }; - } - }, - async changeUserName( data: StrNameChange, token: string, @@ -736,7 +566,7 @@ export const api: API = { token: string, ): Promise> { try { - const response = await fetch(`/api/removeProject/${projectName}`, { + const response = await fetch(`/api/projectdelete/${projectName}`, { method: "DELETE", headers: { "Content-Type": "application/json", diff --git a/frontend/src/API/GenApi.ts b/frontend/src/API/GenApi.ts deleted file mode 100644 index 8ca851b..0000000 --- a/frontend/src/API/GenApi.ts +++ /dev/null @@ -1,358 +0,0 @@ -/* eslint-disable */ -/* tslint:disable */ -/* - * --------------------------------------------------------------- - * ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ## - * ## ## - * ## AUTHOR: acacode ## - * ## SOURCE: https://github.com/acacode/swagger-typescript-api ## - * --------------------------------------------------------------- - */ - -export interface TypesNewUser { - /** @example "password123" */ - password?: string; - /** @example "username123" */ - username?: string; -} - -export interface TypesToken { - token?: string; -} - -export type QueryParamsType = Record; -export type ResponseFormat = keyof Omit; - -export interface FullRequestParams extends Omit { - /** set parameter to `true` for call `securityWorker` for this request */ - secure?: boolean; - /** request path */ - path: string; - /** content type of request body */ - type?: ContentType; - /** query params */ - query?: QueryParamsType; - /** format of response (i.e. response.json() -> format: "json") */ - format?: ResponseFormat; - /** request body */ - body?: unknown; - /** base url */ - baseUrl?: string; - /** request cancellation token */ - cancelToken?: CancelToken; -} - -export type RequestParams = Omit; - -export interface ApiConfig { - baseUrl?: string; - baseApiParams?: Omit; - securityWorker?: (securityData: SecurityDataType | null) => Promise | RequestParams | void; - customFetch?: typeof fetch; -} - -export interface HttpResponse extends Response { - data: D; - error: E; -} - -type CancelToken = Symbol | string | number; - -export enum ContentType { - Json = "application/json", - FormData = "multipart/form-data", - UrlEncoded = "application/x-www-form-urlencoded", - Text = "text/plain", -} - -export class HttpClient { - public baseUrl: string = "//localhost:8080/api"; - private securityData: SecurityDataType | null = null; - private securityWorker?: ApiConfig["securityWorker"]; - private abortControllers = new Map(); - private customFetch = (...fetchParams: Parameters) => fetch(...fetchParams); - - private baseApiParams: RequestParams = { - credentials: "same-origin", - headers: {}, - redirect: "follow", - referrerPolicy: "no-referrer", - }; - - constructor(apiConfig: ApiConfig = {}) { - Object.assign(this, apiConfig); - } - - public setSecurityData = (data: SecurityDataType | null) => { - this.securityData = data; - }; - - protected encodeQueryParam(key: string, value: any) { - const encodedKey = encodeURIComponent(key); - return `${encodedKey}=${encodeURIComponent(typeof value === "number" ? value : `${value}`)}`; - } - - protected addQueryParam(query: QueryParamsType, key: string) { - return this.encodeQueryParam(key, query[key]); - } - - protected addArrayQueryParam(query: QueryParamsType, key: string) { - const value = query[key]; - return value.map((v: any) => this.encodeQueryParam(key, v)).join("&"); - } - - protected toQueryString(rawQuery?: QueryParamsType): string { - const query = rawQuery || {}; - const keys = Object.keys(query).filter((key) => "undefined" !== typeof query[key]); - return keys - .map((key) => (Array.isArray(query[key]) ? this.addArrayQueryParam(query, key) : this.addQueryParam(query, key))) - .join("&"); - } - - protected addQueryParams(rawQuery?: QueryParamsType): string { - const queryString = this.toQueryString(rawQuery); - return queryString ? `?${queryString}` : ""; - } - - private contentFormatters: Record any> = { - [ContentType.Json]: (input: any) => - input !== null && (typeof input === "object" || typeof input === "string") ? JSON.stringify(input) : input, - [ContentType.Text]: (input: any) => (input !== null && typeof input !== "string" ? JSON.stringify(input) : input), - [ContentType.FormData]: (input: any) => - Object.keys(input || {}).reduce((formData, key) => { - const property = input[key]; - formData.append( - key, - property instanceof Blob - ? property - : typeof property === "object" && property !== null - ? JSON.stringify(property) - : `${property}`, - ); - return formData; - }, new FormData()), - [ContentType.UrlEncoded]: (input: any) => this.toQueryString(input), - }; - - protected mergeRequestParams(params1: RequestParams, params2?: RequestParams): RequestParams { - return { - ...this.baseApiParams, - ...params1, - ...(params2 || {}), - headers: { - ...(this.baseApiParams.headers || {}), - ...(params1.headers || {}), - ...((params2 && params2.headers) || {}), - }, - }; - } - - protected createAbortSignal = (cancelToken: CancelToken): AbortSignal | undefined => { - if (this.abortControllers.has(cancelToken)) { - const abortController = this.abortControllers.get(cancelToken); - if (abortController) { - return abortController.signal; - } - return void 0; - } - - const abortController = new AbortController(); - this.abortControllers.set(cancelToken, abortController); - return abortController.signal; - }; - - public abortRequest = (cancelToken: CancelToken) => { - const abortController = this.abortControllers.get(cancelToken); - - if (abortController) { - abortController.abort(); - this.abortControllers.delete(cancelToken); - } - }; - - public request = async ({ - body, - secure, - path, - type, - query, - format, - baseUrl, - cancelToken, - ...params - }: FullRequestParams): Promise> => { - const secureParams = - ((typeof secure === "boolean" ? secure : this.baseApiParams.secure) && - this.securityWorker && - (await this.securityWorker(this.securityData))) || - {}; - const requestParams = this.mergeRequestParams(params, secureParams); - const queryString = query && this.toQueryString(query); - const payloadFormatter = this.contentFormatters[type || ContentType.Json]; - const responseFormat = format || requestParams.format; - - return this.customFetch(`${baseUrl || this.baseUrl || ""}${path}${queryString ? `?${queryString}` : ""}`, { - ...requestParams, - headers: { - ...(requestParams.headers || {}), - ...(type && type !== ContentType.FormData ? { "Content-Type": type } : {}), - }, - signal: (cancelToken ? this.createAbortSignal(cancelToken) : requestParams.signal) || null, - body: typeof body === "undefined" || body === null ? null : payloadFormatter(body), - }).then(async (response) => { - const r = response as HttpResponse; - r.data = null as unknown as T; - r.error = null as unknown as E; - - const data = !responseFormat - ? r - : await response[responseFormat]() - .then((data) => { - if (r.ok) { - r.data = data; - } else { - r.error = data; - } - return r; - }) - .catch((e) => { - r.error = e; - return r; - }); - - if (cancelToken) { - this.abortControllers.delete(cancelToken); - } - - if (!response.ok) throw data; - return data; - }); - }; -} - -/** - * @title TTime API - * @version 0.0.1 - * @license AGPL (https://www.gnu.org/licenses/agpl-3.0.html) - * @baseUrl //localhost:8080/api - * @externalDocs https://swagger.io/resources/open-api/ - * @contact - * - * This is the API for TTime, a time tracking application. - */ -export class GenApi extends HttpClient { - login = { - /** - * @description Logs in a user and returns a JWT token - * - * @tags Auth - * @name LoginCreate - * @summary Login - * @request POST:/login - */ - loginCreate: (body: TypesNewUser, params: RequestParams = {}) => - this.request({ - path: `/login`, - method: "POST", - body: body, - type: ContentType.Json, - format: "json", - ...params, - }), - }; - loginrenew = { - /** - * @description Renews the users token. - * - * @tags Auth - * @name LoginrenewCreate - * @summary LoginRenews - * @request POST:/loginrenew - * @secure - */ - loginrenewCreate: (params: RequestParams = {}) => - this.request({ - path: `/loginrenew`, - method: "POST", - secure: true, - format: "json", - ...params, - }), - }; - promoteToAdmin = { - /** - * @description Promote chosen user to site admin - * - * @tags User - * @name PromoteToAdminCreate - * @summary PromoteToAdmin - * @request POST:/promoteToAdmin - * @secure - */ - promoteToAdminCreate: (NewUser: TypesNewUser, params: RequestParams = {}) => - this.request({ - path: `/promoteToAdmin`, - method: "POST", - body: NewUser, - secure: true, - type: ContentType.Json, - ...params, - }), - }; - register = { - /** - * @description Register a new user - * - * @tags Auth - * @name RegisterCreate - * @summary Register - * @request POST:/register - */ - registerCreate: (NewUser: TypesNewUser, params: RequestParams = {}) => - this.request({ - path: `/register`, - method: "POST", - body: NewUser, - type: ContentType.Json, - ...params, - }), - }; - userdelete = { - /** - * @description UserDelete deletes a user from the database - * - * @tags User - * @name UserdeleteDelete - * @summary UserDelete - * @request DELETE:/userdelete/{username} - * @secure - */ - userdeleteDelete: (username: string, params: RequestParams = {}) => - this.request({ - path: `/userdelete/${username}`, - method: "DELETE", - secure: true, - type: ContentType.Json, - ...params, - }), - }; - users = { - /** - * @description lists all users - * - * @tags User - * @name GetUsers - * @summary ListsAllUsers - * @request GET:/users/all - * @secure - */ - getUsers: (params: RequestParams = {}) => - this.request({ - path: `/users/all`, - method: "GET", - secure: true, - format: "json", - ...params, - }), - }; -} diff --git a/frontend/src/Components/AddMember.tsx b/frontend/src/Components/AddMember.tsx index 194afe8..d29be68 100644 --- a/frontend/src/Components/AddMember.tsx +++ b/frontend/src/Components/AddMember.tsx @@ -1,10 +1,5 @@ import { APIResponse, api } from "../API/API"; - -export interface NewProjMember { - username: string; - role: string; - projectname: string; -} +import { NewProjMember } from "../Types/goTypes"; /** * Tries to add a member to a project @@ -26,7 +21,7 @@ function AddMember(props: { memberToAdd: NewProjMember }): boolean { props.memberToAdd, localStorage.getItem("accessToken") ?? "", ) - .then((response: APIResponse) => { + .then((response: APIResponse) => { if (response.success) { alert("Member added"); added = true; diff --git a/frontend/src/Components/AddProject.tsx b/frontend/src/Components/AddProject.tsx index e2ad8b9..f5f4a08 100644 --- a/frontend/src/Components/AddProject.tsx +++ b/frontend/src/Components/AddProject.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; import { APIResponse, api } from "../API/API"; -import { NewProject } from "../Types/goTypes"; +import { NewProject, Project } from "../Types/goTypes"; import InputField from "./InputField"; import Logo from "../assets/Logo.svg"; import Button from "./Button"; @@ -10,26 +10,27 @@ import Button from "./Button"; * @param {Object} props - Project name and description * @returns {boolean} True if created, false if not */ -function CreateProject(props: { name: string; description: string }): void { +function CreateProject(props: { name: string; description: string }): boolean { const project: NewProject = { name: props.name, description: props.description, }; + let created = false; + api .createProject(project, localStorage.getItem("accessToken") ?? "") - .then((response: APIResponse) => { + .then((response: APIResponse) => { if (response.success) { - alert("Project added!"); + created = true; } else { - alert("Project NOT added!"); console.error(response.message); } }) .catch((error) => { - alert("Project NOT added!"); console.error("An error occurred during creation:", error); }); + return created; } /** @@ -47,10 +48,7 @@ function AddProject(): JSX.Element { className="bg-white rounded px-8 pt-6 pb-8 mb-4 items-center justify-center flex flex-col w-fit h-fit" onSubmit={(e) => { e.preventDefault(); - CreateProject({ - name: name, - description: description, - }); + CreateProject({ name: name, description: description }); }} > Create a new project -
- { - e.preventDefault(); - setName(e.target.value); - }} - /> - { - e.preventDefault(); - setDescription(e.target.value); - }} - /> -
+ { + setName(e.target.value); + }} + /> + { + setDescription(e.target.value); + }} + />
- ); -} diff --git a/frontend/src/Components/ChangeUsername.tsx b/frontend/src/Components/ChangeUsername.tsx index 78d7da9..e297a04 100644 --- a/frontend/src/Components/ChangeUsername.tsx +++ b/frontend/src/Components/ChangeUsername.tsx @@ -1,26 +1,61 @@ -import { APIResponse, api } from "../API/API"; -import { StrNameChange } from "../Types/goTypes"; +import React, { useState } from "react"; +import InputField from "./InputField"; +import { api } from "../API/API"; -function ChangeUsername(props: { nameChange: StrNameChange }): void { - if (props.nameChange.newName === "") { - alert("You have to select a new name"); - return; - } - api - .changeUserName(props.nameChange, localStorage.getItem("accessToken") ?? "") - .then((response: APIResponse) => { - if (response.success) { - alert("Name changed successfully"); - location.reload(); - } else { - alert("Name not changed"); - console.error(response.message); +function ChangeUsername(): JSX.Element { + const [newUsername, setNewUsername] = useState(""); + const [errorMessage, setErrorMessage] = useState(""); + + const handleChange = (e: React.ChangeEvent): void => { + setNewUsername(e.target.value); + }; + + const handleSubmit = async (): Promise => { + try { + // Call the API function to change the username + const token = localStorage.getItem("accessToken"); + if (!token) { + throw new Error("Access token not found"); } - }) - .catch((error) => { - alert("Name not changed"); - console.error("An error occurred during change:", error); + + const response = await api.changeUserName( + { prevName: "currentName", newName: newUsername }, + token, + ); + + if (response.success) { + // Optionally, add a success message or redirect the user + console.log("Username changed successfully"); + } else { + // Handle the error message + console.error("Failed to change username:", response.message); + setErrorMessage(response.message ?? "Failed to change username"); + } + } catch (error) { + console.error("Error changing username:", error); + // Optionally, handle the error + setErrorMessage("Failed to change username"); + } + }; + + const handleButtonClick = (): void => { + handleSubmit().catch((error) => { + console.error("Error in handleSubmit:", error); }); + }; + + return ( +
+ + {errorMessage &&
{errorMessage}
} + +
+ ); } export default ChangeUsername; diff --git a/frontend/src/Components/DeleteProject.tsx b/frontend/src/Components/DeleteProject.tsx deleted file mode 100644 index 4add857..0000000 --- a/frontend/src/Components/DeleteProject.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { api, APIResponse } from "../API/API"; - -/** - * Use to delete a project from the system - * @param {string} props.projectToDelete - The projectname of project to delete - * @returns {void} Nothing - * @example - * const exampleProjectName = "project"; - * DeleteProject({ projectToDelete: exampleProjectName }); - */ - -function DeleteProject(props: { projectToDelete: string }): void { - api - .removeProject( - props.projectToDelete, - localStorage.getItem("accessToken") ?? "", - ) - .then((response: APIResponse) => { - if (response.success) { - alert("Project has been deleted!"); - location.reload(); - } else { - alert("Project has not been deleted"); - console.error(response.message); - } - }) - .catch((error) => { - alert("project has not been deleted"); - console.error("An error occurred during deletion:", error); - }); -} - -export default DeleteProject; diff --git a/frontend/src/Components/DeleteUser.tsx b/frontend/src/Components/DeleteUser.tsx index 7c5e8e8..d1dbc7f 100644 --- a/frontend/src/Components/DeleteUser.tsx +++ b/frontend/src/Components/DeleteUser.tsx @@ -3,7 +3,7 @@ import { api, APIResponse } from "../API/API"; /** * Use to remove a user from the system - * @param {string} props.usernameToDelete - The username of user to remove + * @param props - The username of user to remove * @returns {boolean} True if removed, false if not * @example * const exampleUsername = "user"; @@ -29,7 +29,7 @@ function DeleteUser(props: { usernameToDelete: string }): boolean { }) .catch((error) => { alert("User has not been deleted"); - console.error("An error occurred during deletion:", error); + console.error("An error occurred during creation:", error); }); return removed; } diff --git a/frontend/src/Components/DisplayUnsignedReports.tsx b/frontend/src/Components/DisplayUnsignedReports.tsx index 232cb31..780f20c 100644 --- a/frontend/src/Components/DisplayUnsignedReports.tsx +++ b/frontend/src/Components/DisplayUnsignedReports.tsx @@ -1,7 +1,12 @@ import { useState, useEffect } from "react"; import { Link, useParams } from "react-router-dom"; -import { api } from "../API/API"; -import { WeeklyReport } from "../Types/goTypes"; + +interface UnsignedReports { + projectName: string; + username: string; + week: number; + signed: boolean; +} /** * Renders a component that displays the projects a user is a part of and links to the projects start-page. @@ -9,25 +14,80 @@ import { WeeklyReport } from "../Types/goTypes"; */ function DisplayUserProject(): JSX.Element { const { projectName } = useParams(); - const [unsignedReports, setUnsignedReports] = useState([]); + const [unsignedReports, setUnsignedReports] = useState([]); //const navigate = useNavigate(); - useEffect(() => { - const getUnsignedReports = async (): Promise => { - const token = localStorage.getItem("accessToken") ?? ""; - const response = await api.getUnsignedReportsInProject( - projectName ?? "", - token, - ); - console.log(response); - if (response.success) { - setUnsignedReports(response.data ?? []); - } else { - console.error(response.message); - } - }; + // const getUnsignedReports = async (): Promise => { + // const token = localStorage.getItem("accessToken") ?? ""; + // const response = await api.getUserProjects(token); + // console.log(response); + // if (response.success) { + // setUnsignedReports(response.data ?? []); + // } else { + // console.error(response.message); + // } + // }; + + // const handleReportClick = async (projectName: string): Promise => { + // const username = localStorage.getItem("username") ?? ""; + // const token = localStorage.getItem("accessToken") ?? ""; + // const response = await api.checkIfProjectManager( + // username, + // projectName, + // token, + // ); + // if (response.success) { + // if (response.data) { + // navigate(`/PMProjectPage/${projectName}`); + // } else { + // navigate(`/project/${projectName}`); + // } + // } else { + // // handle error + // console.error(response.message); + // } + // }; + + const getUnsignedReports = async (): Promise => { + // Simulate a delay + await Promise.resolve(); + + // Use mock data + const reports: UnsignedReports[] = [ + { + projectName: "projecttest", + username: "user1", + week: 2, + signed: false, + }, + { + projectName: "projecttest", + username: "user2", + week: 2, + signed: false, + }, + { + projectName: "projecttest", + username: "user3", + week: 2, + signed: false, + }, + { + projectName: "projecttest", + username: "user4", + week: 2, + signed: false, + }, + ]; + + // Set the state with the mock data + setUnsignedReports(reports); + }; + + // Call getProjects when the component mounts + useEffect(() => { void getUnsignedReports(); - }, [projectName]); // Include 'projectName' in the dependency array + }, []); return ( <> @@ -35,40 +95,32 @@ function DisplayUserProject(): JSX.Element { All Unsigned Reports In: {projectName}{" "}
- {unsignedReports.map((unsignedReport: WeeklyReport, index: number) => ( -

-
-
- UserID: -

{unsignedReport.userId}

- Week: -

{unsignedReport.week}

- Total Time: -

- {unsignedReport.developmentTime + - unsignedReport.meetingTime + - unsignedReport.adminTime + - unsignedReport.ownWorkTime + - unsignedReport.studyTime + - unsignedReport.testingTime} -

- Signed: -

NO

-
-
-
- -

- View Report -

- + {unsignedReports.map( + (unsignedReport: UnsignedReports, index: number) => ( +

+
+
+

{unsignedReport.username}

+ Week: +

{unsignedReport.week}

+ Signed: +

NO

+
+
+
+ +

+ View Report +

+ +
-

-

- ))} + + ), + )}
); diff --git a/frontend/src/Components/DisplayUserProjects.tsx b/frontend/src/Components/DisplayUserProjects.tsx index 65bd4f5..494e6b0 100644 --- a/frontend/src/Components/DisplayUserProjects.tsx +++ b/frontend/src/Components/DisplayUserProjects.tsx @@ -1,7 +1,6 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { Project } from "../Types/goTypes"; import { useNavigate } from "react-router-dom"; -import GetProjects from "./GetProjects"; import { api } from "../API/API"; /** @@ -12,20 +11,23 @@ function DisplayUserProject(): JSX.Element { const [projects, setProjects] = useState([]); const navigate = useNavigate(); - GetProjects({ - setProjectsProp: setProjects, - username: localStorage.getItem("username") ?? "", - }); + const getProjects = async (): Promise => { + const token = localStorage.getItem("accessToken") ?? ""; + const response = await api.getUserProjects(token); + console.log(response); + if (response.success) { + setProjects(response.data ?? []); + } else { + console.error(response.message); + } + }; const handleProjectClick = async (projectName: string): Promise => { const token = localStorage.getItem("accessToken") ?? ""; const response = await api.checkIfProjectManager(projectName, token); console.log(response.data); if (response.success) { - if ( - (response.data as unknown as { isProjectManager: boolean }) - .isProjectManager - ) { + if (response.data === true) { navigate(`/PMProjectPage/${projectName}`); } else { navigate(`/project/${projectName}`); @@ -36,6 +38,11 @@ function DisplayUserProject(): JSX.Element { } }; + // Call getProjects when the component mounts + useEffect(() => { + void getProjects(); + }, []); + return ( <>

Your Projects

diff --git a/frontend/src/Components/EditWeeklyReport.tsx b/frontend/src/Components/EditWeeklyReport.tsx index d56ee42..384359e 100644 --- a/frontend/src/Components/EditWeeklyReport.tsx +++ b/frontend/src/Components/EditWeeklyReport.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from "react"; -import { WeeklyReport, UpdateWeeklyReport } from "../Types/goTypes"; +import { WeeklyReport, NewWeeklyReport } from "../Types/goTypes"; import { api } from "../API/API"; import { useNavigate, useParams } from "react-router-dom"; import Button from "./Button"; @@ -22,7 +22,6 @@ export default function GetWeeklyReport(): JSX.Element { projectName: string; fetchedWeek: string; }>(); - const username = localStorage.getItem("userName") ?? ""; console.log(projectName, fetchedWeek); useEffect(() => { @@ -61,9 +60,8 @@ export default function GetWeeklyReport(): JSX.Element { void fetchWeeklyReport(); }, [projectName, fetchedWeek, token]); - const handleUpdateWeeklyReport = async (): Promise => { - const updateWeeklyReport: UpdateWeeklyReport = { - userName: username, + const handleNewWeeklyReport = async (): Promise => { + const newWeeklyReport: NewWeeklyReport = { projectName: projectName ?? "", week, developmentTime, @@ -74,7 +72,7 @@ export default function GetWeeklyReport(): JSX.Element { testingTime, }; - await api.updateWeeklyReport(updateWeeklyReport, token); + await api.submitWeeklyReport(newWeeklyReport, token); }; const navigate = useNavigate(); @@ -91,8 +89,7 @@ export default function GetWeeklyReport(): JSX.Element { return; } e.preventDefault(); - void handleUpdateWeeklyReport(); - alert("Changes submitted"); + void handleNewWeeklyReport(); navigate(-1); }} > @@ -131,12 +128,7 @@ export default function GetWeeklyReport(): JSX.Element { }} onKeyDown={(event) => { const keyValue = event.key; - if ( - !/\d/.test(keyValue) && - keyValue !== "Backspace" && - keyValue !== "ArrowLeft" && - keyValue !== "ArrowRight" - ) + if (!/\d/.test(keyValue) && keyValue !== "Backspace") event.preventDefault(); }} /> @@ -160,12 +152,7 @@ export default function GetWeeklyReport(): JSX.Element { }} onKeyDown={(event) => { const keyValue = event.key; - if ( - !/\d/.test(keyValue) && - keyValue !== "Backspace" && - keyValue !== "ArrowLeft" && - keyValue !== "ArrowRight" - ) + if (!/\d/.test(keyValue) && keyValue !== "Backspace") event.preventDefault(); }} /> @@ -189,12 +176,7 @@ export default function GetWeeklyReport(): JSX.Element { }} onKeyDown={(event) => { const keyValue = event.key; - if ( - !/\d/.test(keyValue) && - keyValue !== "Backspace" && - keyValue !== "ArrowLeft" && - keyValue !== "ArrowRight" - ) + if (!/\d/.test(keyValue) && keyValue !== "Backspace") event.preventDefault(); }} /> @@ -218,12 +200,7 @@ export default function GetWeeklyReport(): JSX.Element { }} onKeyDown={(event) => { const keyValue = event.key; - if ( - !/\d/.test(keyValue) && - keyValue !== "Backspace" && - keyValue !== "ArrowLeft" && - keyValue !== "ArrowRight" - ) + if (!/\d/.test(keyValue) && keyValue !== "Backspace") event.preventDefault(); }} /> @@ -247,12 +224,7 @@ export default function GetWeeklyReport(): JSX.Element { }} onKeyDown={(event) => { const keyValue = event.key; - if ( - !/\d/.test(keyValue) && - keyValue !== "Backspace" && - keyValue !== "ArrowLeft" && - keyValue !== "ArrowRight" - ) + if (!/\d/.test(keyValue) && keyValue !== "Backspace") event.preventDefault(); }} /> @@ -276,12 +248,7 @@ export default function GetWeeklyReport(): JSX.Element { }} onKeyDown={(event) => { const keyValue = event.key; - if ( - !/\d/.test(keyValue) && - keyValue !== "Backspace" && - keyValue !== "ArrowLeft" && - keyValue !== "ArrowRight" - ) + if (!/\d/.test(keyValue) && keyValue !== "Backspace") event.preventDefault(); }} /> diff --git a/frontend/src/Components/GetProjectTimes.tsx b/frontend/src/Components/GetProjectTimes.tsx deleted file mode 100644 index 38288ec..0000000 --- a/frontend/src/Components/GetProjectTimes.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import { Dispatch, SetStateAction, useEffect } from "react"; -import { api } from "../API/API"; - -/** - * Interface for reported time per category + total time reported - */ -export interface projectTimes { - admin: number; - development: number; - meeting: number; - own_work: number; - study: number; - testing: number; - totalTime?: number; -} - -/** - * Gets all reported times for this project - * @param {Dispatch} props.setTimesProp - A setStateAction for the map you want to put times in - * @param {string} props.projectName - Username - * @returns {void} Nothing - * @example - * const projectName = "Example"; - * const [times, setTimes] = useState(); - * GetProjectTimes({ setTimesProp: setTimes, projectName: projectName }); - */ -function GetProjectTimes(props: { - setTimesProp: Dispatch>; - projectName: string; -}): void { - const setTimes: Dispatch> = - props.setTimesProp; - useEffect(() => { - const fetchUsers = async (): Promise => { - try { - const token = localStorage.getItem("accessToken") ?? ""; - const response = await api.getProjectTimes(props.projectName, token); - if (response.success && response.data) { - // Calculates total time reported - response.data.totalTime = response.data.admin; - response.data.totalTime += response.data.development; - response.data.totalTime += response.data.meeting; - response.data.totalTime += response.data.own_work; - response.data.totalTime += response.data.study; - response.data.totalTime += response.data.testing; - setTimes(response.data); - } else { - console.error("Failed to fetch project times:", response.message); - } - } catch (error) { - console.error("Error fetching times:", error); - } - }; - - void fetchUsers(); - }, [props.projectName, setTimes]); -} - -export default GetProjectTimes; diff --git a/frontend/src/Components/GetProjects.tsx b/frontend/src/Components/GetProjects.tsx index bd6c303..d6ab1f7 100644 --- a/frontend/src/Components/GetProjects.tsx +++ b/frontend/src/Components/GetProjects.tsx @@ -4,17 +4,14 @@ import { api } from "../API/API"; /** * Gets all projects that user is a member of - * @param {Dispatch} props.setProjectsProp - A setStateAction for the array you want to put projects in - * @param {string} props.username - Username + * @param props - A setStateAction for the array you want to put projects in * @returns {void} Nothing * @example - * const username = "Example"; * const [projects, setProjects] = useState([]); - * GetProjects({ setProjectsProp: setProjects, username: username }); + * GetAllUsers({ setProjectsProp: setProjects }); */ function GetProjects(props: { setProjectsProp: Dispatch>; - username: string; }): void { const setProjects: Dispatch> = props.setProjectsProp; @@ -22,7 +19,7 @@ function GetProjects(props: { const fetchUsers = async (): Promise => { try { const token = localStorage.getItem("accessToken") ?? ""; - const response = await api.getUserProjects(props.username, token); + const response = await api.getUserProjects(token); if (response.success) { setProjects(response.data ?? []); } else { @@ -34,7 +31,7 @@ function GetProjects(props: { }; void fetchUsers(); - }, [props.username, setProjects]); + }, [setProjects]); } export default GetProjects; diff --git a/frontend/src/Components/GetUsersInProject.tsx b/frontend/src/Components/GetUsersInProject.tsx index eb32e9b..acdd965 100644 --- a/frontend/src/Components/GetUsersInProject.tsx +++ b/frontend/src/Components/GetUsersInProject.tsx @@ -1,25 +1,20 @@ import { Dispatch, useEffect } from "react"; +import { UserProjectMember } from "../Types/goTypes"; import { api } from "../API/API"; -export interface ProjectMember { - Username: string; - UserRole: string; -} - /** - * Gets all members of a project - * @param string - The project's name - * @param Dispatch - A setStateAction for the array you want to put members in + * Gets all projects that user is a member of + * @param props - A setStateAction for the array you want to put projects in * @returns {void} Nothing * @example - * const [users, setUsers] = useState([]); - * GetUsersInProject({ projectName: props.projectname, setUsersProp: setUsers }); + * const [projects, setProjects] = useState([]); + * GetAllUsers({ setProjectsProp: setProjects }); */ function GetUsersInProject(props: { projectName: string; - setUsersProp: Dispatch>; + setUsersProp: Dispatch>; }): void { - const setUsers: Dispatch> = + const setUsers: Dispatch> = props.setUsersProp; useEffect(() => { const fetchUsers = async (): Promise => { @@ -29,10 +24,10 @@ function GetUsersInProject(props: { if (response.success) { setUsers(response.data ?? []); } else { - console.error("Failed to fetch members:", response.message); + console.error("Failed to fetch projects:", response.message); } } catch (error) { - console.error("Error fetching members:", error); + console.error("Error fetching projects:", error); } }; void fetchUsers(); diff --git a/frontend/src/Components/InputField.tsx b/frontend/src/Components/InputField.tsx index 699d8fa..639b4ca 100644 --- a/frontend/src/Components/InputField.tsx +++ b/frontend/src/Components/InputField.tsx @@ -19,7 +19,7 @@ function InputField(props: { onChange: (e: React.ChangeEvent) => void; }): JSX.Element { return ( -
+
-
- - - ); -} - -export default MemberInfoModal; diff --git a/frontend/src/Components/NewWeeklyReport.tsx b/frontend/src/Components/NewWeeklyReport.tsx index 1bb5cd4..f684b0c 100644 --- a/frontend/src/Components/NewWeeklyReport.tsx +++ b/frontend/src/Components/NewWeeklyReport.tsx @@ -139,12 +139,7 @@ export default function NewWeeklyReport(): JSX.Element { }} onKeyDown={(event) => { const keyValue = event.key; - if ( - !/\d/.test(keyValue) && - keyValue !== "Backspace" && - keyValue !== "ArrowLeft" && - keyValue !== "ArrowRight" - ) + if (!/\d/.test(keyValue) && keyValue !== "Backspace") event.preventDefault(); }} /> @@ -168,12 +163,7 @@ export default function NewWeeklyReport(): JSX.Element { }} onKeyDown={(event) => { const keyValue = event.key; - if ( - !/\d/.test(keyValue) && - keyValue !== "Backspace" && - keyValue !== "ArrowLeft" && - keyValue !== "ArrowRight" - ) + if (!/\d/.test(keyValue) && keyValue !== "Backspace") event.preventDefault(); }} /> @@ -197,12 +187,7 @@ export default function NewWeeklyReport(): JSX.Element { }} onKeyDown={(event) => { const keyValue = event.key; - if ( - !/\d/.test(keyValue) && - keyValue !== "Backspace" && - keyValue !== "ArrowLeft" && - keyValue !== "ArrowRight" - ) + if (!/\d/.test(keyValue) && keyValue !== "Backspace") event.preventDefault(); }} /> @@ -226,12 +211,7 @@ export default function NewWeeklyReport(): JSX.Element { }} onKeyDown={(event) => { const keyValue = event.key; - if ( - !/\d/.test(keyValue) && - keyValue !== "Backspace" && - keyValue !== "ArrowLeft" && - keyValue !== "ArrowRight" - ) + if (!/\d/.test(keyValue) && keyValue !== "Backspace") event.preventDefault(); }} /> @@ -255,12 +235,7 @@ export default function NewWeeklyReport(): JSX.Element { }} onKeyDown={(event) => { const keyValue = event.key; - if ( - !/\d/.test(keyValue) && - keyValue !== "Backspace" && - keyValue !== "ArrowLeft" && - keyValue !== "ArrowRight" - ) + if (!/\d/.test(keyValue) && keyValue !== "Backspace") event.preventDefault(); }} /> @@ -284,12 +259,7 @@ export default function NewWeeklyReport(): JSX.Element { }} onKeyDown={(event) => { const keyValue = event.key; - if ( - !/\d/.test(keyValue) && - keyValue !== "Backspace" && - keyValue !== "ArrowLeft" && - keyValue !== "ArrowRight" - ) + if (!/\d/.test(keyValue) && keyValue !== "Backspace") event.preventDefault(); }} /> diff --git a/frontend/src/Components/ProjectInfoModal.tsx b/frontend/src/Components/ProjectInfoModal.tsx index 6236089..3075b19 100644 --- a/frontend/src/Components/ProjectInfoModal.tsx +++ b/frontend/src/Components/ProjectInfoModal.tsx @@ -1,49 +1,31 @@ -import { useEffect, useRef, useState } from "react"; +import { useState } from "react"; import Button from "./Button"; -import GetUsersInProject, { ProjectMember } from "./GetUsersInProject"; +import { UserProjectMember } from "../Types/goTypes"; +import GetUsersInProject from "./GetUsersInProject"; import { Link } from "react-router-dom"; -import GetProjectTimes, { projectTimes } from "./GetProjectTimes"; -import DeleteProject from "./DeleteProject"; function ProjectInfoModal(props: { + isVisible: boolean; projectname: string; onClose: () => void; onClick: (username: string) => void; }): JSX.Element { - const [users, setUsers] = useState([]); - const [times, setTimes] = useState(); - const totalTime = useRef(0); + const [users, setUsers] = useState([]); GetUsersInProject({ projectName: props.projectname, setUsersProp: setUsers }); - - GetProjectTimes({ setTimesProp: setTimes, projectName: props.projectname }); - - useEffect(() => { - if (times?.totalTime !== undefined) { - totalTime.current = times.totalTime; - } - }, [times]); + if (!props.isVisible) return <>; return (
-
-
+
+

- {props.projectname} + {localStorage.getItem("projectName") ?? ""}

-
-

Statistics:

-
-
-

Number of members: {users.length}

-

Total time reported: {totalTime.current}

-
-
-

Project members:

-
-
+

Project members:

+
    {users.map((user) => ( @@ -63,44 +45,31 @@ function ProjectInfoModal(props: { ))}
-
+
+
+
+ +
diff --git a/frontend/src/Components/ProjectListAdmin.tsx b/frontend/src/Components/ProjectListAdmin.tsx index 294a131..f25ee47 100644 --- a/frontend/src/Components/ProjectListAdmin.tsx +++ b/frontend/src/Components/ProjectListAdmin.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import { NewProject } from "../Types/goTypes"; import ProjectInfoModal from "./ProjectInfoModal"; -import MemberInfoModal from "./MemberInfoModal"; +import UserInfoModal from "./UserInfoModal"; /** * A list of projects for admin manage projects page, that sets an onClick @@ -18,7 +18,7 @@ export function ProjectListAdmin(props: { projects: NewProject[]; }): JSX.Element { const [projectModalVisible, setProjectModalVisible] = useState(false); - const [projectName, setProjectName] = useState(""); + const [projectname, setProjectname] = useState(""); const [userModalVisible, setUserModalVisible] = useState(false); const [username, setUsername] = useState(""); @@ -28,36 +28,39 @@ export function ProjectListAdmin(props: { }; const handleClickProject = (projectname: string): void => { - setProjectName(projectname); + setProjectname(projectname); + localStorage.setItem("projectName", projectname); setProjectModalVisible(true); }; const handleCloseProject = (): void => { - setProjectName(""); + setProjectname(""); setProjectModalVisible(false); }; const handleCloseUser = (): void => { - setUsername(""); + setProjectname(""); setUserModalVisible(false); }; return ( <> - {projectModalVisible && ( - - )} - {userModalVisible && ( - - )} + + { + return; + }} + isVisible={userModalVisible} + username={username} + />
    {props.projects.map((project) => ( diff --git a/frontend/src/Components/ProjectMembers.tsx b/frontend/src/Components/ProjectMembers.tsx index 52e8559..60ffcd9 100644 --- a/frontend/src/Components/ProjectMembers.tsx +++ b/frontend/src/Components/ProjectMembers.tsx @@ -1,15 +1,31 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { Link, useParams } from "react-router-dom"; -import GetUsersInProject, { ProjectMember } from "./GetUsersInProject"; +import { api } from "../API/API"; +import { UserProjectMember } from "../Types/goTypes"; function ProjectMembers(): JSX.Element { const { projectName } = useParams(); - const [projectMembers, setProjectMembers] = useState([]); + const [projectMembers, setProjectMembers] = useState([]); - GetUsersInProject({ - projectName: projectName ?? "", - setUsersProp: setProjectMembers, - }); + useEffect(() => { + const getProjectMembers = async (): Promise => { + const token = localStorage.getItem("accessToken") ?? ""; + const response = await api.getAllUsersProject(projectName ?? "", token); + console.log(response); + if (response.success) { + setProjectMembers(response.data ?? []); + } else { + console.error(response.message); + } + }; + + void getProjectMembers(); + }, [projectName]); + + interface ProjectMember { + Username: string; + UserRole: string; + } return ( <> diff --git a/frontend/src/Components/Register.tsx b/frontend/src/Components/Register.tsx index 68e0979..6192637 100644 --- a/frontend/src/Components/Register.tsx +++ b/frontend/src/Components/Register.tsx @@ -22,8 +22,6 @@ export default function Register(): JSX.Element { const response = await api.registerUser(newUser); if (response.success) { alert("User added!"); - setPassword(""); - setUsername(""); } else { alert("User not added"); setErrMessage(response.message ?? "Unknown error"); @@ -49,24 +47,22 @@ export default function Register(): JSX.Element {

    Register New User

    -
    - { - setUsername(e.target.value); - }} - /> - { - setPassword(e.target.value); - }} - /> -
    + { + setUsername(e.target.value); + }} + /> + { + setPassword(e.target.value); + }} + />
    - )} -

    Member of these projects:

    - -
    -
); diff --git a/frontend/src/Components/UserListAdmin.tsx b/frontend/src/Components/UserListAdmin.tsx index 76cae9f..c08b05c 100644 --- a/frontend/src/Components/UserListAdmin.tsx +++ b/frontend/src/Components/UserListAdmin.tsx @@ -1,5 +1,6 @@ import { useState } from "react"; import UserInfoModal from "./UserInfoModal"; +import DeleteUser from "./DeleteUser"; /** * A list of users for admin manage users page, that sets an onClick @@ -29,7 +30,9 @@ export function UserListAdmin(props: { users: string[] }): JSX.Element { return ( <> DeleteUser} isVisible={modalVisible} username={username} /> diff --git a/frontend/src/Components/UserProjectListAdmin.tsx b/frontend/src/Components/UserProjectListAdmin.tsx index bc85c5b..1b7b923 100644 --- a/frontend/src/Components/UserProjectListAdmin.tsx +++ b/frontend/src/Components/UserProjectListAdmin.tsx @@ -1,17 +1,35 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; +import { api } from "../API/API"; import { Project } from "../Types/goTypes"; -import GetProjects from "./GetProjects"; -function UserProjectListAdmin(props: { username: string }): JSX.Element { +function UserProjectListAdmin(): JSX.Element { const [projects, setProjects] = useState([]); - GetProjects({ setProjectsProp: setProjects, username: props.username }); + useEffect(() => { + const fetchProjects = async (): Promise => { + try { + const token = localStorage.getItem("accessToken") ?? ""; + // const username = props.username; + + const response = await api.getUserProjects(token); + if (response.success) { + setProjects(response.data ?? []); + } else { + console.error("Failed to fetch projects:", response.message); + } + } catch (error) { + console.error("Error fetching projects:", error); + } + }; + + void fetchProjects(); + }, []); return ( -
-
    +
    +
      {projects.map((project) => ( -
    • +
    • {project.name}
    • ))} diff --git a/frontend/src/Components/ViewOtherTimeReport.tsx b/frontend/src/Components/ViewOtherTimeReport.tsx index bde0529..32e0716 100644 --- a/frontend/src/Components/ViewOtherTimeReport.tsx +++ b/frontend/src/Components/ViewOtherTimeReport.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from "react"; -import { WeeklyReport } from "../Types/goTypes"; +import { WeeklyReport, NewWeeklyReport } from "../Types/goTypes"; import { api } from "../API/API"; import { useNavigate, useParams } from "react-router-dom"; import Button from "./Button"; @@ -18,7 +18,6 @@ export default function GetOtherUsersReport(): JSX.Element { const [ownWorkTime, setOwnWorkTime] = useState(0); const [studyTime, setStudyTime] = useState(0); const [testingTime, setTestingTime] = useState(0); - const [reportId, setReportId] = useState(0); const token = localStorage.getItem("accessToken") ?? ""; const { projectName } = useParams(); @@ -46,7 +45,6 @@ export default function GetOtherUsersReport(): JSX.Element { studyTime: 0, testingTime: 0, }; - setReportId(report.reportId); setWeek(report.week); setDevelopmentTime(report.developmentTime); setMeetingTime(report.meetingTime); @@ -63,23 +61,30 @@ export default function GetOtherUsersReport(): JSX.Element { }); const handleSignWeeklyReport = async (): Promise => { - await api.signReport(reportId, token); + const newWeeklyReport: NewWeeklyReport = { + projectName: projectName ?? "", + week, + developmentTime, + meetingTime, + adminTime, + ownWorkTime, + studyTime, + testingTime, + }; + + await api.submitWeeklyReport(newWeeklyReport, token); }; const navigate = useNavigate(); return ( <> -

      - {" "} - UserId: {username}'s Report -

      +

      {username}'s Report

      { e.preventDefault(); void handleSignWeeklyReport(); - alert("Report successfully signed!"); navigate(-1); }} > @@ -107,10 +112,7 @@ export default function GetOtherUsersReport(): JSX.Element { type="text" min="0" className="border-2 border-black rounded-md text-center w-1/2" - defaultValue={ - developmentTime === 0 ? "" : developmentTime - } - readOnly + value={developmentTime === 0 ? "" : developmentTime} /> @@ -121,8 +123,7 @@ export default function GetOtherUsersReport(): JSX.Element { type="text" min="0" className="border-2 border-black rounded-md text-center w-1/2" - defaultValue={meetingTime === 0 ? "" : meetingTime} - readOnly + value={meetingTime === 0 ? "" : meetingTime} /> @@ -133,8 +134,7 @@ export default function GetOtherUsersReport(): JSX.Element { type="text" min="0" className="border-2 border-black rounded-md text-center w-1/2" - defaultValue={adminTime === 0 ? "" : adminTime} - readOnly + value={adminTime === 0 ? "" : adminTime} /> @@ -145,8 +145,7 @@ export default function GetOtherUsersReport(): JSX.Element { type="text" min="0" className="border-2 border-black rounded-md text-center w-1/2" - defaultValue={ownWorkTime === 0 ? "" : ownWorkTime} - readOnly + value={ownWorkTime === 0 ? "" : ownWorkTime} /> @@ -157,8 +156,7 @@ export default function GetOtherUsersReport(): JSX.Element { type="text" min="0" className="border-2 border-black rounded-md text-center w-1/2" - defaultValue={studyTime === 0 ? "" : studyTime} - readOnly + value={studyTime === 0 ? "" : studyTime} /> @@ -169,8 +167,7 @@ export default function GetOtherUsersReport(): JSX.Element { type="text" min="0" className="border-2 border-black rounded-md text-center w-1/2" - defaultValue={testingTime === 0 ? "" : testingTime} - readOnly + value={testingTime === 0 ? "" : testingTime} /> diff --git a/frontend/src/Containers/GenApiDemo.tsx b/frontend/src/Containers/GenApiDemo.tsx deleted file mode 100644 index 27092d8..0000000 --- a/frontend/src/Containers/GenApiDemo.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { useEffect } from "react"; -import { GenApi } from "../API/GenApi"; - -// Instantiation of the API -const api = new GenApi(); - -export function GenApiDemo(): JSX.Element { - // Sync wrapper around the loginCreate method - const register = async (): Promise => { - const response = await api.login.loginCreate({ - username: "admin", - password: "admin", - }); - console.log(response.data); // This should be the inner type of the response - }; - - // Call the wrapper - useEffect(() => { - void register(); - }); - - return <>; -} diff --git a/frontend/src/Pages/AdminPages/AdminChangeUsername.tsx b/frontend/src/Pages/AdminPages/AdminChangeUsername.tsx new file mode 100644 index 0000000..b130fae --- /dev/null +++ b/frontend/src/Pages/AdminPages/AdminChangeUsername.tsx @@ -0,0 +1,28 @@ +import BackButton from "../../Components/BackButton"; +import BasicWindow from "../../Components/BasicWindow"; +import Button from "../../Components/Button"; +import ChangeUsername from "../../Components/ChangeUsername"; + +function AdminChangeUsername(): JSX.Element { + const content = ( + <> + + + ); + + const buttons = ( + <> +