Dashboard, courses with counters, schedule, meetings, deadlines, readings with per-user progress (mark and unmark) and paginated published materials with tags and files, behind golem15.user's jwt.auth. Responses are built field by field with Warsaw RFC 3339 timestamps; bad query parameters answer 422 in Laravel's shape.
114 lines
3.4 KiB
Go
114 lines
3.4 KiB
Go
package api
|
|
|
|
import (
|
|
"net/http"
|
|
"strconv"
|
|
"time"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/lagoon"
|
|
"git.golem15.com/jakub/sm-bm-plugin/models"
|
|
)
|
|
|
|
// Rule tables of the query parameters, in the order the errors are listed.
|
|
// lagoon.ParseRules panics on a bad rule, so a mistake here stops start-up.
|
|
var (
|
|
dateRule = `nullable|regex:/^\d{4}-\d{2}-\d{2}$/|date`
|
|
|
|
// rangeRules check from and to alone; rangeRulesFrom also orders them
|
|
// when both are given.
|
|
rangeRules = []lagoon.RequestRule{
|
|
{Field: "from", Rules: lagoon.ParseRules(dateRule)},
|
|
{Field: "to", Rules: lagoon.ParseRules(dateRule)},
|
|
}
|
|
rangeRulesFrom = []lagoon.RequestRule{
|
|
{Field: "from", Rules: lagoon.ParseRules(dateRule)},
|
|
{Field: "to", Rules: lagoon.ParseRules(dateRule + "|after_or_equal:from")},
|
|
}
|
|
courseFilterRules = []lagoon.RequestRule{
|
|
{Field: "course", Rules: lagoon.ParseRules("nullable|integer|min:1")},
|
|
}
|
|
coursesRules = []lagoon.RequestRule{
|
|
{Field: "semester", Rules: lagoon.ParseRules("nullable|integer|between:1,4")},
|
|
}
|
|
materialsRules = []lagoon.RequestRule{
|
|
{Field: "course", Rules: lagoon.ParseRules("nullable|integer|min:1")},
|
|
{Field: "tag", Rules: lagoon.ParseRules("nullable|string|max:100")},
|
|
{Field: "type", Rules: append(lagoon.ParseRules("nullable"), lagoon.In(models.MaterialTypes...))},
|
|
{Field: "page", Rules: lagoon.ParseRules("nullable|integer|min:1")},
|
|
{Field: "per_page", Rules: lagoon.ParseRules("nullable|integer|between:1,100")},
|
|
}
|
|
)
|
|
|
|
// query collects the named query parameters that are present into the input
|
|
// map lagoon.ValidateRequest takes. A repeated parameter keeps its first
|
|
// value.
|
|
func query(r *http.Request, names ...string) map[string]any {
|
|
q := r.URL.Query()
|
|
in := make(map[string]any, len(names))
|
|
for _, name := range names {
|
|
if vals, ok := q[name]; ok && len(vals) > 0 {
|
|
in[name] = vals[0]
|
|
}
|
|
}
|
|
return in
|
|
}
|
|
|
|
// validate checks in against rules and answers 422 itself when it fails.
|
|
func (h *Handlers) validate(w http.ResponseWriter, r *http.Request, in map[string]any, rules []lagoon.RequestRule) bool {
|
|
errs, err := lagoon.ValidateRequest(r.Context(), nil, in, rules, h.translator())
|
|
if err != nil {
|
|
writeOpaque500(w)
|
|
return false
|
|
}
|
|
if len(errs) > 0 {
|
|
writeValidation(w, errs)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// str returns a validated string parameter, or "" when it is absent.
|
|
func str(in map[string]any, name string) string {
|
|
s, _ := in[name].(string)
|
|
return s
|
|
}
|
|
|
|
// uintParam returns a validated positive integer parameter, or 0 when it is
|
|
// absent.
|
|
func uintParam(in map[string]any, name string) uint {
|
|
n, err := strconv.ParseUint(str(in, name), 10, 64)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return uint(n)
|
|
}
|
|
|
|
// intParam returns a validated integer parameter, or def when it is absent.
|
|
func intParam(in map[string]any, name string, def int) int {
|
|
n, err := strconv.Atoi(str(in, name))
|
|
if err != nil {
|
|
return def
|
|
}
|
|
return n
|
|
}
|
|
|
|
// dayStart returns midnight in Europe/Warsaw of a validated YYYY-MM-DD
|
|
// parameter, and false when it is absent.
|
|
func dayStart(in map[string]any, name string) (time.Time, bool) {
|
|
s := str(in, name)
|
|
if s == "" {
|
|
return time.Time{}, false
|
|
}
|
|
t, err := models.ParseDate(s)
|
|
if err != nil {
|
|
return time.Time{}, false
|
|
}
|
|
return t, true
|
|
}
|
|
|
|
// today returns midnight of now's calendar day in Europe/Warsaw.
|
|
func today(now time.Time) time.Time {
|
|
y, m, d := now.In(models.Location).Date()
|
|
return time.Date(y, m, d, 0, 0, 0, 0, models.Location)
|
|
}
|