Skip to content
Open
2 changes: 2 additions & 0 deletions cmd/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ func (app *application) mount() http.Handler {
r.Route("/applications", func(r chi.Router) {
r.Get("/", app.listApplicationsHandler)
r.Get("/stats", app.getApplicationStatsHandler)
r.Get("/enabled", app.getApplicationsEnabled)
r.Get("/{applicationID}", app.getApplication)
r.Get("/{applicationID}/resume-url", app.getResumeDownloadURLHandler)

Expand Down Expand Up @@ -238,6 +239,7 @@ func (app *application) mount() http.Handler {
r.Get("/hackathon-date-range", app.getHackathonDateRange)
r.Post("/hackathon-date-range", app.setHackathonDateRange)
r.Put("/scan-types", app.updateScanTypesHandler)
r.Put("/applications-enabled", app.setApplicationsEnabled)
})

r.Route("/applications", func(r chi.Router) {
Expand Down
4 changes: 4 additions & 0 deletions cmd/api/applications.go
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,10 @@ type EmailListResponse struct {
Count int `json:"count"`
}

type ApplicationsEnabledResponse struct {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Get and set response are the same struct just keep them as one ApplicationsEnabledResponse

Enabled bool `json:"enabled"`
}

// setApplicationStatus sets the final status on an application
//
// @Summary Set application status (Super Admin)
Expand Down
68 changes: 68 additions & 0 deletions cmd/api/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"errors"
"net/http"
"strconv"
"time"

"github.com/hackutd/portal/internal/store"
Expand Down Expand Up @@ -414,3 +415,70 @@ func (app *application) setHackathonDateRange(w http.ResponseWriter, r *http.Req
app.internalServerError(w, r, err)
}
}

// getApplicationsEnabled returns whether applications are currently open
//
// @Summary Get applications enabled status
// @Description Returns whether the application portal is currently open for submissions
// @Tags applications
// @Produce json
// @Success 200 {object} ApplicationsEnabledResponse
// @Failure 401 {object} object{error=string}
// @Failure 500 {object} object{error=string}
// @Security CookieAuth
// @Router /applications/enabled [get]
func (app *application) getApplicationsEnabled(w http.ResponseWriter, r *http.Request) {
enabled, err := app.store.Application.GetApplicationsEnabled(r.Context())
if err != nil {
app.internalServerError(w, r, err)
return
}

response := ApplicationsEnabledResponse{
Enabled: enabled,
}

if err = app.jsonResponse(w, http.StatusOK, response); err != nil {
app.internalServerError(w, r, err)
return
}
}

// setApplicationsEnabled updates whether applications are currently open
//
// @Summary Set applications enabled status
// @Description Sets whether the application portal is currently open for submissions. Requires SuperAdmin privileges.
// @Tags superadmin
// @Produce json
// @Param enabled query bool true "Enable or disable applications"
// @Success 200 {object} ApplicationsEnabledResponse
// @Failure 400 {object} object{error=string}
// @Failure 401 {object} object{error=string}
// @Failure 403 {object} object{error=string}
// @Failure 500 {object} object{error=string}
// @Security CookieAuth
// @Router /superadmin/settings/applications-enabled [put]
func (app *application) setApplicationsEnabled(w http.ResponseWriter, r *http.Request) {
enabled, err := strconv.ParseBool(r.URL.Query().Get("enabled"))

if err != nil {
app.badRequestResponse(w, r, errors.New("enabled must be a boolean value"))
return
}

enabled, err = app.store.Application.SetApplicationsEnabled(r.Context(), enabled)
if err != nil {
app.internalServerError(w, r, err)
return
}

//NOTE: Following existing design pattern of Get response and Set response structs
response := ApplicationsEnabledResponse{
Enabled: enabled,
}

if err = app.jsonResponse(w, http.StatusOK, response); err != nil {
app.internalServerError(w, r, err)
return
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DELETE FROM settings WHERE key = 'applications_enabled';
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
INSERT INTO settings (key, value) VALUES ('applications_enabled', 'true'::jsonb)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sure migrations are numbered correctly. There shouldn't be 4 up down migrations for 17. 1 up and 1 down per migration

ON CONFLICT (key) DO NOTHING;
133 changes: 133 additions & 0 deletions docs/docs.go
Original file line number Diff line number Diff line change
Expand Up @@ -1482,6 +1482,53 @@ const docTemplate = `{
}
}
},
"/applications/enabled": {
"get": {
"security": [
{
"CookieAuth": []
}
],
"description": "Returns whether the application portal is currently open for submissions",
"produces": [
"application/json"
],
"tags": [
"applications"
],
"summary": "Get applications enabled status",
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.ApplicationsEnabledResponse"
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string"
}
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string"
}
}
}
}
}
}
},
"/applications/me": {
"get": {
"security": [
Expand Down Expand Up @@ -2366,6 +2413,84 @@ const docTemplate = `{
}
}
},
"/superadmin/settings/applications-enabled": {
"put": {
"security": [
{
"CookieAuth": []
}
],
"description": "Sets whether the application portal is currently open for submissions. Requires SuperAdmin privileges.",
"produces": [
"application/json"
],
"tags": [
"superadmin"
],
"summary": "Set applications enabled status",
"parameters": [
{
"type": "boolean",
"description": "Enable or disable applications",
"name": "enabled",
"in": "query",
"required": true
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/main.ApplicationsEnabledResponse"
}
},
"400": {
"description": "Bad Request",
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string"
}
}
}
},
"401": {
"description": "Unauthorized",
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string"
}
}
}
},
"403": {
"description": "Forbidden",
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string"
}
}
}
},
"500": {
"description": "Internal Server Error",
"schema": {
"type": "object",
"properties": {
"error": {
"type": "string"
}
}
}
}
}
}
},
"/superadmin/settings/hackathon-date-range": {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like some endpoints were deleted make sure to generate the docs before PR

"get": {
"security": [
Expand Down Expand Up @@ -3326,6 +3451,14 @@ const docTemplate = `{
}
}
},
"main.ApplicationsEnabledResponse": {
"type": "object",
"properties": {
"enabled": {
"type": "boolean"
}
}
},
"main.CheckEmailResponse": {
"type": "object",
"properties": {
Expand Down
37 changes: 37 additions & 0 deletions internal/store/applications.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"encoding/json"
"errors"
"fmt"
"strconv"
"strings"
"time"
)
Expand Down Expand Up @@ -806,3 +807,39 @@ func (s *ApplicationsStore) GetEmailsByStatus(ctx context.Context, status Applic

return users, rows.Err()
}

func (s *ApplicationsStore) GetApplicationsEnabled(ctx context.Context) (bool, error) {
ctx, cancel := context.WithTimeout(ctx, QueryTimeoutDuration)
defer cancel()

query := `
SELECT value
FROM settings
WHERE key = 'applications_enabled'
`
var value bool
err := s.db.QueryRowContext(ctx, query).Scan(&value)
if err != nil {
return false, err // We won't handle err here, (because if the setting doesn't exist, we want it to error instead of defaulting to false)
}
return value, nil
}

func (s *ApplicationsStore) SetApplicationsEnabled(ctx context.Context, enabled bool) (bool, error) {
ctx, cancel := context.WithTimeout(ctx, QueryTimeoutDuration)
defer cancel()

query := `
UPDATE settings
SET value = $1::jsonb
WHERE key = 'applications_enabled'
RETURNING value`

var value bool
err := s.db.QueryRowContext(ctx, query, strconv.FormatBool(enabled)).Scan(&value)
if err != nil {
return value, err
}

return value, nil
}
10 changes: 10 additions & 0 deletions internal/store/mock_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,16 @@ func (m *MockApplicationStore) GetEmailsByStatus(ctx context.Context, status App
return args.Get(0).([]UserEmailInfo), args.Error(1)
}

func (m *MockApplicationStore) GetApplicationsEnabled(ctx context.Context) (bool, error) {
args := m.Called()
return args.Get(0).(bool), args.Error(1)
}

func (m *MockApplicationStore) SetApplicationsEnabled(ctx context.Context, enabled bool) (bool, error) {
args := m.Called()
return args.Get(0).(bool), args.Error(1)
}

// mock implementation of the Settings interface
type MockSettingsStore struct {
mock.Mock
Expand Down
2 changes: 2 additions & 0 deletions internal/store/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ type Storage struct {
GetStats(ctx context.Context) (*ApplicationStats, error)
SetStatus(ctx context.Context, id string, status ApplicationStatus) (*Application, error)
GetEmailsByStatus(ctx context.Context, status ApplicationStatus) ([]UserEmailInfo, error)
GetApplicationsEnabled(ctx context.Context) (bool, error)
SetApplicationsEnabled(ctx context.Context, enabled bool) (bool, error)
}
Settings interface {
GetShortAnswerQuestions(ctx context.Context) ([]ShortAnswerQuestion, error)
Expand Down