diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 8026f58..322c812 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -19,19 +19,174 @@ const docTemplate = `{ "host": "{{.Host}}", "basePath": "{{.BasePath}}", "paths": { - "/api/register": { + "/login": { + "post": { + "description": "logs the user in and returns a jwt token", + "consumes": [ + "application/json" + ], + "produces": [ + "text/plain" + ], + "tags": [ + "User" + ], + "summary": "login", + "parameters": [ + { + "description": "login info", + "name": "NewUser", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/types.NewUser" + } + } + ], + "responses": { + "200": { + "description": "Successfully signed token for user", + "schema": { + "type": "Token" + } + }, + "400": { + "description": "Bad request", + "schema": { + "type": "string" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "string" + } + } + } + } + }, + "/loginerenew": { + "post": { + "security": [ + { + "bererToken": [] + } + ], + "description": "renews the users token", + "consumes": [ + "application/json" + ], + "produces": [ + "text/plain" + ], + "tags": [ + "User" + ], + "summary": "LoginRenews", + "responses": { + "200": { + "description": "Successfully signed token for user", + "schema": { + "type": "Token" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "string" + } + } + } + } + }, + "/promoteToAdmin": { + "post": { + "description": "promote chosen user to admin", + "consumes": [ + "application/json" + ], + "produces": [ + "text/plain" + ], + "tags": [ + "User" + ], + "summary": "PromoteToAdmin", + "parameters": [ + { + "description": "user info", + "name": "NewUser", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/types.NewUser" + } + } + ], + "responses": { + "200": { + "description": "Successfully prometed user", + "schema": { + "type": "json" + } + }, + "400": { + "description": "bad request", + "schema": { + "type": "string" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "string" + } + } + } + } + }, + "/register": { "post": { "description": "Register a new user", "consumes": [ "application/json" ], "produces": [ - "application/json" + "text/plain" ], "tags": [ "User" ], - "summary": "Register a new user", + "summary": "Register", + "parameters": [ + { + "description": "User to register", + "name": "NewUser", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/types.NewUser" + } + } + ], "responses": { "200": { "description": "User added", @@ -53,6 +208,102 @@ const docTemplate = `{ } } } + }, + "/userdelete/{username}": { + "delete": { + "description": "UserDelete deletes a user from the database", + "consumes": [ + "application/json" + ], + "produces": [ + "text/plain" + ], + "tags": [ + "User" + ], + "summary": "UserDelete", + "responses": { + "200": { + "description": "User deleted", + "schema": { + "type": "string" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "string" + } + }, + "403": { + "description": "You can only delete yourself", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "string" + } + } + } + } + }, + "/users/all": { + "get": { + "description": "lists all users", + "consumes": [ + "application/json" + ], + "produces": [ + "text/plain" + ], + "tags": [ + "User" + ], + "summary": "ListsAllUsers", + "responses": { + "200": { + "description": "Successfully signed token for user", + "schema": { + "type": "json" + } + }, + "401": { + "description": "Unauthorized", + "schema": { + "type": "string" + } + }, + "500": { + "description": "Internal server error", + "schema": { + "type": "string" + } + } + } + } + } + }, + "definitions": { + "types.NewUser": { + "type": "object", + "properties": { + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + } + }, + "securityDefinitions": { + "bererToken": { + "type": "apiKey", + "name": "Authorization", + "in": "header" } }, "externalDocs": { diff --git a/backend/internal/database/db.go b/backend/internal/database/db.go index 25dd04b..bc6e1e8 100644 --- a/backend/internal/database/db.go +++ b/backend/internal/database/db.go @@ -201,7 +201,25 @@ func (d *Db) GetProjectId(projectname string) (int, error) { // Creates a new project in the database, associated with a user func (d *Db) AddProject(name string, description string, username string) error { - _, err := d.Exec(projectInsert, name, description, username) + tx := d.MustBegin() + _, err := tx.Exec(projectInsert, name, description, username) + if err != nil { + if err := tx.Rollback(); err != nil { + return err + } + return err + } + _, err = tx.Exec(changeUserRole, "project_manager", username, name) + if err != nil { + if err := tx.Rollback(); err != nil { + return err + } + return err + } + if err := tx.Commit(); err != nil { + return err + } + return err } diff --git a/backend/internal/handlers/handlers_user_related.go b/backend/internal/handlers/handlers_user_related.go index 8f4108c..96fddb7 100644 --- a/backend/internal/handlers/handlers_user_related.go +++ b/backend/internal/handlers/handlers_user_related.go @@ -12,15 +12,16 @@ import ( // Register is a simple handler that registers a new user // -// @Summary Register a new user +// @Summary Register // @Description Register a new user // @Tags User // @Accept json -// @Produce json -// @Success 200 {string} string "User added" -// @Failure 400 {string} string "Bad request" -// @Failure 500 {string} string "Internal server error" -// @Router /api/register [post] +// @Produce plain +// @Param NewUser body types.NewUser true "User to register" +// @Success 200 {string} string "User added" +// @Failure 400 {string} string "Bad request" +// @Failure 500 {string} string "Internal server error" +// @Router /register [post] func (gs *GState) Register(c *fiber.Ctx) error { u := new(types.NewUser) if err := c.BodyParser(u); err != nil { @@ -40,6 +41,17 @@ func (gs *GState) Register(c *fiber.Ctx) error { // 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 +// @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] func (gs *GState) UserDelete(c *fiber.Ctx) error { // Read from path parameters username := c.Params("username") @@ -62,8 +74,21 @@ func (gs *GState) UserDelete(c *fiber.Ctx) error { } // 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 (gs *GState) Login(c *fiber.Ctx) error { // The body type is identical to a NewUser + u := new(types.NewUser) if err := c.BodyParser(u); err != nil { log.Warn("Error parsing body") @@ -94,11 +119,22 @@ func (gs *GState) Login(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusInternalServerError) } - log.Info("Successfully signed token for user:", u.Username) - return c.JSON(fiber.Map{"token": t}) + println("Successfully signed token for user:", u.Username) + return c.JSON(types.Token{Token: t}) } // 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 (gs *GState) LoginRenew(c *fiber.Ctx) error { user := c.Locals("user").(*jwt.Token) @@ -119,10 +155,20 @@ func (gs *GState) LoginRenew(c *fiber.Ctx) error { } log.Info("Successfully renewed token for user:", user.Claims.(jwt.MapClaims)["name"]) - return c.JSON(fiber.Map{"token": t}) + return c.JSON(types.Token{Token: t}) } // 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 (gs *GState) ListAllUsers(c *fiber.Ctx) error { // Get all users from the database users, err := gs.Db.GetAllUsersApplication() @@ -136,6 +182,17 @@ func (gs *GState) ListAllUsers(c *fiber.Ctx) error { return c.JSON(users) } +// @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 prometed user" +// @Failure 400 {string} string "bad request" +// @Failure 401 {string} string "Unauthorized" +// @Failure 500 {string} string "Internal server error" +// @Router /promoteToAdmin [post] func (gs *GState) PromoteToAdmin(c *fiber.Ctx) error { // Extract the username from the request body var newUser types.NewUser diff --git a/backend/internal/types/users.go b/backend/internal/types/users.go index e9dff67..d3f2170 100644 --- a/backend/internal/types/users.go +++ b/backend/internal/types/users.go @@ -27,3 +27,8 @@ type PublicUser struct { UserId string `json:"userId"` Username string `json:"username"` } + +// wrapper type for token +type Token struct { + Token string `json:"token"` +} diff --git a/backend/main.go b/backend/main.go index 9abe995..e578c52 100644 --- a/backend/main.go +++ b/backend/main.go @@ -23,6 +23,10 @@ import ( // @license.name AGPL // @license.url https://www.gnu.org/licenses/agpl-3.0.html +//@securityDefinitions.apikey bererToken +//@in header +//@name Authorization + // @host localhost:8080 // @BasePath /api @@ -79,7 +83,7 @@ func main() { })) // Protected routes (require a valid JWT bearer token authentication header) - server.Post("/api/submitReport", gs.SubmitWeeklyReport) + server.Post("/api/submitWeeklyReport", gs.SubmitWeeklyReport) server.Get("/api/getUserProjects", gs.GetUserProjects) server.Post("/api/loginrenew", gs.LoginRenew) server.Delete("/api/userdelete/:username", gs.UserDelete) // Perhaps just use POST to avoid headaches @@ -89,7 +93,7 @@ func main() { server.Post("/api/signReport", gs.SignReport) server.Put("/api/addUserToProject", gs.AddUserToProjectHandler) server.Post("/api/promoteToAdmin", gs.PromoteToAdmin) - + server.Get("/api/users/all", gs.ListAllUsers) // Announce the port we are listening on and start the server err = server.Listen(fmt.Sprintf(":%d", conf.Port)) if err != nil { diff --git a/frontend/src/Components/AllTimeReportsInProject.tsx b/frontend/src/Components/AllTimeReportsInProject.tsx new file mode 100644 index 0000000..067712e --- /dev/null +++ b/frontend/src/Components/AllTimeReportsInProject.tsx @@ -0,0 +1,109 @@ +import React, { useEffect, useState } from "react"; +import { NewWeeklyReport } from "../Types/goTypes"; +import { Link, useParams } from "react-router-dom"; + +function AllTimeReportsInProject(): JSX.Element { + const { projectName } = useParams(); + const [weeklyReports, setWeeklyReports] = useState([]); + + /* const getWeeklyReports = async (): Promise => { + const token = localStorage.getItem("accessToken") ?? ""; + const response = await api.getWeeklyReports(token); + console.log(response); + if (response.success) { + setWeeklyReports(response.data ?? []); + } else { + console.error(response.message); + } +}; */ + + const getWeeklyReports = async (): Promise => { + const report: NewWeeklyReport[] = [ + { + projectName: projectName ?? "", + week: 10, + developmentTime: 1, + meetingTime: 1, + adminTime: 1, + ownWorkTime: 1, + studyTime: 1, + testingTime: 1, + }, + { + projectName: projectName ?? "", + week: 11, + developmentTime: 1, + meetingTime: 1, + adminTime: 1, + ownWorkTime: 100, + studyTime: 1, + testingTime: 1, + }, + { + projectName: projectName ?? "", + week: 12, + developmentTime: 1, + meetingTime: 1, + adminTime: 1, + ownWorkTime: 1, + studyTime: 1, + testingTime: 1000, + }, + { + projectName: projectName ?? "", + week: 20, + developmentTime: 1, + meetingTime: 1, + adminTime: 1, + ownWorkTime: 1, + studyTime: 1, + testingTime: 10000, + }, + // Add more reports as needed + ]; + setWeeklyReports(report); + await Promise.resolve(); + }; + + // Call getProjects when the component mounts + useEffect(() => { + void getWeeklyReports(); + }, []); + + return ( + <> +
+ {weeklyReports.map((newWeeklyReport, index) => ( + +
+

+ {"Week: "} + {newWeeklyReport.week} +

+

+ {"Total Time: "} + {newWeeklyReport.developmentTime + + newWeeklyReport.meetingTime + + newWeeklyReport.adminTime + + newWeeklyReport.ownWorkTime + + newWeeklyReport.studyTime + + newWeeklyReport.testingTime}{" "} + min +

+

+ {"Signed: "} + YES +

+
+ + ))} +
+ + ); +} + +export default AllTimeReportsInProject; diff --git a/frontend/src/Components/EditWeeklyReport.tsx b/frontend/src/Components/EditWeeklyReport.tsx index b0e8771..3017204 100644 --- a/frontend/src/Components/EditWeeklyReport.tsx +++ b/frontend/src/Components/EditWeeklyReport.tsx @@ -1,11 +1,10 @@ import { useState, useEffect } from "react"; import { NewWeeklyReport } from "../Types/goTypes"; import { api } from "../API/API"; -import { useNavigate } from "react-router-dom"; +import { useNavigate, useParams } from "react-router-dom"; import Button from "./Button"; export default function GetWeeklyReport(): JSX.Element { - const [projectName, setProjectName] = useState(""); const [week, setWeek] = useState(0); const [developmentTime, setDevelopmentTime] = useState(0); const [meetingTime, setMeetingTime] = useState(0); @@ -16,46 +15,48 @@ export default function GetWeeklyReport(): JSX.Element { const token = localStorage.getItem("accessToken") ?? ""; const username = localStorage.getItem("username") ?? ""; + const { projectName } = useParams(); + const { fetchedWeek } = useParams(); + + const fetchWeeklyReport = async (): Promise => { + const response = await api.getWeeklyReport( + username, + projectName ?? "", + fetchedWeek?.toString() ?? "0", + token, + ); + + if (response.success) { + const report: NewWeeklyReport = response.data ?? { + projectName: "", + week: 0, + developmentTime: 0, + meetingTime: 0, + adminTime: 0, + ownWorkTime: 0, + studyTime: 0, + testingTime: 0, + }; + + setWeek(report.week); + setDevelopmentTime(report.developmentTime); + setMeetingTime(report.meetingTime); + setAdminTime(report.adminTime); + setOwnWorkTime(report.ownWorkTime); + setStudyTime(report.studyTime); + setTestingTime(report.testingTime); + } else { + console.error("Failed to fetch weekly report:", response.message); + } + }; useEffect(() => { - const fetchWeeklyReport = async (): Promise => { - const response = await api.getWeeklyReport( - username, - projectName, - week.toString(), - token, - ); - - if (response.success) { - const report: NewWeeklyReport = response.data ?? { - projectName: "", - week: 0, - developmentTime: 0, - meetingTime: 0, - adminTime: 0, - ownWorkTime: 0, - studyTime: 0, - testingTime: 0, - }; - setProjectName(report.projectName); - setWeek(report.week); - setDevelopmentTime(report.developmentTime); - setMeetingTime(report.meetingTime); - setAdminTime(report.adminTime); - setOwnWorkTime(report.ownWorkTime); - setStudyTime(report.studyTime); - setTestingTime(report.testingTime); - } else { - console.error("Failed to fetch weekly report:", response.message); - } - }; - void fetchWeeklyReport(); - }, [projectName, token, username, week]); + }); const handleNewWeeklyReport = async (): Promise => { const newWeeklyReport: NewWeeklyReport = { - projectName, + projectName: projectName ?? "", week, developmentTime, meetingTime, @@ -82,7 +83,7 @@ export default function GetWeeklyReport(): JSX.Element { } e.preventDefault(); void handleNewWeeklyReport(); - navigate("/project"); + navigate(-1); }} >
@@ -233,7 +234,7 @@ export default function GetWeeklyReport(): JSX.Element {