From a9d250efeda303c9cec2f8f22ce393a1dc01c208 Mon Sep 17 00:00:00 2001 From: ImSoZRious <30285202+ImSoZRious@users.noreply.github.com> Date: Fri, 12 Jan 2024 15:50:56 +0700 Subject: [PATCH] feat/return user (#35) * feat: central staff * feat: return user * fix: lint --- di/wire_gen.go | 2 +- docs/docs.go | 39 ++++++++++++++- docs/swagger.json | 39 ++++++++++++++- docs/swagger.yaml | 28 ++++++++++- internal/auth/auth.repository.go | 5 ++ internal/dto/staff.dto.go | 13 +++++ internal/middleware/auth.middleware.go | 23 ++++++--- internal/model/staff.model.go | 18 ++++++- internal/staff/staff.handler.go | 29 ++++++++--- internal/staff/staff.repository.go | 9 +++- internal/staff/staff.service.go | 69 +++++++++++++++++++++----- internal/staff/staff.utils.go | 19 +++++++ migrations/03-add-central-checkin.sql | 11 ++++ tools/gen-central-staff-token.sh | 32 ++++++++++++ tools/gen-staff-token.sh | 3 +- 15 files changed, 301 insertions(+), 38 deletions(-) create mode 100644 internal/dto/staff.dto.go create mode 100644 internal/staff/staff.utils.go create mode 100644 migrations/03-add-central-checkin.sql create mode 100644 tools/gen-central-staff-token.sh diff --git a/di/wire_gen.go b/di/wire_gen.go index 65050ba..d4f27aa 100644 --- a/di/wire_gen.go +++ b/di/wire_gen.go @@ -54,7 +54,7 @@ func Init() (Container, error) { evtregService := evtreg.NewService(zapLogger, evtregRepository, eventCache) evtregHandler := evtreg.NewHandler(evtregService) staffRepository := staff.NewRepository(db) - staffService := staff.NewService(staffRepository, zapLogger) + staffService := staff.NewService(staffRepository, authRepository, zapLogger) staffHandler := staff.NewHandler(staffService, zapLogger) corsHandler := cfgldr.MakeCorsConfig(config) authMiddleware := middleware.NewAuthMiddleware(authRepository, config) diff --git a/docs/docs.go b/docs/docs.go index 00549f3..8d085df 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -374,8 +374,11 @@ const docTemplate = `{ } ], "responses": { - "204": { - "description": "No Content" + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.AttendeeStaffCheckinResponse" + } }, "403": { "description": "Forbidden", @@ -400,6 +403,38 @@ const docTemplate = `{ } }, "definitions": { + "dto.AttendeeStaffCheckinResponse": { + "type": "object", + "properties": { + "already_checkin": { + "type": "boolean" + }, + "user": { + "$ref": "#/definitions/dto.AttendeeStaffCheckinUser" + } + } + }, + "dto.AttendeeStaffCheckinUser": { + "type": "object", + "properties": { + "allergies": { + "type": "string", + "example": "Romantic" + }, + "first_name": { + "type": "string", + "example": "John" + }, + "last_name": { + "type": "string", + "example": "Doe" + }, + "medical_condition": { + "type": "string", + "example": "Unlovable" + } + } + }, "dto.BilingualField": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index bed3858..ec6855d 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -369,8 +369,11 @@ } ], "responses": { - "204": { - "description": "No Content" + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.AttendeeStaffCheckinResponse" + } }, "403": { "description": "Forbidden", @@ -395,6 +398,38 @@ } }, "definitions": { + "dto.AttendeeStaffCheckinResponse": { + "type": "object", + "properties": { + "already_checkin": { + "type": "boolean" + }, + "user": { + "$ref": "#/definitions/dto.AttendeeStaffCheckinUser" + } + } + }, + "dto.AttendeeStaffCheckinUser": { + "type": "object", + "properties": { + "allergies": { + "type": "string", + "example": "Romantic" + }, + "first_name": { + "type": "string", + "example": "John" + }, + "last_name": { + "type": "string", + "example": "Doe" + }, + "medical_condition": { + "type": "string", + "example": "Unlovable" + } + } + }, "dto.BilingualField": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 45e2fc8..4002f5b 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1,4 +1,26 @@ definitions: + dto.AttendeeStaffCheckinResponse: + properties: + already_checkin: + type: boolean + user: + $ref: '#/definitions/dto.AttendeeStaffCheckinUser' + type: object + dto.AttendeeStaffCheckinUser: + properties: + allergies: + example: Romantic + type: string + first_name: + example: John + type: string + last_name: + example: Doe + type: string + medical_condition: + example: Unlovable + type: string + type: object dto.BilingualField: properties: en: @@ -640,8 +662,10 @@ paths: produces: - application/json responses: - "204": - description: No Content + "200": + description: OK + schema: + $ref: '#/definitions/dto.AttendeeStaffCheckinResponse' "403": description: Forbidden schema: diff --git a/internal/auth/auth.repository.go b/internal/auth/auth.repository.go index 3951179..693fba6 100644 --- a/internal/auth/auth.repository.go +++ b/internal/auth/auth.repository.go @@ -8,6 +8,7 @@ import ( type Repository interface { CreateUser(user *model.User) error GetUserByEmail(user *model.User, email string) error + GetUserById(model *model.User, id int) error } type repositoryImpl struct { @@ -41,3 +42,7 @@ func (r *repositoryImpl) GetUserByEmail(user *model.User, email string) error { Where("email = ?", email). First(&user).Error } + +func (r *repositoryImpl) GetUserById(user *model.User, id int) error { + return r.db.Model(user).First(&user, "id = ?", id).Error +} diff --git a/internal/dto/staff.dto.go b/internal/dto/staff.dto.go new file mode 100644 index 0000000..4c744c1 --- /dev/null +++ b/internal/dto/staff.dto.go @@ -0,0 +1,13 @@ +package dto + +type AttendeeStaffCheckinResponse struct { + User *AttendeeStaffCheckinUser `json:"user"` + AlreadyCheckin bool `json:"already_checkin"` +} + +type AttendeeStaffCheckinUser struct { + FirstName string `example:"John" json:"first_name"` + LastName string `example:"Doe" json:"last_name"` + Allergies string `example:"Romantic" json:"allergies"` + MedicalCondition string `example:"Unlovable" json:"medical_condition"` +} diff --git a/internal/middleware/auth.middleware.go b/internal/middleware/auth.middleware.go index cdcd334..cbaf0f3 100644 --- a/internal/middleware/auth.middleware.go +++ b/internal/middleware/auth.middleware.go @@ -41,12 +41,23 @@ func NewAuthMiddleware(userRepo auth.Repository, cfg *cfgldr.Config) AuthMiddlew c.Abort() return } - if staffToken.Valid && staffToken.Claims.(jwt.MapClaims)["role"] == "staff" { - c.Set("role", "staff") - c.Set("faculty", staffToken.Claims.(jwt.MapClaims)["faculty"]) - c.Set("department", staffToken.Claims.(jwt.MapClaims)["department"]) - c.Next() - return + if staffToken.Valid { + switch staffToken.Claims.(jwt.MapClaims)["role"] { + case "faculty-staff": + c.Set("role", "faculty-staff") + c.Set("faculty", staffToken.Claims.(jwt.MapClaims)["faculty"]) + c.Set("department", staffToken.Claims.(jwt.MapClaims)["department"]) + c.Next() + return + case "central-staff": + c.Set("role", "central-staff") + c.Next() + return + default: + utils.ReturnError(c, apperror.InvalidToken) + c.Abort() + return + } } else { utils.ReturnError(c, apperror.InvalidToken) c.Abort() diff --git a/internal/model/staff.model.go b/internal/model/staff.model.go index 6507244..219e324 100644 --- a/internal/model/staff.model.go +++ b/internal/model/staff.model.go @@ -2,7 +2,7 @@ package model import "time" -type AttendeeCheckin struct { +type AttendeeFacultyCheckin struct { Id int `gorm:"primaryKey,autoIncrement"` CreatedAt time.Time `gorm:"not null;autoCreateTime"` UpdatedAt time.Time `gorm:"not null;autoUpdateTime:milli"` @@ -13,3 +13,19 @@ type AttendeeCheckin struct { DepartmentCode string `gorm:"not null"` Department Department `gorm:"foreignKey:DepartmentCode,FacultyCode;references:Code,FacultyCode"` } + +func (AttendeeFacultyCheckin) TableName() string { + return "attendee_faculty_checkins" +} + +type AttendeeCentralCheckin struct { + Id int `gorm:"primaryKey,autoIncrement"` + CreatedAt time.Time `gorm:"not null;autoCreateTime"` + UpdatedAt time.Time `gorm:"not null;autoUpdateTime:milli"` + UserId int `gorm:"not null"` + User User `gorm:"foreignKey:UserId;references:Id"` +} + +func (AttendeeCentralCheckin) TableName() string { + return "attendee_central_checkins" +} diff --git a/internal/staff/staff.handler.go b/internal/staff/staff.handler.go index 0175167..147775b 100644 --- a/internal/staff/staff.handler.go +++ b/internal/staff/staff.handler.go @@ -6,6 +6,7 @@ import ( "github.com/gin-gonic/gin" "github.com/isd-sgcu/oph66-backend/apperror" + "github.com/isd-sgcu/oph66-backend/internal/dto" "github.com/isd-sgcu/oph66-backend/utils" "go.uber.org/zap" ) @@ -35,13 +36,12 @@ type handlerImpl struct { // @router /staff/checkin/{userId} [post] // @param userId path int true "User id" // @Security Bearer -// @success 204 +// @success 200 {object} dto.AttendeeStaffCheckinResponse // @Failure 403 {object} dto.EventInvalidResponse // @Failure 404 {object} dto.EventInvalidResponse -// @Failure 409 {object} dto.EventInvalidResponse // @Failure 500 {object} dto.EventAllErrorResponse func (h *handlerImpl) AttendeeStaffCheckin(c *gin.Context) { - if c.GetString("role") != "staff" { + if c.GetString("role") != "central-staff" && c.GetString("role") != "faculty-staff" { utils.ReturnError(c, apperror.Forbidden) return } @@ -53,13 +53,28 @@ func (h *handlerImpl) AttendeeStaffCheckin(c *gin.Context) { return } - faculty := c.GetString("faculty") - department := c.GetString("department") - apperr := h.service.AttendeeStaffCheckin(userId, department, faculty) + var ( + apperr *apperror.AppError + ciu *dto.AttendeeStaffCheckinUser + alreadyCheckin bool + ) + + switch c.GetString("role") { + case "faculty-staff": + faculty := c.GetString("faculty") + department := c.GetString("department") + ciu, alreadyCheckin, apperr = h.service.AttendeeFacultyStaffCheckin(userId, department, faculty) + case "central-staff": + ciu, alreadyCheckin, apperr = h.service.AttendeeCentralStaffCheckin(userId) + } + if apperr != nil { utils.ReturnError(c, apperr) return } - c.AbortWithStatus(http.StatusNoContent) + c.JSON(http.StatusOK, dto.AttendeeStaffCheckinResponse{ + User: ciu, + AlreadyCheckin: alreadyCheckin, + }) } diff --git a/internal/staff/staff.repository.go b/internal/staff/staff.repository.go index af76a33..45d376e 100644 --- a/internal/staff/staff.repository.go +++ b/internal/staff/staff.repository.go @@ -6,7 +6,8 @@ import ( ) type Repository interface { - CreateCheckin(checkin *model.AttendeeCheckin) error + CreateFacultyCheckin(checkin *model.AttendeeFacultyCheckin) error + CreateCentralCheckin(checkin *model.AttendeeCentralCheckin) error } type repositoryImpl struct { @@ -19,6 +20,10 @@ func NewRepository(db *gorm.DB) Repository { } } -func (r *repositoryImpl) CreateCheckin(checkin *model.AttendeeCheckin) error { +func (r *repositoryImpl) CreateFacultyCheckin(checkin *model.AttendeeFacultyCheckin) error { + return r.db.Model(checkin).Create(checkin).Error +} + +func (r *repositoryImpl) CreateCentralCheckin(checkin *model.AttendeeCentralCheckin) error { return r.db.Model(checkin).Create(checkin).Error } diff --git a/internal/staff/staff.service.go b/internal/staff/staff.service.go index e74d752..2ad6f23 100644 --- a/internal/staff/staff.service.go +++ b/internal/staff/staff.service.go @@ -4,41 +4,84 @@ import ( "errors" "github.com/isd-sgcu/oph66-backend/apperror" + "github.com/isd-sgcu/oph66-backend/internal/auth" + "github.com/isd-sgcu/oph66-backend/internal/dto" "github.com/isd-sgcu/oph66-backend/internal/model" "go.uber.org/zap" "gorm.io/gorm" ) type Service interface { - AttendeeStaffCheckin(userId int, departmentCode string, facultyCode string) *apperror.AppError + AttendeeFacultyStaffCheckin(userId int, departmentCode string, facultyCode string) (ciu *dto.AttendeeStaffCheckinUser, alreadyCheckin bool, apperr *apperror.AppError) + AttendeeCentralStaffCheckin(userId int) (ciu *dto.AttendeeStaffCheckinUser, alreadyCheckin bool, apperr *apperror.AppError) } -func NewService(repo Repository, logger *zap.Logger) Service { +func NewService(repo Repository, authRepo auth.Repository, logger *zap.Logger) Service { return &serviceImpl{ repo, + authRepo, logger, } } type serviceImpl struct { - repo Repository - logger *zap.Logger + repo Repository + authRepo auth.Repository + logger *zap.Logger } -func (s *serviceImpl) AttendeeStaffCheckin(userId int, departmentCode string, facultyCode string) *apperror.AppError { - var checkin model.AttendeeCheckin +func (s *serviceImpl) AttendeeFacultyStaffCheckin(userId int, departmentCode string, facultyCode string) (*dto.AttendeeStaffCheckinUser, bool, *apperror.AppError) { + var checkin model.AttendeeFacultyCheckin checkin.UserId = userId checkin.DepartmentCode = departmentCode checkin.FacultyCode = facultyCode - if err := s.repo.CreateCheckin(&checkin); errors.Is(err, gorm.ErrDuplicatedKey) { - return apperror.AlreadyCheckin - } else if errors.Is(err, gorm.ErrForeignKeyViolated) { - return apperror.NotFound + err := s.repo.CreateFacultyCheckin(&checkin) + if errors.Is(err, gorm.ErrForeignKeyViolated) { + return nil, false, apperror.NotFound + } else if err != nil && !errors.Is(err, gorm.ErrDuplicatedKey) { + s.logger.Error("unable to faculty checkin", zap.Int("userId", userId), zap.String("departmentCode", departmentCode), zap.String("facultyCode", facultyCode), zap.Error(err)) + return nil, false, apperror.InternalError + } + + alreadyCheckin := errors.Is(err, gorm.ErrDuplicatedKey) + + var user model.User + if err := s.authRepo.GetUserById(&user, userId); errors.Is(err, gorm.ErrRecordNotFound) { + return nil, false, apperror.UserNotFound } else if err != nil { - s.logger.Error("unable to checkin", zap.Int("userId", userId), zap.String("departmentCode", departmentCode), zap.String("facultyCode", facultyCode), zap.Error(err)) - return apperror.InternalError + s.logger.Error("unable to find user after faculty checkin", zap.Int("userId", userId), zap.String("departmentCode", departmentCode), zap.String("facultyCode", facultyCode), zap.Error(err)) + return nil, false, apperror.InternalError } - return nil + ciu := UserModelToCheckinUser(&user) + + return &ciu, alreadyCheckin, nil +} + +func (s *serviceImpl) AttendeeCentralStaffCheckin(userId int) (*dto.AttendeeStaffCheckinUser, bool, *apperror.AppError) { + var checkin model.AttendeeCentralCheckin + checkin.UserId = userId + + err := s.repo.CreateCentralCheckin(&checkin) + if errors.Is(err, gorm.ErrForeignKeyViolated) { + return nil, false, apperror.NotFound + } else if err != nil && !errors.Is(err, gorm.ErrDuplicatedKey) { + s.logger.Error("unable to central checkin", zap.Int("userId", userId), zap.Error(err)) + return nil, false, apperror.InternalError + } + + alreadyCheckin := errors.Is(err, gorm.ErrDuplicatedKey) + + var user model.User + if err := s.authRepo.GetUserById(&user, userId); errors.Is(err, gorm.ErrRecordNotFound) { + return nil, false, apperror.UserNotFound + } else if err != nil { + s.logger.Error("unable to find user after central checkin", zap.Int("userId", userId), zap.Error(err)) + return nil, false, apperror.InternalError + } + + ciu := UserModelToCheckinUser(&user) + + return &ciu, alreadyCheckin, nil } diff --git a/internal/staff/staff.utils.go b/internal/staff/staff.utils.go new file mode 100644 index 0000000..3dfed90 --- /dev/null +++ b/internal/staff/staff.utils.go @@ -0,0 +1,19 @@ +package staff + +import ( + "github.com/isd-sgcu/oph66-backend/internal/dto" + "github.com/isd-sgcu/oph66-backend/internal/model" +) + +func UserModelToCheckinUser(m *model.User) dto.AttendeeStaffCheckinUser { + var checkinUser dto.AttendeeStaffCheckinUser + if m == nil { + return checkinUser + } + checkinUser.FirstName = m.FirstName + checkinUser.LastName = m.LastName + checkinUser.Allergies = m.Allergies + checkinUser.MedicalCondition = m.MedicalCondition + + return checkinUser +} diff --git a/migrations/03-add-central-checkin.sql b/migrations/03-add-central-checkin.sql new file mode 100644 index 0000000..51acedf --- /dev/null +++ b/migrations/03-add-central-checkin.sql @@ -0,0 +1,11 @@ +ALTER TABLE attendee_checkins RENAME TO attendee_faculty_checkins; + +CREATE TABLE attendee_central_checkins ( + "id" SERIAL NOT NULL, + "user_id" INT NOT NULL, + "created_at" timestamptz NOT NULL, + "updated_at" timestamptz NOT NULL, + PRIMARY KEY ("id"), + CONSTRAINT "fk_checkins_users" FOREIGN KEY ("user_id") REFERENCES "users" ("id"), + CONSTRAINT "unique_attendee_central_checkins_user_id" UNIQUE ("user_id") +); diff --git a/tools/gen-central-staff-token.sh b/tools/gen-central-staff-token.sh new file mode 100644 index 0000000..7e6d0d6 --- /dev/null +++ b/tools/gen-central-staff-token.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +## Usage: +## ./tools/gen-central-staff-token.sh +## ./tools/gen-central-staff-token.sh + +if [ -f .env ]; then + source .env +else + echo "Error: .env file not found!" + exit 1 +fi + +if [ -z "$JWT_SECRET_KEY" ]; then + echo "Error: JWT_SECRET_KEY is not set in .env file!" + exit 1 +fi + +SECRET_KEY="$JWT_SECRET_KEY" + +HEADER="{\"alg\":\"HS256\",\"typ\":\"JWT\"}" + +PAYLOAD="{\"role\":\"central-staff\"}" + +HEADER_ENCODED=$(echo -n "$HEADER" | base64 -w 0 | tr -d '=' | tr '/+' '_-') +PAYLOAD_ENCODED=$(echo -n "$PAYLOAD" | base64 -w 0 | tr -d '=' | tr '/+' '_-') + +TOKEN="$HEADER_ENCODED.$PAYLOAD_ENCODED" + +SIGNATURE=$(echo -n "$TOKEN" | openssl dgst -sha256 -hmac "$SECRET_KEY" -binary | base64 -w 0 | tr -d '=' | tr '/+' '_-') + +echo "$TOKEN.$SIGNATURE" diff --git a/tools/gen-staff-token.sh b/tools/gen-staff-token.sh index cd8da99..4a76424 100755 --- a/tools/gen-staff-token.sh +++ b/tools/gen-staff-token.sh @@ -18,7 +18,6 @@ fi faculty=$1 department=$2 -faculty_wide=$3 if [ -z "$faculty" ] || [ -z "$department" ]; then echo "Invalid input. Faculty and Department cannot be empty." @@ -34,7 +33,7 @@ SECRET_KEY="$JWT_SECRET_KEY" HEADER="{\"alg\":\"HS256\",\"typ\":\"JWT\"}" -PAYLOAD="{\"role\":\"staff\",\"faculty\":\"$faculty\",\"department\":\"$department\"}" +PAYLOAD="{\"role\":\"faculty-staff\",\"faculty\":\"$faculty\",\"department\":\"$department\"}" HEADER_ENCODED=$(echo -n "$HEADER" | base64 -w 0 | tr -d '=' | tr '/+' '_-') PAYLOAD_ENCODED=$(echo -n "$PAYLOAD" | base64 -w 0 | tr -d '=' | tr '/+' '_-')