diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 00000000..b791b4e3 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,16 @@ +FROM golang:1.19 +LABEL authors="marka" + +WORKDIR /backend + +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . + +RUN go build -o backend . + +EXPOSE 8080 + +CMD ["./backend"] +# docker run -p 8080:8080 backend \ No newline at end of file diff --git a/backend/README.md b/backend/README.md index 5541b8ad..ff4d84c8 100644 --- a/backend/README.md +++ b/backend/README.md @@ -89,15 +89,34 @@ Obtener los detalles públicos de un usuario, dado su correo ``` json { "status": 200, - "message": "User found", + "message": "Student found", "data": { - "empresa": { - "id_empresa": "prueba@prueba", - "nombre": "pruebaEmpresa", - "foto": "foto", - "detalles": "empresa de prueba", - "correo": "prueba@prueba", - "telefono": "12344433" + "student": { + "correo": "estudiante@prueba.com", + "nombre": "Estudiante Actualizado", + "apellido": "Prueba", + "nacimiento": "2002-02-02T00:00:00Z", + "telefono": "12345678", + "carrera": 1, + "semestre": 4, + "cv": "", + "foto": "", + "universidad": "Universidad Del Valle de Guatemala" + } + } +} +``` + +``` json +{ + "status": 200, + "message": "Enterprise found", + "data": { + "company": { + "correo": "empresa@prueba.com", + "nombre": "Empresa de Prueba", + "foto": "", + "detalles": "Detalles de Prueba" } } } @@ -352,6 +371,9 @@ Crea una oferta de trabajo "requisitos" : "string" "salario" : "double" "id_carreras" : "[]string" + "jornada" : "string" + "hora_inicio" : "time" + "hora_fin" : "time" } ``` @@ -378,6 +400,9 @@ Actualiza una oferta de trabajo "requisitos" : "string" "salario" : "double" "id_carreras" : "[]string" + "jornada" : "string" + "hora_inicio" : "time" + "hora_fin" : "time" } ``` @@ -407,28 +432,50 @@ Devuelve la información para las preview de las ofertas disponibles "nombre_carreras": "Ingenieria en Sistemas", "nombre_empresa": "Valve Corporation", "puesto": "Desarrollador de Videojuegos", - "salario": 15000 + "salario": 15000, + "jornada": "Tiempo completo", + "hora_inicio": "0000-01-01T17:00:00Z", + "hora_fin": "0000-01-01T17:00:00Z" }, { "id_oferta": 4, "nombre_carreras": "Ingenieria en Sistemas", "nombre_empresa": "Simán", "puesto": "DataBase Administrator", - "salario": 10000 + "salario": 10000, + "jornada": "Tiempo completo", + "hora_inicio": "0000-01-01T17:00:00Z", + "hora_fin": "0000-01-01T17:00:00Z" }, { "id_oferta": 1, - "nombre_carreras": "Ingenieria en Sistemas, Ingenieria en mecánica industrial", + "nombre_carreras": "Ingenieria en Sistemas", "nombre_empresa": "Empresa INC", "puesto": "Desarrollador Web Junior", - "salario": 5000 + "salario": 5000, + "jornada": "Tiempo completo", + "hora_inicio": "0000-01-01T17:00:00Z", + "hora_fin": "0000-01-01T17:00:00Z" + }, + { + "id_oferta": 1, + "nombre_carreras": "Ingenieria en mecánica industrial", + "nombre_empresa": "Empresa INC", + "puesto": "Desarrollador Web Junior", + "salario": 5000, + "jornada": "Tiempo completo", + "hora_inicio": "0000-01-01T17:00:00Z", + "hora_fin": "0000-01-01T17:00:00Z" }, { "id_oferta": 2, "nombre_carreras": "Ingenieria en Sistemas", "nombre_empresa": "Empresa INC", "puesto": "Desarrollador Full Stack", - "salario": 10000 + "salario": 10000, + "jornada": "Tiempo completo", + "hora_inicio": "0000-01-01T17:00:00Z", + "hora_fin": "0000-01-01T17:00:00Z" } ] } @@ -465,6 +512,9 @@ Devuelve las ofertas de trabajo publicadas por una compañia 2, 3 ] + "jornada": "Tiempo completo", + "hora_inicio": "0000-01-01T17:00:00Z", + "hora_fin": "0000-01-01T17:00:00Z" }, { "id_oferta": 60, @@ -473,6 +523,9 @@ Devuelve las ofertas de trabajo publicadas por una compañia "descripcion": "{\"ops\":[{\"insert\":\"Puesto Dummy\"},{\"attributes\":{\"align\":\"center\"},\"insert\":\"\\n\"}]}", "requisitos": "requisitos dummy", "id_carreras": null + "jornada": "Tiempo completo", + "hora_inicio": "0000-01-01T17:00:00Z", + "hora_fin": "0000-01-01T17:00:00Z" } ] } @@ -511,6 +564,9 @@ Devuelve todos los detalles de una oferta según el ID. Devuelve además la info "descripcion": "Desarrollador web junior encargado de Diseñar, desarrollar, dar mantenimiento y soporte a las aplicaciones web", "requisitos": "Conocimientos en HTML, CSS, Javascript, PHP, MySQL, React, NodeJS", "salario": 5000 + "jornada": "Medio Tiempo", + "hora_inicio": "0000-01-01T08:00:00Z", + "hora_fin": "0000-01-01T12:00:00Z" } } } @@ -524,18 +580,11 @@ Devuelve todos los detalles de una oferta según el ID. Devuelve además la info "Data": "nil" } ``` -### [DELETE] api/offers/ -Elimina una oferta de trabajo. También elimina cualquier postulación asociada a la oferta +### [DELETE] api/offers/?id_oferta=579 +Elimina una oferta de trabajo. También elimina cualquier postulación asociada a la oferta. Se pasa el id de la oferta como query param > **Note** > Auth required -#### Params -``` json -{ - "id_oferta" : int -} -``` - #### Response ``` json { @@ -700,27 +749,6 @@ Elimina una postulación. El usuario se obtiene del token. Se pasa el id de la p --- ## Administradores -### [POST] api/admins -Crea un administrador - -#### Params - -``` json -{ - "id_administrador" : "string" - "nombre" : "string" - "apellido" : "string" -} -``` - -#### Response -``` json -{ - "Status": "200", - "Message": "Admin Created Successfully", - "Data": "nil" -} -``` ### [GET] api/admins/students Retorna información de estudiantes para el panel de administradores @@ -771,7 +799,7 @@ Retorna información de empresas para el panel de administradores ``` ### [POST] api/admins/suspend -Suspende un usuario +Suspende un usuario. Si "suspender" es true, suspende al usuario. Si es false, lo reactiva > **Note** > Auth required @@ -792,3 +820,453 @@ Suspende un usuario "data": null } ``` + +### [DELETE] api/admins/delete/offers?id_oferta=220 +Elimina una oferta de trabajo. También elimina cualquier postulación asociada a la oferta + +> **Note** +> Auth required + +#### Params +Query param + +- "id_oferta": int + +#### Response +``` json +{ + "status": 200, + "message": "Offer deleted successfully", + "data": null +} +``` + +### [POST] api/admins/delete/user?usuario=estudiante@eliminar.com +Elimina un usuario. Elimina toda información asociada al usuario + +> **Note** +> Auth required + +#### Params +Query param + +- "usuario": string + +#### Response +``` json +{ + "status": 200, + "message": "User deleted successfully", + "data": null +} +``` + +### [DELETE] api/admins/postulation?id_postulacion=1 +Elimina la postulación de un estudiante a una oferta. + +> **Note** +> Auth required + +#### Params +Query param + +- "id_postulacion": int + +#### Response +``` json +{ + "status": 200, + "message": "Postulation deleted successfully", + "data": null +} +``` + +### [POST] api/admins/details +Obtener los detalles de administrador de un usuario, dado su correo +> **Note** +> Auth required + +#### Params +``` json +{ + "correo": "string" +} +``` + +#### Response +``` json +{ + "status": 200, + "message": "Enterprise found", + "data": { + "company": { + "correo": "empresa@prueba.com", + "nombre": "Empresa de Prueba", + "foto": "empresa_8900995120.jpg", + "detalles": "Detalles de Prueba", + "suspendido": false + } + } +} +``` + +``` json +{ + "status": 200, + "message": "Student found", + "data": { + "student": { + "correo": "estudiante@prueba.com", + "nombre": "Estudiante Actualizado", + "apellido": "Prueba", + "nacimiento": "2002-02-02T00:00:00Z", + "telefono": "12345678", + "carrera": 1, + "semestre": 4, + "cv": "", + "foto": "", + "universidad": "Universidad Del Valle de Guatemala", + "suspendido": false + } + } +} +``` +### [POST] api/admins/postulations?id_estudiante=prueba@prueba +Devuelve las postulaciones de un Estudiante. +> **Note** +> Auth required + +#### Params +``` json +{ + "id_estudiante": "string" +} +``` + +#### Response +```json +{ + "status": 200, + "message": "Postulations retrieved successfully", + "data": { + "postulations": [ + { + "id_usuario": "", + "nombre": "Javier Alejandro", + "apellido": "Azurdia", + "id_postulacion": 242, + "id_oferta": 402, + "estado": "enviada" + }, + { + "id_usuario": "", + "nombre": "Javier Alejandro", + "apellido": "Azurdia", + "id_postulacion": 244, + "id_oferta": 460, + "estado": "enviada" + }, + { + "id_usuario": "", + "nombre": "Javier Alejandro", + "apellido": "Azurdia", + "id_postulacion": 276, + "id_oferta": 512, + "estado": "Enviada" + } + ] + } +} + +``` + + + +--- + +# File Server +## Fotos de perfil + +### [PUT] /api/users/upload +Sube una foto de perfil de un usuario. El usuario se obtiene del token. +El nombre del archivo no es relevante, usando el usuario del token se genera un nombre único para el archivo. +> **Note** +> Auth required + +Header +``` json +{ + "Content-Type": "multipart/form-data" +} +``` + +Response +``` json +{ + "status": 200, + "message": "File uploaded successfully", + "data": { + "filename": "estudiante_2505480089.jpg" + } +} +``` + +### [GET] /api/uploads/:filename +Devuelve una foto de perfil de un usuario. El nombre del archivo se obtiene en el **query parameter** de la url. + +Esto retorna directamente la imagen, no un json. Se puede usar en un tag img de html. + +### CÓDIGO DE DEMOSTRACIÓN +``` html + + + + + + PFP Upload Test + + +

PFP Upload Test

+ + +
+ + + +
+ +
+ +
+ + +

Obtener URL del Archivo

+
+ + + +
+ + +
+

+ Imagen Cargada +
+ + + + +``` + +## CVs + +### [PUT] /api/students/update/cv +Sube un CV de un estudiante. El usuario se obtiene del token. El nombre del archivo no es relevante, +usando el usuario del token se genera un nombre único para el archivo. + +> **Note** +> Auth required + +Header +``` json +{ + "Content-Type": "multipart/form-data" +} +``` + +Response +``` json +{ + "status": 200, + "message": "File uploaded successfully", + "data": { + "filename":"estudiante_2505480089.pdf" + } +} +``` + +### [GET] /api/cv/:filename +Devuelve un CV de un estudiante. El nombre del archivo se obtiene en el **query parameter** de la url. + +Esto retorna directamente la el PDF, no un json. Se puede usar en un tag embed de html. + +### CÓDIGO DE DEMOSTRACIÓN +``` html + + + + + + PDF Upload Test + + +

PDF Upload Test

+ + +
+ + + +
+ +
+ +
+ + +

Obtener URL del Archivo

+
+ + + +
+ + +
+ +
+ + + + +``` \ No newline at end of file diff --git a/backend/controllers/admin.go b/backend/controllers/admin.go index c66a6fce..e966e3e0 100644 --- a/backend/controllers/admin.go +++ b/backend/controllers/admin.go @@ -4,67 +4,60 @@ import ( "backend/configs" "backend/models" "backend/responses" + "backend/utils" + "fmt" "github.com/gin-gonic/gin" "net/http" "time" ) -type AdministradorInput struct { - IdAdministrador string `json:"id_administrador"` - Nombre string `json:"nombre"` - Apellido string `json:"apellido"` +type EstudianteGetAdmin struct { + IdEstudiante string `json:"id_estudiante"` + Nombre string `json:"nombre"` + Apellido string `json:"apellido"` + Nacimiento time.Time `json:"nacimiento"` + Foto string `json:"foto"` + Suspendido bool `json:"suspendido"` } -func NewAdmin(c *gin.Context) { - var input AdministradorInput +func IsAdmin(c *gin.Context) error { + // Solo retorna un error si el usuario no es un administrador + role, err := utils.TokenExtractRole(c) - if err := c.ShouldBindJSON(&input); err != nil { - c.JSON(http.StatusBadRequest, responses.StandardResponse{ - Status: 400, - Message: "Error binding JSON: " + err.Error(), - Data: nil, - }) - return + if err != nil { + return fmt.Errorf("error getting role from token: %s", err.Error()) } - a := models.Administrador{ - IdAdministrador: input.IdAdministrador, - Nombre: input.Nombre, - Apellido: input.Apellido, + if role != "admin" { + user, err := utils.TokenExtractUsername(c) + + if err != nil { + return fmt.Errorf("error getting username from token: %s", err.Error()) + } + + return fmt.Errorf("user '%s' is not an admin", user) } + fmt.Println("Es admin") + return nil +} - err := configs.DB.Create(&a).Error // Se agrega el administrador a la base de datos +func AdminGetStudents(c *gin.Context) { + var estudiantes []EstudianteGetAdmin + + err := IsAdmin(c) if err != nil { c.JSON(http.StatusBadRequest, responses.StandardResponse{ - Status: 400, - Message: "Error creating. " + err.Error(), + Status: http.StatusBadRequest, + Message: "Error getting privileges: " + err.Error(), Data: nil, }) return } - c.JSON(http.StatusOK, responses.StandardResponse{ - Status: 200, - Message: "Admin Created Successfully", - Data: nil, - }) -} - -type EstudianteGetAdmin struct { - IdEstudiante string `json:"id_estudiante"` - Nombre string `json:"nombre"` - Apellido string `json:"apellido"` - Nacimiento time.Time `json:"nacimiento"` - Suspendido bool `json:"suspendido"` -} - -func GetStudents(c *gin.Context) { - var estudiantes []EstudianteGetAdmin - // Realiza la consulta para obtener la información de los estudiantes con la suspensión - err := configs.DB.Table("estudiante e"). - Select("e.id_estudiante, e.nombre, e.apellido, e.nacimiento, u.suspendido"). + err = configs.DB.Table("estudiante e"). + Select("e.id_estudiante, e.nombre, e.apellido, e.nacimiento, e.foto, u.suspendido"). Joins("INNER JOIN usuario u ON e.id_estudiante = u.usuario"). Scan(&estudiantes).Error @@ -81,7 +74,7 @@ func GetStudents(c *gin.Context) { messageMap := map[string]interface{}{"studets": estudiantes} c.JSON(http.StatusOK, responses.StandardResponse{ - Status: 200, + Status: http.StatusOK, Message: "Students Retrieved Successfully", Data: messageMap, }) @@ -92,21 +85,33 @@ type EmpresaGetAdmin struct { Nombre string `json:"nombre"` Detalles string `json:"detalles"` Telefono string `json:"telefono"` + Foto string `json:"foto"` Suspendido bool `json:"suspendido"` } -func GetCompanies(c *gin.Context) { +func AdminGetCompanies(c *gin.Context) { var empresas []EmpresaGetAdmin + err := IsAdmin(c) + + if err != nil { + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Error getting privileges: " + err.Error(), + Data: nil, + }) + return + } + // Realiza la consulta para obtener la información de las empresas con la suspensión - err := configs.DB.Table("empresa e"). - Select("e.id_empresa, e.nombre, e.detalles, e.telefono, u.suspendido"). + err = configs.DB.Table("empresa e"). + Select("e.id_empresa, e.nombre, e.detalles, e.telefono, e.foto, u.suspendido"). Joins("INNER JOIN usuario u ON e.id_empresa = u.usuario"). Scan(&empresas).Error if err != nil { c.JSON(http.StatusBadRequest, responses.StandardResponse{ - Status: 400, + Status: http.StatusBadRequest, Message: "Error retrieving companies: " + err.Error(), Data: nil, }) @@ -117,7 +122,7 @@ func GetCompanies(c *gin.Context) { messageMap := map[string]interface{}{"companies": empresas} c.JSON(http.StatusOK, responses.StandardResponse{ - Status: 200, + Status: http.StatusOK, Message: "Companies Retrieved Successfully", Data: messageMap, }) @@ -128,24 +133,55 @@ type SuspendAccountInput struct { Suspender bool `json:"suspender"` // True para suspender, false para reactivar } -func SuspendAccount(c *gin.Context) { +func AdminSuspendAccount(c *gin.Context) { var input SuspendAccountInput + err := IsAdmin(c) + + if err != nil { + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Error getting privileges: " + err.Error(), + Data: nil, + }) + return + } + if err := c.ShouldBindJSON(&input); err != nil { - c.JSON(400, responses.StandardResponse{ - Status: 400, - Message: "Error binding JSON: " + err.Error(), + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Invalid input. " + err.Error(), + Data: nil, + }) + return + } + + roleToBeSuspended, err := RoleFromUser(models.Usuario{Usuario: input.IdUsuario}) + + if err != nil { + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Error getting role from the user to be suspended. " + err.Error(), + Data: nil, + }) + return + } + + if roleToBeSuspended == "admin" { + c.JSON(http.StatusForbidden, responses.StandardResponse{ + Status: http.StatusForbidden, + Message: "Cannot suspend an admin account", Data: nil, }) return } // Realiza la consulta para obtener la información de las empresas con la suspensión - err := configs.DB.Model(&models.Usuario{}).Where("usuario = ?", input.IdUsuario).Update("suspendido", input.Suspender).Error + err = configs.DB.Model(&models.Usuario{}).Where("usuario = ?", input.IdUsuario).Update("suspendido", input.Suspender).Error if err != nil { c.JSON(http.StatusBadRequest, responses.StandardResponse{ - Status: 400, + Status: http.StatusBadRequest, Message: "Error suspending account: " + err.Error(), Data: nil, }) @@ -154,7 +190,7 @@ func SuspendAccount(c *gin.Context) { if input.Suspender { c.JSON(http.StatusOK, responses.StandardResponse{ - Status: 200, + Status: http.StatusOK, Message: "Account Suspended Successfully", Data: nil, }) @@ -162,7 +198,7 @@ func SuspendAccount(c *gin.Context) { } c.JSON(http.StatusOK, responses.StandardResponse{ - Status: 200, + Status: http.StatusOK, Message: "Account Reactivated Successfully", Data: nil, }) @@ -178,47 +214,329 @@ type Offer struct { IdCarreras []string `json:"id_carreras"` } -func DeleteOfferAdmin(c *gin.Context) { +func AdminDeleteOffer(c *gin.Context) { // con IDOferta del struct Offer, se elimina la oferta por medio de un query. idOferta := c.Query("id_oferta") - err := configs.DB.Where("id_oferta = ?", idOferta).Delete(&models.Oferta{}).Error + err := IsAdmin(c) if err != nil { c.JSON(http.StatusBadRequest, responses.StandardResponse{ - Status: 400, - Message: "Error deleting offer: " + err.Error(), + Status: http.StatusBadRequest, + Message: "Error getting privileges: " + err.Error(), + Data: nil, + }) + return + } + + err = configs.DB.Where("id_oferta = ?", idOferta).Delete(&models.Oferta{}).Error + + if err != nil { + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Error deleting offer. " + err.Error(), Data: nil, }) return } c.JSON(http.StatusOK, responses.StandardResponse{ - Status: 200, + Status: http.StatusOK, Message: "Offer deleted successfully", Data: nil, }) } -func DeleteUsuario(c *gin.Context) { +func AdminDeletePostulation(c *gin.Context) { + // con IDOferta del struct Offer, se elimina la oferta por medio de un query. + idPostulacion := c.Query("id_postulacion") + + err := IsAdmin(c) + + if err != nil { + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Error getting privileges. " + err.Error(), + Data: nil, + }) + return + } + + err = configs.DB.Where("id_postulacion = ?", idPostulacion).Delete(&models.Postulacion{}).Error + + if err != nil { + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Error deleting postulation: " + err.Error(), + Data: nil, + }) + return + } + c.JSON(http.StatusOK, responses.StandardResponse{ + Status: http.StatusOK, + Message: "Postulation deleted successfully", + Data: nil, + }) +} + +func AdminDeleteUser(c *gin.Context) { idUsuario := c.Query("usuario") - err := configs.DB.Where("usuario = ?", idUsuario).Delete(&models.Usuario{}).Error + err := IsAdmin(c) if err != nil { c.JSON(http.StatusBadRequest, responses.StandardResponse{ - Status: 400, - Message: "Error deleting user: " + err.Error(), + Status: http.StatusBadRequest, + Message: "Error getting privileges. " + err.Error(), + Data: nil, + }) + return + } + + roleToBeDeleted, err := RoleFromUser(models.Usuario{Usuario: idUsuario}) + + if err != nil { + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Error getting role from the user to be deleted. " + err.Error(), + Data: nil, + }) + return + } + + fmt.Println(roleToBeDeleted) + + if roleToBeDeleted == "admin" { + c.JSON(http.StatusForbidden, responses.StandardResponse{ + Status: http.StatusForbidden, + Message: "Cannot delete an admin account", + Data: nil, + }) + return + } + + err = configs.DB.Where("usuario = ?", idUsuario).Delete(&models.Usuario{}).Error + + if err != nil { + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Error deleting user. " + err.Error(), Data: nil, }) return } c.JSON(http.StatusOK, responses.StandardResponse{ - Status: 200, + Status: http.StatusOK, Message: "User deleted successfully", Data: nil, }) } + +type AdminDetailsStudent struct { + Correo string `json:"correo"` + Nombre string `json:"nombre"` + Apellido string `json:"apellido"` + Nacimiento time.Time `json:"nacimiento"` + Telefono string `json:"telefono"` + Carrera int `json:"carrera"` + Semestre int `json:"semestre"` + CV string `json:"cv"` + Foto string `json:"foto"` + Universidad string `json:"universidad"` + Suspendido bool `json:"suspendido"` +} + +type AdminDetailsEnterprise struct { + Correo string `json:"correo"` + Nombre string `json:"nombre"` + Foto string `json:"foto"` + Detalles string `json:"detalles"` + Suspendido bool `json:"suspendido"` +} + +func AdminGetUserDetails(c *gin.Context) { + var input UserDetailsInput // Correo del usuario a buscar + + if err := c.ShouldBindJSON(&input); err != nil { + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Invalid input. " + err.Error(), + Data: nil, + }) + return + } + + err := IsAdmin(c) + + if err != nil { + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Error getting privileges. " + err.Error(), + Data: nil, + }) + return + } + + userType, err := RoleFromUser(models.Usuario{Usuario: input.Correo}) + + if err != nil { + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "User not found. " + err.Error(), + Data: nil, + }) + return + } + + switch userType { + case "student": + var usuario models.Usuario + var estudiante models.Estudiante + + err = configs.DB.Where("usuario = ?", input.Correo).First(&usuario).Error + if err != nil { + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "User not found. " + err.Error(), + Data: nil, + }) + return + } + + err = configs.DB.Where("id_estudiante = ?", input.Correo).First(&estudiante).Error + if err != nil { + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Student not found. " + err.Error(), + Data: nil, + }) + return + } + + c.JSON(http.StatusOK, responses.StandardResponse{ + Status: http.StatusOK, + Message: "Student found", + Data: map[string]interface{}{ + "student": AdminDetailsStudent{ + Correo: estudiante.Correo, + Nombre: estudiante.Nombre, + Apellido: estudiante.Apellido, + Nacimiento: estudiante.Nacimiento, + Telefono: estudiante.Telefono, + Carrera: estudiante.Carrera, + Semestre: estudiante.Semestre, + CV: estudiante.CV, + Foto: estudiante.Foto, + Universidad: estudiante.Universidad, + Suspendido: usuario.Suspendido, + }, + }, + }) + case "enterprise": + var usuario models.Usuario + err = configs.DB.Where("usuario = ?", input.Correo).First(&usuario).Error + + if err != nil { + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "User not found. " + err.Error(), + Data: nil, + }) + return + } + + var empresa models.Empresa + err = configs.DB.Where("id_empresa = ?", input.Correo).First(&empresa).Error + if err != nil { + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Enterprise not found. " + err.Error(), + Data: nil, + }) + return + } + + c.JSON(http.StatusOK, responses.StandardResponse{ + Status: http.StatusOK, + Message: "Enterprise found", + Data: map[string]interface{}{ + "company": AdminDetailsEnterprise{ + Correo: empresa.Correo, + Nombre: empresa.Nombre, + Foto: empresa.Foto, + Detalles: empresa.Detalles, + Suspendido: usuario.Suspendido, + }, + }, + }) + case "admin": + c.JSON(http.StatusForbidden, responses.StandardResponse{ + Status: http.StatusForbidden, + Message: "Admins cannot be viewed", + Data: nil, + }) + default: + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "User not found. " + err.Error(), + Data: nil, + }) + } +} + +type PostulationsResults struct { + IdUsuario string `json:"id_usuario"` + Nombre string `json:"nombre"` + Apellido string `json:"apellido"` + IdPostulacion int `json:"id_postulacion"` + IdOferta int `json:"id_oferta"` + Estado string `json:"estado"` +} + +func GetPostulationsOfStudentAsAdmin(c *gin.Context) { + // Devolver lo mismo que GetPostulationsFromStudent pero como admin. + + var results []PostulationsResults + var data map[string]interface{} + + // id del estudiante como query. + idEstudiante := c.Query("id_estudiante") + + err := IsAdmin(c) + + if err != nil { + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Error getting privileges. " + err.Error(), + Data: nil, + }) + return + } + + err = configs.DB.Raw("select u.usuario, e.nombre, e.apellido, p.id_postulacion, p.id_oferta, p.estado from usuario u join estudiante e on u.usuario = e.id_estudiante join postulacion p on e.id_estudiante = p.id_estudiante where u.usuario = ?", idEstudiante).Scan(&results).Error + fmt.Println(err) + fmt.Println(results) + + if err != nil { + c.JSON(400, responses.StandardResponse{ + Status: 400, + Message: "Error getting postulations", + Data: nil, + }) + return + } + + // meter los resultados en un mapa + data = map[string]interface{}{ + "postulations": results, + } + + c.JSON(200, responses.StandardResponse{ + Status: 200, + Message: "Postulations retrieved successfully", + Data: data, + }) +} diff --git a/backend/controllers/auth.go b/backend/controllers/auth.go index b9ac42a4..17224921 100644 --- a/backend/controllers/auth.go +++ b/backend/controllers/auth.go @@ -8,38 +8,9 @@ import ( "github.com/gin-gonic/gin" "golang.org/x/crypto/bcrypt" "net/http" + "time" ) -type RegisterInput struct { - Usuario string `json:"usuario"` - Contra string `json:"contra"` -} - -func Register(c *gin.Context) { - var input RegisterInput - - // En esta línea se hace el binding del JSON que viene en el body del request a la variable input - if err := c.ShouldBindJSON(&input); err != nil { - c.JSON(400, responses.StandardResponse{Status: 400, Message: "Invalid input", Data: nil}) - return - } - - u := models.Usuario{ - Usuario: input.Usuario, - Contra: input.Contra, - Suspendido: false, - } - - _, err := u.SaveUser() - - if err != nil { - c.JSON(400, responses.StandardResponse{Status: 400, Message: "Error creating user", Data: nil}) - return - } - - c.JSON(200, responses.StandardResponse{Status: 200, Message: "Usuario created successfully", Data: nil}) -} - type LoginInput struct { Usuario string `json:"usuario"` Contra string `json:"contra"` @@ -49,7 +20,11 @@ func Login(c *gin.Context) { var input LoginInput if err := c.ShouldBindJSON(&input); err != nil { - c.JSON(400, responses.StandardResponse{Status: 400, Message: "Invalid input", Data: nil}) + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Invalid input. " + err.Error(), + Data: nil, + }) return } @@ -62,8 +37,8 @@ func Login(c *gin.Context) { if err != nil { c.JSON(http.StatusUnauthorized, responses.StandardResponse{ - Status: 401, - Message: "Invalid credentials: " + err.Error(), + Status: http.StatusUnauthorized, + Message: "Invalid credentials", Data: nil, }) return @@ -74,7 +49,7 @@ func Login(c *gin.Context) { if err != nil { c.JSON(http.StatusNotFound, responses.StandardResponse{ Status: 404, - Message: "Invalid credentials", + Message: "Could not verify role: " + err.Error(), Data: nil, }) return @@ -180,19 +155,13 @@ func RoleFromUser(usuario models.Usuario) (string, error) { } func RoleFromToken(c *gin.Context) (string, error) { - username, err := utils.ExtractTokenUsername(c) + username, err := utils.TokenExtractUsername(c) if err != nil { return "", err } - u, err := models.GetUserByUsername(username) - - if err != nil { - return "", err - } - - role, err := RoleFromUser(u) + role, err := RoleFromUser(models.Usuario{Usuario: username}) if err != nil { return "", err @@ -201,8 +170,8 @@ func RoleFromToken(c *gin.Context) (string, error) { return role, nil } -func CurrentUser(c *gin.Context) { - username, err := utils.ExtractTokenUsername(c) +func GetCurrentUserDetails(c *gin.Context) { + username, err := utils.TokenExtractUsername(c) if err != nil { c.JSON(http.StatusBadRequest, responses.StandardResponse{ @@ -286,6 +255,26 @@ func CurrentUser(c *gin.Context) { ) } +type PublicDetailsStudent struct { + Correo string `json:"correo"` + Nombre string `json:"nombre"` + Apellido string `json:"apellido"` + Nacimiento time.Time `json:"nacimiento"` + Telefono string `json:"telefono"` + Carrera int `json:"carrera"` + Semestre int `json:"semestre"` + CV string `json:"cv"` + Foto string `json:"foto"` + Universidad string `json:"universidad"` +} + +type PublicDetailsEnterprise struct { + Correo string `json:"correo"` + Nombre string `json:"nombre"` + Foto string `json:"foto"` + Detalles string `json:"detalles"` +} + type UserDetailsInput struct { Correo string `json:"correo"` } @@ -294,63 +283,92 @@ func GetUserDetails(c *gin.Context) { var input UserDetailsInput if err := c.ShouldBindJSON(&input); err != nil { - c.JSON(400, responses.StandardResponse{Status: 400, Message: "Invalid input", Data: nil}) + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Invalid input. " + err.Error(), + Data: nil, + }) return } var estudiante models.Estudiante var empresa models.Empresa - var administrador models.Administrador - err := configs.DB.Where("correo = ?", input.Correo).First(&estudiante).Error - if err == nil { - c.JSON(http.StatusOK, responses.StandardResponse{ - Status: 200, - Message: "User found", - Data: map[string]interface{}{ - "estudiante": estudiante, - }, + userType, err := RoleFromUser(models.Usuario{Usuario: input.Correo}) + + if err != nil { + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "User not found. " + err.Error(), + Data: nil, }) return } - err = configs.DB.Where("correo = ?", input.Correo).First(&empresa).Error + switch userType { + case "student": + err = configs.DB.Where("id_estudiante = ?", input.Correo).First(&estudiante).Error + if err != nil { + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Student not found. " + err.Error(), + Data: nil, + }) + return + } - if err == nil { c.JSON(http.StatusOK, responses.StandardResponse{ - Status: 200, - Message: "User found", + Status: http.StatusOK, + Message: "Student found", Data: map[string]interface{}{ - "empresa": empresa, + "student": PublicDetailsStudent{ + Correo: estudiante.Correo, + Nombre: estudiante.Nombre, + Apellido: estudiante.Apellido, + Nacimiento: estudiante.Nacimiento, + Telefono: estudiante.Telefono, + Carrera: estudiante.Carrera, + Semestre: estudiante.Semestre, + CV: estudiante.CV, + Foto: estudiante.Foto, + Universidad: estudiante.Universidad, + }, }, }) - return - } + case "enterprise": + err = configs.DB.Where("id_empresa = ?", input.Correo).First(&empresa).Error + if err != nil { + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Enterprise not found. " + err.Error(), + Data: nil, + }) + return + } - err = configs.DB.Where("correo = ?", input.Correo).First(&administrador).Error - - if err == nil { - c.JSON(http.StatusUnauthorized, responses.StandardResponse{ - Status: 401, - Message: "Access denied", + c.JSON(http.StatusOK, responses.StandardResponse{ + Status: http.StatusOK, + Message: "Enterprise found", + Data: map[string]interface{}{ + "company": PublicDetailsEnterprise{ + Correo: empresa.Correo, + Nombre: empresa.Nombre, + Foto: empresa.Foto, + Detalles: empresa.Detalles, + }, + }, + }) + case "admin": + c.JSON(http.StatusForbidden, responses.StandardResponse{ + Status: http.StatusForbidden, + Message: "Admins cannot be viewed", Data: nil, }) - return - } - - if err != nil { + default: c.JSON(http.StatusBadRequest, responses.StandardResponse{ - Status: 400, + Status: http.StatusBadRequest, Message: "User not found. " + err.Error(), Data: nil, }) - return } - - c.JSON(http.StatusOK, responses.StandardResponse{ - Status: 400, - Message: "User not found", - Data: nil, - }, - ) } diff --git a/backend/controllers/carrer.go b/backend/controllers/carrer.go index 190e272f..f611ffd0 100644 --- a/backend/controllers/carrer.go +++ b/backend/controllers/carrer.go @@ -5,6 +5,7 @@ import ( "backend/models" "backend/responses" "github.com/gin-gonic/gin" + "net/http" ) type CareerInput struct { @@ -15,10 +16,21 @@ type CareerInput struct { func NewCareer(c *gin.Context) { var input CareerInput + err := IsAdmin(c) + + if err != nil { + c.JSON(http.StatusUnauthorized, responses.StandardResponse{ + Status: http.StatusUnauthorized, + Message: "Error getting privileges: " + err.Error(), + Data: nil, + }) + return + } + if err := c.ShouldBindJSON(&input); err != nil { - c.JSON(400, responses.StandardResponse{ - Status: 400, - Message: "Error binding JSON: " + err.Error(), + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Invalid input" + err.Error(), Data: nil, }) return @@ -29,23 +41,22 @@ func NewCareer(c *gin.Context) { Descripcion: input.Descripcion, } - err := configs.DB.Create(&carrera).Error + err = configs.DB.Create(&carrera).Error if err != nil { - c.JSON(400, responses.StandardResponse{ - Status: 400, - Message: "Error creating career. " + err.Error(), + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Error creating career" + err.Error(), Data: nil, }) return } - c.JSON(200, responses.StandardResponse{ - Status: 200, - Message: "Carrera created successfully", + c.JSON(http.StatusOK, responses.StandardResponse{ + Status: http.StatusOK, + Message: "Career created successfully", Data: nil, }) - } func GetCareers(c *gin.Context) { @@ -54,9 +65,9 @@ func GetCareers(c *gin.Context) { err := configs.DB.Find(&careers).Error if err != nil { - c.JSON(400, responses.StandardResponse{ - Status: 400, - Message: "Error getting careers", + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Error retrieving careers" + err.Error(), Data: nil, }) return @@ -68,8 +79,8 @@ func GetCareers(c *gin.Context) { "careers": careers, } - c.JSON(200, responses.StandardResponse{ - Status: 200, + c.JSON(http.StatusOK, responses.StandardResponse{ + Status: http.StatusOK, Message: "Careers retrieved successfully", Data: data, }) diff --git a/backend/controllers/chat.go b/backend/controllers/chat.go index 2a8b1c82..c8f68fc3 100644 --- a/backend/controllers/chat.go +++ b/backend/controllers/chat.go @@ -80,7 +80,8 @@ func GetMessages(c *gin.Context) { join estudiante es on m.id_emisor = es.id_estudiante or m.id_receptor = es.id_estudiante join empresa em on m.id_emisor = em.id_empresa or m.id_receptor = em.id_empresa where (id_emisor = ? and id_receptor = ?) - or (id_emisor = ? and id_receptor = ?);` + or (id_emisor = ? and id_receptor = ?) + order by m.tiempo asc;` // Ejecutamos la consulta SQL pura con parámetros inputID.ID_usuario err := configs.DB.Raw(query, inputID.ID_emisor, inputID.ID_emisor, inputID.ID_receptor, inputID.ID_receptor, @@ -179,3 +180,39 @@ func GetLastChat(c *gin.Context) { Data: messageMap, }) } + +type DeleteChatInput struct { + Id_Postulacion string `json:"id_postulacion"` +} + +func DeleteChat(c *gin.Context) { + + // Obtener el id_postulacion desde los query parameters + idPostulacion := c.Query("id_postulacion") + + // Verifica si el valor del parámetro está presente + if idPostulacion == "" { + c.JSON(400, responses.StandardResponse{ + Status: 400, + Message: "Missing id_postulacion parameter", + Data: nil, + }) + return + } + + err := configs.DB.Where("id_postulacion = ?", idPostulacion).Delete(&models.Mensaje{}).Error + if err != nil { + c.JSON(400, responses.StandardResponse{ + Status: 400, + Message: "Error deleting chat", + Data: nil, + }) + return + } + + c.JSON(200, responses.StandardResponse{ + Status: 200, + Message: "Chat deleted successfully", + Data: nil, + }) +} diff --git a/backend/controllers/company.go b/backend/controllers/company.go index 9963a506..1eec9197 100644 --- a/backend/controllers/company.go +++ b/backend/controllers/company.go @@ -4,6 +4,7 @@ import ( "backend/configs" "backend/models" "backend/responses" + "backend/utils" "github.com/gin-gonic/gin" "github.com/lib/pq" "net/http" @@ -12,7 +13,6 @@ import ( type EmpresaInput struct { Nombre string `json:"nombre"` Detalles string `json:"detalles"` - Foto string `json:"foto"` Correo string `json:"correo"` Telefono string `json:"telefono"` Contra string `json:"contra"` @@ -23,8 +23,8 @@ func NewCompany(c *gin.Context) { if err := c.ShouldBindJSON(&input); err != nil { c.JSON(http.StatusBadRequest, responses.StandardResponse{ - Status: 400, - Message: "Error binding JSON: " + err.Error(), + Status: http.StatusBadRequest, + Message: "Invalid input. " + err.Error(), Data: nil, }) return @@ -33,7 +33,6 @@ func NewCompany(c *gin.Context) { e := models.Empresa{ IdEmpresa: input.Correo, Nombre: input.Nombre, - Foto: input.Foto, Detalles: input.Detalles, Correo: input.Correo, Telefono: input.Telefono, @@ -49,7 +48,7 @@ func NewCompany(c *gin.Context) { if err != nil { if pqErr, ok := err.(*pq.Error); ok && pqErr.Code == "23505" { c.JSON(http.StatusConflict, responses.StandardResponse{ - Status: 409, + Status: http.StatusConflict, Message: "User with this email already exists", Data: nil, }) @@ -57,7 +56,7 @@ func NewCompany(c *gin.Context) { } c.JSON(http.StatusBadRequest, responses.StandardResponse{ - Status: 400, + Status: http.StatusBadRequest, Message: "Error creating user. " + err.Error(), Data: nil, }) @@ -68,7 +67,7 @@ func NewCompany(c *gin.Context) { if err != nil { c.JSON(http.StatusBadRequest, responses.StandardResponse{ - Status: 400, + Status: http.StatusBadRequest, Message: "Error creating company. " + err.Error(), Data: nil, }) @@ -76,7 +75,7 @@ func NewCompany(c *gin.Context) { } c.JSON(http.StatusOK, responses.StandardResponse{ - Status: 200, + Status: http.StatusOK, Message: "Company created successfully", Data: nil, }) @@ -86,35 +85,54 @@ func UpdateCompanies(c *gin.Context) { var input EmpresaInput if err := c.ShouldBindJSON(&input); err != nil { - c.JSON(400, responses.StandardResponse{ - Status: 400, - Message: "Error binding JSON: " + err.Error(), + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Invalid input. " + err.Error(), + Data: nil, + }) + return + } + + user, err := utils.TokenExtractUsername(c) + + if err != nil { + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Could not retrieve info from token. " + err.Error(), + Data: nil, + }) + return + } + + // Se verifica que el usuario sea el mismo que el de la empresa + if user != input.Correo { + c.JSON(http.StatusUnauthorized, responses.StandardResponse{ + Status: http.StatusUnauthorized, + Message: "User " + user + " is not authorized to update company " + input.Correo, Data: nil, }) return } // No se puede actualizar el correo/id de la empresa - err := configs.DB.Model(&models.Empresa{}).Where("id_empresa = ?", input.Correo).Updates(models.Empresa{ + err = configs.DB.Model(&models.Empresa{}).Where("id_empresa = ?", input.Correo).Updates(models.Empresa{ Nombre: input.Nombre, Detalles: input.Detalles, - Foto: input.Foto, Telefono: input.Telefono, }).Error if err != nil { c.JSON(http.StatusBadRequest, responses.StandardResponse{ - Status: 400, - Message: "Error updating", + Status: http.StatusBadRequest, + Message: "Error updating company. " + err.Error(), Data: nil, }) return } c.JSON(http.StatusOK, responses.StandardResponse{ - Status: 200, + Status: http.StatusOK, Message: "Company updated successfully", Data: nil, }) - } diff --git a/backend/controllers/files.go b/backend/controllers/files.go index 9f7f3cfd..ae383502 100644 --- a/backend/controllers/files.go +++ b/backend/controllers/files.go @@ -17,7 +17,7 @@ import ( func UpdateProfilePicture() gin.HandlerFunc { return func(c *gin.Context) { - user, err := utils.ExtractTokenUsername(c) + user, err := utils.TokenExtractUsername(c) acceptedFileTypes := []string{"png", "jpg", "jpeg"} if err != nil { @@ -34,10 +34,10 @@ func UpdateProfilePicture() gin.HandlerFunc { fmt.Println("Username upload: " + user_stripped) // single file - file, _ := c.FormFile("file") + fileHeader, _ := c.FormFile("file") // get the file type from filename - fileType := file.Filename[strings.LastIndex(file.Filename, ".")+1:] + fileType := fileHeader.Filename[strings.LastIndex(fileHeader.Filename, ".")+1:] if !utils.Contains(acceptedFileTypes, fileType) { c.JSON(http.StatusBadRequest, responses.StandardResponse{ @@ -51,10 +51,9 @@ func UpdateProfilePicture() gin.HandlerFunc { randomNumber := rand.Intn(9999999999-1111111111) + 1111111111 newFileName := user_stripped + "_" + fmt.Sprint(randomNumber) + "." + fileType - file.Filename = newFileName + fileHeader.Filename = newFileName dst := "./uploads/" + newFileName - fmt.Println("File: " + dst) // Eliminar archivos antiguos con el mismo prefijo de usuario if err := deleteFilesWithPrefix("./uploads/", user_stripped); err != nil { @@ -67,7 +66,7 @@ func UpdateProfilePicture() gin.HandlerFunc { } // Actualizar en base de datos - userType, err := utils.ExtractTokenUserType(c) + userType, err := utils.TokenExtractRole(c) if err != nil { c.JSON(http.StatusUnauthorized, responses.StandardResponse{ @@ -85,7 +84,7 @@ func UpdateProfilePicture() gin.HandlerFunc { } // Save locally - err = c.SaveUploadedFile(file, dst) + err = c.SaveUploadedFile(fileHeader, dst) if err != nil { c.JSON(http.StatusInternalServerError, responses.StandardResponse{ Status: http.StatusInternalServerError, @@ -97,7 +96,126 @@ func UpdateProfilePicture() gin.HandlerFunc { // send the file via HTTP to the file server url := "http://ec2-13-57-42-212.us-west-1.compute.amazonaws.com/upload/" - bearerToken := "Bearer " + utils.ExtractToken(c) + bearerToken := "Bearer " + utils.ExtractTokenFromRequest(c) + + if err := utils.UploadFileToServer(url, bearerToken, fileHeader, dst); err != nil { + c.JSON(http.StatusInternalServerError, responses.StandardResponse{ + Status: http.StatusInternalServerError, + Message: "Failed to upload file to server: " + err.Error(), + Data: nil, + }) + return + } + + // Eliminar el archivo local. Ya no es necesario, ya que se subió al servidor de archivos + //fmt.Println("Deleting file: " + dst) + if err := os.Remove(dst); err != nil { + c.JSON(http.StatusInternalServerError, responses.StandardResponse{ + Status: http.StatusInternalServerError, + Message: "Failed to delete local file: " + err.Error(), + Data: nil, + }) + return + } + + c.JSON(http.StatusOK, responses.StandardResponse{ + Status: http.StatusOK, + Message: "File uploaded successfully", + Data: map[string]interface{}{ + "filename": newFileName, + }, + }) + } +} + +func UpdateCV() gin.HandlerFunc { + return func(c *gin.Context) { + user, err := utils.TokenExtractUsername(c) + acceptedFileTypes := []string{"pdf"} + + if err != nil { + c.JSON(http.StatusUnauthorized, responses.StandardResponse{ + Status: http.StatusUnauthorized, + Message: "Unauthorized. Cannot get information from token. " + err.Error(), + Data: nil, + }) + return + } + + // strip username from email. ignoring everything after @ + user_stripped := user[:strings.Index(user, "@")] + fmt.Println("Username upload: " + user_stripped) + + // single file + file, _ := c.FormFile("file") + + // get the file type from filename + fileType := file.Filename[strings.LastIndex(file.Filename, ".")+1:] + + if !utils.Contains(acceptedFileTypes, fileType) { + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Invalid file type. Accepted file types are " + strings.Join(acceptedFileTypes, ", "), + Data: nil, + }) + return + } + + randomNumber := rand.Intn(9999999999-1111111111) + 1111111111 + newFileName := user_stripped + "_" + fmt.Sprint(randomNumber) + "." + fileType + + file.Filename = newFileName + + dst := "./uploads/pdf/" + newFileName + fmt.Println("File: " + dst) + + // Eliminar archivos antiguos con el mismo prefijo de usuario + if err := deleteFilesWithPrefix("./uploads/", user_stripped); err != nil { + c.JSON(http.StatusInternalServerError, responses.StandardResponse{ + Status: http.StatusInternalServerError, + Message: "Failed to delete old files: " + err.Error(), + Data: nil, + }) + return + } + + // Actualizar en base de datos + userType, err := utils.TokenExtractRole(c) + + if err != nil { + c.JSON(http.StatusUnauthorized, responses.StandardResponse{ + Status: http.StatusUnauthorized, + Message: "Unauthorized. Cannot get information from token. " + err.Error(), + Data: nil, + }) + return + } + + if userType != "student" { + c.JSON(http.StatusUnauthorized, responses.StandardResponse{ + Status: http.StatusUnauthorized, + Message: "Unauthorized. Only students can upload CVs.", + Data: nil, + }) + return + } + + err = configs.DB.Model(&models.Estudiante{}).Where("correo = ?", user).Updates(models.Estudiante{CV: newFileName}).Error + + // Save locally + err = c.SaveUploadedFile(file, dst) + if err != nil { + c.JSON(http.StatusInternalServerError, responses.StandardResponse{ + Status: http.StatusInternalServerError, + Message: "Failed to save file: " + err.Error(), + Data: nil, + }) + return + } + + // send the file via HTTP to the file server + url := "http://ec2-13-57-42-212.us-west-1.compute.amazonaws.com/upload/pdf/" + bearerToken := "Bearer " + utils.ExtractTokenFromRequest(c) if err := utils.UploadFileToServer(url, bearerToken, file, dst); err != nil { c.JSON(http.StatusInternalServerError, responses.StandardResponse{ @@ -108,6 +226,16 @@ func UpdateProfilePicture() gin.HandlerFunc { return } + // Eliminar el archivo local. Ya no es necesario, ya que se subió al servidor de archivos + if err := os.Remove(dst); err != nil { + c.JSON(http.StatusInternalServerError, responses.StandardResponse{ + Status: http.StatusInternalServerError, + Message: "Failed to delete local file: " + err.Error(), + Data: nil, + }) + return + } + c.JSON(http.StatusOK, responses.StandardResponse{ Status: http.StatusOK, Message: "File uploaded successfully", @@ -125,7 +253,7 @@ func deleteFilesWithPrefix(directory, prefix string) error { } for _, file := range files { - fmt.Println(file.Name()) + //fmt.Println(file.Name()) if strings.HasPrefix(file.Name(), prefix) { filePath := filepath.Join(directory, file.Name()) fmt.Println("Deleting file: " + filePath) @@ -138,7 +266,7 @@ func deleteFilesWithPrefix(directory, prefix string) error { return nil } -func GetFile() gin.HandlerFunc { +func GetProfilePicture() gin.HandlerFunc { return func(c *gin.Context) { filename := c.Param("filename") fileURL := configs.FileServer + filename @@ -192,3 +320,58 @@ func GetFile() gin.HandlerFunc { } } } + +func GetCV() gin.HandlerFunc { + return func(c *gin.Context) { + filename := c.Param("filename") + fileURL := configs.FileServer + "pdf/" + filename + + // Realizar una solicitud GET al servidor de archivos externo + resp, err := http.Get(fileURL) + if err != nil { + c.JSON(http.StatusNotFound, responses.StandardResponse{ + Status: http.StatusNotFound, + Message: "Error al obtener el archivo: " + err.Error(), + Data: nil, + }) + return + } + + defer func(Body io.ReadCloser) { + err := Body.Close() + if err != nil { + c.JSON(http.StatusInternalServerError, responses.StandardResponse{ + Status: http.StatusInternalServerError, + Message: "Error al cerrar el cuerpo de la respuesta del servidor de archivos externo: " + err.Error(), + Data: nil, + }) + return + } + }(resp.Body) + + if resp.StatusCode != http.StatusOK { + c.JSON(http.StatusNotFound, responses.StandardResponse{ + Status: http.StatusNotFound, + Message: "Archivo no encontrado en el servidor de archivos externo", + Data: nil, + }) + return + } + + // Configurar las cabeceras de la respuesta para el cliente + c.Header("Content-Type", resp.Header.Get("Content-Type")) + c.Header("Content-Disposition", "inline; filename="+filename) + c.Header("Content-Length", resp.Header.Get("Content-Length")) + + // Copiar el cuerpo de la respuesta del servidor de archivos al cuerpo de la respuesta de Gin + _, err = io.Copy(c.Writer, resp.Body) + if err != nil { + c.JSON(http.StatusInternalServerError, responses.StandardResponse{ + Status: http.StatusInternalServerError, + Message: "Error al copiar el cuerpo de la respuesta del servidor de archivos externo: " + err.Error(), + Data: nil, + }) + return + } + } +} diff --git a/backend/controllers/offer.go b/backend/controllers/offer.go index fc8ed92a..0c0c2405 100644 --- a/backend/controllers/offer.go +++ b/backend/controllers/offer.go @@ -6,27 +6,34 @@ import ( "backend/responses" "backend/utils" "database/sql" - "fmt" "github.com/gin-gonic/gin" "net/http" + "strconv" + "time" ) type OfferInput struct { - IDEmpresa string `json:"id_empresa"` - Puesto string `json:"puesto"` - Descripcion string `json:"descripcion"` - Requisitos string `json:"requisitos"` - Salario float64 `json:"salario"` - IdCarreras []string `json:"id_carreras"` + IDEmpresa string `json:"id_empresa"` + Puesto string `json:"puesto"` + Descripcion string `json:"descripcion"` + Requisitos string `json:"requisitos"` + Salario float64 `json:"salario"` + IdCarreras []string `json:"id_carreras"` + Jornada string `json:"jornada"` + HoraInicio time.Time `json:"hora_inicio"` + HoraFin time.Time `json:"hora_fin"` } type AfterInsert struct { - IdOferta int `json:"id_oferta"` - IDEmpresa string `json:"id_empresa"` - Puesto string `json:"puesto"` - Descripcion string `json:"descripcion"` - Requisitos string `json:"requisitos"` - Salario float64 `json:"salario"` + IdOferta int `json:"id_oferta"` + IDEmpresa string `json:"id_empresa"` + Puesto string `json:"puesto"` + Descripcion string `json:"descripcion"` + Requisitos string `json:"requisitos"` + Salario float64 `json:"salario"` + Jornada string `json:"jornada"` + HoraInicio time.Time `json:"hora_inicio"` + HoraFin time.Time `json:"hora_fin"` } type AfterInsert2 struct { @@ -38,45 +45,64 @@ func NewOffer(c *gin.Context) { var input OfferInput if err := c.BindJSON(&input); err != nil { - c.JSON(400, responses.StandardResponse{ - Status: 400, + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, Message: "Invalid input: " + err.Error(), Data: nil, }) return } + user, err := utils.TokenExtractUsername(c) + + if err != nil { + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Error extracting information from token: " + err.Error(), + Data: nil, + }) + return + } + + if user != input.IDEmpresa { + c.JSON(http.StatusForbidden, responses.StandardResponse{ + Status: http.StatusForbidden, + Message: "The user in the token does not match the one in the request body", + Data: nil, + }) + return + } + offer := models.Oferta{ IDEmpresa: input.IDEmpresa, Puesto: input.Puesto, Descripcion: input.Descripcion, Requisitos: input.Requisitos, Salario: input.Salario, + Jornada: input.Jornada, + HoraInicio: input.HoraInicio, + HoraFin: input.HoraFin, } - fmt.Println(offer) - var inserted AfterInsert - err := configs.DB.Raw("INSERT INTO oferta (id_empresa, puesto, descripcion, requisitos, salario) VALUES (?, ?, ?, ?, ?) RETURNING id_oferta, id_empresa, puesto, descripcion, requisitos, salario", offer.IDEmpresa, offer.Puesto, offer.Descripcion, offer.Requisitos, offer.Salario).Scan(&inserted).Error + err = configs.DB.Raw("INSERT INTO oferta (id_empresa, puesto, descripcion, requisitos, salario, jornada, hora_inicio, hora_fin) VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING id_oferta, id_empresa, puesto, descripcion, requisitos, salario, jornada, hora_inicio, hora_fin", offer.IDEmpresa, offer.Puesto, offer.Descripcion, offer.Requisitos, offer.Salario, offer.Jornada, offer.HoraInicio, offer.HoraFin).Scan(&inserted).Error if err != nil { - c.JSON(400, responses.StandardResponse{ - Status: 400, + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, Message: "Error creating offer: " + err.Error(), Data: nil, }) return } - fmt.Println("\ncarreras: ", input.IdCarreras) - // Insert into oferta_carrera table for _, idCarrera := range input.IdCarreras { var inserted2 AfterInsert2 err = configs.DB.Raw("INSERT INTO oferta_carrera (id_oferta, id_carrera) VALUES (?, ?) RETURNING id_oferta, id_carrera", inserted.IdOferta, idCarrera).Scan(&inserted2).Error if err != nil { - c.JSON(400, responses.StandardResponse{ - Status: 400, + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, Message: "Error creating oferta_carrera: " + err.Error(), Data: nil, }) @@ -84,50 +110,96 @@ func NewOffer(c *gin.Context) { } } - c.JSON(200, responses.StandardResponse{ - Status: 200, - Message: "Offer and oferta_carrera created successfully", + c.JSON(http.StatusOK, responses.StandardResponse{ + Status: http.StatusOK, + Message: "Offer created successfully", Data: nil, }) } type OfferUpdateInput struct { - Id_Oferta int `json:"id_oferta"` - IDEmpresa string `json:"id_empresa"` - Puesto string `json:"puesto"` - Descripcion string `json:"descripcion"` - Requisitos string `json:"requisitos"` - Salario float64 `json:"salario"` - IdCarreras []string `json:"id_carreras"` + Id_Oferta int `json:"id_oferta"` + IDEmpresa string `json:"id_empresa"` + Puesto string `json:"puesto"` + Descripcion string `json:"descripcion"` + Requisitos string `json:"requisitos"` + Salario float64 `json:"salario"` + IdCarreras []string `json:"id_carreras"` + Jornada string `json:"jornada"` + HoraInicio time.Time `json:"hora_inicio"` + HoraFin time.Time `json:"hora_fin"` } func UpdateOffer(c *gin.Context) { var input OfferUpdateInput - var offer models.Oferta + var updatedOffer models.Oferta if err := c.BindJSON(&input); err != nil { - c.JSON(400, responses.StandardResponse{ - Status: 400, + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, Message: "Invalid input: " + err.Error(), Data: nil, }) return } - offer = models.Oferta{ - IDEmpresa: input.IDEmpresa, + user, err := utils.TokenExtractUsername(c) + + if err != nil { + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Error extracting information from token: " + err.Error(), + Data: nil, + }) + return + } + + originalOffer := models.Oferta{} + err = configs.DB.Where("id_oferta = ? AND id_empresa = ?", input.Id_Oferta, input.IDEmpresa).First(&originalOffer).Error + + if err != nil { + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Error getting original offer: " + err.Error(), + Data: nil, + }) + return + } + + if originalOffer.IDEmpresa != user { + c.JSON(http.StatusForbidden, responses.StandardResponse{ + Status: http.StatusForbidden, + Message: "The user in the token does not match the owner of the offer", + Data: nil, + }) + return + } + + if user != input.IDEmpresa { + c.JSON(http.StatusForbidden, responses.StandardResponse{ + Status: http.StatusForbidden, + Message: "The user in the token does not match the one in the request body", + Data: nil, + }) + return + } + + updatedOffer = models.Oferta{ Puesto: input.Puesto, Descripcion: input.Descripcion, Requisitos: input.Requisitos, Salario: input.Salario, + Jornada: input.Jornada, + HoraInicio: input.HoraInicio, + HoraFin: input.HoraFin, } - err := configs.DB.Model(&offer).Where("id_oferta = ?", input.Id_Oferta).Updates(offer).Error + err = configs.DB.Model(&updatedOffer).Where("id_oferta = ? AND id_empresa = ?", input.Id_Oferta, input.IDEmpresa).Updates(updatedOffer).Error if err != nil { - c.JSON(400, responses.StandardResponse{ - Status: 400, + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, Message: "Error updating offer: " + err.Error(), Data: nil, }) @@ -138,8 +210,8 @@ func UpdateOffer(c *gin.Context) { err = configs.DB.Where("id_oferta = ?", input.Id_Oferta).Delete(&models.OfertaCarrera{}).Error if err != nil { - c.JSON(400, responses.StandardResponse{ - Status: 400, + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, Message: "Error deleting oferta_carrera to update them: " + err.Error(), Data: nil, }) @@ -147,12 +219,28 @@ func UpdateOffer(c *gin.Context) { } // Insert into oferta_carrera table - for _, idCarrera := range input.IdCarreras { - var inserted2 AfterInsert2 - err = configs.DB.Raw("INSERT INTO oferta_carrera (id_oferta, id_carrera) VALUES (?, ?) RETURNING id_oferta, id_carrera", input.Id_Oferta, idCarrera).Scan(&inserted2).Error + for _, idCarreraStr := range input.IdCarreras { + // Convierte la cadena 'idCarreraStr' a un entero 'idCarrera' + idCarrera, err := strconv.Atoi(idCarreraStr) if err != nil { - c.JSON(400, responses.StandardResponse{ - Status: 400, + // Manejar el error si la conversión falla + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Error converting 'idCarrera' to int: " + err.Error(), + Data: nil, + }) + return + } + + ofertaCarrera := models.OfertaCarrera{ + IdOferta: input.Id_Oferta, + IdCarrera: idCarrera, + } + + // Insertar en la tabla oferta_carrera usando Gorm + if err := configs.DB.Create(&ofertaCarrera).Error; err != nil { + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, Message: "Error creating oferta_carrera: " + err.Error(), Data: nil, }) @@ -160,8 +248,8 @@ func UpdateOffer(c *gin.Context) { } } - c.JSON(200, responses.StandardResponse{ - Status: 200, + c.JSON(http.StatusOK, responses.StandardResponse{ + Status: http.StatusOK, Message: "Offer updated successfully", Data: nil, }) @@ -178,8 +266,8 @@ func GetOffer(c *gin.Context) { var input OfferGet if err := c.ShouldBindJSON(&input); err != nil { - c.JSON(400, responses.StandardResponse{ - Status: 400, + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, Message: "Invalid input: " + err.Error(), Data: nil, }) @@ -189,8 +277,8 @@ func GetOffer(c *gin.Context) { err := configs.DB.Where("id_oferta = ?", input.Id_Oferta).First(&offer).Error if err != nil { - c.JSON(400, responses.StandardResponse{ - Status: 400, + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, Message: "Error getting offer: " + err.Error(), Data: nil, }) @@ -200,8 +288,8 @@ func GetOffer(c *gin.Context) { err = configs.DB.Where("id_empresa = ?", offer.IDEmpresa).First(&Company).Error if err != nil { - c.JSON(400, responses.StandardResponse{ - Status: 400, + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, Message: "Error getting company: " + err.Error(), Data: nil, }) @@ -213,8 +301,8 @@ func GetOffer(c *gin.Context) { "company": Company, } - c.JSON(200, responses.StandardResponse{ - Status: 200, + c.JSON(http.StatusOK, responses.StandardResponse{ + Status: http.StatusOK, Message: "Offer retrieved successfully", Data: data, }) @@ -225,13 +313,16 @@ type GetOfferByCompanyInput struct { } type GetOfferByCompanyResponse struct { - Id_Oferta int `json:"id_oferta"` - IDEmpresa string `json:"id_empresa"` - Puesto string `json:"puesto"` - Descripcion string `json:"descripcion"` - Requisitos string `json:"requisitos"` - Salario float64 `json:"salario"` - IdCarreras []int `json:"id_carreras"` + Id_Oferta int `json:"id_oferta"` + IDEmpresa string `json:"id_empresa"` + Puesto string `json:"puesto"` + Descripcion string `json:"descripcion"` + Requisitos string `json:"requisitos"` + Salario float64 `json:"salario"` + IdCarreras []int `json:"id_carreras"` + Jornada string `json:"jornada"` + HoraInicio time.Time `json:"hora_inicio"` + HoraFin time.Time `json:"hora_fin"` } func GetOfferByCompany(c *gin.Context) { @@ -240,8 +331,8 @@ func GetOfferByCompany(c *gin.Context) { var input GetOfferByCompanyInput if err := c.ShouldBindJSON(&input); err != nil { - c.JSON(400, responses.StandardResponse{ - Status: 400, + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, Message: "Invalid input: " + err.Error(), Data: nil, }) @@ -256,7 +347,10 @@ func GetOfferByCompany(c *gin.Context) { o.descripcion, o.requisitos, o.salario, - oc.id_carrera + oc.id_carrera, + o.jornada, + o.hora_inicio, + o.hora_fin FROM oferta o LEFT JOIN @@ -268,8 +362,8 @@ func GetOfferByCompany(c *gin.Context) { rows, err := configs.DB.Raw(query, input.Id_Empresa).Rows() if err != nil { - c.JSON(400, responses.StandardResponse{ - Status: 400, + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, Message: "Error getting offers: " + err.Error(), Data: nil, }) @@ -305,6 +399,9 @@ func GetOfferByCompany(c *gin.Context) { &offer.Requisitos, &offer.Salario, &idCarrera, + &offer.Jornada, + &offer.HoraInicio, + &offer.HoraFin, ); err != nil { c.JSON(400, responses.StandardResponse{ Status: 400, @@ -342,8 +439,8 @@ func GetOfferByCompany(c *gin.Context) { "offers": offersResponse, } - c.JSON(200, responses.StandardResponse{ - Status: 200, + c.JSON(http.StatusOK, responses.StandardResponse{ + Status: http.StatusOK, Message: "Offers retrieved successfully", Data: data, }) @@ -359,39 +456,59 @@ func DeleteOffer(c *gin.Context) { // Verifica si el valor del parámetro está presente if idOferta == "" { - c.JSON(400, responses.StandardResponse{ - Status: 400, - Message: "Missing id_oferta parameter", + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Missing id_oferta query parameter", + Data: nil, + }) + return + } + + user, err := utils.TokenExtractUsername(c) + + if err != nil { + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Error extracting information from token: " + err.Error(), Data: nil, }) return } - // @mark, esto lo hace la base de datos con el ON DELETE CASCADE. - // Delete oferta_carrera - err := configs.DB.Where("id_oferta = ?", idOferta).Delete(&models.OfertaCarrera{}).Error + // Verifica si el usuario es el dueño de la oferta + var offer models.Oferta + err = configs.DB.Where("id_oferta = ?", idOferta).First(&offer).Error + if err != nil { - c.JSON(400, responses.StandardResponse{ - Status: 400, - Message: "Error deleting oferta_carrera: " + err.Error(), + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Error getting offer: " + err.Error(), + Data: nil, + }) + return + } + + if offer.IDEmpresa != user { + c.JSON(http.StatusForbidden, responses.StandardResponse{ + Status: http.StatusForbidden, + Message: "The user in the token does not match the owner of the offer", Data: nil, }) return } - // Delete oferta - err = configs.DB.Where("id_oferta = ?", idOferta).Delete(&models.Oferta{}).Error + err = configs.DB.Where("id_oferta = ? AND id_empresa = ?", idOferta, user).Delete(&models.Oferta{}).Error if err != nil { - c.JSON(400, responses.StandardResponse{ - Status: 400, + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, Message: "Error deleting oferta: " + err.Error(), Data: nil, }) return } - c.JSON(200, responses.StandardResponse{ - Status: 200, + c.JSON(http.StatusOK, responses.StandardResponse{ + Status: http.StatusOK, Message: "Offer deleted successfully", Data: nil, }) @@ -402,15 +519,15 @@ func GetApplicants(c *gin.Context) { var tokenUsername string if err := c.ShouldBindJSON(&input); err != nil { - c.JSON(400, responses.StandardResponse{ - Status: 400, - Message: "Error binding JSON. " + err.Error(), + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Invalid input. " + err.Error(), Data: nil, }) return } - tokenUsername, err := utils.ExtractTokenUsername(c) + tokenUsername, err := utils.TokenExtractUsername(c) if err != nil { c.JSON(http.StatusBadRequest, responses.StandardResponse{ @@ -423,12 +540,21 @@ func GetApplicants(c *gin.Context) { // Verificación con el token para que no se pueda ver las postulaciones de otras empresas var offer models.Oferta - err = configs.DB.Where("id_oferta = ? AND id_empresa = ?", input.IdOferta, tokenUsername).First(&offer).Error + err = configs.DB.Where("id_oferta = ?", input.IdOferta).First(&offer).Error if err != nil { + c.JSON(http.StatusNotFound, responses.StandardResponse{ + Status: http.StatusNotFound, + Message: "Error getting offer. " + err.Error(), + Data: nil, + }) + return + } + + if offer.IDEmpresa != tokenUsername { c.JSON(http.StatusForbidden, responses.StandardResponse{ Status: http.StatusForbidden, - Message: "Error verifying ownership of the offer. " + err.Error(), + Message: "The user in the token does not match the owner of the offer", Data: nil, }) return diff --git a/backend/controllers/postulation.go b/backend/controllers/postulation.go index e55f3460..961b673a 100644 --- a/backend/controllers/postulation.go +++ b/backend/controllers/postulation.go @@ -5,10 +5,10 @@ import ( "backend/models" "backend/responses" "backend/utils" + "fmt" "github.com/gin-gonic/gin" "github.com/lib/pq" "net/http" - "strings" "time" ) @@ -22,6 +22,10 @@ type GetPostulationInput struct { IdOferta int `json:"id_oferta"` } +type PuestoResult struct { + Puesto string +} + func NewPostulation(c *gin.Context) { var input PostulationInput @@ -34,6 +38,26 @@ func NewPostulation(c *gin.Context) { return } + user, err := utils.TokenExtractUsername(c) + + if err != nil { + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Unauthorized. Cannot get information from token. " + err.Error(), + Data: nil, + }) + return + } + + if user != input.IdEstudiante { + c.JSON(http.StatusForbidden, responses.StandardResponse{ + Status: http.StatusForbidden, + Message: "The user in the token does not match the user in the request.", + Data: nil, + }) + return + } + postulation := models.Postulacion{ IdOferta: input.IdOferta, IdEstudiante: input.IdEstudiante, @@ -42,40 +66,57 @@ func NewPostulation(c *gin.Context) { var inserted models.PostulacionGet - // TODO: Delete Raw - err := configs.DB.Raw("INSERT INTO postulacion (id_oferta, id_estudiante, estado) VALUES (?, ?, ?) RETURNING id_postulacion, id_oferta, id_estudiante, estado", postulation.IdOferta, postulation.IdEstudiante, postulation.Estado).Scan(&inserted).Error + err = configs.DB.Raw("INSERT INTO postulacion (id_oferta, id_estudiante, estado) VALUES (?, ?, ?) RETURNING id_postulacion, id_oferta, id_estudiante, estado", postulation.IdOferta, postulation.IdEstudiante, postulation.Estado).Scan(&inserted).Error if err != nil { if pqErr, ok := err.(*pq.Error); ok && pqErr.Code == "23505" { c.JSON(http.StatusConflict, responses.StandardResponse{ - Status: 409, + Status: http.StatusConflict, Message: "This postulation already exists", Data: nil, }) return } - c.JSON(400, responses.StandardResponse{ - Status: 400, + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, Message: "Error creating. " + err.Error(), Data: nil, }) return } + var resultado PuestoResult + + // Obtener el valor de "puesto" de la oferta + err = configs.DB.Model(models.Oferta{}).Select("puesto").Where("id_oferta = ?", input.IdOferta).Scan(&resultado).Error + if err != nil { + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Error getting 'puesto' from oferta. " + err.Error(), + Data: nil, + }) + return + } + + puesto := resultado.Puesto + + // Mensaje con el valor de "puesto" + mensaje := fmt.Sprintf("Hola, me acabo de postular al puesto de '%s'.", puesto) + // Nuevo query - err = configs.DB.Exec("INSERT INTO mensaje (id_postulacion, id_emisor, id_receptor, mensaje, tiempo) VALUES (?, ?, (SELECT id_empresa FROM oferta WHERE id_oferta = ?), 'Hola, me acabo de postular a esta oferta.', ?)", inserted.IdPostulacion, inserted.IdEstudiante, inserted.IdOferta, time.Now()).Error + err = configs.DB.Exec("INSERT INTO mensaje (id_postulacion, id_emisor, id_receptor, mensaje, tiempo) VALUES (?, ?, (SELECT id_empresa FROM oferta WHERE id_oferta = ?), ?, ?)", inserted.IdPostulacion, inserted.IdEstudiante, input.IdOferta, mensaje, time.Now()).Error if err != nil { - c.JSON(400, responses.StandardResponse{ - Status: 400, + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, Message: "Error creating initial message. " + err.Error(), Data: nil, }) return } - c.JSON(200, responses.StandardResponse{ - Status: 200, + c.JSON(http.StatusOK, responses.StandardResponse{ + Status: http.StatusOK, Message: "Postulation created successfully", Data: nil, }) @@ -100,76 +141,25 @@ type PostulationResult struct { func GetOfferPreviews(c *gin.Context) { var postulations []models.ViewPrevPostulaciones - var data map[string]interface{} err := configs.DB.Find(&postulations).Error if err != nil { - c.JSON(400, responses.StandardResponse{ - Status: 400, - Message: "Error getting postulations", + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Error getting postulations. " + err.Error(), Data: nil, }) return } - groupedPostulations := make(map[int][]string) - for _, p := range postulations { - groupedPostulations[p.IdOferta] = append(groupedPostulations[p.IdOferta], p.NombreCarrera) - } - - combinedPostulations := make([]map[string]interface{}, 0) - for id, carreras := range groupedPostulations { - combinedCarreras := strings.Join(carreras, ", ") - - combinedPostulations = append(combinedPostulations, map[string]interface{}{ - "id_oferta": id, - "puesto": getPuestosByIDOferta(postulations, id)[0], - "nombre_empresa": getNombreEmpresaByIDOferta(postulations, id), - "nombre_carreras": combinedCarreras, - "salario": getSalarioByIDOferta(postulations, id), - }) - } - - data = map[string]interface{}{ - "postulations": combinedPostulations, - } - - c.JSON(200, responses.StandardResponse{ - Status: 200, - Message: "Postulations retrieved successfully", - Data: data, + c.JSON(http.StatusOK, responses.StandardResponse{ + Status: http.StatusOK, + Message: "Previews of offers retrieved successfully", + Data: map[string]interface{}{"postulations": postulations}, }) } -func getPuestosByIDOferta(postulations []models.ViewPrevPostulaciones, id int) []string { - var puestos []string - for _, p := range postulations { - if p.IdOferta == id { - puestos = append(puestos, p.Puesto) - } - } - return puestos -} - -func getNombreEmpresaByIDOferta(postulations []models.ViewPrevPostulaciones, id int) string { - for _, p := range postulations { - if p.IdOferta == id { - return p.NombreEmpresa - } - } - return "" -} - -func getSalarioByIDOferta(postulations []models.ViewPrevPostulaciones, id int) float64 { - for _, p := range postulations { - if p.IdOferta == id { - return p.Salario - } - } - return 0 -} - type PostulationFromStudentResult struct { IDPostulacion int `json:"id_postulacion"` IDOferta int `json:"id_oferta"` @@ -180,28 +170,21 @@ type PostulationFromStudentResult struct { Salario float64 `json:"salario"` } -func GetPostulactionFromStudent(c *gin.Context) { +func GetPostulationFromStudent(c *gin.Context) { var results []PostulationFromStudentResult var data map[string]interface{} - // obten el id del estudiante a partir del token. - idEstudiante, err := utils.ExtractTokenUsername(c) + idEstudiante, err := utils.TokenExtractUsername(c) + if err != nil { - c.JSON(400, responses.StandardResponse{ - Status: 400, - Message: "Error getting id estudiante", + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Could not retrieve info from token. " + err.Error(), Data: nil, }) return } - // en postgreSQL: - //select id_postulacion, o.id_oferta, id_empresa, puesto, descripcion, requisitos, salario - //from postulacion p - //join oferta o - //on p.id_oferta = o.id_oferta - //where id_estudiante = 'mor21146@uvg.edu.gt'; - err = configs.DB.Raw("select id_postulacion, o.id_oferta, id_empresa, puesto, descripcion, requisitos, salario from postulacion p join oferta o on p.id_oferta = o.id_oferta where id_estudiante = ?", idEstudiante).Scan(&results).Error if err != nil { @@ -227,7 +210,7 @@ func GetPostulactionFromStudent(c *gin.Context) { func RetirePostulation(c *gin.Context) { input := c.Query("id_postulacion") - user, err := utils.ExtractTokenUsername(c) + user, err := utils.TokenExtractUsername(c) if input == "" { c.JSON(http.StatusBadRequest, responses.StandardResponse{ @@ -250,7 +233,7 @@ func RetirePostulation(c *gin.Context) { // verify that the postulation exists var postulation models.Postulacion - err = configs.DB.Where("id_postulacion = ? AND id_estudiante = ?", input, user).First(&postulation).Error + err = configs.DB.Where("id_postulacion = ?", input).First(&postulation).Error if err != nil { c.JSON(http.StatusNotFound, responses.StandardResponse{ @@ -261,6 +244,16 @@ func RetirePostulation(c *gin.Context) { return } + // verify that the postulation belongs to the user + if postulation.IdEstudiante != user { + c.JSON(http.StatusForbidden, responses.StandardResponse{ + Status: http.StatusForbidden, + Message: "The postulation does not belong to the user in the token", + Data: nil, + }) + return + } + err = configs.DB.Where("id_postulacion = ? AND id_estudiante = ?", input, user).Delete(&models.Postulacion{}).Error if err != nil { diff --git a/backend/controllers/students.go b/backend/controllers/students.go index 0b283d08..2f2bd571 100644 --- a/backend/controllers/students.go +++ b/backend/controllers/students.go @@ -4,6 +4,7 @@ import ( "backend/configs" "backend/models" "backend/responses" + "backend/utils" "github.com/gin-gonic/gin" "github.com/lib/pq" "net/http" @@ -97,13 +98,58 @@ func NewStudent(c *gin.Context) { }) } +type EstudianteUpdateInput struct { + Nombre string `json:"nombre"` + Apellido string `json:"apellido"` + Nacimiento string `json:"nacimiento"` + Correo string `json:"correo"` + Telefono string `json:"telefono"` + Carrera int `json:"carrera"` + Semestre int `json:"semestre"` + Foto string `json:"foto"` + Universidad string `json:"universidad"` +} + func UpdateStudent(c *gin.Context) { - var input EstudianteInput + var input EstudianteUpdateInput if err := c.ShouldBindJSON(&input); err != nil { - c.JSON(400, responses.StandardResponse{ - Status: 400, - Message: "Error binding JSON: " + err.Error(), + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Invalid input. " + err.Error(), + Data: nil, + }) + return + } + + var originalStudent models.Estudiante + err := configs.DB.Where("id_estudiante = ?", input.Correo).First(&originalStudent).Error + + if err != nil { + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Student '" + input.Correo + "' not found: " + err.Error(), + Data: nil, + }) + return + } + + user, err := utils.TokenExtractUsername(c) + + if err != nil { + c.JSON(http.StatusBadRequest, responses.StandardResponse{ + Status: http.StatusBadRequest, + Message: "Could not retrieve info from token. " + err.Error(), + Data: nil, + }) + return + } + + // Se verifica que el usuario sea el mismo que el del estudiante + if user != input.Correo { + c.JSON(http.StatusUnauthorized, responses.StandardResponse{ + Status: http.StatusUnauthorized, + Message: "User " + user + " is not authorized to update student " + input.Correo, Data: nil, }) return @@ -111,23 +157,31 @@ func UpdateStudent(c *gin.Context) { nacimiento, _ := time.Parse("2006-01-02", input.Nacimiento) - var inserted models.EstudianteGet + // Crear una instancia del modelo Estudiante con los datos actualizados + updatedStudent := models.Estudiante{ + Nombre: input.Nombre, + Apellido: input.Apellido, + Nacimiento: nacimiento, + Telefono: input.Telefono, + Carrera: input.Carrera, + Semestre: input.Semestre, + Universidad: input.Universidad, + } - err := configs.DB.Raw("UPDATE estudiante SET nombre = ?, apellido = ?, nacimiento = ?, telefono = ?, carrera = ?, semestre = ?, cv = ?, foto = ?, universidad = ? WHERE id_estudiante = ? RETURNING id_estudiante", input.Nombre, input.Apellido, nacimiento, input.Telefono, input.Carrera, input.Semestre, input.CV, input.Foto, input.Universidad, input.Correo).Scan(&inserted).Error + err = configs.DB.Model(&models.Estudiante{}).Where("id_estudiante = ?", input.Correo).Updates(updatedStudent).Error if err != nil { c.JSON(http.StatusBadRequest, responses.StandardResponse{ - Status: 400, - Message: "Error updating. " + err.Error(), + Status: http.StatusBadRequest, + Message: "Error updating student. " + err.Error(), Data: nil, }) return } c.JSON(http.StatusOK, responses.StandardResponse{ - Status: 200, + Status: http.StatusOK, Message: "Student updated successfully", Data: nil, }) - } diff --git a/backend/main.go b/backend/main.go index 942d547e..85822975 100644 --- a/backend/main.go +++ b/backend/main.go @@ -12,6 +12,10 @@ func main() { panic(err) } + if _, err := configs.CreateDirIfNotExist("./uploads/pdf"); err != nil { + panic(err) + } + router := gin.Default() router.Use(CORS()) diff --git a/backend/middlewares/middlewares.go b/backend/middlewares/middlewares.go index a4dd69af..810a1222 100644 --- a/backend/middlewares/middlewares.go +++ b/backend/middlewares/middlewares.go @@ -13,7 +13,7 @@ func JwtAuthentication() gin.HandlerFunc { if err != nil { c.JSON(http.StatusUnauthorized, responses.StandardResponse{ Status: 401, - Message: "Unauthorized: " + err.Error(), + Message: "Unauthorized, token is invalid: " + err.Error(), Data: nil, }) diff --git a/backend/models/administrador.go b/backend/models/administrador.go index b83afa69..c04d4329 100644 --- a/backend/models/administrador.go +++ b/backend/models/administrador.go @@ -1,9 +1,9 @@ package models type Administrador struct { - IdAdministrador string `json:"id_administrador"` - Nombre string `json:"nombre"` - Apellido string `json:"apellido"` + IdAdmin string `json:"id_admin"` + Nombre string `json:"nombre"` + Apellido string `json:"apellido"` } func (Administrador) TableName() string { diff --git a/backend/models/oferta.go b/backend/models/oferta.go index 902dd289..fd0d762c 100644 --- a/backend/models/oferta.go +++ b/backend/models/oferta.go @@ -1,11 +1,16 @@ package models +import "time" + type Oferta struct { - IDEmpresa string `json:"id_empresa"` - Puesto string `json:"puesto"` - Descripcion string `json:"descripcion"` - Requisitos string `json:"requisitos"` - Salario float64 `json:"salario"` + IDEmpresa string `json:"id_empresa"` + Puesto string `json:"puesto"` + Descripcion string `json:"descripcion"` + Requisitos string `json:"requisitos"` + Salario float64 `json:"salario"` + Jornada string `json:"jornada"` + HoraInicio time.Time `json:"hora_inicio"` + HoraFin time.Time `json:"hora_fin"` } // TableName Esta función se llama automáticamente cuando se hace un Create() en el ORM, acá va el nombre como aparece en Postgres @@ -14,12 +19,15 @@ func (Oferta) TableName() string { } type OfertaGet struct { - Id_Oferta int `json:"id_oferta"` - IDEmpresa string `json:"id_empresa"` - Puesto string `json:"puesto"` - Descripcion string `json:"descripcion"` - Requisitos string `json:"requisitos"` - Salario float64 `json:"salario"` + Id_Oferta int `json:"id_oferta"` + IDEmpresa string `json:"id_empresa"` + Puesto string `json:"puesto"` + Descripcion string `json:"descripcion"` + Requisitos string `json:"requisitos"` + Salario float64 `json:"salario"` + Jornada string `json:"jornada"` + HoraInicio time.Time `json:"hora_inicio"` + HoraFin time.Time `json:"hora_fin"` } // TableName Esta función se llama automáticamente cuando se hace un Create() en el ORM, acá va el nombre como aparece en Postgres diff --git a/backend/models/viewPrevPostulaciones.go b/backend/models/viewPrevPostulaciones.go index a8f94ac4..03cb3008 100644 --- a/backend/models/viewPrevPostulaciones.go +++ b/backend/models/viewPrevPostulaciones.go @@ -1,11 +1,16 @@ package models +import "time" + type ViewPrevPostulaciones struct { - IdOferta int `json:"id_oferta"` - Puesto string `json:"puesto"` - NombreEmpresa string `json:"nombre_empresa"` - NombreCarrera string `json:"nombre_carrera"` - Salario float64 `json:"salario"` + IdOferta int `json:"id_oferta"` + Puesto string `json:"puesto"` + NombreEmpresa string `json:"nombre_empresa"` + NombreCarrera string `json:"nombre_carrera"` + Salario float64 `json:"salario"` + Jornada string `json:"jornada"` + HoraInicio time.Time `json:"hora_inicio"` + HoraFin time.Time `json:"hora_fin"` } func (ViewPrevPostulaciones) TableName() string { diff --git a/backend/routes/routes_handler.go b/backend/routes/routes_handler.go index 4c10d9c8..8cd22b77 100644 --- a/backend/routes/routes_handler.go +++ b/backend/routes/routes_handler.go @@ -10,14 +10,14 @@ func Routes(router *gin.Engine) { // Rutas públicas public := router.Group("/api") - public.POST("/register", controllers.Register) public.POST("/login", controllers.Login) public.POST("/students", controllers.NewStudent) public.POST("/companies", controllers.NewCompany) public.GET("/postulations/previews", controllers.GetOfferPreviews) // Rutas de archivos - public.GET("/uploads/:filename", controllers.GetFile()) + public.GET("/uploads/:filename", controllers.GetProfilePicture()) + public.GET("cv/:filename", controllers.GetCV()) // Rutas protegidas // Mensajes @@ -27,12 +27,13 @@ func Routes(router *gin.Engine) { messages.POST("/send", controllers.SendMessage) messages.POST("/get", controllers.GetMessages) messages.POST("/getLast", controllers.GetLastChat) + messages.DELETE("/delete", controllers.DeleteChat) // Usuarios users := router.Group("api/users") users.Use(middlewares.JwtAuthentication()) - users.GET("/", controllers.CurrentUser) + users.GET("/", controllers.GetCurrentUserDetails) users.POST("/details", controllers.GetUserDetails) users.PUT("/upload", controllers.UpdateProfilePicture()) @@ -41,6 +42,7 @@ func Routes(router *gin.Engine) { students.Use(middlewares.JwtAuthentication()) students.PUT("/update", controllers.UpdateStudent) + students.PUT("/update/cv", controllers.UpdateCV()) // Carreras careers := router.Group("api/careers") @@ -52,19 +54,20 @@ func Routes(router *gin.Engine) { // Empresas companies := router.Group("api/companies") companies.Use(middlewares.JwtAuthentication()) - companies.PUT("/update", controllers.UpdateCompanies) // Administradores admins := router.Group("api/admins") admins.Use(middlewares.JwtAuthentication()) - admins.POST("/", controllers.NewAdmin) - admins.GET("/students", controllers.GetStudents) - admins.GET("/companies", controllers.GetCompanies) - admins.POST("/suspend", controllers.SuspendAccount) - admins.POST("/offers", controllers.DeleteOfferAdmin) - admins.POST("/deleteUser", controllers.DeleteUsuario) + admins.GET("/students", controllers.AdminGetStudents) + admins.GET("/companies", controllers.AdminGetCompanies) + admins.POST("/suspend", controllers.AdminSuspendAccount) + admins.DELETE("/delete/offers", controllers.AdminDeleteOffer) + admins.POST("/delete/user", controllers.AdminDeleteUser) + admins.DELETE("/postulation", controllers.AdminDeletePostulation) + admins.POST("/details", controllers.AdminGetUserDetails) + admins.POST("/postulations", controllers.GetPostulationsOfStudentAsAdmin) // Ofertas offers := router.Group("api/offers") @@ -81,6 +84,6 @@ func Routes(router *gin.Engine) { postulations := router.Group("api/postulations") postulations.Use(middlewares.JwtAuthentication()) postulations.POST("/", controllers.NewPostulation) - postulations.GET("/getFromStudent", controllers.GetPostulactionFromStudent) + postulations.GET("/getFromStudent", controllers.GetPostulationFromStudent) postulations.DELETE("/", controllers.RetirePostulation) } diff --git a/backend/tests/auth_student_test.go b/backend/tests/auth_student_test.go index fdd39a4a..70b0a534 100644 --- a/backend/tests/auth_student_test.go +++ b/backend/tests/auth_student_test.go @@ -51,9 +51,32 @@ func TestLogin(t *testing.T) { // no es necesario eliminar usuarios. router.ServeHTTP(w, req) - fmt.Println(w.Body.String()) // Comprueba la respuesta HTTP y el cuerpo de la respuesta assert.Equal(t, http.StatusOK, w.Code, "Status code is not 200") + + // get to /api/users to get the user data sending the token from login + var LoginResponse struct { + Status int `json:"status"` + Message string `json:"message"` + Data struct { + Role string `json:"role"` + Token string `json:"token"` + } `json:"data"` + } + + err := json.Unmarshal(w.Body.Bytes(), &LoginResponse) + assert.NoError(t, err, "Error unmarshalling login response") + + w = httptest.NewRecorder() + + req = httptest.NewRequest("GET", "/api/users/", nil) + + req.Header.Set("Authorization", "Bearer "+LoginResponse.Data.Token) + + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code, "Status code is not 200") + } func TestNewStudent(t *testing.T) { @@ -90,7 +113,7 @@ func TestUpdateStudent(t *testing.T) { assert.Equal(t, http.StatusOK, w.Code, "Status code is not 200") // Paso 2: Extraer el token de la respuesta - var loginResponse struct { + var LoginResponse struct { Status int `json:"status"` Message string `json:"message"` Data struct { @@ -99,7 +122,7 @@ func TestUpdateStudent(t *testing.T) { } `json:"data"` } - err := json.Unmarshal(w.Body.Bytes(), &loginResponse) + err := json.Unmarshal(w.Body.Bytes(), &LoginResponse) assert.NoError(t, err, "Error unmarshalling login response") // Paso 3: Usar el token para hacer la actualización del estudiante @@ -109,7 +132,7 @@ func TestUpdateStudent(t *testing.T) { body = bytes.NewBufferString(jsonData) req = httptest.NewRequest("PUT", "/api/students/update", body) - req.Header.Set("Authorization", "Bearer "+loginResponse.Data.Token) + req.Header.Set("Authorization", "Bearer "+LoginResponse.Data.Token) router.ServeHTTP(w, req) diff --git a/backend/tests/load/main.py b/backend/tests/load/main.py index 956529d6..651accb0 100644 --- a/backend/tests/load/main.py +++ b/backend/tests/load/main.py @@ -2,15 +2,21 @@ import concurrent.futures # URL y JSON de solicitud -url = "https://whole-letisha-markalbrand56.koyeb.app/api/login" +url = "http://127.0.0.1:8080/api/login" payload = { - "usuario": "prueba@prueba", - "contra": "prueba" + "usuario": "empresa@prueba.com", + "contra": "empresaprueba" } +fails = 0 # Función para enviar una solicitud HTTP def send_request(url, payload): + # wait between 0.0 and 2.0 seconds + import time + import random + time.sleep(random.random() * 2) + try: response = requests.post(url, json=payload) if response.status_code == 200: diff --git a/backend/utils/file.go b/backend/utils/file.go index 439ae5e9..45a89759 100644 --- a/backend/utils/file.go +++ b/backend/utils/file.go @@ -19,9 +19,9 @@ func Contains(slice []string, item string) bool { return false } -func UploadFileToServer(url string, bearer string, file *multipart.FileHeader, dst string) error { +func UploadFileToServer(url string, bearer string, fileHeader *multipart.FileHeader, dst string) error { // Abrir el archivo - f, err := file.Open() + f, err := fileHeader.Open() if err != nil { return err } diff --git a/backend/utils/token.go b/backend/utils/token.go index 0e4f94b9..3351f7a1 100644 --- a/backend/utils/token.go +++ b/backend/utils/token.go @@ -13,8 +13,6 @@ import ( func GenerateToken(username string, userType string) (string, error) { tokenLifespan, err := strconv.Atoi(os.Getenv("TOKEN_HOUR_LIFESPAN")) - fmt.Println("tokenLifespan: ", tokenLifespan) - if err != nil { return "", err } @@ -30,7 +28,7 @@ func GenerateToken(username string, userType string) (string, error) { } func TokenValid(c *gin.Context) error { - tokenString := ExtractToken(c) + tokenString := ExtractTokenFromRequest(c) _, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) @@ -43,7 +41,7 @@ func TokenValid(c *gin.Context) error { return nil } -func ExtractToken(c *gin.Context) string { +func ExtractTokenFromRequest(c *gin.Context) string { token := c.Query("token") if token != "" { return token @@ -55,9 +53,9 @@ func ExtractToken(c *gin.Context) string { return "" } -func ExtractTokenUsername(c *gin.Context) (string, error) { +func TokenExtractUsername(c *gin.Context) (string, error) { - tokenString := ExtractToken(c) + tokenString := ExtractTokenFromRequest(c) token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"]) @@ -76,9 +74,9 @@ func ExtractTokenUsername(c *gin.Context) (string, error) { return "", nil } -func ExtractTokenUserType(c *gin.Context) (string, error) { +func TokenExtractRole(c *gin.Context) (string, error) { - tokenString := ExtractToken(c) + tokenString := ExtractTokenFromRequest(c) token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])