package forms import ( "bytes" "errors" "io" "net/http" "strconv" "strings" "time" "github.com/rs/zerolog/log" "librapi/services" ) const MaxFileSize = 200 // in MB var ( ErrInvalidUsername = errors.New("username must not be empty") ErrInvalidPassword = errors.New("password must not be empty") ErrInvalidCredentials = errors.New("bad credentials") ErrInvalidName = errors.New("resource name must not be empty") ErrInvalidEditor = errors.New("resource editor must not be empty") ErrInvalidYear = errors.New("invalid year, unable to parse") ErrInvalidYearRange = errors.New("invalid year, can't be greater than today") ErrInvalidAuthors = errors.New("must at least contains one author") ErrFileMaxSizeReached = errors.New("max file size reached, must be <= 200MB") ErrFileOpen = errors.New("unable to open file from form") ErrFileUnreadable = errors.New("unable to read the file") ) type StrList = []string type FormFieldType interface { int | string | StrList | UploadFile } type FormField[T FormFieldType] struct { Name string Value T Err string } type UploadFile struct { filename string content []byte size int64 } func (uf *UploadFile) GetFilename() string { return uf.filename } func (uf *UploadFile) CheckSize() error { if uf.size > (MaxFileSize << 20) { return ErrFileMaxSizeReached } return nil } type UploadForm struct { Name FormField[string] Description FormField[string] Editor FormField[string] Authors FormField[StrList] Year FormField[int] Keywords FormField[StrList] File FormField[UploadFile] Error string Method string } func UploadFormFromRequest(r *http.Request) UploadForm { uf := NewUploadForm() name := r.FormValue(uf.Name.Name) if name == "" { uf.Name.Err = ErrInvalidName.Error() } uf.Name.Value = name uf.Description.Value = r.FormValue(uf.Description.Name) editor := r.FormValue(uf.Editor.Name) if editor == "" { uf.Editor.Err = ErrInvalidEditor.Error() } uf.Editor.Value = editor if a := r.FormValue(uf.Authors.Name); a != "" { uf.Authors.Value = strings.Split(a, ",") } else { uf.Authors.Err = ErrInvalidAuthors.Error() } year, errParse := strconv.Atoi(r.FormValue(uf.Year.Name)) if errParse != nil { log.Err(errParse).Msg("unable to parse date") uf.Year.Err = ErrInvalidYear.Error() } if year > time.Now().Year() { log.Error().Msg("bad date range") uf.Year.Err = ErrInvalidYearRange.Error() } uf.Year.Value = year if kw := r.FormValue(uf.Keywords.Name); kw != "" { uf.Keywords.Value = strings.Split(kw, ",") } uf.sanitize() file, fileh, err := r.FormFile(uf.File.Name) if err != nil { log.Err(err).Msg("unable to get file from form") uf.File.Err = ErrFileOpen.Error() return uf } defer file.Close() content, err := io.ReadAll(file) if err != nil { log.Err(err).Msg("unable to get read file from form") uf.File.Err = ErrFileOpen.Error() return uf } uf.File.Value = UploadFile{ filename: fileh.Filename, content: content, size: fileh.Size, } if err := uf.File.Value.CheckSize(); err != nil { uf.File.Err = err.Error() } return uf } func NewUploadForm() UploadForm { return UploadForm{ Name: FormField[string]{ Name: "name", }, Description: FormField[string]{ Name: "description", }, Editor: FormField[string]{ Name: "editor", }, Authors: FormField[StrList]{ Name: "authors", }, Year: FormField[int]{ Name: "year", }, Keywords: FormField[StrList]{ Name: "keywords", }, File: FormField[UploadFile]{ Name: "file", }, Method: http.MethodPost, } } func (uf *UploadForm) HasErrors() bool { return uf.Name.Err != "" || uf.Authors.Err != "" || uf.Editor.Err != "" || uf.Year.Err != "" || uf.Keywords.Err != "" || uf.File.Err != "" } func (uf *UploadForm) IsSuccess() bool { return uf.Method == http.MethodPost && uf.Error == "" && !uf.HasErrors() } func (uf *UploadForm) IntoResource() *services.Resource { bm := &services.Resource{ Name: uf.Name.Value, Editor: uf.Editor.Value, Authors: uf.Authors.Value, Year: uf.Year.Value, Keywords: nil, Content: bytes.NewBuffer(uf.File.Value.content), } if desc := uf.Description.Value; desc != "" { bm.Description = &desc } if keywords := uf.Keywords.Value; len(keywords) > 0 { bm.Keywords = keywords } return bm } func (uf *UploadForm) sanitize() { uf.Name.Value = strings.TrimSpace(uf.Name.Value) uf.Editor.Value = strings.TrimSpace(uf.Editor.Value) uf.Description.Value = strings.TrimSpace(uf.Description.Value) authors := []string{} for _, a := range uf.Authors.Value { if a == "" { continue } authors = append(authors, strings.TrimSpace(a)) } uf.Authors.Value = authors keywords := []string{} for _, k := range uf.Keywords.Value { if k == "" { continue } keywords = append(keywords, strings.TrimSpace(k)) } uf.Keywords.Value = keywords } type LoginForm struct { Username FormField[string] Password FormField[string] Error error Method string } func NewLoginForm() LoginForm { return LoginForm{ Username: FormField[string]{ Name: "username", }, Password: FormField[string]{ Name: "password", }, Method: http.MethodPost, } } func LoginFormFromRequest(r *http.Request) LoginForm { lf := NewLoginForm() username := r.FormValue(lf.Username.Name) if username == "" { lf.Username.Err = ErrInvalidUsername.Error() } lf.Username.Value = username password := r.FormValue(lf.Password.Name) if password == "" { lf.Password.Err = ErrInvalidPassword.Error() } lf.Password.Value = password return lf } func (lf *LoginForm) HasErrors() bool { return lf.Username.Err != "" || lf.Password.Err != "" } func (lf *LoginForm) IsSuccess() bool { return lf.Method == http.MethodPost && lf.Error != nil && !lf.HasErrors() } type SearchForm struct { Search FormField[string] Error error Method string Results []services.Resource } func SearchFormFromRequest(r *http.Request) SearchForm { sf := NewSearchForm() sf.Search.Value = strings.TrimSpace(r.FormValue(sf.Search.Name)) return sf } func NewSearchForm() SearchForm { return SearchForm{ Search: FormField[string]{ Name: "search", }, Method: http.MethodPost, } }