diff --git a/.github/workflows/container_builds.yml b/.github/workflows/container_builds.yml index a1c23180..db1ac798 100644 --- a/.github/workflows/container_builds.yml +++ b/.github/workflows/container_builds.yml @@ -1,12 +1,8 @@ -name: Container ECR builds +name: Container ECR build + deploy on: push: branches: - main - paths: - - "frontend/**" - - "backend/**" - - "provider-middleware/*" permissions: id-token: write @@ -14,6 +10,7 @@ permissions: jobs: setup-env: + if: github.repository == 'UnlockedLabs/UnlockEdv2' || github.repository == 'PThorpe92/UnlockEdv2' runs-on: ubuntu-latest outputs: frontend_changes: ${{ steps.frontend_changes.outputs.changed }} @@ -43,15 +40,20 @@ jobs: echo "Middleware changes: ${{ steps.middleware_changes.outputs.changed }}" build-and-push: + if: github.repository == 'UnlockedLabs/UnlockEdv2' || github.repository == 'PThorpe92/UnlockEdv2' needs: setup-env runs-on: ubuntu-latest steps: + - name: Checkout code + uses: actions/checkout@v3 + - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@v4 with: - role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/GithubActionsRole + role-to-assume: ${{ secrets.AWS_IAM_ROLE }} aws-region: us-west-2 + mask-aws-account-id: true - name: Log in to Amazon ECR id: login-ecr @@ -63,17 +65,31 @@ jobs: - name: Build and push frontend image if: ${{ needs.setup-env.outputs.frontend_changes != '0' }} run: | - docker buildx build --platform linux/amd64 -t ${{ steps.login-ecr.outputs.registry }}/frontend:latest -f frontend/Dockerfile . - docker push ${{ steps.login-ecr.outputs.registry }}/frontend:latest + docker buildx build --platform linux/amd64 -t=${{ steps.login-ecr.outputs.registry }}/frontend:latest --push frontend/. - name: Build and push backend image if: ${{ needs.setup-env.outputs.backend_changes != '0' }} run: | - docker buildx build --platform linux/amd64 -t ${{ steps.login-ecr.outputs.registry }}/unlockedv2:latest -f backend/Dockerfile . - docker push ${{ steps.login-ecr.outputs.registry }}/unlockedv2:latest + docker buildx build --platform linux/amd64 -t=${{ steps.login-ecr.outputs.registry }}/unlockedv2:latest --push -f backend/Dockerfile . - name: Build and push middleware image if: ${{ needs.setup-env.outputs.middleware_changes != '0' }} run: | - docker buildx build --platform linux/amd64 -t ${{ steps.login-ecr.outputs.registry }}/provider_middleware:latest -f provider-middleware/Dockerfile . - docker push ${{ steps.login-ecr.outputs.registry }}/provider_middleware:latest + docker buildx build --platform linux/amd64 -t=${{ steps.login-ecr.outputs.registry }}/provider_middleware:latest --push -f provider-middleware/Dockerfile . + + deploy-to-staging: + if: github.repository == 'UnlockedLabs/UnlockEdv2' || github.repository == 'PThorpe92/UnlockEdv2' + + needs: build-and-push + runs-on: ubuntu-latest + steps: + - name: Set up kubectl + uses: azure/setup-kubectl@v1 + with: + version: v1.21.0 + + - name: Set up Kubeconfig + run: | + mkdir -p $HOME/.kube && touch $HOME/.kube/config + echo "${{ secrets.KUBECTL_CONFIG }}" | base64 -d > $HOME/.kube/config + kubectl rollout restart deployment diff --git a/.gitignore b/.gitignore index 1a9d69ab..093511b2 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ .github/workflows/test.yml /frontend/.husky/staged-files jar +/config/logs /logs/*.log /logs/*.json /frontend/public/build diff --git a/backend/src/database/users.go b/backend/src/database/users.go index c92d6822..3e152c35 100644 --- a/backend/src/database/users.go +++ b/backend/src/database/users.go @@ -68,7 +68,7 @@ func (db *DB) AssignTempPasswordToUser(id uint) (string, error) { if err != nil { return "", err } - if user.Role == "admin" { + if user.Role == models.Admin { return "", errors.New("user not found, or user is admin and cannot have password reset") } psw := user.CreateTempPassword() diff --git a/backend/src/handlers/actions.go b/backend/src/handlers/actions.go index 857aff75..11a3923d 100644 --- a/backend/src/handlers/actions.go +++ b/backend/src/handlers/actions.go @@ -139,11 +139,7 @@ func (srv *Server) HandleGetUsers(w http.ResponseWriter, r *http.Request) { Message: "Successfully fetched users from provider", Meta: models.NewPaginationInfo(page, perPage, int64(total)), } - if err := srv.WriteResponse(w, http.StatusOK, response); err != nil { - log.Error("Error writing response:" + err.Error()) - srv.ErrorResponse(w, http.StatusInternalServerError, err.Error()) - return - } + srv.WriteResponse(w, http.StatusOK, response) return } else { delete(cachedProviderUsers, uint(service.ProviderPlatformID)) @@ -159,12 +155,7 @@ func (srv *Server) HandleGetUsers(w http.ResponseWriter, r *http.Request) { total, responseUsers := paginateUsers(externalUsers, page, perPage) cachedProviderUsers[uint(service.ProviderPlatformID)] = CachedProviderUsers{Users: externalUsers, LastUpdated: time.Now()} response := models.PaginatedResource[models.ImportUser]{Data: responseUsers, Message: "Successfully fetched users from provider", Meta: models.NewPaginationInfo(page, perPage, int64(total))} - if err := srv.WriteResponse(w, http.StatusOK, response); err != nil { - log.Error("Error writing response:" + err.Error()) - srv.ErrorResponse(w, http.StatusInternalServerError, err.Error()) - delete(cachedProviderUsers, uint(service.ProviderPlatformID)) - return - } + srv.WriteResponse(w, http.StatusOK, response) } func (srv *Server) HandleImportPrograms(w http.ResponseWriter, r *http.Request) { @@ -180,11 +171,7 @@ func (srv *Server) HandleImportPrograms(w http.ResponseWriter, r *http.Request) srv.ErrorResponse(w, http.StatusInternalServerError, err.Error()) return } - if err := srv.WriteResponse(w, http.StatusOK, models.Resource[interface{}]{Message: "Successfully imported courses"}); err != nil { - log.Error("Error writing response:" + err.Error()) - srv.ErrorResponse(w, http.StatusInternalServerError, err.Error()) - return - } + srv.WriteResponse(w, http.StatusOK, models.Resource[interface{}]{Message: "Successfully imported courses"}) } func (srv *Server) HandleImportMilestones(w http.ResponseWriter, r *http.Request) { @@ -210,10 +197,7 @@ func (srv *Server) HandleImportMilestones(w http.ResponseWriter, r *http.Request } } } - if err := srv.WriteResponse(w, http.StatusOK, models.Resource[interface{}]{Message: "Successfully imported Milestones"}); err != nil { - log.Error("Failed to write response") - srv.ErrorResponse(w, http.StatusInternalServerError, "error writing response") - } + srv.WriteResponse(w, http.StatusOK, models.Resource[interface{}]{Message: "Successfully imported Milestones"}) } func (srv *Server) getProgramsAndMappingsForProvider(providerID uint) ([]models.Program, []models.ProviderUserMapping, error) { diff --git a/backend/src/handlers/activity_handler.go b/backend/src/handlers/activity_handler.go index bd04ae63..783650af 100644 --- a/backend/src/handlers/activity_handler.go +++ b/backend/src/handlers/activity_handler.go @@ -7,8 +7,6 @@ import ( "net/http" "strconv" "time" - - log "github.com/sirupsen/logrus" ) func (srv *Server) registerActivityRoutes() { @@ -38,19 +36,16 @@ func (srv *Server) HandleGetActivityByUserID(w http.ResponseWriter, r *http.Requ srv.ErrorResponse(w, http.StatusInternalServerError, "Failed to get activities") return } - if err = srv.WriteResponse(w, http.StatusOK, map[string]interface{}{ + srv.WriteResponse(w, http.StatusOK, map[string]interface{}{ "activities": activities, - }); err != nil { - srv.ErrorResponse(w, http.StatusInternalServerError, "Failed to write response") - log.Error("Failed to write response", err) - } + }) } /**** * @Query Params: * ?year=: year (default last year) ****/ - func (srv *Server) HandleGetDailyActivityByUserID(w http.ResponseWriter, r *http.Request) { +func (srv *Server) HandleGetDailyActivityByUserID(w http.ResponseWriter, r *http.Request) { // Parse userID from path userID, err := strconv.Atoi(r.PathValue("id")) if err != nil { @@ -79,15 +74,11 @@ func (srv *Server) HandleGetActivityByUserID(w http.ResponseWriter, r *http.Requ } // Write response - if err = srv.WriteResponse(w, http.StatusOK, map[string]interface{}{ + srv.WriteResponse(w, http.StatusOK, map[string]interface{}{ "activities": activities, - }); err != nil { - srv.ErrorResponse(w, http.StatusInternalServerError, "Failed to write response") - log.Error("Failed to write response", err) - } + }) } - func (srv *Server) HandleGetProgramActivity(w http.ResponseWriter, r *http.Request) { programID, err := strconv.Atoi(r.PathValue("id")) if err != nil { @@ -100,13 +91,10 @@ func (srv *Server) HandleGetProgramActivity(w http.ResponseWriter, r *http.Reque srv.ErrorResponse(w, http.StatusInternalServerError, "Failed to get activities") return } - if err = srv.WriteResponse(w, http.StatusOK, map[string]interface{}{ + srv.WriteResponse(w, http.StatusOK, map[string]interface{}{ "count": count, "activities": activities, - }); err != nil { - srv.ErrorResponse(w, http.StatusInternalServerError, "Failed to write response") - log.Error("Failed to write response", err) - } + }) } func (srv *Server) HandleCreateActivity(w http.ResponseWriter, r *http.Request) { @@ -119,8 +107,5 @@ func (srv *Server) HandleCreateActivity(w http.ResponseWriter, r *http.Request) srv.ErrorResponse(w, http.StatusInternalServerError, "Failed to create activity") return } - if err := srv.WriteResponse(w, http.StatusOK, activity); err != nil { - srv.ErrorResponse(w, http.StatusInternalServerError, "Failed to write response") - log.Error("Failed to write response", err) - } + srv.WriteResponse(w, http.StatusOK, activity) } diff --git a/backend/src/handlers/auth.go b/backend/src/handlers/auth.go index ee441cb5..087fa373 100644 --- a/backend/src/handlers/auth.go +++ b/backend/src/handlers/auth.go @@ -167,11 +167,7 @@ func (srv *Server) handleCheckAuth(w http.ResponseWriter, r *http.Request) { if orySession, ok := orySessions[user.ID]; ok { // check the cached session hash + expiration if orySession.Active && orySession.TokenHash == cookieHash && orySession.Expires.After(time.Now()) { - if err := srv.WriteResponse(w, http.StatusOK, user); err != nil { - srv.ErrorResponse(w, http.StatusInternalServerError, err.Error()) - log.Error("Error writing response: " + err.Error()) - return - } + srv.WriteResponse(w, http.StatusOK, user) log.WithFields(fields).Info("checked cached session active for user") return } @@ -182,11 +178,7 @@ func (srv *Server) handleCheckAuth(w http.ResponseWriter, r *http.Request) { srv.ErrorResponse(w, http.StatusUnauthorized, "ory session was not valid, please login again") return } - if err := srv.WriteResponse(w, http.StatusOK, user); err != nil { - log.Error("Error writing response: " + err.Error()) - srv.ErrorResponse(w, http.StatusUnauthorized, "unauthorized, ory session not found") - return - } + srv.WriteResponse(w, http.StatusOK, user) return } http.Error(w, "Unauthorized", http.StatusUnauthorized) @@ -298,11 +290,7 @@ func (srv *Server) handleResetPassword(w http.ResponseWriter, r *http.Request) { return } } - if err = srv.WriteResponse(w, http.StatusOK, "Password reset successfully"); err != nil { - srv.ErrorResponse(w, http.StatusInternalServerError, err.Error()) - log.Error("Error writing response: " + err.Error()) - return - } + srv.WriteResponse(w, http.StatusOK, "Password reset successfully") } func validatePassword(pass string) bool { diff --git a/backend/src/handlers/dashboard.go b/backend/src/handlers/dashboard.go index d9ab3a29..d25b78af 100644 --- a/backend/src/handlers/dashboard.go +++ b/backend/src/handlers/dashboard.go @@ -15,38 +15,35 @@ func (srv *Server) registerDashboardRoutes() { } func (srv *Server) HandleStudentDashboard(w http.ResponseWriter, r *http.Request) { + fields := log.Fields{"handler": "HandleStudentDashboard"} userId, err := strconv.Atoi(r.PathValue("id")) if err != nil { - log.Errorf("Error parsing user ID: %v", err) + fields["error"] = err.Error() + log.WithFields(fields).Errorf("Error parsing user ID: %v", err) srv.ErrorResponse(w, http.StatusBadRequest, err.Error()) return } studentDashboard, err := srv.Db.GetStudentDashboardInfo(userId) if err != nil { - log.Errorf("Error getting user dashboard info: %v", err) - srv.ErrorResponse(w, http.StatusInternalServerError, err.Error()) - return - } - if err := srv.WriteResponse(w, http.StatusOK, studentDashboard); err != nil { - log.Errorf("user dashboard endpoint: error writing response: %v", err) + fields["error"] = err.Error() + log.WithFields(fields).Errorf("Error getting user dashboard info: %v", err) srv.ErrorResponse(w, http.StatusInternalServerError, err.Error()) return } + srv.WriteResponse(w, http.StatusOK, studentDashboard) } func (srv *Server) HandleAdminDashboard(w http.ResponseWriter, r *http.Request) { + fields := log.Fields{"handler": "HandleAdminDashboard"} claims := r.Context().Value(ClaimsKey).(*Claims) adminDashboard, err := srv.Db.GetAdminDashboardInfo(claims.FacilityID) if err != nil { - log.Errorf("Error getting user dashboard info: %v", err) - srv.ErrorResponse(w, http.StatusInternalServerError, err.Error()) - return - } - if err := srv.WriteResponse(w, http.StatusOK, adminDashboard); err != nil { - log.Errorf("user dashboard endpoint: error writing response: %v", err) + fields["error"] = err.Error() + log.WithFields(fields).Errorf("Error getting user dashboard info: %v", err) srv.ErrorResponse(w, http.StatusInternalServerError, err.Error()) return } + srv.WriteResponse(w, http.StatusOK, adminDashboard) } /** @@ -70,11 +67,7 @@ func (srv *Server) HandleUserCatalogue(w http.ResponseWriter, r *http.Request) { srv.ErrorResponse(w, http.StatusInternalServerError, err.Error()) return } - if err := srv.WriteResponse(w, http.StatusOK, userCatalogue); err != nil { - log.Errorf("user catalogue endpoint: error writing response: %v", err) - srv.ErrorResponse(w, http.StatusInternalServerError, err.Error()) - return - } + srv.WriteResponse(w, http.StatusOK, userCatalogue) } func (srv *Server) HandleUserPrograms(w http.ResponseWriter, r *http.Request) { @@ -100,9 +93,5 @@ func (srv *Server) HandleUserPrograms(w http.ResponseWriter, r *http.Request) { "num_completed": numCompleted, "total_time": totalTime, } - if err := srv.WriteResponse(w, http.StatusOK, response); err != nil { - log.Errorf("user programs endpoint: error writing response: %v", err) - srv.ErrorResponse(w, http.StatusInternalServerError, err.Error()) - return - } + srv.WriteResponse(w, http.StatusOK, response) } diff --git a/backend/src/handlers/facilities_handler.go b/backend/src/handlers/facilities_handler.go index ecd21e77..e84adddf 100644 --- a/backend/src/handlers/facilities_handler.go +++ b/backend/src/handlers/facilities_handler.go @@ -31,10 +31,7 @@ func (srv *Server) HandleIndexFacilities(w http.ResponseWriter, r *http.Request) Message: "facilities fetched successfully", Data: facilities, } - if err = srv.WriteResponse(w, http.StatusOK, response); err != nil { - log.WithFields(fields).Error("error writing response") - http.Error(w, err.Error(), http.StatusInternalServerError) - } + srv.WriteResponse(w, http.StatusOK, response) } func (srv *Server) HandleShowFacility(w http.ResponseWriter, r *http.Request) { @@ -56,10 +53,7 @@ func (srv *Server) HandleShowFacility(w http.ResponseWriter, r *http.Request) { Data: make([]models.Facility, 0), } response.Data = append(response.Data, *facility) - if err = srv.WriteResponse(w, http.StatusOK, response); err != nil { - log.WithFields(fields).Error("error writing response") - http.Error(w, err.Error(), http.StatusInternalServerError) - } + srv.WriteResponse(w, http.StatusOK, response) } func (srv *Server) HandleCreateFacility(w http.ResponseWriter, r *http.Request) { @@ -84,11 +78,7 @@ func (srv *Server) HandleCreateFacility(w http.ResponseWriter, r *http.Request) Data: []models.Facility{*newFacility}, Message: "Facility created successfully", } - if err = srv.WriteResponse(w, http.StatusOK, response); err != nil { - fields["error"] = err - log.Error("Error writing response") - srv.ErrorResponse(w, http.StatusInternalServerError, err.Error()) - } + srv.WriteResponse(w, http.StatusOK, response) } func (srv *Server) HandleUpdateFacility(w http.ResponseWriter, r *http.Request) { @@ -121,11 +111,7 @@ func (srv *Server) HandleUpdateFacility(w http.ResponseWriter, r *http.Request) Data: []models.Facility{*toReturn}, Message: "facility successfully updated", } - if err = srv.WriteResponse(w, http.StatusOK, response); err != nil { - fields["error"] = err - log.WithFields(fields).Error("error writing response") - http.Error(w, err.Error(), http.StatusInternalServerError) - } + srv.WriteResponse(w, http.StatusOK, response) } func (srv *Server) HandleDeleteFacility(w http.ResponseWriter, r *http.Request) { diff --git a/backend/src/handlers/left_menu_handler.go b/backend/src/handlers/left_menu_handler.go index a5a0a626..125694a9 100644 --- a/backend/src/handlers/left_menu_handler.go +++ b/backend/src/handlers/left_menu_handler.go @@ -24,11 +24,7 @@ func (srv *Server) handleGetLeftMenu(w http.ResponseWriter, r *http.Request) { response := models.Resource[models.LeftMenuLink]{ Data: links, } - if err = srv.WriteResponse(w, http.StatusOK, response); err != nil { - log.Error("Error writing response: " + err.Error()) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } + srv.WriteResponse(w, http.StatusOK, response) } func (srv *Server) handlePostLeftMenuLinks(w http.ResponseWriter, r *http.Request) { diff --git a/backend/src/handlers/login_flow.go b/backend/src/handlers/login_flow.go index 12371e31..65265c57 100644 --- a/backend/src/handlers/login_flow.go +++ b/backend/src/handlers/login_flow.go @@ -65,10 +65,8 @@ func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) { Secure: true, Path: "/", }) - if err = s.WriteResponse(w, http.StatusOK, user); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } + s.WriteResponse(w, http.StatusOK, user) + return } else { http.Error(w, "Invalid username or password", http.StatusUnauthorized) return diff --git a/backend/src/handlers/milestones_handler.go b/backend/src/handlers/milestones_handler.go index 4f4b8775..9f2740e2 100644 --- a/backend/src/handlers/milestones_handler.go +++ b/backend/src/handlers/milestones_handler.go @@ -51,10 +51,7 @@ func (srv *Server) HandleIndexMilestones(w http.ResponseWriter, r *http.Request) Meta: paginationData, Data: milestones, } - if err = srv.WriteResponse(w, http.StatusOK, response); err != nil { - log.Error("Error writing response: " + err.Error()) - srv.ErrorResponse(w, http.StatusBadRequest, err.Error()) - } + srv.WriteResponse(w, http.StatusOK, response) } func (srv *Server) HandleCreateMilestone(w http.ResponseWriter, r *http.Request) { @@ -68,11 +65,7 @@ func (srv *Server) HandleCreateMilestone(w http.ResponseWriter, r *http.Request) srv.ErrorResponse(w, http.StatusInternalServerError, err.Error()) return } - if err := srv.WriteResponse(w, http.StatusCreated, miles); err != nil { - log.Error("Error writing response: " + err.Error()) - srv.ErrorResponse(w, http.StatusBadRequest, err.Error()) - return - } + srv.WriteResponse(w, http.StatusCreated, miles) } func (srv *Server) HandleDeleteMilestone(w http.ResponseWriter, r *http.Request) { @@ -117,9 +110,5 @@ func (srv *Server) HandleUpdateMilestone(w http.ResponseWriter, r *http.Request) srv.ErrorResponse(w, http.StatusInternalServerError, err.Error()) return } - if err := srv.WriteResponse(w, http.StatusOK, toUpdate); err != nil { - log.Error("Error writing response: " + err.Error()) - srv.ErrorResponse(w, http.StatusBadRequest, err.Error()) - return - } + srv.WriteResponse(w, http.StatusOK, toUpdate) } diff --git a/backend/src/handlers/oidc.go b/backend/src/handlers/oidc.go index b3b860fc..beea25ce 100644 --- a/backend/src/handlers/oidc.go +++ b/backend/src/handlers/oidc.go @@ -24,10 +24,7 @@ func (srv *Server) HandleGetAllClients(w http.ResponseWriter, r *http.Request) { Data: clients, Message: "Successfully fetched all registered clients", } - if err := srv.WriteResponse(w, http.StatusOK, response); err != nil { - log.Error(r, err) - srv.ErrorResponse(w, http.StatusInternalServerError, err.Error()) - } + srv.WriteResponse(w, http.StatusOK, response) } type RegisterClientRequest struct { @@ -86,8 +83,5 @@ func (srv *Server) HandleRegisterClient(w http.ResponseWriter, r *http.Request) } else { response.Message = "Client successfully created" } - if err := srv.WriteResponse(w, http.StatusCreated, response); err != nil { - log.Error(r, err) - srv.ErrorResponse(w, http.StatusInternalServerError, err.Error()) - } + srv.WriteResponse(w, http.StatusCreated, response) } diff --git a/backend/src/handlers/ory.go b/backend/src/handlers/ory.go index c622c520..76dc30d4 100644 --- a/backend/src/handlers/ory.go +++ b/backend/src/handlers/ory.go @@ -20,9 +20,7 @@ func (srv *Server) handleDeleteAllKratosIdentities(w http.ResponseWriter, r *htt if err := srv.deleteAllKratosIdentities(); err != nil { http.Error(w, "error communicating with Ory Kratos", http.StatusInternalServerError) } - if err := srv.WriteResponse(w, http.StatusOK, "identities deleted successfully"); err != nil { - http.Error(w, "error writing response", http.StatusInternalServerError) - } + srv.WriteResponse(w, http.StatusOK, "identities deleted successfully") } func (srv *Server) deleteAllKratosIdentities() error { @@ -51,11 +49,11 @@ func (srv *Server) deleteAllKratosIdentities() error { func (srv *Server) deleteIdentityInKratos(kratosId *string) error { resp, err := srv.OryClient.IdentityAPI.DeleteIdentity(context.Background(), *kratosId).Execute() if err != nil { - log.WithFields(log.Fields{"identity": kratosId}).Errorln("unable to delete identity from Ory Kratos") + log.WithField("identity", kratosId).Errorln("unable to delete identity from Ory Kratos") return err } if resp.StatusCode != 204 { - log.WithFields(log.Fields{"identity": kratosId}).Errorln("unable to delete identity from Ory Kratos") + log.WithField("identity", kratosId).Errorln("unable to delete identity from Ory Kratos") return errors.New("unable to delete identity from Ory isntance") } return nil diff --git a/backend/src/handlers/outcomes_handler.go b/backend/src/handlers/outcomes_handler.go index 0e4e9fb8..ea44de90 100644 --- a/backend/src/handlers/outcomes_handler.go +++ b/backend/src/handlers/outcomes_handler.go @@ -41,10 +41,7 @@ func (srv *Server) HandleGetOutcomes(w http.ResponseWriter, r *http.Request) { response := models.PaginatedResource[models.Outcome]{} response.Meta = models.NewPaginationInfo(page, perPage, total) response.Data = outcome - if err = srv.WriteResponse(w, http.StatusOK, response); err != nil { - log.Error("handler: getOutcomes: ", err.Error()) - http.Error(w, err.Error(), http.StatusInternalServerError) - } + srv.WriteResponse(w, http.StatusOK, response) } func (srv *Server) HandleCreateOutcome(w http.ResponseWriter, r *http.Request) { @@ -102,10 +99,7 @@ func (srv *Server) HandleUpdateOutcome(w http.ResponseWriter, r *http.Request) { } response := models.Resource[models.Outcome]{} response.Data = append(response.Data, *updatedOutcome) - if err := srv.WriteResponse(w, http.StatusOK, response); err != nil { - log.Error("handler: updateOutcome: ", err.Error()) - http.Error(w, err.Error(), http.StatusInternalServerError) - } + srv.WriteResponse(w, http.StatusOK, response) } func (srv *Server) HandleDeleteOutcome(w http.ResponseWriter, r *http.Request) { diff --git a/backend/src/handlers/programs_handler.go b/backend/src/handlers/programs_handler.go index 8710c5c0..0de955c0 100644 --- a/backend/src/handlers/programs_handler.go +++ b/backend/src/handlers/programs_handler.go @@ -46,11 +46,7 @@ func (srv *Server) HandleIndexPrograms(w http.ResponseWriter, r *http.Request) { Meta: paginationData, Data: programs, } - if err = srv.WriteResponse(w, http.StatusOK, response); err != nil { - log.Error("Error writing response: " + err.Error()) - srv.ErrorResponse(w, http.StatusBadRequest, err.Error()) - return - } + srv.WriteResponse(w, http.StatusOK, response) } func (srv *Server) HandleShowProgram(w http.ResponseWriter, r *http.Request) { @@ -66,11 +62,7 @@ func (srv *Server) HandleShowProgram(w http.ResponseWriter, r *http.Request) { srv.ErrorResponse(w, http.StatusBadRequest, err.Error()) return } - if err = srv.WriteResponse(w, http.StatusOK, program); err != nil { - log.Error("Error writing response: " + err.Error()) - srv.ErrorResponse(w, http.StatusBadRequest, err.Error()) - return - } + srv.WriteResponse(w, http.StatusOK, program) } func (srv *Server) HandleCreateProgram(w http.ResponseWriter, r *http.Request) { @@ -116,11 +108,7 @@ func (srv *Server) HandleUpdateProgram(w http.ResponseWriter, r *http.Request) { srv.ErrorResponse(w, http.StatusBadRequest, err.Error()) return } - if err := srv.WriteResponse(w, http.StatusOK, updated); err != nil { - log.Error("Error writing response: " + err.Error()) - srv.ErrorResponse(w, http.StatusBadRequest, err.Error()) - return - } + srv.WriteResponse(w, http.StatusOK, updated) } func (srv *Server) HandleDeleteProgram(w http.ResponseWriter, r *http.Request) { @@ -167,15 +155,8 @@ func (srv *Server) HandleFavoriteProgram(w http.ResponseWriter, r *http.Request) srv.ErrorResponse(w, http.StatusBadRequest, err.Error()) return } - if err := srv.WriteResponse(w, http.StatusNoContent, nil); err != nil { - log.Error("Error writing response: " + err.Error()) - srv.ErrorResponse(w, http.StatusBadRequest, err.Error()) - return - } - } - if err := srv.WriteResponse(w, http.StatusOK, nil); err != nil { - log.Error("Error writing response: " + err.Error()) - srv.ErrorResponse(w, http.StatusBadRequest, err.Error()) + srv.WriteResponse(w, http.StatusNoContent, nil) return } + srv.WriteResponse(w, http.StatusOK, nil) } diff --git a/backend/src/handlers/provider_platform_handler.go b/backend/src/handlers/provider_platform_handler.go index 935fe5a2..22edb0df 100644 --- a/backend/src/handlers/provider_platform_handler.go +++ b/backend/src/handlers/provider_platform_handler.go @@ -39,9 +39,7 @@ func (srv *Server) HandleIndexProviders(w http.ResponseWriter, r *http.Request) response.Data = platforms } log.Info("Found "+strconv.Itoa(int(total)), " provider platforms") - if err = srv.WriteResponse(w, http.StatusOK, response); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } + srv.WriteResponse(w, http.StatusOK, response) } func (srv *Server) HandleShowProvider(w http.ResponseWriter, r *http.Request) { @@ -60,9 +58,7 @@ func (srv *Server) HandleShowProvider(w http.ResponseWriter, r *http.Request) { Data: []models.ProviderPlatform{*platform}, Message: "Provider platform found", } - if err = srv.WriteResponse(w, http.StatusOK, response); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } + srv.WriteResponse(w, http.StatusOK, response) } func (srv *Server) HandleCreateProvider(w http.ResponseWriter, r *http.Request) { @@ -85,10 +81,7 @@ func (srv *Server) HandleCreateProvider(w http.ResponseWriter, r *http.Request) Message: "Provider platform created successfully", } response.Data = append(response.Data, *newProv) - if err = srv.WriteResponse(w, http.StatusOK, response); err != nil { - log.Error("Error writing response: ", err.Error()) - srv.ErrorResponse(w, http.StatusInternalServerError, err.Error()) - } + srv.WriteResponse(w, http.StatusOK, response) } func (srv *Server) HandleUpdateProvider(w http.ResponseWriter, r *http.Request) { @@ -116,9 +109,7 @@ func (srv *Server) HandleUpdateProvider(w http.ResponseWriter, r *http.Request) Data: make([]models.ProviderPlatform, 0), } response.Data = append(response.Data, *updated) - if err = srv.WriteResponse(w, http.StatusOK, response); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } + srv.WriteResponse(w, http.StatusOK, response) } func (srv *Server) HandleDeleteProvider(w http.ResponseWriter, r *http.Request) { diff --git a/backend/src/handlers/provider_user_management.go b/backend/src/handlers/provider_user_management.go index 77d67e49..37a86229 100644 --- a/backend/src/handlers/provider_user_management.go +++ b/backend/src/handlers/provider_user_management.go @@ -67,11 +67,7 @@ func (srv *Server) HandleMapProviderUser(w http.ResponseWriter, r *http.Request) return } } - if err = srv.WriteResponse(w, http.StatusCreated, mapping); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - log.Errorln("Error writing response for map-provider-user") - return - } + srv.WriteResponse(w, http.StatusCreated, mapping) } type ImportUserResponse struct { @@ -162,10 +158,7 @@ func (srv *Server) HandleImportProviderUsers(w http.ResponseWriter, r *http.Requ response.Data = append(response.Data, userResponse) } response.Message = "Provider users imported, please check for any accounts that couldn't be created" - if err := srv.WriteResponse(w, http.StatusOK, response); err != nil { - log.Errorln("Error writing response for import-provider-users") - srv.ErrorResponse(w, http.StatusInternalServerError, err.Error()) - } + srv.WriteResponse(w, http.StatusOK, response) } func (srv *Server) registerProviderLogin(provider *models.ProviderPlatform, user *models.User) error { @@ -213,10 +206,7 @@ func (srv *Server) handleCreateProviderUserAccount(w http.ResponseWriter, r *htt srv.ErrorResponse(w, http.StatusInternalServerError, err.Error()) return } - if err := srv.WriteResponse(w, http.StatusCreated, nil); err != nil { - srv.ErrorResponse(w, http.StatusInternalServerError, err.Error()) - return - } + srv.WriteResponse(w, http.StatusCreated, nil) } func (srv *Server) createAndRegisterProviderUserAccount(provider *models.ProviderPlatform, user *models.User) error { diff --git a/backend/src/handlers/provider_user_mapping_handler.go b/backend/src/handlers/provider_user_mapping_handler.go index 94ecf801..54c2af89 100644 --- a/backend/src/handlers/provider_user_mapping_handler.go +++ b/backend/src/handlers/provider_user_mapping_handler.go @@ -24,10 +24,7 @@ func (srv *Server) handleGetMappingsForUser(w http.ResponseWriter, r *http.Reque if err != nil { srv.ErrorResponse(w, http.StatusInternalServerError, err.Error()) } - if err = srv.WriteResponse(w, http.StatusOK, mappings); err != nil { - srv.ErrorResponse(w, http.StatusInternalServerError, err.Error()) - return - } + srv.WriteResponse(w, http.StatusOK, mappings) } func (srv *Server) handleCreateProviderUserMapping(w http.ResponseWriter, r *http.Request) { @@ -43,10 +40,7 @@ func (srv *Server) handleCreateProviderUserMapping(w http.ResponseWriter, r *htt srv.ErrorResponse(w, http.StatusInternalServerError, err.Error()) return } - if err = srv.WriteResponse(w, http.StatusCreated, mapping); err != nil { - srv.ErrorResponse(w, http.StatusInternalServerError, err.Error()) - return - } + srv.WriteResponse(w, http.StatusCreated, mapping) } func (srv *Server) handleDeleteProviderUserMapping(w http.ResponseWriter, r *http.Request) { @@ -64,8 +58,5 @@ func (srv *Server) handleDeleteProviderUserMapping(w http.ResponseWriter, r *htt srv.ErrorResponse(w, http.StatusInternalServerError, err.Error()) return } - if err = srv.WriteResponse(w, http.StatusNoContent, nil); err != nil { - srv.ErrorResponse(w, http.StatusInternalServerError, err.Error()) - return - } + srv.WriteResponse(w, http.StatusNoContent, nil) } diff --git a/backend/src/handlers/server.go b/backend/src/handlers/server.go index 8558154a..b60f2ee2 100644 --- a/backend/src/handlers/server.go +++ b/backend/src/handlers/server.go @@ -238,17 +238,21 @@ func (srv *Server) GetPaginationInfo(r *http.Request) (int, int) { return intPage, intPerPage } -func (srv *Server) WriteResponse(w http.ResponseWriter, status int, data interface{}) error { +func (srv *Server) WriteResponse(w http.ResponseWriter, status int, data interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) if data == nil { - return nil + return } if resp, ok := data.(string); ok { _, err := w.Write([]byte(resp)) - return err + log.Error("error writing response: ", err) + return + } + if err := json.NewEncoder(w).Encode(data); err != nil { + log.Error("Error writing json response", err) + srv.ErrorResponse(w, http.StatusInternalServerError, "error writing response") } - return json.NewEncoder(w).Encode(data) } func (srv *Server) ErrorResponse(w http.ResponseWriter, status int, message string) { diff --git a/backend/src/handlers/upload_handler.go b/backend/src/handlers/upload_handler.go index af2e6bfc..9ad9d2cd 100644 --- a/backend/src/handlers/upload_handler.go +++ b/backend/src/handlers/upload_handler.go @@ -52,11 +52,7 @@ func (srv *Server) handleUploadHandler(w http.ResponseWriter, r *http.Request) { srv.ErrorResponse(w, http.StatusInternalServerError, err.Error()) return } - if err := srv.WriteResponse(w, http.StatusOK, response); err != nil { - log.Error("Error writing response: " + err.Error()) - srv.ErrorResponse(w, http.StatusInternalServerError, err.Error()) - return - } + srv.WriteResponse(w, http.StatusOK, response) } func (srv *Server) handleHostPhotos(w http.ResponseWriter, r *http.Request) { diff --git a/backend/src/handlers/user_activity_handler.go b/backend/src/handlers/user_activity_handler.go index 87597210..3d9d760c 100644 --- a/backend/src/handlers/user_activity_handler.go +++ b/backend/src/handlers/user_activity_handler.go @@ -93,9 +93,7 @@ func (srv *Server) handleGetAllUserActivities(w http.ResponseWriter, r *http.Req Meta: pagination, Data: activities, } - if err := srv.WriteResponse(w, http.StatusOK, response); err != nil { - srv.ErrorResponse(w, http.StatusInternalServerError, string(err.Error())) - } + srv.WriteResponse(w, http.StatusOK, response) } func (srv *Server) handleGetUserActivityByID(w http.ResponseWriter, r *http.Request) { @@ -120,8 +118,5 @@ func (srv *Server) handleGetUserActivityByID(w http.ResponseWriter, r *http.Requ Meta: pagination, Data: activity, } - if err := srv.WriteResponse(w, http.StatusOK, response); err != nil { - log.Debug("Error writing response: ", err) - srv.ErrorResponse(w, http.StatusInternalServerError, err.Error()) - } + srv.WriteResponse(w, http.StatusOK, response) } diff --git a/backend/src/handlers/user_handler.go b/backend/src/handlers/user_handler.go index 3a924719..69e51459 100644 --- a/backend/src/handlers/user_handler.go +++ b/backend/src/handlers/user_handler.go @@ -55,10 +55,7 @@ func (srv *Server) HandleIndexUsers(w http.ResponseWriter, r *http.Request) { Data: users, Meta: paginationData, } - if err := srv.WriteResponse(w, http.StatusOK, response); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } + srv.WriteResponse(w, http.StatusOK, response) } func (srv *Server) HandleGetUnmappedUsers(w http.ResponseWriter, r *http.Request, providerId string) { @@ -84,11 +81,7 @@ func (srv *Server) HandleGetUnmappedUsers(w http.ResponseWriter, r *http.Request Meta: paginationData, Message: "unmapped users returned successfully", } - if err := srv.WriteResponse(w, http.StatusOK, response); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - log.Errorln("Error writing response: ", err) - return - } + srv.WriteResponse(w, http.StatusOK, response) } func (srv *Server) HandleGetUsersWithLogins(w http.ResponseWriter, r *http.Request) { @@ -108,10 +101,7 @@ func (srv *Server) HandleGetUsersWithLogins(w http.ResponseWriter, r *http.Reque Total: total, } response := models.PaginatedResource[database.UserWithLogins]{Data: users, Meta: paginationData, Message: "users with mappings returned successfully"} - if err := srv.WriteResponse(w, http.StatusOK, response); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } + srv.WriteResponse(w, http.StatusOK, response) } /** @@ -136,9 +126,7 @@ func (srv *Server) HandleShowUser(w http.ResponseWriter, r *http.Request) { return } response.Data = append(response.Data, *user) - if err := srv.WriteResponse(w, http.StatusOK, response); err != nil { - srv.ErrorResponse(w, http.StatusInternalServerError, "error writing response") - } + srv.WriteResponse(w, http.StatusOK, response) } type NewUserResponse struct { @@ -192,9 +180,7 @@ func (srv *Server) HandleCreateUser(w http.ResponseWriter, r *http.Request) { log.Printf("Error creating user in kratos: %v", err) } } - if err := srv.WriteResponse(w, http.StatusCreated, response); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } + srv.WriteResponse(w, http.StatusCreated, response) } /** @@ -251,9 +237,7 @@ func (srv *Server) HandleUpdateUser(w http.ResponseWriter, r *http.Request) { } response := models.Resource[models.User]{} response.Data = append(response.Data, *updatedUser) - if err := srv.WriteResponse(w, http.StatusOK, response); err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } + srv.WriteResponse(w, http.StatusOK, response) } type TempPasswordRequest struct { @@ -261,9 +245,11 @@ type TempPasswordRequest struct { } func (srv *Server) HandleResetStudentPassword(w http.ResponseWriter, r *http.Request) { + fields := log.Fields{"handler": "HandleResetStudentPassword"} temp := TempPasswordRequest{} if err := json.NewDecoder(r.Body).Decode(&temp); err != nil { - log.Error("Parsing form failed, using JSON", err.Error()) + fields["error"] = err.Error() + log.WithFields(fields).Error("Parsing form failed, using JSON", err.Error()) srv.ErrorResponse(w, http.StatusBadRequest, err.Error()) return } @@ -272,38 +258,35 @@ func (srv *Server) HandleResetStudentPassword(w http.ResponseWriter, r *http.Req newPass, err := srv.Db.AssignTempPasswordToUser(uint(temp.UserID)) if err != nil { response["message"] = err.Error() - err = srv.WriteResponse(w, http.StatusInternalServerError, response) - if err != nil { - log.Error("Error writing response: ", err.Error()) - srv.ErrorResponse(w, http.StatusInternalServerError, err.Error()) - } + fields["error"] = err.Error() + log.WithFields(fields).Errorln("error assigning password to user") + srv.WriteResponse(w, http.StatusInternalServerError, response) return } response["temp_password"] = newPass response["message"] = "Temporary password assigned" user, err := srv.Db.GetUserByID(uint(temp.UserID)) if err != nil { - log.Errorf("Exising user not found, this should never happen: %v", temp.UserID) + fields["error"] = err.Error() + log.WithFields(fields).Errorf("Exising user not found, this should never happen: %v", temp.UserID) http.Error(w, "internal server error: existing user not found", http.StatusInternalServerError) return } if user.KratosID == "" { err := srv.handleCreateUserKratos(user.Username, newPass) if err != nil { - log.Errorf("Error creating user in kratos: %v", err) + fields["error"] = err.Error() + log.WithFields(fields).Errorf("Error creating user in kratos: %v", err) http.Error(w, "internal server error: error creating user in kratos", http.StatusInternalServerError) return } } else { if err := srv.handleUpdatePasswordKratos(user, newPass); err != nil { - log.Error("Error updating password for new kratos user") + fields["error"] = err.Error() + log.WithFields(fields).Error("Error updating password for new kratos user") srv.ErrorResponse(w, http.StatusInternalServerError, err.Error()) return } } - if err := srv.WriteResponse(w, http.StatusOK, response); err != nil { - log.Error("Error writing response: ", err.Error()) - srv.ErrorResponse(w, http.StatusInternalServerError, err.Error()) - return - } + srv.WriteResponse(w, http.StatusOK, response) } diff --git a/backend/src/service.go b/backend/src/service.go index a9a7ed68..11dceb4e 100644 --- a/backend/src/service.go +++ b/backend/src/service.go @@ -55,15 +55,6 @@ func GetProviderService(prov *models.ProviderPlatform) (*ProviderService, error) Timeout: time.Second * 20, }, } - // send initial test request with the provider ID, to see if the service exists - test := "/" - request := newService.Request(test) - resp, err := newService.Client.Do(request) - if err != nil || resp.StatusCode != http.StatusOK { - // send the required information to the middleware to satisfy the request - log.WithFields(log.Fields{"error": err, "status": resp.StatusCode}).Error("Error creating provider service") - return nil, err - } return &newService, nil } diff --git a/build b/build index c7799f47..2e162c6e 100755 --- a/build +++ b/build @@ -3,15 +3,15 @@ help="Usage: ./build prod | dev [-f] | migrate-fresh | seed" if [[ "$1" == "prod" ]]; then - docker compose -f docker-compose.yml -f config/docker-compose.prod.yml up --build + docker compose -f docker-compose.yml -f config/docker-compose.prod.yml up --build --force-recreate exit 0 fi if [[ "$1" == "dev" ]]; then if [[ "$2" == "-f" ]]; then - docker compose -f docker-compose.yml -f config/docker-compose.fe-dev.yml up --build + docker compose -f docker-compose.yml -f config/docker-compose.fe-dev.yml up --build --force-recreate else - docker compose up --build + docker compose up --build --force-recreate fi fi diff --git a/docker-compose.yml b/docker-compose.yml index 6f853dac..4fb8413a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -9,10 +9,11 @@ services: - "5432:5432" volumes: - postgres_data:/var/lib/postgresql/data - - logs:/var/log/postgresql + - logs:/docker-entrypoint-initdb.d/logs - config:/docker-entrypoint-initdb.d - command: > - postgres -c 'logging_collector=on' -c 'log_destination=jsonlog' -c 'log_directory=/var/log/postgresql' -c log_line_prefix='%t [%u]' -c 'log_connections=on' -c 'log_file_mode=0644' -c 'log_filename=postgres-%Y-%m-%d.log' + command: + > # this feels kinda hacky: we have to log to a sub directory in a directory owned by the user :1000 so postgres doesn't complain + postgres -c 'logging_collector=on' -c 'log_destination=jsonlog' -c 'log_directory=/docker-entrypoint-initdb.d/logs' -c log_line_prefix='%t [%u]' -c 'log_connections=on' -c 'log_file_mode=0644' -c 'log_filename=postgres-%Y-%m-%d.log' healthcheck: test: ["CMD-SHELL", "pg_isready -U unlocked"] interval: 10s diff --git a/frontend/Dockerfile b/frontend/Dockerfile index e2821193..d4736a84 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,9 +1,7 @@ FROM node:21-alpine3.19 as builder WORKDIR /app/ COPY . . -RUN npm install yarn -RUN yarn install -RUN yarn run build +RUN npm install && npm run build FROM nginx:1.21.3-alpine COPY --from=builder /app/dist /usr/share/nginx/html diff --git a/provider-middleware/README.md b/provider-middleware/README.md index 002c73f0..7defb8e3 100644 --- a/provider-middleware/README.md +++ b/provider-middleware/README.md @@ -1,8 +1,8 @@ ## Provider Platform Middleware This middleware is intended to run as a service in the same cluster, connected to the same database as an instance of UnlockEdv2. -It will handy the resource intensive business of fetching, parsing, deduping data from platform integrations, and eventually will run -scheduled jobs for these tasks which it will pull off a queue. +It will handy the resource intensive business of fetching, parsing, de-duping data from platform integrations, and eventually will run +scheduled jobs for these tasks which it will pull off a queue. (Currently looking at using NATS for caching and queue) ### **API** @@ -51,9 +51,9 @@ type ProviderServiceInterface interface { // and then define a concrete implementation of the interface in the `{provider_name}.go` file. ``` -typically they will need the database connection along with any relevant information needed to make the calls. -The concrete implementation of each ProviderServiceInterface should always store the info needed to communicate with the Provider, -and have helper methods that can be called to make requests, convert data types, etc. +Typically they will need the database connection along with any relevant information needed to make the calls. +The concrete implementation of each `ProviderServiceInterface` should always store the info needed to communicate with the Provider, (headers, cookies, login/session info, etc) +and have helper methods that can be called to make requests, canonicalize data types, etc. When the provider middleware gets a request, it looks up the provider id in the database, and depending on the Type of provider, it instantiates a new instance of the concrete type and returns it so the methods defined in the interface can be called. @@ -86,8 +86,6 @@ func (sh *ServiceHandler) initService(r *http.Request) (ProviderServiceInterface } ``` -TODOS: +**TODO:** -Currently there is an in-memory cache on the backend for the `GetUsers` method, because there are so many that are returned. We will need a more efficient and production ready solution to this, most likely the same -service that will host the job queue, can also contain a cache. We also will want to save the dates of the last requests made for things like `Activities`, and find a way to limit the amount of responses returned so -we can process the data more efficiently. +Currently there is an in-memory cache on the backend for the `GetUsers` method, because there are so many that are returned. We will need a more efficient and production ready solution to this, most likely the same service that will host the job queue, can also contain a cache. We also will want to save the dates of the last requests made for things like `Activities`, and find a way to limit the amount of responses returned so we can process the data more efficiently. diff --git a/provider-middleware/data.go b/provider-middleware/data.go index 10320f6e..449b793a 100644 --- a/provider-middleware/data.go +++ b/provider-middleware/data.go @@ -169,20 +169,3 @@ type Role struct { Kind string `json:"kind"` Id string `json:"id"` } - -type UnlockEdImportMilestone struct { - UserID int `json:"user_id"` - ExternalProgramID string `json:"external_program_id"` - ExternalID string `json:"external_id"` - Type string `json:"type"` - IsCompleted bool `json:"is_completed"` -} - -type UnlockEdImportActivity struct { - ExternalUserID string `json:"external_user_id"` - ExternalProgramID string `json:"external_program_id"` - Type string `json:"type"` - TotalTime int `json:"total_time"` - Date string `json:"date"` - ExternalContentID string `json:"external_content_id"` -}