Refactor user validation
Validate each user field for creation/modification via API and web UI
This commit is contained in:
parent
291bf96d15
commit
e45cc2d2aa
40 changed files with 567 additions and 400 deletions
107
api/payload.go
107
api/payload.go
|
@ -167,113 +167,6 @@ func decodeFeedModificationRequest(r io.ReadCloser) (*feedModificationRequest, e
|
||||||
return &feed, nil
|
return &feed, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type userCreationRequest struct {
|
|
||||||
Username string `json:"username"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
IsAdmin bool `json:"is_admin"`
|
|
||||||
GoogleID string `json:"google_id"`
|
|
||||||
OpenIDConnectID string `json:"openid_connect_id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeUserCreationRequest(r io.ReadCloser) (*userCreationRequest, error) {
|
|
||||||
defer r.Close()
|
|
||||||
|
|
||||||
var request userCreationRequest
|
|
||||||
decoder := json.NewDecoder(r)
|
|
||||||
if err := decoder.Decode(&request); err != nil {
|
|
||||||
return nil, fmt.Errorf("Unable to decode user creation JSON object: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &request, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type userModificationRequest struct {
|
|
||||||
Username *string `json:"username"`
|
|
||||||
Password *string `json:"password"`
|
|
||||||
IsAdmin *bool `json:"is_admin"`
|
|
||||||
Theme *string `json:"theme"`
|
|
||||||
Language *string `json:"language"`
|
|
||||||
Timezone *string `json:"timezone"`
|
|
||||||
EntryDirection *string `json:"entry_sorting_direction"`
|
|
||||||
Stylesheet *string `json:"stylesheet"`
|
|
||||||
GoogleID *string `json:"google_id"`
|
|
||||||
OpenIDConnectID *string `json:"openid_connect_id"`
|
|
||||||
EntriesPerPage *int `json:"entries_per_page"`
|
|
||||||
KeyboardShortcuts *bool `json:"keyboard_shortcuts"`
|
|
||||||
ShowReadingTime *bool `json:"show_reading_time"`
|
|
||||||
EntrySwipe *bool `json:"entry_swipe"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *userModificationRequest) Update(user *model.User) {
|
|
||||||
if u.Username != nil {
|
|
||||||
user.Username = *u.Username
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.Password != nil {
|
|
||||||
user.Password = *u.Password
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.IsAdmin != nil {
|
|
||||||
user.IsAdmin = *u.IsAdmin
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.Theme != nil {
|
|
||||||
user.Theme = *u.Theme
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.Language != nil {
|
|
||||||
user.Language = *u.Language
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.Timezone != nil {
|
|
||||||
user.Timezone = *u.Timezone
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.EntryDirection != nil {
|
|
||||||
user.EntryDirection = *u.EntryDirection
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.Stylesheet != nil {
|
|
||||||
user.Stylesheet = *u.Stylesheet
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.GoogleID != nil {
|
|
||||||
user.GoogleID = *u.GoogleID
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.OpenIDConnectID != nil {
|
|
||||||
user.OpenIDConnectID = *u.OpenIDConnectID
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.EntriesPerPage != nil {
|
|
||||||
user.EntriesPerPage = *u.EntriesPerPage
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.KeyboardShortcuts != nil {
|
|
||||||
user.KeyboardShortcuts = *u.KeyboardShortcuts
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.ShowReadingTime != nil {
|
|
||||||
user.ShowReadingTime = *u.ShowReadingTime
|
|
||||||
}
|
|
||||||
|
|
||||||
if u.EntrySwipe != nil {
|
|
||||||
user.EntrySwipe = *u.EntrySwipe
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeUserModificationRequest(r io.ReadCloser) (*userModificationRequest, error) {
|
|
||||||
defer r.Close()
|
|
||||||
|
|
||||||
var request userModificationRequest
|
|
||||||
decoder := json.NewDecoder(r)
|
|
||||||
if err := decoder.Decode(&request); err != nil {
|
|
||||||
return nil, fmt.Errorf("Unable to decode user modification JSON object: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &request, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeEntryStatusRequest(r io.ReadCloser) ([]int64, string, error) {
|
func decodeEntryStatusRequest(r io.ReadCloser) ([]int64, string, error) {
|
||||||
type payload struct {
|
type payload struct {
|
||||||
EntryIDs []int64 `json:"entry_ids"`
|
EntryIDs []int64 `json:"entry_ids"`
|
||||||
|
|
|
@ -218,24 +218,3 @@ func TestUpdateFeedToFetchViaProxy(t *testing.T) {
|
||||||
t.Errorf(`The field FetchViaProxy should be %v`, value)
|
t.Errorf(`The field FetchViaProxy should be %v`, value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUpdateUserTheme(t *testing.T) {
|
|
||||||
theme := "Example 2"
|
|
||||||
changes := &userModificationRequest{Theme: &theme}
|
|
||||||
user := &model.User{Theme: "Example"}
|
|
||||||
changes.Update(user)
|
|
||||||
|
|
||||||
if user.Theme != theme {
|
|
||||||
t.Errorf(`Unexpected value, got %q instead of %q`, user.Theme, theme)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUserThemeWhenNotSet(t *testing.T) {
|
|
||||||
changes := &userModificationRequest{}
|
|
||||||
user := &model.User{Theme: "Example"}
|
|
||||||
changes.Update(user)
|
|
||||||
|
|
||||||
if user.Theme != "Example" {
|
|
||||||
t.Error(`The user Theme should not be modified`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
40
api/user.go
40
api/user.go
|
@ -5,12 +5,14 @@
|
||||||
package api // import "miniflux.app/api"
|
package api // import "miniflux.app/api"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
json_parser "encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"miniflux.app/http/request"
|
"miniflux.app/http/request"
|
||||||
"miniflux.app/http/response/json"
|
"miniflux.app/http/response/json"
|
||||||
"miniflux.app/model"
|
"miniflux.app/model"
|
||||||
|
"miniflux.app/validator"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (h *handler) currentUser(w http.ResponseWriter, r *http.Request) {
|
func (h *handler) currentUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -29,50 +31,38 @@ func (h *handler) createUser(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
userCreationRequest, err := decodeUserCreationRequest(r.Body)
|
var userCreationRequest model.UserCreationRequest
|
||||||
if err != nil {
|
if err := json_parser.NewDecoder(r.Body).Decode(&userCreationRequest); err != nil {
|
||||||
json.BadRequest(w, r, err)
|
json.BadRequest(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user := model.NewUser()
|
if validationErr := validator.ValidateUserCreationWithPassword(h.store, &userCreationRequest); validationErr != nil {
|
||||||
user.Username = userCreationRequest.Username
|
json.BadRequest(w, r, validationErr.Error())
|
||||||
user.Password = userCreationRequest.Password
|
|
||||||
user.IsAdmin = userCreationRequest.IsAdmin
|
|
||||||
user.GoogleID = userCreationRequest.GoogleID
|
|
||||||
user.OpenIDConnectID = userCreationRequest.OpenIDConnectID
|
|
||||||
|
|
||||||
if err := user.ValidateUserCreation(); err != nil {
|
|
||||||
json.BadRequest(w, r, err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.store.UserExists(user.Username) {
|
user, err := h.store.CreateUser(&userCreationRequest)
|
||||||
json.BadRequest(w, r, errors.New("This user already exists"))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = h.store.CreateUser(user)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
json.ServerError(w, r, err)
|
json.ServerError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user.Password = ""
|
|
||||||
json.Created(w, r, user)
|
json.Created(w, r, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *handler) updateUser(w http.ResponseWriter, r *http.Request) {
|
func (h *handler) updateUser(w http.ResponseWriter, r *http.Request) {
|
||||||
userID := request.RouteInt64Param(r, "userID")
|
userID := request.RouteInt64Param(r, "userID")
|
||||||
userChanges, err := decodeUserModificationRequest(r.Body)
|
|
||||||
if err != nil {
|
var userModificationRequest model.UserModificationRequest
|
||||||
|
if err := json_parser.NewDecoder(r.Body).Decode(&userModificationRequest); err != nil {
|
||||||
json.BadRequest(w, r, err)
|
json.BadRequest(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
originalUser, err := h.store.UserByID(userID)
|
originalUser, err := h.store.UserByID(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
json.BadRequest(w, r, errors.New("Unable to fetch this user from the database"))
|
json.ServerError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,18 +77,18 @@ func (h *handler) updateUser(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if userChanges.IsAdmin != nil && *userChanges.IsAdmin {
|
if userModificationRequest.IsAdmin != nil && *userModificationRequest.IsAdmin {
|
||||||
json.BadRequest(w, r, errors.New("Only administrators can change permissions of standard users"))
|
json.BadRequest(w, r, errors.New("Only administrators can change permissions of standard users"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
userChanges.Update(originalUser)
|
if validationErr := validator.ValidateUserModification(h.store, originalUser.ID, &userModificationRequest); validationErr != nil {
|
||||||
if err := originalUser.ValidateUserModification(); err != nil {
|
json.BadRequest(w, r, validationErr.Error())
|
||||||
json.BadRequest(w, r, err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
userModificationRequest.Patch(originalUser)
|
||||||
if err = h.store.UpdateUser(originalUser); err != nil {
|
if err = h.store.UpdateUser(originalUser); err != nil {
|
||||||
json.ServerError(w, r, err)
|
json.ServerError(w, r, err)
|
||||||
return
|
return
|
||||||
|
|
|
@ -12,29 +12,31 @@ import (
|
||||||
"miniflux.app/logger"
|
"miniflux.app/logger"
|
||||||
"miniflux.app/model"
|
"miniflux.app/model"
|
||||||
"miniflux.app/storage"
|
"miniflux.app/storage"
|
||||||
|
"miniflux.app/validator"
|
||||||
)
|
)
|
||||||
|
|
||||||
func createAdmin(store *storage.Storage) {
|
func createAdmin(store *storage.Storage) {
|
||||||
user := model.NewUser()
|
userCreationRequest := &model.UserCreationRequest{
|
||||||
user.Username = config.Opts.AdminUsername()
|
Username: config.Opts.AdminUsername(),
|
||||||
user.Password = config.Opts.AdminPassword()
|
Password: config.Opts.AdminPassword(),
|
||||||
user.IsAdmin = true
|
IsAdmin: true,
|
||||||
|
|
||||||
if user.Username == "" || user.Password == "" {
|
|
||||||
user.Username, user.Password = askCredentials()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := user.ValidateUserCreation(); err != nil {
|
if userCreationRequest.Username == "" || userCreationRequest.Password == "" {
|
||||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
userCreationRequest.Username, userCreationRequest.Password = askCredentials()
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if store.UserExists(user.Username) {
|
if store.UserExists(userCreationRequest.Username) {
|
||||||
logger.Info(`User %q already exists, skipping creation`, user.Username)
|
logger.Info(`User %q already exists, skipping creation`, userCreationRequest.Username)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := store.CreateUser(user); err != nil {
|
if validationErr := validator.ValidateUserCreationWithPassword(store, userCreationRequest); validationErr != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s\n", validationErr)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := store.CreateUser(userCreationRequest); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
fmt.Fprintf(os.Stderr, "%v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,9 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"miniflux.app/model"
|
||||||
"miniflux.app/storage"
|
"miniflux.app/storage"
|
||||||
|
"miniflux.app/validator"
|
||||||
)
|
)
|
||||||
|
|
||||||
func resetPassword(store *storage.Storage) {
|
func resetPassword(store *storage.Storage) {
|
||||||
|
@ -24,9 +26,11 @@ func resetPassword(store *storage.Storage) {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
user.Password = password
|
userModificationRequest := &model.UserModificationRequest{
|
||||||
if err := user.ValidatePassword(); err != nil {
|
Password: &password,
|
||||||
fmt.Fprintf(os.Stderr, "%v\n", err)
|
}
|
||||||
|
if validationErr := validator.ValidateUserModification(store, user.ID, userModificationRequest); validationErr != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s\n", validationErr)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -244,6 +244,10 @@ var translations = map[string]string{
|
||||||
"error.user_mandatory_fields": "Der Benutzername ist obligatorisch.",
|
"error.user_mandatory_fields": "Der Benutzername ist obligatorisch.",
|
||||||
"error.api_key_already_exists": "Dieser API-Schlüssel ist bereits vorhanden.",
|
"error.api_key_already_exists": "Dieser API-Schlüssel ist bereits vorhanden.",
|
||||||
"error.unable_to_create_api_key": "Dieser API-Schlüssel kann nicht erstellt werden.",
|
"error.unable_to_create_api_key": "Dieser API-Schlüssel kann nicht erstellt werden.",
|
||||||
|
"error.invalid_theme": "Ungültiges Thema.",
|
||||||
|
"error.invalid_language": "Ungültige Sprache.",
|
||||||
|
"error.invalid_timezone": "Ungültige Zeitzone.",
|
||||||
|
"error.invalid_entry_direction": "Ungültige Sortierreihenfolge.",
|
||||||
"form.feed.label.title": "Titel",
|
"form.feed.label.title": "Titel",
|
||||||
"form.feed.label.site_url": "Webseite-URL",
|
"form.feed.label.site_url": "Webseite-URL",
|
||||||
"form.feed.label.feed_url": "Abonnement-URL",
|
"form.feed.label.feed_url": "Abonnement-URL",
|
||||||
|
@ -582,6 +586,10 @@ var translations = map[string]string{
|
||||||
"error.unable_to_update_user": "Unable to update this user.",
|
"error.unable_to_update_user": "Unable to update this user.",
|
||||||
"error.unable_to_update_feed": "Unable to update this feed.",
|
"error.unable_to_update_feed": "Unable to update this feed.",
|
||||||
"error.subscription_not_found": "Unable to find any subscription.",
|
"error.subscription_not_found": "Unable to find any subscription.",
|
||||||
|
"error.invalid_theme": "Invalid theme.",
|
||||||
|
"error.invalid_language": "Invalid language.",
|
||||||
|
"error.invalid_timezone": "Invalid timezone.",
|
||||||
|
"error.invalid_entry_direction": "Invalid entry direction.",
|
||||||
"error.empty_file": "This file is empty.",
|
"error.empty_file": "This file is empty.",
|
||||||
"error.bad_credentials": "Invalid username or password.",
|
"error.bad_credentials": "Invalid username or password.",
|
||||||
"error.fields_mandatory": "All fields are mandatory.",
|
"error.fields_mandatory": "All fields are mandatory.",
|
||||||
|
@ -924,6 +932,10 @@ var translations = map[string]string{
|
||||||
"error.user_mandatory_fields": "El nombre de usuario es obligatorio.",
|
"error.user_mandatory_fields": "El nombre de usuario es obligatorio.",
|
||||||
"error.api_key_already_exists": "Esta clave API ya existe.",
|
"error.api_key_already_exists": "Esta clave API ya existe.",
|
||||||
"error.unable_to_create_api_key": "No se puede crear esta clave API.",
|
"error.unable_to_create_api_key": "No se puede crear esta clave API.",
|
||||||
|
"error.invalid_theme": "Tema no válido.",
|
||||||
|
"error.invalid_language": "Idioma no válido.",
|
||||||
|
"error.invalid_timezone": "Zona horaria no válida.",
|
||||||
|
"error.invalid_entry_direction": "Dirección de entrada no válida.",
|
||||||
"form.feed.label.title": "Título",
|
"form.feed.label.title": "Título",
|
||||||
"form.feed.label.site_url": "URL del sitio",
|
"form.feed.label.site_url": "URL del sitio",
|
||||||
"form.feed.label.feed_url": "URL de la fuente",
|
"form.feed.label.feed_url": "URL de la fuente",
|
||||||
|
@ -936,7 +948,6 @@ var translations = map[string]string{
|
||||||
"form.feed.label.rewrite_rules": "Reglas de reescribir",
|
"form.feed.label.rewrite_rules": "Reglas de reescribir",
|
||||||
"form.feed.label.blocklist_rules": "Reglas de Filtrado(Bloquear)",
|
"form.feed.label.blocklist_rules": "Reglas de Filtrado(Bloquear)",
|
||||||
"form.feed.label.keeplist_rules": "Reglas de Filtrado(Permitir)",
|
"form.feed.label.keeplist_rules": "Reglas de Filtrado(Permitir)",
|
||||||
"form.feed.label.blocklist_rules": "Reglas de Blacklist",
|
|
||||||
"form.feed.label.ignore_http_cache": "Ignorar caché HTTP",
|
"form.feed.label.ignore_http_cache": "Ignorar caché HTTP",
|
||||||
"form.feed.label.fetch_via_proxy": "Buscar a través de proxy",
|
"form.feed.label.fetch_via_proxy": "Buscar a través de proxy",
|
||||||
"form.feed.label.disabled": "No actualice este feed",
|
"form.feed.label.disabled": "No actualice este feed",
|
||||||
|
@ -1255,6 +1266,10 @@ var translations = map[string]string{
|
||||||
"error.user_mandatory_fields": "Le nom d'utilisateur est obligatoire.",
|
"error.user_mandatory_fields": "Le nom d'utilisateur est obligatoire.",
|
||||||
"error.api_key_already_exists": "Cette clé d'API existe déjà.",
|
"error.api_key_already_exists": "Cette clé d'API existe déjà.",
|
||||||
"error.unable_to_create_api_key": "Impossible de créer cette clé d'API.",
|
"error.unable_to_create_api_key": "Impossible de créer cette clé d'API.",
|
||||||
|
"error.invalid_theme": "Thème non valide.",
|
||||||
|
"error.invalid_language": "Langue non valide.",
|
||||||
|
"error.invalid_timezone": "Fuseau horaire non valide.",
|
||||||
|
"error.invalid_entry_direction": "Ordre de trie non valide.",
|
||||||
"form.feed.label.title": "Titre",
|
"form.feed.label.title": "Titre",
|
||||||
"form.feed.label.site_url": "URL du site web",
|
"form.feed.label.site_url": "URL du site web",
|
||||||
"form.feed.label.feed_url": "URL du flux",
|
"form.feed.label.feed_url": "URL du flux",
|
||||||
|
@ -1605,6 +1620,10 @@ var translations = map[string]string{
|
||||||
"error.user_mandatory_fields": "Il nome utente è obbligatorio.",
|
"error.user_mandatory_fields": "Il nome utente è obbligatorio.",
|
||||||
"error.api_key_already_exists": "Questa chiave API esiste già.",
|
"error.api_key_already_exists": "Questa chiave API esiste già.",
|
||||||
"error.unable_to_create_api_key": "Impossibile creare questa chiave API.",
|
"error.unable_to_create_api_key": "Impossibile creare questa chiave API.",
|
||||||
|
"error.invalid_theme": "Tema non valido.",
|
||||||
|
"error.invalid_language": "Lingua non valida.",
|
||||||
|
"error.invalid_timezone": "Fuso orario non valido.",
|
||||||
|
"error.invalid_entry_direction": "Ordinamento non valido.",
|
||||||
"form.feed.label.title": "Titolo",
|
"form.feed.label.title": "Titolo",
|
||||||
"form.feed.label.site_url": "URL del sito",
|
"form.feed.label.site_url": "URL del sito",
|
||||||
"form.feed.label.feed_url": "URL del feed",
|
"form.feed.label.feed_url": "URL del feed",
|
||||||
|
@ -1935,6 +1954,10 @@ var translations = map[string]string{
|
||||||
"error.user_mandatory_fields": "ユーザー名が必要です。",
|
"error.user_mandatory_fields": "ユーザー名が必要です。",
|
||||||
"error.api_key_already_exists": "このAPIキーは既に存在します。",
|
"error.api_key_already_exists": "このAPIキーは既に存在します。",
|
||||||
"error.unable_to_create_api_key": "このAPIキーを作成できません。",
|
"error.unable_to_create_api_key": "このAPIキーを作成できません。",
|
||||||
|
"error.invalid_theme": "テーマが無効です。",
|
||||||
|
"error.invalid_language": "言語が無効です。",
|
||||||
|
"error.invalid_timezone": "タイムゾーンが無効です。",
|
||||||
|
"error.invalid_entry_direction": "ソート順が無効です。",
|
||||||
"form.feed.label.title": "タイトル",
|
"form.feed.label.title": "タイトル",
|
||||||
"form.feed.label.site_url": "サイト URL",
|
"form.feed.label.site_url": "サイト URL",
|
||||||
"form.feed.label.feed_url": "フィード URL",
|
"form.feed.label.feed_url": "フィード URL",
|
||||||
|
@ -2265,6 +2288,10 @@ var translations = map[string]string{
|
||||||
"error.user_mandatory_fields": "Gebruikersnaam is verplicht",
|
"error.user_mandatory_fields": "Gebruikersnaam is verplicht",
|
||||||
"error.api_key_already_exists": "This API Key already exists.",
|
"error.api_key_already_exists": "This API Key already exists.",
|
||||||
"error.unable_to_create_api_key": "Kan deze API-sleutel niet maken.",
|
"error.unable_to_create_api_key": "Kan deze API-sleutel niet maken.",
|
||||||
|
"error.invalid_theme": "Ongeldig thema.",
|
||||||
|
"error.invalid_language": "Ongeldige taal.",
|
||||||
|
"error.invalid_timezone": "Ongeldige tijdzone.",
|
||||||
|
"error.invalid_entry_direction": "Ongeldige sorteervolgorde.",
|
||||||
"form.feed.label.title": "Naam",
|
"form.feed.label.title": "Naam",
|
||||||
"form.feed.label.site_url": "Website URL",
|
"form.feed.label.site_url": "Website URL",
|
||||||
"form.feed.label.feed_url": "Feed URL",
|
"form.feed.label.feed_url": "Feed URL",
|
||||||
|
@ -2615,6 +2642,10 @@ var translations = map[string]string{
|
||||||
"error.user_mandatory_fields": "Nazwa użytkownika jest obowiązkowa.",
|
"error.user_mandatory_fields": "Nazwa użytkownika jest obowiązkowa.",
|
||||||
"error.api_key_already_exists": "Deze API-sleutel bestaat al.",
|
"error.api_key_already_exists": "Deze API-sleutel bestaat al.",
|
||||||
"error.unable_to_create_api_key": "Nie można utworzyć tego klucza API.",
|
"error.unable_to_create_api_key": "Nie można utworzyć tego klucza API.",
|
||||||
|
"error.invalid_theme": "Nieprawidłowy motyw.",
|
||||||
|
"error.invalid_language": "Nieprawidłowy język.",
|
||||||
|
"error.invalid_timezone": "Nieprawidłowa strefa czasowa.",
|
||||||
|
"error.invalid_entry_direction": "Nieprawidłowa kolejność sortowania.",
|
||||||
"form.feed.label.title": "Tytuł",
|
"form.feed.label.title": "Tytuł",
|
||||||
"form.feed.label.site_url": "URL strony",
|
"form.feed.label.site_url": "URL strony",
|
||||||
"form.feed.label.feed_url": "URL kanału",
|
"form.feed.label.feed_url": "URL kanału",
|
||||||
|
@ -2969,6 +3000,10 @@ var translations = map[string]string{
|
||||||
"error.user_mandatory_fields": "O nome de usuário é obrigatório.",
|
"error.user_mandatory_fields": "O nome de usuário é obrigatório.",
|
||||||
"error.api_key_already_exists": "Essa chave de API já existe.",
|
"error.api_key_already_exists": "Essa chave de API já existe.",
|
||||||
"error.unable_to_create_api_key": "Não foi possível criar uma chave de API.",
|
"error.unable_to_create_api_key": "Não foi possível criar uma chave de API.",
|
||||||
|
"error.invalid_theme": "Tema inválido.",
|
||||||
|
"error.invalid_language": "Idioma inválido.",
|
||||||
|
"error.invalid_timezone": "Fuso horário inválido.",
|
||||||
|
"error.invalid_entry_direction": "Direção de entrada inválida.",
|
||||||
"form.feed.label.title": "Título",
|
"form.feed.label.title": "Título",
|
||||||
"form.feed.label.site_url": "URL do site",
|
"form.feed.label.site_url": "URL do site",
|
||||||
"form.feed.label.feed_url": "URL da fonte",
|
"form.feed.label.feed_url": "URL da fonte",
|
||||||
|
@ -3301,6 +3336,10 @@ var translations = map[string]string{
|
||||||
"error.user_mandatory_fields": "Имя пользователя обязательно.",
|
"error.user_mandatory_fields": "Имя пользователя обязательно.",
|
||||||
"error.api_key_already_exists": "Этот ключ API уже существует.",
|
"error.api_key_already_exists": "Этот ключ API уже существует.",
|
||||||
"error.unable_to_create_api_key": "Невозможно создать этот ключ API.",
|
"error.unable_to_create_api_key": "Невозможно создать этот ключ API.",
|
||||||
|
"error.invalid_theme": "Неверная тема.",
|
||||||
|
"error.invalid_language": "Неверный язык.",
|
||||||
|
"error.invalid_timezone": "Неверный часовой пояс.",
|
||||||
|
"error.invalid_entry_direction": "Неверное направление входа.",
|
||||||
"form.feed.label.title": "Название",
|
"form.feed.label.title": "Название",
|
||||||
"form.feed.label.site_url": "URL сайта",
|
"form.feed.label.site_url": "URL сайта",
|
||||||
"form.feed.label.feed_url": "URL подписки",
|
"form.feed.label.feed_url": "URL подписки",
|
||||||
|
@ -3635,6 +3674,10 @@ var translations = map[string]string{
|
||||||
"error.user_mandatory_fields": "必须填写用户名",
|
"error.user_mandatory_fields": "必须填写用户名",
|
||||||
"error.api_key_already_exists": "此API密钥已存在。",
|
"error.api_key_already_exists": "此API密钥已存在。",
|
||||||
"error.unable_to_create_api_key": "无法创建此API密钥。",
|
"error.unable_to_create_api_key": "无法创建此API密钥。",
|
||||||
|
"error.invalid_theme": "无效的主题。",
|
||||||
|
"error.invalid_language": "语言无效。",
|
||||||
|
"error.invalid_timezone": "无效的时区。",
|
||||||
|
"error.invalid_entry_direction": "无效的输入方向。",
|
||||||
"form.feed.label.title": "标题",
|
"form.feed.label.title": "标题",
|
||||||
"form.feed.label.site_url": "站点 URL",
|
"form.feed.label.site_url": "站点 URL",
|
||||||
"form.feed.label.feed_url": "源 URL",
|
"form.feed.label.feed_url": "源 URL",
|
||||||
|
@ -3740,15 +3783,15 @@ var translations = map[string]string{
|
||||||
}
|
}
|
||||||
|
|
||||||
var translationsChecksums = map[string]string{
|
var translationsChecksums = map[string]string{
|
||||||
"de_DE": "66d6feafa5f92c35b10f8c07aa693afe1ea7dca73708cdebfc4cf3edb0478512",
|
"de_DE": "c8d6021599cfda4f853bd5ec1e1b065f03633ada9211ee22879ea778ba464572",
|
||||||
"en_US": "fa1771f155b439b46f282069a1628dc95d4170a5d7db14c1b90fa753936e7856",
|
"en_US": "781a7a6b54f439d76fe56fca7cb07412a04e71edebf53563f5cca27a0cd2533a",
|
||||||
"es_ES": "5b65c97c14e4b1f833ce6047be4b8b29bcb777d7a7a21420d1fb619584746649",
|
"es_ES": "4d602461f5ed9c4aaf59e8828d2b09d0cc45d06ba77d89ba0ef9662b580aebc0",
|
||||||
"fr_FR": "b58d74f73de2e775cc3f883f2fb19451e306ff32772f2fa8cb3c630d9df395d3",
|
"fr_FR": "3a0a008d0857fa5eb8a018ce5e348d7ccabe08a67849c72c6e7611e6b5b49aa7",
|
||||||
"it_IT": "a488f947b4e0cd8149c4cf7aaa71a8c59976a06559046fbc5b36aa167caaa84c",
|
"it_IT": "7222e3610ad3741aa7aff957f70524b63ffe3f6729198899231765335861a108",
|
||||||
"ja_JP": "9a3d1484c46be56286f9abf06b596576a3ae44f571d72a40d2eba5bfb02dd921",
|
"ja_JP": "f0ab6dd77c78717d25d88baad39c487c913720be4b3473a3f0aa3aa538318deb",
|
||||||
"nl_NL": "3b91987a9f6640e827c73ca11bb0f9bc58ff79359e792038746e273cd7ae546d",
|
"nl_NL": "1e0872b89fb78a6de2ae989d054963226146c3eeff4b2883cf2bf8df96c13846",
|
||||||
"pl_PL": "ff1dab97559d16331c374c63a91000b0c27f796bd96595e34ca324eb68a7c06e",
|
"pl_PL": "2513808a13925549c9ba27c52a20916d18a5222dd8ba6a14520798766889b076",
|
||||||
"pt_BR": "524c0f0dcd81988acd286900481e1a2e0ca9cf789d752e22da8bd5fe31b6acf3",
|
"pt_BR": "b5fc4d9e0dedc554579154f2fff772b108baf317c9a952d688db0df260674b3b",
|
||||||
"ru_RU": "805c698b8a053860f737a145acc3616c67c8d80bf1de89bf6bb27fee40e885f2",
|
"ru_RU": "d9bedead0757deae57da909c7d5297853c2186acb8ebf7cf91d0eef7c1a17d19",
|
||||||
"zh_CN": "0c6be862c7bd997337696161a1c1d3d2ec5c7adab9c33f29c09d906fcc015a79",
|
"zh_CN": "2526d0139ca0a2004f2db0864cbc9c3da55c3c7f45e1a244fea3c39d5d39e0f9",
|
||||||
}
|
}
|
||||||
|
|
|
@ -239,6 +239,10 @@
|
||||||
"error.user_mandatory_fields": "Der Benutzername ist obligatorisch.",
|
"error.user_mandatory_fields": "Der Benutzername ist obligatorisch.",
|
||||||
"error.api_key_already_exists": "Dieser API-Schlüssel ist bereits vorhanden.",
|
"error.api_key_already_exists": "Dieser API-Schlüssel ist bereits vorhanden.",
|
||||||
"error.unable_to_create_api_key": "Dieser API-Schlüssel kann nicht erstellt werden.",
|
"error.unable_to_create_api_key": "Dieser API-Schlüssel kann nicht erstellt werden.",
|
||||||
|
"error.invalid_theme": "Ungültiges Thema.",
|
||||||
|
"error.invalid_language": "Ungültige Sprache.",
|
||||||
|
"error.invalid_timezone": "Ungültige Zeitzone.",
|
||||||
|
"error.invalid_entry_direction": "Ungültige Sortierreihenfolge.",
|
||||||
"form.feed.label.title": "Titel",
|
"form.feed.label.title": "Titel",
|
||||||
"form.feed.label.site_url": "Webseite-URL",
|
"form.feed.label.site_url": "Webseite-URL",
|
||||||
"form.feed.label.feed_url": "Abonnement-URL",
|
"form.feed.label.feed_url": "Abonnement-URL",
|
||||||
|
|
|
@ -227,6 +227,10 @@
|
||||||
"error.unable_to_update_user": "Unable to update this user.",
|
"error.unable_to_update_user": "Unable to update this user.",
|
||||||
"error.unable_to_update_feed": "Unable to update this feed.",
|
"error.unable_to_update_feed": "Unable to update this feed.",
|
||||||
"error.subscription_not_found": "Unable to find any subscription.",
|
"error.subscription_not_found": "Unable to find any subscription.",
|
||||||
|
"error.invalid_theme": "Invalid theme.",
|
||||||
|
"error.invalid_language": "Invalid language.",
|
||||||
|
"error.invalid_timezone": "Invalid timezone.",
|
||||||
|
"error.invalid_entry_direction": "Invalid entry direction.",
|
||||||
"error.empty_file": "This file is empty.",
|
"error.empty_file": "This file is empty.",
|
||||||
"error.bad_credentials": "Invalid username or password.",
|
"error.bad_credentials": "Invalid username or password.",
|
||||||
"error.fields_mandatory": "All fields are mandatory.",
|
"error.fields_mandatory": "All fields are mandatory.",
|
||||||
|
|
|
@ -239,6 +239,10 @@
|
||||||
"error.user_mandatory_fields": "El nombre de usuario es obligatorio.",
|
"error.user_mandatory_fields": "El nombre de usuario es obligatorio.",
|
||||||
"error.api_key_already_exists": "Esta clave API ya existe.",
|
"error.api_key_already_exists": "Esta clave API ya existe.",
|
||||||
"error.unable_to_create_api_key": "No se puede crear esta clave API.",
|
"error.unable_to_create_api_key": "No se puede crear esta clave API.",
|
||||||
|
"error.invalid_theme": "Tema no válido.",
|
||||||
|
"error.invalid_language": "Idioma no válido.",
|
||||||
|
"error.invalid_timezone": "Zona horaria no válida.",
|
||||||
|
"error.invalid_entry_direction": "Dirección de entrada no válida.",
|
||||||
"form.feed.label.title": "Título",
|
"form.feed.label.title": "Título",
|
||||||
"form.feed.label.site_url": "URL del sitio",
|
"form.feed.label.site_url": "URL del sitio",
|
||||||
"form.feed.label.feed_url": "URL de la fuente",
|
"form.feed.label.feed_url": "URL de la fuente",
|
||||||
|
@ -251,7 +255,6 @@
|
||||||
"form.feed.label.rewrite_rules": "Reglas de reescribir",
|
"form.feed.label.rewrite_rules": "Reglas de reescribir",
|
||||||
"form.feed.label.blocklist_rules": "Reglas de Filtrado(Bloquear)",
|
"form.feed.label.blocklist_rules": "Reglas de Filtrado(Bloquear)",
|
||||||
"form.feed.label.keeplist_rules": "Reglas de Filtrado(Permitir)",
|
"form.feed.label.keeplist_rules": "Reglas de Filtrado(Permitir)",
|
||||||
"form.feed.label.blocklist_rules": "Reglas de Blacklist",
|
|
||||||
"form.feed.label.ignore_http_cache": "Ignorar caché HTTP",
|
"form.feed.label.ignore_http_cache": "Ignorar caché HTTP",
|
||||||
"form.feed.label.fetch_via_proxy": "Buscar a través de proxy",
|
"form.feed.label.fetch_via_proxy": "Buscar a través de proxy",
|
||||||
"form.feed.label.disabled": "No actualice este feed",
|
"form.feed.label.disabled": "No actualice este feed",
|
||||||
|
|
|
@ -239,6 +239,10 @@
|
||||||
"error.user_mandatory_fields": "Le nom d'utilisateur est obligatoire.",
|
"error.user_mandatory_fields": "Le nom d'utilisateur est obligatoire.",
|
||||||
"error.api_key_already_exists": "Cette clé d'API existe déjà.",
|
"error.api_key_already_exists": "Cette clé d'API existe déjà.",
|
||||||
"error.unable_to_create_api_key": "Impossible de créer cette clé d'API.",
|
"error.unable_to_create_api_key": "Impossible de créer cette clé d'API.",
|
||||||
|
"error.invalid_theme": "Thème non valide.",
|
||||||
|
"error.invalid_language": "Langue non valide.",
|
||||||
|
"error.invalid_timezone": "Fuseau horaire non valide.",
|
||||||
|
"error.invalid_entry_direction": "Ordre de trie non valide.",
|
||||||
"form.feed.label.title": "Titre",
|
"form.feed.label.title": "Titre",
|
||||||
"form.feed.label.site_url": "URL du site web",
|
"form.feed.label.site_url": "URL du site web",
|
||||||
"form.feed.label.feed_url": "URL du flux",
|
"form.feed.label.feed_url": "URL du flux",
|
||||||
|
|
|
@ -239,6 +239,10 @@
|
||||||
"error.user_mandatory_fields": "Il nome utente è obbligatorio.",
|
"error.user_mandatory_fields": "Il nome utente è obbligatorio.",
|
||||||
"error.api_key_already_exists": "Questa chiave API esiste già.",
|
"error.api_key_already_exists": "Questa chiave API esiste già.",
|
||||||
"error.unable_to_create_api_key": "Impossibile creare questa chiave API.",
|
"error.unable_to_create_api_key": "Impossibile creare questa chiave API.",
|
||||||
|
"error.invalid_theme": "Tema non valido.",
|
||||||
|
"error.invalid_language": "Lingua non valida.",
|
||||||
|
"error.invalid_timezone": "Fuso orario non valido.",
|
||||||
|
"error.invalid_entry_direction": "Ordinamento non valido.",
|
||||||
"form.feed.label.title": "Titolo",
|
"form.feed.label.title": "Titolo",
|
||||||
"form.feed.label.site_url": "URL del sito",
|
"form.feed.label.site_url": "URL del sito",
|
||||||
"form.feed.label.feed_url": "URL del feed",
|
"form.feed.label.feed_url": "URL del feed",
|
||||||
|
|
|
@ -239,6 +239,10 @@
|
||||||
"error.user_mandatory_fields": "ユーザー名が必要です。",
|
"error.user_mandatory_fields": "ユーザー名が必要です。",
|
||||||
"error.api_key_already_exists": "このAPIキーは既に存在します。",
|
"error.api_key_already_exists": "このAPIキーは既に存在します。",
|
||||||
"error.unable_to_create_api_key": "このAPIキーを作成できません。",
|
"error.unable_to_create_api_key": "このAPIキーを作成できません。",
|
||||||
|
"error.invalid_theme": "テーマが無効です。",
|
||||||
|
"error.invalid_language": "言語が無効です。",
|
||||||
|
"error.invalid_timezone": "タイムゾーンが無効です。",
|
||||||
|
"error.invalid_entry_direction": "ソート順が無効です。",
|
||||||
"form.feed.label.title": "タイトル",
|
"form.feed.label.title": "タイトル",
|
||||||
"form.feed.label.site_url": "サイト URL",
|
"form.feed.label.site_url": "サイト URL",
|
||||||
"form.feed.label.feed_url": "フィード URL",
|
"form.feed.label.feed_url": "フィード URL",
|
||||||
|
|
|
@ -239,6 +239,10 @@
|
||||||
"error.user_mandatory_fields": "Gebruikersnaam is verplicht",
|
"error.user_mandatory_fields": "Gebruikersnaam is verplicht",
|
||||||
"error.api_key_already_exists": "This API Key already exists.",
|
"error.api_key_already_exists": "This API Key already exists.",
|
||||||
"error.unable_to_create_api_key": "Kan deze API-sleutel niet maken.",
|
"error.unable_to_create_api_key": "Kan deze API-sleutel niet maken.",
|
||||||
|
"error.invalid_theme": "Ongeldig thema.",
|
||||||
|
"error.invalid_language": "Ongeldige taal.",
|
||||||
|
"error.invalid_timezone": "Ongeldige tijdzone.",
|
||||||
|
"error.invalid_entry_direction": "Ongeldige sorteervolgorde.",
|
||||||
"form.feed.label.title": "Naam",
|
"form.feed.label.title": "Naam",
|
||||||
"form.feed.label.site_url": "Website URL",
|
"form.feed.label.site_url": "Website URL",
|
||||||
"form.feed.label.feed_url": "Feed URL",
|
"form.feed.label.feed_url": "Feed URL",
|
||||||
|
|
|
@ -241,6 +241,10 @@
|
||||||
"error.user_mandatory_fields": "Nazwa użytkownika jest obowiązkowa.",
|
"error.user_mandatory_fields": "Nazwa użytkownika jest obowiązkowa.",
|
||||||
"error.api_key_already_exists": "Deze API-sleutel bestaat al.",
|
"error.api_key_already_exists": "Deze API-sleutel bestaat al.",
|
||||||
"error.unable_to_create_api_key": "Nie można utworzyć tego klucza API.",
|
"error.unable_to_create_api_key": "Nie można utworzyć tego klucza API.",
|
||||||
|
"error.invalid_theme": "Nieprawidłowy motyw.",
|
||||||
|
"error.invalid_language": "Nieprawidłowy język.",
|
||||||
|
"error.invalid_timezone": "Nieprawidłowa strefa czasowa.",
|
||||||
|
"error.invalid_entry_direction": "Nieprawidłowa kolejność sortowania.",
|
||||||
"form.feed.label.title": "Tytuł",
|
"form.feed.label.title": "Tytuł",
|
||||||
"form.feed.label.site_url": "URL strony",
|
"form.feed.label.site_url": "URL strony",
|
||||||
"form.feed.label.feed_url": "URL kanału",
|
"form.feed.label.feed_url": "URL kanału",
|
||||||
|
|
|
@ -239,6 +239,10 @@
|
||||||
"error.user_mandatory_fields": "O nome de usuário é obrigatório.",
|
"error.user_mandatory_fields": "O nome de usuário é obrigatório.",
|
||||||
"error.api_key_already_exists": "Essa chave de API já existe.",
|
"error.api_key_already_exists": "Essa chave de API já existe.",
|
||||||
"error.unable_to_create_api_key": "Não foi possível criar uma chave de API.",
|
"error.unable_to_create_api_key": "Não foi possível criar uma chave de API.",
|
||||||
|
"error.invalid_theme": "Tema inválido.",
|
||||||
|
"error.invalid_language": "Idioma inválido.",
|
||||||
|
"error.invalid_timezone": "Fuso horário inválido.",
|
||||||
|
"error.invalid_entry_direction": "Direção de entrada inválida.",
|
||||||
"form.feed.label.title": "Título",
|
"form.feed.label.title": "Título",
|
||||||
"form.feed.label.site_url": "URL do site",
|
"form.feed.label.site_url": "URL do site",
|
||||||
"form.feed.label.feed_url": "URL da fonte",
|
"form.feed.label.feed_url": "URL da fonte",
|
||||||
|
|
|
@ -241,6 +241,10 @@
|
||||||
"error.user_mandatory_fields": "Имя пользователя обязательно.",
|
"error.user_mandatory_fields": "Имя пользователя обязательно.",
|
||||||
"error.api_key_already_exists": "Этот ключ API уже существует.",
|
"error.api_key_already_exists": "Этот ключ API уже существует.",
|
||||||
"error.unable_to_create_api_key": "Невозможно создать этот ключ API.",
|
"error.unable_to_create_api_key": "Невозможно создать этот ключ API.",
|
||||||
|
"error.invalid_theme": "Неверная тема.",
|
||||||
|
"error.invalid_language": "Неверный язык.",
|
||||||
|
"error.invalid_timezone": "Неверный часовой пояс.",
|
||||||
|
"error.invalid_entry_direction": "Неверное направление входа.",
|
||||||
"form.feed.label.title": "Название",
|
"form.feed.label.title": "Название",
|
||||||
"form.feed.label.site_url": "URL сайта",
|
"form.feed.label.site_url": "URL сайта",
|
||||||
"form.feed.label.feed_url": "URL подписки",
|
"form.feed.label.feed_url": "URL подписки",
|
||||||
|
|
|
@ -237,6 +237,10 @@
|
||||||
"error.user_mandatory_fields": "必须填写用户名",
|
"error.user_mandatory_fields": "必须填写用户名",
|
||||||
"error.api_key_already_exists": "此API密钥已存在。",
|
"error.api_key_already_exists": "此API密钥已存在。",
|
||||||
"error.unable_to_create_api_key": "无法创建此API密钥。",
|
"error.unable_to_create_api_key": "无法创建此API密钥。",
|
||||||
|
"error.invalid_theme": "无效的主题。",
|
||||||
|
"error.invalid_language": "语言无效。",
|
||||||
|
"error.invalid_timezone": "无效的时区。",
|
||||||
|
"error.invalid_entry_direction": "无效的输入方向。",
|
||||||
"form.feed.label.title": "标题",
|
"form.feed.label.title": "标题",
|
||||||
"form.feed.label.site_url": "站点 URL",
|
"form.feed.label.site_url": "站点 URL",
|
||||||
"form.feed.label.feed_url": "源 URL",
|
"form.feed.label.feed_url": "源 URL",
|
||||||
|
|
|
@ -29,11 +29,11 @@ func TestAllKeysHaveValue(t *testing.T) {
|
||||||
switch value := v.(type) {
|
switch value := v.(type) {
|
||||||
case string:
|
case string:
|
||||||
if value == "" {
|
if value == "" {
|
||||||
t.Fatalf(`The key %q for the language %q have an empty string as value`, k, language)
|
t.Errorf(`The key %q for the language %q have an empty string as value`, k, language)
|
||||||
}
|
}
|
||||||
case []string:
|
case []string:
|
||||||
if len(value) == 0 {
|
if len(value) == 0 {
|
||||||
t.Fatalf(`The key %q for the language %q have an empty list as value`, k, language)
|
t.Errorf(`The key %q for the language %q have an empty list as value`, k, language)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
21
model/model.go
Normal file
21
model/model.go
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
// Copyright 2021 Frédéric Guillot. All rights reserved.
|
||||||
|
// Use of this source code is governed by the Apache 2.0
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package model // import "miniflux.app/model"
|
||||||
|
|
||||||
|
// OptionalString populates an optional string field.
|
||||||
|
func OptionalString(value string) *string {
|
||||||
|
if value != "" {
|
||||||
|
return &value
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptionalInt populates an optional int field.
|
||||||
|
func OptionalInt(value int) *int {
|
||||||
|
if value > 0 {
|
||||||
|
return &value
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -4,8 +4,6 @@
|
||||||
|
|
||||||
package model // import "miniflux.app/model"
|
package model // import "miniflux.app/model"
|
||||||
|
|
||||||
import "miniflux.app/errors"
|
|
||||||
|
|
||||||
// Themes returns the list of available themes.
|
// Themes returns the list of available themes.
|
||||||
func Themes() map[string]string {
|
func Themes() map[string]string {
|
||||||
return map[string]string{
|
return map[string]string{
|
||||||
|
@ -29,14 +27,3 @@ func ThemeColor(theme string) string {
|
||||||
return "#fff"
|
return "#fff"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateTheme validates theme value.
|
|
||||||
func ValidateTheme(theme string) error {
|
|
||||||
for key := range Themes() {
|
|
||||||
if key == theme {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return errors.NewLocalizedError("Invalid theme")
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,19 +0,0 @@
|
||||||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
|
||||||
// Use of this source code is governed by the Apache 2.0
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package model // import "miniflux.app/model"
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestValidateTheme(t *testing.T) {
|
|
||||||
for _, status := range []string{"light_serif", "dark_sans_serif", "system_serif"} {
|
|
||||||
if err := ValidateTheme(status); err != nil {
|
|
||||||
t.Error(`A valid theme should not generate any error`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := ValidateTheme("invalid"); err == nil {
|
|
||||||
t.Error(`An invalid theme should generate a error`)
|
|
||||||
}
|
|
||||||
}
|
|
108
model/user.go
108
model/user.go
|
@ -5,7 +5,6 @@
|
||||||
package model // import "miniflux.app/model"
|
package model // import "miniflux.app/model"
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"miniflux.app/timezone"
|
"miniflux.app/timezone"
|
||||||
|
@ -15,7 +14,7 @@ import (
|
||||||
type User struct {
|
type User struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
Password string `json:"password,omitempty"`
|
Password string `json:"-"`
|
||||||
IsAdmin bool `json:"is_admin"`
|
IsAdmin bool `json:"is_admin"`
|
||||||
Theme string `json:"theme"`
|
Theme string `json:"theme"`
|
||||||
Language string `json:"language"`
|
Language string `json:"language"`
|
||||||
|
@ -28,56 +27,93 @@ type User struct {
|
||||||
KeyboardShortcuts bool `json:"keyboard_shortcuts"`
|
KeyboardShortcuts bool `json:"keyboard_shortcuts"`
|
||||||
ShowReadingTime bool `json:"show_reading_time"`
|
ShowReadingTime bool `json:"show_reading_time"`
|
||||||
EntrySwipe bool `json:"entry_swipe"`
|
EntrySwipe bool `json:"entry_swipe"`
|
||||||
LastLoginAt *time.Time `json:"last_login_at,omitempty"`
|
LastLoginAt *time.Time `json:"last_login_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewUser returns a new User.
|
// UserCreationRequest represents the request to create a user.
|
||||||
func NewUser() *User {
|
type UserCreationRequest struct {
|
||||||
return &User{}
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
IsAdmin bool `json:"is_admin"`
|
||||||
|
GoogleID string `json:"google_id"`
|
||||||
|
OpenIDConnectID string `json:"openid_connect_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateUserCreation validates new user.
|
// UserModificationRequest represents the request to update a user.
|
||||||
func (u User) ValidateUserCreation() error {
|
type UserModificationRequest struct {
|
||||||
if err := u.ValidateUserLogin(); err != nil {
|
Username *string `json:"username"`
|
||||||
return err
|
Password *string `json:"password"`
|
||||||
}
|
Theme *string `json:"theme"`
|
||||||
|
Language *string `json:"language"`
|
||||||
return u.ValidatePassword()
|
Timezone *string `json:"timezone"`
|
||||||
|
EntryDirection *string `json:"entry_sorting_direction"`
|
||||||
|
Stylesheet *string `json:"stylesheet"`
|
||||||
|
GoogleID *string `json:"google_id"`
|
||||||
|
OpenIDConnectID *string `json:"openid_connect_id"`
|
||||||
|
EntriesPerPage *int `json:"entries_per_page"`
|
||||||
|
IsAdmin *bool `json:"is_admin"`
|
||||||
|
KeyboardShortcuts *bool `json:"keyboard_shortcuts"`
|
||||||
|
ShowReadingTime *bool `json:"show_reading_time"`
|
||||||
|
EntrySwipe *bool `json:"entry_swipe"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ValidateUserModification validates user modification payload.
|
// Patch updates the User object with the modification request.
|
||||||
func (u User) ValidateUserModification() error {
|
func (u *UserModificationRequest) Patch(user *User) {
|
||||||
if u.Theme != "" {
|
if u.Username != nil {
|
||||||
return ValidateTheme(u.Theme)
|
user.Username = *u.Username
|
||||||
}
|
}
|
||||||
|
|
||||||
if u.Password != "" {
|
if u.Password != nil {
|
||||||
return u.ValidatePassword()
|
user.Password = *u.Password
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
if u.IsAdmin != nil {
|
||||||
}
|
user.IsAdmin = *u.IsAdmin
|
||||||
|
|
||||||
// ValidateUserLogin validates user credential requirements.
|
|
||||||
func (u User) ValidateUserLogin() error {
|
|
||||||
if u.Username == "" {
|
|
||||||
return errors.New("The username is mandatory")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if u.Password == "" {
|
if u.Theme != nil {
|
||||||
return errors.New("The password is mandatory")
|
user.Theme = *u.Theme
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
if u.Language != nil {
|
||||||
}
|
user.Language = *u.Language
|
||||||
|
|
||||||
// ValidatePassword validates user password requirements.
|
|
||||||
func (u User) ValidatePassword() error {
|
|
||||||
if u.Password != "" && len(u.Password) < 6 {
|
|
||||||
return errors.New("The password must have at least 6 characters")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
if u.Timezone != nil {
|
||||||
|
user.Timezone = *u.Timezone
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.EntryDirection != nil {
|
||||||
|
user.EntryDirection = *u.EntryDirection
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.Stylesheet != nil {
|
||||||
|
user.Stylesheet = *u.Stylesheet
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.GoogleID != nil {
|
||||||
|
user.GoogleID = *u.GoogleID
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.OpenIDConnectID != nil {
|
||||||
|
user.OpenIDConnectID = *u.OpenIDConnectID
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.EntriesPerPage != nil {
|
||||||
|
user.EntriesPerPage = *u.EntriesPerPage
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.KeyboardShortcuts != nil {
|
||||||
|
user.KeyboardShortcuts = *u.KeyboardShortcuts
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.ShowReadingTime != nil {
|
||||||
|
user.ShowReadingTime = *u.ShowReadingTime
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.EntrySwipe != nil {
|
||||||
|
user.EntrySwipe = *u.EntrySwipe
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UseTimezone converts last login date to the given timezone.
|
// UseTimezone converts last login date to the given timezone.
|
||||||
|
|
|
@ -1,61 +0,0 @@
|
||||||
// Copyright 2017 Frédéric Guillot. All rights reserved.
|
|
||||||
// Use of this source code is governed by the Apache 2.0
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package model // import "miniflux.app/model"
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestValidateUserCreation(t *testing.T) {
|
|
||||||
user := &User{}
|
|
||||||
if err := user.ValidateUserCreation(); err == nil {
|
|
||||||
t.Error(`An empty user should generate an error`)
|
|
||||||
}
|
|
||||||
|
|
||||||
user = &User{Username: "test", Password: ""}
|
|
||||||
if err := user.ValidateUserCreation(); err == nil {
|
|
||||||
t.Error(`User without password should generate an error`)
|
|
||||||
}
|
|
||||||
|
|
||||||
user = &User{Username: "test", Password: "a"}
|
|
||||||
if err := user.ValidateUserCreation(); err == nil {
|
|
||||||
t.Error(`Passwords shorter than 6 characters should generate an error`)
|
|
||||||
}
|
|
||||||
|
|
||||||
user = &User{Username: "", Password: "secret"}
|
|
||||||
if err := user.ValidateUserCreation(); err == nil {
|
|
||||||
t.Error(`An empty username should generate an error`)
|
|
||||||
}
|
|
||||||
|
|
||||||
user = &User{Username: "test", Password: "secret"}
|
|
||||||
if err := user.ValidateUserCreation(); err != nil {
|
|
||||||
t.Error(`A valid user should not generate any error`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidateUserModification(t *testing.T) {
|
|
||||||
user := &User{}
|
|
||||||
if err := user.ValidateUserModification(); err != nil {
|
|
||||||
t.Error(`There is no changes, so we should not have an error`)
|
|
||||||
}
|
|
||||||
|
|
||||||
user = &User{Theme: "system_serif"}
|
|
||||||
if err := user.ValidateUserModification(); err != nil {
|
|
||||||
t.Error(`A valid theme should not generate any errors`)
|
|
||||||
}
|
|
||||||
|
|
||||||
user = &User{Theme: "invalid theme"}
|
|
||||||
if err := user.ValidateUserModification(); err == nil {
|
|
||||||
t.Error(`An invalid theme should generate an error`)
|
|
||||||
}
|
|
||||||
|
|
||||||
user = &User{Password: "test123"}
|
|
||||||
if err := user.ValidateUserModification(); err != nil {
|
|
||||||
t.Error(`A valid password should not generate any errors`)
|
|
||||||
}
|
|
||||||
|
|
||||||
user = &User{Password: "a"}
|
|
||||||
if err := user.ValidateUserModification(); err == nil {
|
|
||||||
t.Error(`An invalid password should generate an error`)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -57,6 +57,10 @@ func (g *googleProvider) GetProfile(ctx context.Context, code string) (*Profile,
|
||||||
return profile, nil
|
return profile, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (g *googleProvider) PopulateUserCreationWithProfileID(user *model.UserCreationRequest, profile *Profile) {
|
||||||
|
user.GoogleID = profile.ID
|
||||||
|
}
|
||||||
|
|
||||||
func (g *googleProvider) PopulateUserWithProfileID(user *model.User, profile *Profile) {
|
func (g *googleProvider) PopulateUserWithProfileID(user *model.User, profile *Profile) {
|
||||||
user.GoogleID = profile.ID
|
user.GoogleID = profile.ID
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,10 @@ func (o *oidcProvider) GetProfile(ctx context.Context, code string) (*Profile, e
|
||||||
return profile, nil
|
return profile, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (o *oidcProvider) PopulateUserCreationWithProfileID(user *model.UserCreationRequest, profile *Profile) {
|
||||||
|
user.OpenIDConnectID = profile.ID
|
||||||
|
}
|
||||||
|
|
||||||
func (o *oidcProvider) PopulateUserWithProfileID(user *model.User, profile *Profile) {
|
func (o *oidcProvider) PopulateUserWithProfileID(user *model.User, profile *Profile) {
|
||||||
user.OpenIDConnectID = profile.ID
|
user.OpenIDConnectID = profile.ID
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ type Provider interface {
|
||||||
GetUserExtraKey() string
|
GetUserExtraKey() string
|
||||||
GetRedirectURL(state string) string
|
GetRedirectURL(state string) string
|
||||||
GetProfile(ctx context.Context, code string) (*Profile, error)
|
GetProfile(ctx context.Context, code string) (*Profile, error)
|
||||||
|
PopulateUserCreationWithProfileID(user *model.UserCreationRequest, profile *Profile)
|
||||||
PopulateUserWithProfileID(user *model.User, profile *Profile)
|
PopulateUserWithProfileID(user *model.User, profile *Profile)
|
||||||
UnsetUserProfileID(user *model.User)
|
UnsetUserProfileID(user *model.User)
|
||||||
}
|
}
|
||||||
|
|
|
@ -166,13 +166,17 @@ func (e *EntryQueryBuilder) WithDirection(direction string) *EntryQueryBuilder {
|
||||||
|
|
||||||
// WithLimit set the limit.
|
// WithLimit set the limit.
|
||||||
func (e *EntryQueryBuilder) WithLimit(limit int) *EntryQueryBuilder {
|
func (e *EntryQueryBuilder) WithLimit(limit int) *EntryQueryBuilder {
|
||||||
e.limit = limit
|
if limit > 0 {
|
||||||
|
e.limit = limit
|
||||||
|
}
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithOffset set the offset.
|
// WithOffset set the offset.
|
||||||
func (e *EntryQueryBuilder) WithOffset(offset int) *EntryQueryBuilder {
|
func (e *EntryQueryBuilder) WithOffset(offset int) *EntryQueryBuilder {
|
||||||
e.offset = offset
|
if offset > 0 {
|
||||||
|
e.offset = offset
|
||||||
|
}
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -370,11 +374,11 @@ func (e *EntryQueryBuilder) buildSorting() string {
|
||||||
parts = append(parts, fmt.Sprintf(`%s`, e.direction))
|
parts = append(parts, fmt.Sprintf(`%s`, e.direction))
|
||||||
}
|
}
|
||||||
|
|
||||||
if e.limit != 0 {
|
if e.limit > 0 {
|
||||||
parts = append(parts, fmt.Sprintf(`LIMIT %d`, e.limit))
|
parts = append(parts, fmt.Sprintf(`LIMIT %d`, e.limit))
|
||||||
}
|
}
|
||||||
|
|
||||||
if e.offset != 0 {
|
if e.offset > 0 {
|
||||||
parts = append(parts, fmt.Sprintf(`OFFSET %d`, e.offset))
|
parts = append(parts, fmt.Sprintf(`OFFSET %d`, e.offset))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -54,12 +54,13 @@ func (s *Storage) AnotherUserExists(userID int64, username string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateUser creates a new user.
|
// CreateUser creates a new user.
|
||||||
func (s *Storage) CreateUser(user *model.User) (err error) {
|
func (s *Storage) CreateUser(userCreationRequest *model.UserCreationRequest) (*model.User, error) {
|
||||||
hashedPassword := ""
|
var hashedPassword string
|
||||||
if user.Password != "" {
|
if userCreationRequest.Password != "" {
|
||||||
hashedPassword, err = hashPassword(user.Password)
|
var err error
|
||||||
|
hashedPassword, err = hashPassword(userCreationRequest.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -87,10 +88,18 @@ func (s *Storage) CreateUser(user *model.User) (err error) {
|
||||||
|
|
||||||
tx, err := s.db.Begin()
|
tx, err := s.db.Begin()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf(`store: unable to start transaction: %v`, err)
|
return nil, fmt.Errorf(`store: unable to start transaction: %v`, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tx.QueryRow(query, user.Username, hashedPassword, user.IsAdmin, user.GoogleID, user.OpenIDConnectID).Scan(
|
var user model.User
|
||||||
|
err = tx.QueryRow(
|
||||||
|
query,
|
||||||
|
userCreationRequest.Username,
|
||||||
|
hashedPassword,
|
||||||
|
userCreationRequest.IsAdmin,
|
||||||
|
userCreationRequest.GoogleID,
|
||||||
|
userCreationRequest.OpenIDConnectID,
|
||||||
|
).Scan(
|
||||||
&user.ID,
|
&user.ID,
|
||||||
&user.Username,
|
&user.Username,
|
||||||
&user.IsAdmin,
|
&user.IsAdmin,
|
||||||
|
@ -108,26 +117,26 @@ func (s *Storage) CreateUser(user *model.User) (err error) {
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return fmt.Errorf(`store: unable to create user: %v`, err)
|
return nil, fmt.Errorf(`store: unable to create user: %v`, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = tx.Exec(`INSERT INTO categories (user_id, title) VALUES ($1, $2)`, user.ID, "All")
|
_, err = tx.Exec(`INSERT INTO categories (user_id, title) VALUES ($1, $2)`, user.ID, "All")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return fmt.Errorf(`store: unable to create user default category: %v`, err)
|
return nil, fmt.Errorf(`store: unable to create user default category: %v`, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = tx.Exec(`INSERT INTO integrations (user_id) VALUES ($1)`, user.ID)
|
_, err = tx.Exec(`INSERT INTO integrations (user_id) VALUES ($1)`, user.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return fmt.Errorf(`store: unable to create integration row: %v`, err)
|
return nil, fmt.Errorf(`store: unable to create integration row: %v`, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tx.Commit(); err != nil {
|
if err := tx.Commit(); err != nil {
|
||||||
return fmt.Errorf(`store: unable to commit transaction: %v`, err)
|
return nil, fmt.Errorf(`store: unable to commit transaction: %v`, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateUser updates a user.
|
// UpdateUser updates a user.
|
||||||
|
@ -353,7 +362,7 @@ func (s *Storage) UserByAPIKey(token string) (*model.User, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Storage) fetchUser(query string, args ...interface{}) (*model.User, error) {
|
func (s *Storage) fetchUser(query string, args ...interface{}) (*model.User, error) {
|
||||||
user := model.NewUser()
|
var user model.User
|
||||||
err := s.db.QueryRow(query, args...).Scan(
|
err := s.db.QueryRow(query, args...).Scan(
|
||||||
&user.ID,
|
&user.ID,
|
||||||
&user.Username,
|
&user.Username,
|
||||||
|
@ -378,7 +387,7 @@ func (s *Storage) fetchUser(query string, args ...interface{}) (*model.User, err
|
||||||
return nil, fmt.Errorf(`store: unable to fetch user: %v`, err)
|
return nil, fmt.Errorf(`store: unable to fetch user: %v`, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return user, nil
|
return &user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveUser deletes a user.
|
// RemoveUser deletes a user.
|
||||||
|
@ -446,7 +455,7 @@ func (s *Storage) Users() (model.Users, error) {
|
||||||
|
|
||||||
var users model.Users
|
var users model.Users
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
user := model.NewUser()
|
var user model.User
|
||||||
err := rows.Scan(
|
err := rows.Scan(
|
||||||
&user.ID,
|
&user.ID,
|
||||||
&user.Username,
|
&user.Username,
|
||||||
|
@ -469,7 +478,7 @@ func (s *Storage) Users() (model.Users, error) {
|
||||||
return nil, fmt.Errorf(`store: unable to fetch users row: %v`, err)
|
return nil, fmt.Errorf(`store: unable to fetch users row: %v`, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
users = append(users, user)
|
users = append(users, &user)
|
||||||
}
|
}
|
||||||
|
|
||||||
return users, nil
|
return users, nil
|
||||||
|
|
|
@ -312,13 +312,103 @@ func TestUpdateUserThemeWithInvalidValue(t *testing.T) {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
theme := "something that doesn't exists"
|
theme := "invalid"
|
||||||
_, err = client.UpdateUser(user.ID, &miniflux.UserModificationRequest{Theme: &theme})
|
_, err = client.UpdateUser(user.ID, &miniflux.UserModificationRequest{Theme: &theme})
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal(`Updating a user Theme with an invalid value should raise an error`)
|
t.Fatal(`Updating a user Theme with an invalid value should raise an error`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUpdateUserLanguageWithInvalidValue(t *testing.T) {
|
||||||
|
username := getRandomUsername()
|
||||||
|
client := miniflux.New(testBaseURL, testAdminUsername, testAdminPassword)
|
||||||
|
user, err := client.CreateUser(username, testStandardPassword, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
language := "invalid"
|
||||||
|
_, err = client.UpdateUser(user.ID, &miniflux.UserModificationRequest{Language: &language})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal(`Updating a user language with an invalid value should raise an error`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateUserTimezoneWithInvalidValue(t *testing.T) {
|
||||||
|
username := getRandomUsername()
|
||||||
|
client := miniflux.New(testBaseURL, testAdminUsername, testAdminPassword)
|
||||||
|
user, err := client.CreateUser(username, testStandardPassword, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
timezone := "invalid"
|
||||||
|
_, err = client.UpdateUser(user.ID, &miniflux.UserModificationRequest{Timezone: &timezone})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal(`Updating a user timezone with an invalid value should raise an error`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateUserEntriesPerPageWithInvalidValue(t *testing.T) {
|
||||||
|
username := getRandomUsername()
|
||||||
|
client := miniflux.New(testBaseURL, testAdminUsername, testAdminPassword)
|
||||||
|
user, err := client.CreateUser(username, testStandardPassword, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entriesPerPage := -5
|
||||||
|
_, err = client.UpdateUser(user.ID, &miniflux.UserModificationRequest{EntriesPerPage: &entriesPerPage})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal(`Updating a user EntriesPerPage with an invalid value should raise an error`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateUserEntryDirectionWithInvalidValue(t *testing.T) {
|
||||||
|
username := getRandomUsername()
|
||||||
|
client := miniflux.New(testBaseURL, testAdminUsername, testAdminPassword)
|
||||||
|
user, err := client.CreateUser(username, testStandardPassword, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entryDirection := "invalid"
|
||||||
|
_, err = client.UpdateUser(user.ID, &miniflux.UserModificationRequest{EntryDirection: &entryDirection})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal(`Updating a user EntryDirection with an invalid value should raise an error`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateUserPasswordWithInvalidValue(t *testing.T) {
|
||||||
|
username := getRandomUsername()
|
||||||
|
client := miniflux.New(testBaseURL, testAdminUsername, testAdminPassword)
|
||||||
|
user, err := client.CreateUser(username, testStandardPassword, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
password := "short"
|
||||||
|
_, err = client.UpdateUser(user.ID, &miniflux.UserModificationRequest{Password: &password})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal(`Updating a user password with an invalid value should raise an error`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateUserWithEmptyUsernameValue(t *testing.T) {
|
||||||
|
username := getRandomUsername()
|
||||||
|
client := miniflux.New(testBaseURL, testAdminUsername, testAdminPassword)
|
||||||
|
user, err := client.CreateUser(username, testStandardPassword, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
newUsername := ""
|
||||||
|
_, err = client.UpdateUser(user.ID, &miniflux.UserModificationRequest{Username: &newUsername})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal(`Updating a user with an empty username should raise an error`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCannotCreateDuplicateUser(t *testing.T) {
|
func TestCannotCreateDuplicateUser(t *testing.T) {
|
||||||
username := getRandomUsername()
|
username := getRandomUsername()
|
||||||
client := miniflux.New(testBaseURL, testAdminUsername, testAdminPassword)
|
client := miniflux.New(testBaseURL, testAdminUsername, testAdminPassword)
|
||||||
|
@ -329,7 +419,7 @@ func TestCannotCreateDuplicateUser(t *testing.T) {
|
||||||
|
|
||||||
_, err = client.CreateUser(username, testStandardPassword, false)
|
_, err = client.CreateUser(username, testStandardPassword, false)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal(`Duplicate users should not be allowed`)
|
t.Fatal(`Duplicated users should not be allowed`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -54,10 +54,6 @@ func (s *SettingsForm) Validate() error {
|
||||||
return errors.NewLocalizedError("error.settings_mandatory_fields")
|
return errors.NewLocalizedError("error.settings_mandatory_fields")
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.EntriesPerPage < 1 {
|
|
||||||
return errors.NewLocalizedError("error.entries_per_page_invalid")
|
|
||||||
}
|
|
||||||
|
|
||||||
if s.Confirmation == "" {
|
if s.Confirmation == "" {
|
||||||
// Firefox insists on auto-completing the password field.
|
// Firefox insists on auto-completing the password field.
|
||||||
// If the confirmation field is blank, the user probably
|
// If the confirmation field is blank, the user probably
|
||||||
|
@ -67,10 +63,6 @@ func (s *SettingsForm) Validate() error {
|
||||||
if s.Password != s.Confirmation {
|
if s.Password != s.Confirmation {
|
||||||
return errors.NewLocalizedError("error.different_passwords")
|
return errors.NewLocalizedError("error.different_passwords")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(s.Password) < 6 {
|
|
||||||
return errors.NewLocalizedError("error.password_min_length")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -61,21 +61,3 @@ func TestConfirmationIncorrect(t *testing.T) {
|
||||||
t.Error("Validate should return an error")
|
t.Error("Validate should return an error")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEntriesPerPageNotValid(t *testing.T) {
|
|
||||||
settings := &SettingsForm{
|
|
||||||
Username: "user",
|
|
||||||
Password: "hunter2",
|
|
||||||
Confirmation: "hunter2",
|
|
||||||
Theme: "default",
|
|
||||||
Language: "en_US",
|
|
||||||
Timezone: "UTC",
|
|
||||||
EntryDirection: "asc",
|
|
||||||
EntriesPerPage: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := settings.Validate()
|
|
||||||
if err == nil {
|
|
||||||
t.Error("Validate should return an error")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -29,10 +29,6 @@ func (u UserForm) ValidateCreation() error {
|
||||||
return errors.NewLocalizedError("error.different_passwords")
|
return errors.NewLocalizedError("error.different_passwords")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(u.Password) < 6 {
|
|
||||||
return errors.NewLocalizedError("error.password_min_length")
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -55,15 +51,6 @@ func (u UserForm) ValidateModification() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ToUser returns a User from the form values.
|
|
||||||
func (u UserForm) ToUser() *model.User {
|
|
||||||
return &model.User{
|
|
||||||
Username: u.Username,
|
|
||||||
Password: u.Password,
|
|
||||||
IsAdmin: u.IsAdmin,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge updates the fields of the given user.
|
// Merge updates the fields of the given user.
|
||||||
func (u UserForm) Merge(user *model.User) *model.User {
|
func (u UserForm) Merge(user *model.User) *model.User {
|
||||||
user.Username = u.Username
|
user.Username = u.Username
|
||||||
|
|
|
@ -177,10 +177,8 @@ func (m *middleware) handleAuthProxy(next http.Handler) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
sess := session.New(m.store, request.SessionID(r))
|
|
||||||
clientIP := request.ClientIP(r)
|
clientIP := request.ClientIP(r)
|
||||||
|
logger.Info("[AuthProxy] [ClientIP=%s] Received authenticated requested for %q", clientIP, username)
|
||||||
logger.Info("[AuthProxy] Successful auth for %s", username)
|
|
||||||
|
|
||||||
user, err := m.store.UserByUsername(username)
|
user, err := m.store.UserByUsername(username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -189,16 +187,14 @@ func (m *middleware) handleAuthProxy(next http.Handler) http.Handler {
|
||||||
}
|
}
|
||||||
|
|
||||||
if user == nil {
|
if user == nil {
|
||||||
|
logger.Error("[AuthProxy] [ClientIP=%s] %q doesn't exist", clientIP, username)
|
||||||
|
|
||||||
if !config.Opts.IsAuthProxyUserCreationAllowed() {
|
if !config.Opts.IsAuthProxyUserCreationAllowed() {
|
||||||
html.Forbidden(w, r)
|
html.Forbidden(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user = model.NewUser()
|
if user, err = m.store.CreateUser(&model.UserCreationRequest{Username: username}); err != nil {
|
||||||
user.Username = username
|
|
||||||
user.IsAdmin = false
|
|
||||||
|
|
||||||
if err := m.store.CreateUser(user); err != nil {
|
|
||||||
html.ServerError(w, r, err)
|
html.ServerError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -210,9 +206,11 @@ func (m *middleware) handleAuthProxy(next http.Handler) http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("[AuthProxy] username=%s just logged in", user.Username)
|
logger.Info("[AuthProxy] [ClientIP=%s] username=%s just logged in", clientIP, user.Username)
|
||||||
|
|
||||||
m.store.SetLastLogin(user.ID)
|
m.store.SetLastLogin(user.ID)
|
||||||
|
|
||||||
|
sess := session.New(m.store, request.SessionID(r))
|
||||||
sess.SetLanguage(user.Language)
|
sess.SetLanguage(user.Language)
|
||||||
sess.SetTheme(user.Theme)
|
sess.SetTheme(user.Theme)
|
||||||
|
|
||||||
|
|
|
@ -103,12 +103,11 @@ func (h *handler) oauth2Callback(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
user = model.NewUser()
|
userCreationRequest := &model.UserCreationRequest{Username: profile.Username}
|
||||||
user.Username = profile.Username
|
authProvider.PopulateUserCreationWithProfileID(userCreationRequest, profile)
|
||||||
user.IsAdmin = false
|
|
||||||
authProvider.PopulateUserWithProfileID(user, profile)
|
|
||||||
|
|
||||||
if err := h.store.CreateUser(user); err != nil {
|
user, err = h.store.CreateUser(userCreationRequest)
|
||||||
|
if err != nil {
|
||||||
html.ServerError(w, r, err)
|
html.ServerError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,7 @@ import (
|
||||||
"miniflux.app/ui/form"
|
"miniflux.app/ui/form"
|
||||||
"miniflux.app/ui/session"
|
"miniflux.app/ui/session"
|
||||||
"miniflux.app/ui/view"
|
"miniflux.app/ui/view"
|
||||||
|
"miniflux.app/validator"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (h *handler) updateSettings(w http.ResponseWriter, r *http.Request) {
|
func (h *handler) updateSettings(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -51,8 +52,18 @@ func (h *handler) updateSettings(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.store.AnotherUserExists(loggedUser.ID, settingsForm.Username) {
|
userModificationRequest := &model.UserModificationRequest{
|
||||||
view.Set("errorMessage", "error.user_already_exists")
|
Username: model.OptionalString(settingsForm.Username),
|
||||||
|
Password: model.OptionalString(settingsForm.Password),
|
||||||
|
Theme: model.OptionalString(settingsForm.Theme),
|
||||||
|
Language: model.OptionalString(settingsForm.Language),
|
||||||
|
Timezone: model.OptionalString(settingsForm.Timezone),
|
||||||
|
EntryDirection: model.OptionalString(settingsForm.EntryDirection),
|
||||||
|
EntriesPerPage: model.OptionalInt(settingsForm.EntriesPerPage),
|
||||||
|
}
|
||||||
|
|
||||||
|
if validationErr := validator.ValidateUserModification(h.store, loggedUser.ID, userModificationRequest); validationErr != nil {
|
||||||
|
view.Set("errorMessage", validationErr.TranslationKey)
|
||||||
html.OK(w, r, view.Render("settings"))
|
html.OK(w, r, view.Render("settings"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,9 +54,9 @@ func (h *handler) showWebManifest(w http.ResponseWriter, r *http.Request) {
|
||||||
ThemeColor: themeColor,
|
ThemeColor: themeColor,
|
||||||
BackgroundColor: themeColor,
|
BackgroundColor: themeColor,
|
||||||
Icons: []webManifestIcon{
|
Icons: []webManifestIcon{
|
||||||
webManifestIcon{Source: route.Path(h.router, "appIcon", "filename", "icon-120.png"), Sizes: "120x120", Type: "image/png"},
|
{Source: route.Path(h.router, "appIcon", "filename", "icon-120.png"), Sizes: "120x120", Type: "image/png"},
|
||||||
webManifestIcon{Source: route.Path(h.router, "appIcon", "filename", "icon-192.png"), Sizes: "192x192", Type: "image/png"},
|
{Source: route.Path(h.router, "appIcon", "filename", "icon-192.png"), Sizes: "192x192", Type: "image/png"},
|
||||||
webManifestIcon{Source: route.Path(h.router, "appIcon", "filename", "icon-512.png"), Sizes: "512x512", Type: "image/png"},
|
{Source: route.Path(h.router, "appIcon", "filename", "icon-512.png"), Sizes: "512x512", Type: "image/png"},
|
||||||
},
|
},
|
||||||
ShareTarget: webManifestShareTarget{
|
ShareTarget: webManifestShareTarget{
|
||||||
Action: route.Path(h.router, "bookmarklet"),
|
Action: route.Path(h.router, "bookmarklet"),
|
||||||
|
|
|
@ -11,9 +11,11 @@ import (
|
||||||
"miniflux.app/http/response/html"
|
"miniflux.app/http/response/html"
|
||||||
"miniflux.app/http/route"
|
"miniflux.app/http/route"
|
||||||
"miniflux.app/logger"
|
"miniflux.app/logger"
|
||||||
|
"miniflux.app/model"
|
||||||
"miniflux.app/ui/form"
|
"miniflux.app/ui/form"
|
||||||
"miniflux.app/ui/session"
|
"miniflux.app/ui/session"
|
||||||
"miniflux.app/ui/view"
|
"miniflux.app/ui/view"
|
||||||
|
"miniflux.app/validator"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (h *handler) saveUser(w http.ResponseWriter, r *http.Request) {
|
func (h *handler) saveUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
@ -50,8 +52,19 @@ func (h *handler) saveUser(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
newUser := userForm.ToUser()
|
userCreationRequest := &model.UserCreationRequest{
|
||||||
if err := h.store.CreateUser(newUser); err != nil {
|
Username: userForm.Username,
|
||||||
|
Password: userForm.Password,
|
||||||
|
IsAdmin: userForm.IsAdmin,
|
||||||
|
}
|
||||||
|
|
||||||
|
if validationErr := validator.ValidateUserCreationWithPassword(h.store, userCreationRequest); validationErr != nil {
|
||||||
|
view.Set("errorMessage", validationErr.TranslationKey)
|
||||||
|
html.OK(w, r, view.Render("create_user"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := h.store.CreateUser(userCreationRequest); err != nil {
|
||||||
logger.Error("[UI:SaveUser] %v", err)
|
logger.Error("[UI:SaveUser] %v", err)
|
||||||
view.Set("errorMessage", "error.unable_to_create_user")
|
view.Set("errorMessage", "error.unable_to_create_user")
|
||||||
html.OK(w, r, view.Render("create_user"))
|
html.OK(w, r, view.Render("create_user"))
|
||||||
|
|
|
@ -17,13 +17,13 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (h *handler) updateUser(w http.ResponseWriter, r *http.Request) {
|
func (h *handler) updateUser(w http.ResponseWriter, r *http.Request) {
|
||||||
user, err := h.store.UserByID(request.UserID(r))
|
loggedUser, err := h.store.UserByID(request.UserID(r))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
html.ServerError(w, r, err)
|
html.ServerError(w, r, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !user.IsAdmin {
|
if !loggedUser.IsAdmin {
|
||||||
html.Forbidden(w, r)
|
html.Forbidden(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -45,9 +45,9 @@ func (h *handler) updateUser(w http.ResponseWriter, r *http.Request) {
|
||||||
sess := session.New(h.store, request.SessionID(r))
|
sess := session.New(h.store, request.SessionID(r))
|
||||||
view := view.New(h.tpl, r, sess)
|
view := view.New(h.tpl, r, sess)
|
||||||
view.Set("menu", "settings")
|
view.Set("menu", "settings")
|
||||||
view.Set("user", user)
|
view.Set("user", loggedUser)
|
||||||
view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
|
view.Set("countUnread", h.store.CountUnreadEntries(loggedUser.ID))
|
||||||
view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(user.ID))
|
view.Set("countErrorFeeds", h.store.CountUserFeedsWithErrors(loggedUser.ID))
|
||||||
view.Set("selected_user", selectedUser)
|
view.Set("selected_user", selectedUser)
|
||||||
view.Set("form", userForm)
|
view.Set("form", userForm)
|
||||||
|
|
||||||
|
|
126
validator/user.go
Normal file
126
validator/user.go
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
// Copyright 2021 Frédéric Guillot. All rights reserved.
|
||||||
|
// Use of this source code is governed by the Apache 2.0
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package validator // import "miniflux.app/validator"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"miniflux.app/locale"
|
||||||
|
"miniflux.app/model"
|
||||||
|
"miniflux.app/storage"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidateUserCreationWithPassword validates user creation with a password.
|
||||||
|
func ValidateUserCreationWithPassword(store *storage.Storage, request *model.UserCreationRequest) *ValidationError {
|
||||||
|
if request.Username == "" {
|
||||||
|
return NewValidationError("error.user_mandatory_fields")
|
||||||
|
}
|
||||||
|
|
||||||
|
if store.UserExists(request.Username) {
|
||||||
|
return NewValidationError("error.user_already_exists")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validatePassword(request.Password); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateUserModification validates user modifications.
|
||||||
|
func ValidateUserModification(store *storage.Storage, userID int64, changes *model.UserModificationRequest) *ValidationError {
|
||||||
|
if changes.Username != nil {
|
||||||
|
if *changes.Username == "" {
|
||||||
|
return NewValidationError("error.user_mandatory_fields")
|
||||||
|
} else if store.AnotherUserExists(userID, *changes.Username) {
|
||||||
|
return NewValidationError("error.user_already_exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if changes.Password != nil {
|
||||||
|
if err := validatePassword(*changes.Password); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if changes.Theme != nil {
|
||||||
|
if err := validateTheme(*changes.Theme); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if changes.Language != nil {
|
||||||
|
if err := validateLanguage(*changes.Language); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if changes.Timezone != nil {
|
||||||
|
if err := validateTimezone(store, *changes.Timezone); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if changes.EntryDirection != nil {
|
||||||
|
if err := validateEntryDirection(*changes.EntryDirection); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if changes.EntriesPerPage != nil {
|
||||||
|
if err := validateEntriesPerPage(*changes.EntriesPerPage); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validatePassword(password string) *ValidationError {
|
||||||
|
if len(password) < 6 {
|
||||||
|
return NewValidationError("error.password_min_length")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateTheme(theme string) *ValidationError {
|
||||||
|
themes := model.Themes()
|
||||||
|
if _, found := themes[theme]; !found {
|
||||||
|
return NewValidationError("error.invalid_theme")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateLanguage(language string) *ValidationError {
|
||||||
|
languages := locale.AvailableLanguages()
|
||||||
|
if _, found := languages[language]; !found {
|
||||||
|
return NewValidationError("error.invalid_language")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateTimezone(store *storage.Storage, timezone string) *ValidationError {
|
||||||
|
timezones, err := store.Timezones()
|
||||||
|
if err != nil {
|
||||||
|
return NewValidationError(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, found := timezones[timezone]; !found {
|
||||||
|
return NewValidationError("error.invalid_timezone")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateEntryDirection(direction string) *ValidationError {
|
||||||
|
if direction != "asc" && direction != "desc" {
|
||||||
|
return NewValidationError("error.invalid_entry_direction")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateEntriesPerPage(entriesPerPage int) *ValidationError {
|
||||||
|
if entriesPerPage < 1 {
|
||||||
|
return NewValidationError("error.entries_per_page_invalid")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
29
validator/validator.go
Normal file
29
validator/validator.go
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
// Copyright 2021 Frédéric Guillot. All rights reserved.
|
||||||
|
// Use of this source code is governed by the Apache 2.0
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package validator // import "miniflux.app/validator"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"miniflux.app/locale"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ValidationError represents a validation error.
|
||||||
|
type ValidationError struct {
|
||||||
|
TranslationKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewValidationError initializes a validation error.
|
||||||
|
func NewValidationError(translationKey string) *ValidationError {
|
||||||
|
return &ValidationError{TranslationKey: translationKey}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ValidationError) String() string {
|
||||||
|
return locale.NewPrinter("en_US").Printf(v.TranslationKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ValidationError) Error() error {
|
||||||
|
return errors.New(v.String())
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue