diff --git a/backend/Makefile b/backend/Makefile index 039340c..0ffc557 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -34,6 +34,7 @@ clean: rm -f plantuml.jar rm -f erd.png rm -f config.toml + rm -f database.txt # Test target test: db.sqlite3 @@ -105,6 +106,7 @@ 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 0009c17..7a08b0e 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -21,21 +21,21 @@ const docTemplate = `{ "paths": { "/login": { "post": { - "description": "logs the user in and returns a jwt token", + "description": "Logs in a user and returns a JWT token", "consumes": [ "application/json" ], "produces": [ - "text/plain" + "application/json" ], "tags": [ - "User" + "Auth" ], - "summary": "login", + "summary": "Login", "parameters": [ { - "description": "login info", - "name": "NewUser", + "description": "User credentials", + "name": "body", "in": "body", "required": true, "schema": { @@ -45,9 +45,9 @@ const docTemplate = `{ ], "responses": { "200": { - "description": "Successfully signed token for user", + "description": "JWT token", "schema": { - "type": "Token" + "$ref": "#/definitions/types.Token" } }, "400": { @@ -71,29 +71,26 @@ const docTemplate = `{ } } }, - "/loginerenew": { + "/loginrenew": { "post": { "security": [ { - "bererToken": [] + "JWT": [] } ], - "description": "renews the users token", - "consumes": [ + "description": "Renews the users token.", + "produces": [ "application/json" ], - "produces": [ - "text/plain" - ], "tags": [ - "User" + "Auth" ], "summary": "LoginRenews", "responses": { "200": { "description": "Successfully signed token for user", "schema": { - "type": "Token" + "$ref": "#/definitions/types.Token" } }, "401": { @@ -113,7 +110,12 @@ const docTemplate = `{ }, "/promoteToAdmin": { "post": { - "description": "promote chosen user to admin", + "security": [ + { + "JWT": [] + } + ], + "description": "Promote chosen user to site admin", "consumes": [ "application/json" ], @@ -139,7 +141,7 @@ const docTemplate = `{ "200": { "description": "Successfully promoted user", "schema": { - "type": "json" + "$ref": "#/definitions/types.Token" } }, "400": { @@ -173,7 +175,7 @@ const docTemplate = `{ "text/plain" ], "tags": [ - "User" + "Auth" ], "summary": "Register", "parameters": [ @@ -211,6 +213,11 @@ const docTemplate = `{ }, "/userdelete/{username}": { "delete": { + "security": [ + { + "JWT": [] + } + ], "description": "UserDelete deletes a user from the database", "consumes": [ "application/json" @@ -252,12 +259,14 @@ const docTemplate = `{ }, "/users/all": { "get": { - "description": "lists all users", - "consumes": [ - "application/json" + "security": [ + { + "JWT": [] + } ], + "description": "lists all users", "produces": [ - "text/plain" + "application/json" ], "tags": [ "User" @@ -265,9 +274,12 @@ const docTemplate = `{ "summary": "ListsAllUsers", "responses": { "200": { - "description": "Successfully signed token for user", + "description": "Successfully returned all users", "schema": { - "type": "json" + "type": "array", + "items": { + "type": "string" + } } }, "401": { @@ -291,16 +303,27 @@ const docTemplate = `{ "type": "object", "properties": { "password": { - "type": "string" + "type": "string", + "example": "password123" }, "username": { + "type": "string", + "example": "username123" + } + } + }, + "types.Token": { + "type": "object", + "properties": { + "token": { "type": "string" } } } }, "securityDefinitions": { - "bererToken": { + "JWT": { + "description": "Use the JWT token provided by the login endpoint to authenticate requests. **Prefix the token with \"Bearer \".**", "type": "apiKey", "name": "Authorization", "in": "header" diff --git a/backend/internal/handlers/projects/GetUserProject.go b/backend/internal/handlers/projects/GetUserProject.go index 99ed63b..6c80515 100644 --- a/backend/internal/handlers/projects/GetUserProject.go +++ b/backend/internal/handlers/projects/GetUserProject.go @@ -4,15 +4,16 @@ import ( db "ttime/internal/database" "github.com/gofiber/fiber/v2" - "github.com/golang-jwt/jwt/v5" + "github.com/gofiber/fiber/v2/log" ) // GetUserProjects returns all projects that the user is a member of func GetUserProjects(c *fiber.Ctx) error { - // First we get the username from the token - user := c.Locals("user").(*jwt.Token) - claims := user.Claims.(jwt.MapClaims) - username := claims["name"].(string) + username := c.Params("username") + if username == "" { + log.Info("No username provided") + return c.Status(400).SendString("No username provided") + } // 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 266127d..6c5d455 100644 --- a/backend/internal/handlers/projects/ProjectRoleChange.go +++ b/backend/internal/handlers/projects/ProjectRoleChange.go @@ -24,7 +24,13 @@ func ProjectRoleChange(c *fiber.Ctx) error { return c.Status(400).SendString(err.Error()) } - log.Info("Changing role for user: ", username, " in project: ", data.Projectname, " to: ", data.Role) + // 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) // Dubble diping and checcking if current user is if ismanager, err := db.GetDb(c).IsProjectManager(username, data.Projectname); err != nil { @@ -36,7 +42,7 @@ func ProjectRoleChange(c *fiber.Ctx) error { } // Change the user's role within the project in the database - if err := db.GetDb(c).ChangeUserRole(username, data.Projectname, data.Role); err != nil { + if err := db.GetDb(c).ChangeUserRole(data.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 1cae76c..5ac5df0 100644 --- a/backend/internal/handlers/users/ListAllUsers.go +++ b/backend/internal/handlers/users/ListAllUsers.go @@ -7,16 +7,17 @@ import ( "github.com/gofiber/fiber/v2/log" ) -// 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] +// @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 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 c4d6c60..42c52a5 100644 --- a/backend/internal/handlers/users/Login.go +++ b/backend/internal/handlers/users/Login.go @@ -10,18 +10,19 @@ import ( "github.com/golang-jwt/jwt/v5" ) -// 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] +// @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 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 78eadfd..3926ce4 100644 --- a/backend/internal/handlers/users/LoginRenew.go +++ b/backend/internal/handlers/users/LoginRenew.go @@ -9,34 +9,40 @@ import ( "github.com/golang-jwt/jwt/v5" ) -// 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] +// @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 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() - renewed := jwt.MapClaims{ + + // Create token with old claims, but new expiration time + token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ "name": claims["name"], "admin": claims["admin"], "exp": claims["exp"], - } - token := jwt.NewWithClaims(jwt.SigningMethodHS256, renewed) + }) + + // Sign it with top secret key t, err := token.SignedString([]byte("secret")) if err != nil { log.Warn("Error signing token") - return c.SendStatus(fiber.StatusInternalServerError) + return c.SendStatus(fiber.StatusInternalServerError) // 500 } 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 4a21758..3f0a6d3 100644 --- a/backend/internal/handlers/users/PromoteToAdmin.go +++ b/backend/internal/handlers/users/PromoteToAdmin.go @@ -8,17 +8,20 @@ import ( "github.com/gofiber/fiber/v2/log" ) -// @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] +// @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 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 9977246..b9e0c78 100644 --- a/backend/internal/handlers/users/Register.go +++ b/backend/internal/handlers/users/Register.go @@ -8,11 +8,9 @@ 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 User +// @Tags Auth // @Accept json // @Produce plain // @Param NewUser body types.NewUser true "User to register" @@ -20,6 +18,8 @@ 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 5957c2d..491a1b3 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 88b4f06..37cc8c2 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"` - Password string `json:"password"` + Username string `json:"username" example:"username123"` + Password string `json:"password" example:"password123"` } // 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 4c2056e..6e65386 100644 --- a/backend/main.go +++ b/backend/main.go @@ -25,15 +25,16 @@ import ( // @license.name AGPL // @license.url https://www.gnu.org/licenses/agpl-3.0.html -//@securityDefinitions.apikey bererToken -//@in header -//@name Authorization +// @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 ".** // @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. @@ -111,7 +112,8 @@ func main() { // All project related routes // projectGroup := api.Group("/project") // Not currently in use - api.Get("/getUserProjects", projects.GetUserProjects) + api.Get("/getProjectTimes/:projectName", projects.GetProjectTimesHandler) + api.Get("/getUserProjects/:username", 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 new file mode 100644 index 0000000..c49d006 --- /dev/null +++ b/frontend/.prettierignore @@ -0,0 +1,2 @@ +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 0a85e70..c1480fb 100644 --- a/frontend/src/API/API.ts +++ b/frontend/src/API/API.ts @@ -1,13 +1,16 @@ +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"; /** @@ -73,10 +76,7 @@ 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,6 +88,17 @@ 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. @@ -113,10 +124,14 @@ 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(token: string): Promise>; + getUserProjects( + username: string, + token: string, + ): Promise>; /** Gets a project by its id. * @param {number} id The id of the project to retrieve. @@ -124,6 +139,16 @@ 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. @@ -133,7 +158,18 @@ interface API { getAllUsersProject( projectName: string, token: string, - ): Promise>; + ): 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>; + /** * Changes the username of a user in the database. * @param {StrNameChange} data The object containing the previous and new username. @@ -144,10 +180,21 @@ 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, @@ -249,7 +296,7 @@ export const api: API = { async createProject( project: NewProject, token: string, - ): Promise> { + ): Promise> { try { const response = await fetch("/api/project", { method: "POST", @@ -263,18 +310,17 @@ export const api: API = { if (!response.ok) { return { success: false, message: "Failed to create project" }; } else { - const data = (await response.json()) as Project; - return { success: true, data }; + return { success: true }; } } 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", @@ -316,9 +362,39 @@ export const api: API = { } }, - async getUserProjects(token: string): Promise> { + async changeUserRole( + roleInfo: ProjectRoleChange, + token: string, + ): Promise> { try { - const response = await fetch("/api/getUserProjects", { + 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}`, { method: "GET", headers: { "Content-Type": "application/json", @@ -343,6 +419,37 @@ 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, @@ -374,6 +481,37 @@ 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, @@ -510,7 +648,7 @@ export const api: API = { async getAllUsersProject( projectName: string, token: string, - ): Promise> { + ): Promise> { try { const response = await fetch(`/api/getUsersProject/${projectName}`, { method: "GET", @@ -526,7 +664,7 @@ export const api: API = { message: "Failed to get users", }); } else { - const data = (await response.json()) as UserProjectMember[]; + const data = (await response.json()) as ProjectMember[]; return Promise.resolve({ success: true, data }); } } catch (e) { @@ -537,6 +675,38 @@ 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, @@ -566,7 +736,7 @@ export const api: API = { token: string, ): Promise> { try { - const response = await fetch(`/api/projectdelete/${projectName}`, { + const response = await fetch(`/api/removeProject/${projectName}`, { method: "DELETE", headers: { "Content-Type": "application/json", diff --git a/frontend/src/API/GenApi.ts b/frontend/src/API/GenApi.ts new file mode 100644 index 0000000..8ca851b --- /dev/null +++ b/frontend/src/API/GenApi.ts @@ -0,0 +1,358 @@ +/* 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 d29be68..194afe8 100644 --- a/frontend/src/Components/AddMember.tsx +++ b/frontend/src/Components/AddMember.tsx @@ -1,5 +1,10 @@ import { APIResponse, api } from "../API/API"; -import { NewProjMember } from "../Types/goTypes"; + +export interface NewProjMember { + username: string; + role: string; + projectname: string; +} /** * Tries to add a member to a project @@ -21,7 +26,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 f5f4a08..e2ad8b9 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, Project } from "../Types/goTypes"; +import { NewProject } from "../Types/goTypes"; import InputField from "./InputField"; import Logo from "../assets/Logo.svg"; import Button from "./Button"; @@ -10,27 +10,26 @@ 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 }): boolean { +function CreateProject(props: { name: string; description: string }): void { 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) { - created = true; + alert("Project added!"); } 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; } /** @@ -48,7 +47,10 @@ 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 - { - setName(e.target.value); - }} - /> - { - setDescription(e.target.value); - }} - /> +
+ { + e.preventDefault(); + setName(e.target.value); + }} + /> + { + e.preventDefault(); + setDescription(e.target.value); + }} + /> +
+ ); +} diff --git a/frontend/src/Components/ChangeUsername.tsx b/frontend/src/Components/ChangeUsername.tsx index e297a04..78d7da9 100644 --- a/frontend/src/Components/ChangeUsername.tsx +++ b/frontend/src/Components/ChangeUsername.tsx @@ -1,61 +1,26 @@ -import React, { useState } from "react"; -import InputField from "./InputField"; -import { api } from "../API/API"; - -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"); - } - - const response = await api.changeUserName( - { prevName: "currentName", newName: newUsername }, - token, - ); +import { APIResponse, api } from "../API/API"; +import { StrNameChange } from "../Types/goTypes"; +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) { - // Optionally, add a success message or redirect the user - console.log("Username changed successfully"); + alert("Name changed successfully"); + location.reload(); } else { - // Handle the error message - console.error("Failed to change username:", response.message); - setErrorMessage(response.message ?? "Failed to change username"); + alert("Name not changed"); + console.error(response.message); } - } 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); + }) + .catch((error) => { + alert("Name not changed"); + console.error("An error occurred during change:", error); }); - }; - - return ( -
- - {errorMessage &&
{errorMessage}
} - -
- ); } export default ChangeUsername; diff --git a/frontend/src/Components/DeleteProject.tsx b/frontend/src/Components/DeleteProject.tsx new file mode 100644 index 0000000..4add857 --- /dev/null +++ b/frontend/src/Components/DeleteProject.tsx @@ -0,0 +1,33 @@ +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 d1dbc7f..7c5e8e8 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 props - The username of user to remove + * @param {string} props.usernameToDelete - 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 creation:", error); + console.error("An error occurred during deletion:", error); }); return removed; } diff --git a/frontend/src/Components/DisplayUnsignedReports.tsx b/frontend/src/Components/DisplayUnsignedReports.tsx index 780f20c..232cb31 100644 --- a/frontend/src/Components/DisplayUnsignedReports.tsx +++ b/frontend/src/Components/DisplayUnsignedReports.tsx @@ -1,12 +1,7 @@ import { useState, useEffect } from "react"; import { Link, useParams } from "react-router-dom"; - -interface UnsignedReports { - projectName: string; - username: string; - week: number; - signed: boolean; -} +import { api } from "../API/API"; +import { WeeklyReport } from "../Types/goTypes"; /** * Renders a component that displays the projects a user is a part of and links to the projects start-page. @@ -14,80 +9,25 @@ interface UnsignedReports { */ function DisplayUserProject(): JSX.Element { const { projectName } = useParams(); - const [unsignedReports, setUnsignedReports] = useState([]); + const [unsignedReports, setUnsignedReports] = useState([]); //const navigate = useNavigate(); - - // 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(() => { + 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); + } + }; + void getUnsignedReports(); - }, []); + }, [projectName]); // Include 'projectName' in the dependency array return ( <> @@ -95,32 +35,40 @@ function DisplayUserProject(): JSX.Element { All Unsigned Reports In: {projectName}{" "}
- {unsignedReports.map( - (unsignedReport: UnsignedReports, index: number) => ( -

-
-
-

{unsignedReport.username}

- Week: -

{unsignedReport.week}

- Signed: -

NO

-
-
-
- -

- View Report -

- -
+ {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 +

+
-

- ), - )} +
+

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

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

-

Project members:

-
+
+

Statistics:

+
+
+

Number of members: {users.length}

+

Total time reported: {totalTime.current}

+
+
+

Project members:

+
+
    {users.map((user) => ( @@ -45,31 +63,44 @@ function ProjectInfoModal(props: { ))}
-
-
-
diff --git a/frontend/src/Components/ProjectListAdmin.tsx b/frontend/src/Components/ProjectListAdmin.tsx index f25ee47..294a131 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 UserInfoModal from "./UserInfoModal"; +import MemberInfoModal from "./MemberInfoModal"; /** * 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,39 +28,36 @@ export function ProjectListAdmin(props: { }; const handleClickProject = (projectname: string): void => { - setProjectname(projectname); - localStorage.setItem("projectName", projectname); + setProjectName(projectname); setProjectModalVisible(true); }; const handleCloseProject = (): void => { - setProjectname(""); + setProjectName(""); setProjectModalVisible(false); }; const handleCloseUser = (): void => { - setProjectname(""); + setUsername(""); setUserModalVisible(false); }; return ( <> - - { - return; - }} - isVisible={userModalVisible} - username={username} - /> + {projectModalVisible && ( + + )} + {userModalVisible && ( + + )}
    {props.projects.map((project) => ( diff --git a/frontend/src/Components/ProjectMembers.tsx b/frontend/src/Components/ProjectMembers.tsx index 60ffcd9..52e8559 100644 --- a/frontend/src/Components/ProjectMembers.tsx +++ b/frontend/src/Components/ProjectMembers.tsx @@ -1,31 +1,15 @@ -import { useEffect, useState } from "react"; +import { useState } from "react"; import { Link, useParams } from "react-router-dom"; -import { api } from "../API/API"; -import { UserProjectMember } from "../Types/goTypes"; +import GetUsersInProject, { ProjectMember } from "./GetUsersInProject"; function ProjectMembers(): JSX.Element { const { projectName } = useParams(); - const [projectMembers, setProjectMembers] = useState([]); + const [projectMembers, setProjectMembers] = useState([]); - 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; - } + GetUsersInProject({ + projectName: projectName ?? "", + setUsersProp: setProjectMembers, + }); return ( <> diff --git a/frontend/src/Components/Register.tsx b/frontend/src/Components/Register.tsx index 6192637..68e0979 100644 --- a/frontend/src/Components/Register.tsx +++ b/frontend/src/Components/Register.tsx @@ -22,6 +22,8 @@ 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"); @@ -47,22 +49,24 @@ 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 c08b05c..76cae9f 100644 --- a/frontend/src/Components/UserListAdmin.tsx +++ b/frontend/src/Components/UserListAdmin.tsx @@ -1,6 +1,5 @@ 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 @@ -30,9 +29,7 @@ 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 1b7b923..bc85c5b 100644 --- a/frontend/src/Components/UserProjectListAdmin.tsx +++ b/frontend/src/Components/UserProjectListAdmin.tsx @@ -1,35 +1,17 @@ -import { useEffect, useState } from "react"; -import { api } from "../API/API"; +import { useState } from "react"; import { Project } from "../Types/goTypes"; +import GetProjects from "./GetProjects"; -function UserProjectListAdmin(): JSX.Element { +function UserProjectListAdmin(props: { username: string }): JSX.Element { const [projects, setProjects] = useState([]); - 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(); - }, []); + GetProjects({ setProjectsProp: setProjects, username: props.username }); return ( -
-
    +
    +
      {projects.map((project) => ( -
    • +
    • {project.name}
    • ))} diff --git a/frontend/src/Components/ViewOtherTimeReport.tsx b/frontend/src/Components/ViewOtherTimeReport.tsx index 32e0716..bde0529 100644 --- a/frontend/src/Components/ViewOtherTimeReport.tsx +++ b/frontend/src/Components/ViewOtherTimeReport.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from "react"; -import { WeeklyReport, NewWeeklyReport } from "../Types/goTypes"; +import { WeeklyReport } from "../Types/goTypes"; import { api } from "../API/API"; import { useNavigate, useParams } from "react-router-dom"; import Button from "./Button"; @@ -18,6 +18,7 @@ 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(); @@ -45,6 +46,7 @@ export default function GetOtherUsersReport(): JSX.Element { studyTime: 0, testingTime: 0, }; + setReportId(report.reportId); setWeek(report.week); setDevelopmentTime(report.developmentTime); setMeetingTime(report.meetingTime); @@ -61,30 +63,23 @@ export default function GetOtherUsersReport(): JSX.Element { }); const handleSignWeeklyReport = async (): Promise => { - const newWeeklyReport: NewWeeklyReport = { - projectName: projectName ?? "", - week, - developmentTime, - meetingTime, - adminTime, - ownWorkTime, - studyTime, - testingTime, - }; - - await api.submitWeeklyReport(newWeeklyReport, token); + await api.signReport(reportId, token); }; const navigate = useNavigate(); return ( <> -

      {username}'s Report

      +

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

      { e.preventDefault(); void handleSignWeeklyReport(); + alert("Report successfully signed!"); navigate(-1); }} > @@ -112,7 +107,10 @@ export default function GetOtherUsersReport(): JSX.Element { type="text" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={developmentTime === 0 ? "" : developmentTime} + defaultValue={ + developmentTime === 0 ? "" : developmentTime + } + readOnly /> @@ -123,7 +121,8 @@ export default function GetOtherUsersReport(): JSX.Element { type="text" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={meetingTime === 0 ? "" : meetingTime} + defaultValue={meetingTime === 0 ? "" : meetingTime} + readOnly /> @@ -134,7 +133,8 @@ export default function GetOtherUsersReport(): JSX.Element { type="text" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={adminTime === 0 ? "" : adminTime} + defaultValue={adminTime === 0 ? "" : adminTime} + readOnly /> @@ -145,7 +145,8 @@ export default function GetOtherUsersReport(): JSX.Element { type="text" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={ownWorkTime === 0 ? "" : ownWorkTime} + defaultValue={ownWorkTime === 0 ? "" : ownWorkTime} + readOnly /> @@ -156,7 +157,8 @@ export default function GetOtherUsersReport(): JSX.Element { type="text" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={studyTime === 0 ? "" : studyTime} + defaultValue={studyTime === 0 ? "" : studyTime} + readOnly /> @@ -167,7 +169,8 @@ export default function GetOtherUsersReport(): JSX.Element { type="text" min="0" className="border-2 border-black rounded-md text-center w-1/2" - value={testingTime === 0 ? "" : testingTime} + defaultValue={testingTime === 0 ? "" : testingTime} + readOnly /> diff --git a/frontend/src/Containers/GenApiDemo.tsx b/frontend/src/Containers/GenApiDemo.tsx new file mode 100644 index 0000000..27092d8 --- /dev/null +++ b/frontend/src/Containers/GenApiDemo.tsx @@ -0,0 +1,23 @@ +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 deleted file mode 100644 index b130fae..0000000 --- a/frontend/src/Pages/AdminPages/AdminChangeUsername.tsx +++ /dev/null @@ -1,28 +0,0 @@ -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 = ( - <> -