From 25cc0d24477d6bbecc1406721663a2693aa15b39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Guillot?= Date: Sun, 1 Mar 2020 17:38:29 -0800 Subject: [PATCH] Add per-application API Keys --- api/api.go | 4 +- api/middleware.go | 56 +++++++-- client/README.md | 4 + client/client.go | 13 +- client/request.go | 9 +- database/migration.go | 2 +- database/sql.go | 12 ++ database/sql/schema_version_27.sql | 10 ++ locale/translations.go | 150 ++++++++++++++++++++++-- locale/translations/de_DE.json | 13 ++ locale/translations/en_US.json | 13 ++ locale/translations/es_ES.json | 13 ++ locale/translations/fr_FR.json | 13 ++ locale/translations/it_IT.json | 13 ++ locale/translations/ja_JP.json | 13 ++ locale/translations/nl_NL.json | 13 ++ locale/translations/pl_PL.json | 13 ++ locale/translations/ru_RU.json | 13 ++ locale/translations/zh_CN.json | 13 ++ model/api_key.go | 33 ++++++ storage/api_key.go | 104 ++++++++++++++++ storage/user.go | 24 ++++ template/common.go | 23 ++-- template/html/api_keys.html | 72 ++++++++++++ template/html/common/settings_menu.html | 6 +- template/html/create_api_key.html | 23 ++++ template/html/integrations.html | 15 --- template/html/users.html | 5 + template/views.go | 123 ++++++++++++++++--- ui/api_key_create.go | 34 ++++++ ui/api_key_list.go | 39 ++++++ ui/api_key_remove.go | 24 ++++ ui/api_key_save.go | 58 +++++++++ ui/form/api_key.go | 32 +++++ ui/ui.go | 6 + 35 files changed, 940 insertions(+), 71 deletions(-) create mode 100644 database/sql/schema_version_27.sql create mode 100644 model/api_key.go create mode 100644 storage/api_key.go create mode 100644 template/html/api_keys.html create mode 100644 template/html/create_api_key.html create mode 100644 ui/api_key_create.go create mode 100644 ui/api_key_list.go create mode 100644 ui/api_key_remove.go create mode 100644 ui/api_key_save.go create mode 100644 ui/form/api_key.go diff --git a/api/api.go b/api/api.go index f86d6e6a..71122ceb 100644 --- a/api/api.go +++ b/api/api.go @@ -17,7 +17,9 @@ func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool, feedHa handler := &handler{store, pool, feedHandler} sr := router.PathPrefix("/v1").Subrouter() - sr.Use(newMiddleware(store).serve) + middleware := newMiddleware(store) + sr.Use(middleware.apiKeyAuth) + sr.Use(middleware.basicAuth) sr.HandleFunc("/users", handler.createUser).Methods("POST") sr.HandleFunc("/users", handler.users).Methods("GET") sr.HandleFunc("/users/{userID:[0-9]+}", handler.userByID).Methods("GET") diff --git a/api/middleware.go b/api/middleware.go index 09a6f8cf..4c6f95ef 100644 --- a/api/middleware.go +++ b/api/middleware.go @@ -22,39 +22,81 @@ func newMiddleware(s *storage.Storage) *middleware { return &middleware{s} } -// BasicAuth handles HTTP basic authentication. -func (m *middleware) serve(next http.Handler) http.Handler { +func (m *middleware) apiKeyAuth(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + clientIP := request.ClientIP(r) + token := r.Header.Get("X-Auth-Token") + + if token == "" { + logger.Debug("[API][TokenAuth] [ClientIP=%s] No API Key provided, go to the next middleware", clientIP) + next.ServeHTTP(w, r) + return + } + + user, err := m.store.UserByAPIKey(token) + if err != nil { + logger.Error("[API][TokenAuth] %v", err) + json.ServerError(w, r, err) + return + } + + if user == nil { + logger.Error("[API][TokenAuth] [ClientIP=%s] No user found with the given API key", clientIP) + json.Unauthorized(w, r) + return + } + + logger.Info("[API][TokenAuth] [ClientIP=%s] User authenticated: %s", clientIP, user.Username) + m.store.SetLastLogin(user.ID) + m.store.SetAPIKeyUsedTimestamp(user.ID, token) + + ctx := r.Context() + ctx = context.WithValue(ctx, request.UserIDContextKey, user.ID) + ctx = context.WithValue(ctx, request.UserTimezoneContextKey, user.Timezone) + ctx = context.WithValue(ctx, request.IsAdminUserContextKey, user.IsAdmin) + ctx = context.WithValue(ctx, request.IsAuthenticatedContextKey, true) + + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func (m *middleware) basicAuth(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if request.IsAuthenticated(r) { + next.ServeHTTP(w, r) + return + } + w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) clientIP := request.ClientIP(r) username, password, authOK := r.BasicAuth() if !authOK { - logger.Debug("[API] No authentication headers sent") + logger.Debug("[API][BasicAuth] [ClientIP=%s] No authentication headers sent", clientIP) json.Unauthorized(w, r) return } if err := m.store.CheckPassword(username, password); err != nil { - logger.Error("[API] [ClientIP=%s] Invalid username or password: %s", clientIP, username) + logger.Error("[API][BasicAuth] [ClientIP=%s] Invalid username or password: %s", clientIP, username) json.Unauthorized(w, r) return } user, err := m.store.UserByUsername(username) if err != nil { - logger.Error("[API] %v", err) + logger.Error("[API][BasicAuth] %v", err) json.ServerError(w, r, err) return } if user == nil { - logger.Error("[API] [ClientIP=%s] User not found: %s", clientIP, username) + logger.Error("[API][BasicAuth] [ClientIP=%s] User not found: %s", clientIP, username) json.Unauthorized(w, r) return } - logger.Info("[API] User authenticated: %s", username) + logger.Info("[API][BasicAuth] [ClientIP=%s] User authenticated: %s", clientIP, username) m.store.SetLastLogin(user.ID) ctx := r.Context() diff --git a/client/README.md b/client/README.md index 1791d7f2..dfeb97cb 100644 --- a/client/README.md +++ b/client/README.md @@ -24,8 +24,12 @@ import ( ) func main() { + // Authentication with username/password: client := miniflux.New("https://api.example.org", "admin", "secret") + // Authentication with an API Key: + client := miniflux.New("https://api.example.org", "my-secret-token") + // Fetch all feeds. feeds, err := client.Feeds() if err != nil { diff --git a/client/client.go b/client/client.go index 10683720..e2be7278 100644 --- a/client/client.go +++ b/client/client.go @@ -18,6 +18,14 @@ type Client struct { request *request } +// New returns a new Miniflux client. +func New(endpoint string, credentials ...string) *Client { + if len(credentials) == 2 { + return &Client{request: &request{endpoint: endpoint, username: credentials[0], password: credentials[1]}} + } + return &Client{request: &request{endpoint: endpoint, apiKey: credentials[0]}} +} + // Me returns the logged user information. func (c *Client) Me() (*User, error) { body, err := c.request.Get("/v1/me") @@ -448,11 +456,6 @@ func (c *Client) ToggleBookmark(entryID int64) error { return nil } -// New returns a new Miniflux client. -func New(endpoint, username, password string) *Client { - return &Client{request: &request{endpoint: endpoint, username: username, password: password}} -} - func buildFilterQueryString(path string, filter *Filter) string { if filter != nil { values := url.Values{} diff --git a/client/request.go b/client/request.go index adeda71b..03372973 100644 --- a/client/request.go +++ b/client/request.go @@ -38,6 +38,7 @@ type request struct { endpoint string username string password string + apiKey string } func (r *request) Get(path string) (io.ReadCloser, error) { @@ -75,7 +76,10 @@ func (r *request) execute(method, path string, data interface{}) (io.ReadCloser, Method: method, Header: r.buildHeaders(), } - request.SetBasicAuth(r.username, r.password) + + if r.username != "" && r.password != "" { + request.SetBasicAuth(r.username, r.password) + } if data != nil { switch data.(type) { @@ -131,6 +135,9 @@ func (r *request) buildHeaders() http.Header { headers.Add("User-Agent", userAgent) headers.Add("Content-Type", "application/json") headers.Add("Accept", "application/json") + if r.apiKey != "" { + headers.Add("X-Auth-Token", r.apiKey) + } return headers } diff --git a/database/migration.go b/database/migration.go index 60700627..fff815bc 100644 --- a/database/migration.go +++ b/database/migration.go @@ -12,7 +12,7 @@ import ( "miniflux.app/logger" ) -const schemaVersion = 26 +const schemaVersion = 27 // Migrate executes database migrations. func Migrate(db *sql.DB) { diff --git a/database/sql.go b/database/sql.go index 4deaff42..d79a4529 100644 --- a/database/sql.go +++ b/database/sql.go @@ -156,6 +156,17 @@ UPDATE users SET theme='dark_serif' WHERE theme='black'; "schema_version_26": `alter table entries add column changed_at timestamp with time zone; update entries set changed_at = published_at; alter table entries alter column changed_at set not null; +`, + "schema_version_27": `create table api_keys ( + id serial not null, + user_id int not null references users(id) on delete cascade, + token text not null unique, + description text not null, + last_used_at timestamp with time zone, + created_at timestamp with time zone default now(), + primary key(id), + unique (user_id, description) +); `, "schema_version_3": `create table tokens ( id text not null, @@ -211,6 +222,7 @@ var SqlMapChecksums = map[string]string{ "schema_version_24": "1224754c5b9c6b4038599852bbe72656d21b09cb018d3970bd7c00f0019845bf", "schema_version_25": "5262d2d4c88d637b6603a1fcd4f68ad257bd59bd1adf89c58a18ee87b12050d7", "schema_version_26": "64f14add40691f18f514ac0eed10cd9b19c83a35e5c3d8e0bce667e0ceca9094", + "schema_version_27": "4235396b37fd7f52ff6f7526416042bb1649701233e2d99f0bcd583834a0a967", "schema_version_3": "a54745dbc1c51c000f74d4e5068f1e2f43e83309f023415b1749a47d5c1e0f12", "schema_version_4": "216ea3a7d3e1704e40c797b5dc47456517c27dbb6ca98bf88812f4f63d74b5d9", "schema_version_5": "46397e2f5f2c82116786127e9f6a403e975b14d2ca7b652a48cd1ba843e6a27c", diff --git a/database/sql/schema_version_27.sql b/database/sql/schema_version_27.sql new file mode 100644 index 00000000..ea4f3d08 --- /dev/null +++ b/database/sql/schema_version_27.sql @@ -0,0 +1,10 @@ +create table api_keys ( + id serial not null, + user_id int not null references users(id) on delete cascade, + token text not null unique, + description text not null, + last_used_at timestamp with time zone, + created_at timestamp with time zone default now(), + primary key(id), + unique (user_id, description) +); diff --git a/locale/translations.go b/locale/translations.go index 18b629b1..ca350e07 100644 --- a/locale/translations.go +++ b/locale/translations.go @@ -49,6 +49,8 @@ var translations = map[string]string{ "menu.add_user": "Benutzer anlegen", "menu.flush_history": "Verlauf leeren", "menu.feed_entries": "Artikel", + "menu.api_keys": "API-Schlüssel", + "menu.create_api_key": "Erstellen Sie einen neuen API-Schlüssel", "search.label": "Suche", "search.placeholder": "Suche...", "pagination.next": "Nächste", @@ -176,6 +178,14 @@ var translations = map[string]string{ "page.sessions.table.user_agent": "Benutzeragent", "page.sessions.table.actions": "Aktionen", "page.sessions.table.current_session": "Aktuelle Sitzung", + "page.api_keys.title": "API-Schlüssel", + "page.api_keys.table.description": "Beschreibung", + "page.api_keys.table.token": "Zeichen", + "page.api_keys.table.last_used_at": "Zuletzt verwendeten", + "page.api_keys.table.created_at": "Erstellungsdatum", + "page.api_keys.table.actions": "Aktionen", + "page.api_keys.never_used": "Nie benutzt", + "page.new_api_key.title": "Neuer API-Schlüssel", "alert.no_bookmark": "Es existiert derzeit kein Lesezeichen.", "alert.no_category": "Es ist keine Kategorie vorhanden.", "alert.no_category_entry": "Es befindet sich kein Artikel in dieser Kategorie.", @@ -213,6 +223,8 @@ var translations = map[string]string{ "error.settings_mandatory_fields": "Die Felder für Benutzername, Thema, Sprache und Zeitzone sind obligatorisch.", "error.feed_mandatory_fields": "Die URL und die Kategorie sind obligatorisch.", "error.user_mandatory_fields": "Der Benutzername ist obligatorisch.", + "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.", "form.feed.label.title": "Titel", "form.feed.label.site_url": "Webseite-URL", "form.feed.label.feed_url": "Abonnement-URL", @@ -262,6 +274,7 @@ var translations = map[string]string{ "form.integration.nunux_keeper_activate": "Artikel in Nunux Keeper speichern", "form.integration.nunux_keeper_endpoint": "Nunux Keeper API-Endpunkt", "form.integration.nunux_keeper_api_key": "Nunux Keeper API-Schlüssel", + "form.api_key.label.description": "API-Schlüsselbezeichnung", "form.submit.loading": "Lade...", "form.submit.saving": "Speichern...", "time_elapsed.not_yet": "noch nicht", @@ -359,6 +372,8 @@ var translations = map[string]string{ "menu.add_user": "Add user", "menu.flush_history": "Flush history", "menu.feed_entries": "Entries", + "menu.api_keys": "API Keys", + "menu.create_api_key": "Create a new API key", "search.label": "Search", "search.placeholder": "Search...", "pagination.next": "Next", @@ -486,6 +501,14 @@ var translations = map[string]string{ "page.sessions.table.user_agent": "User Agent", "page.sessions.table.actions": "Actions", "page.sessions.table.current_session": "Current Session", + "page.api_keys.title": "API Keys", + "page.api_keys.table.description": "Description", + "page.api_keys.table.token": "Token", + "page.api_keys.table.last_used_at": "Last Used", + "page.api_keys.table.created_at": "Creation Date", + "page.api_keys.table.actions": "Actions", + "page.api_keys.never_used": "Never Used", + "page.new_api_key.title": "New API Key", "alert.no_bookmark": "There is no bookmark at the moment.", "alert.no_category": "There is no category.", "alert.no_category_entry": "There are no articles in this category.", @@ -523,6 +546,8 @@ var translations = map[string]string{ "error.settings_mandatory_fields": "The username, theme, language and timezone fields are mandatory.", "error.feed_mandatory_fields": "The URL and the category are mandatory.", "error.user_mandatory_fields": "The username is mandatory.", + "error.api_key_already_exists": "This API Key already exists.", + "error.unable_to_create_api_key": "Unable to create this API Key.", "form.feed.label.title": "Title", "form.feed.label.site_url": "Site URL", "form.feed.label.feed_url": "Feed URL", @@ -572,6 +597,7 @@ var translations = map[string]string{ "form.integration.nunux_keeper_activate": "Save articles to Nunux Keeper", "form.integration.nunux_keeper_endpoint": "Nunux Keeper API Endpoint", "form.integration.nunux_keeper_api_key": "Nunux Keeper API key", + "form.api_key.label.description": "API Key Label", "form.submit.loading": "Loading...", "form.submit.saving": "Saving...", "time_elapsed.not_yet": "not yet", @@ -649,6 +675,8 @@ var translations = map[string]string{ "menu.add_user": "Agregar usuario", "menu.flush_history": "Borrar historial", "menu.feed_entries": "Artículos", + "menu.api_keys": "Claves API", + "menu.create_api_key": "Crear una nueva clave API", "search.label": "Buscar", "search.placeholder": "Búsqueda...", "pagination.next": "Siguiente", @@ -776,6 +804,14 @@ var translations = map[string]string{ "page.sessions.table.user_agent": "Agente de usuario", "page.sessions.table.actions": "Acciones", "page.sessions.table.current_session": "Sesión actual", + "page.api_keys.title": "Claves API", + "page.api_keys.table.description": "Descripción", + "page.api_keys.table.token": "simbólico", + "page.api_keys.table.last_used_at": "Último utilizado", + "page.api_keys.table.created_at": "Fecha de creación", + "page.api_keys.table.actions": "Acciones", + "page.api_keys.never_used": "Nunca usado", + "page.new_api_key.title": "Nueva clave API", "alert.no_bookmark": "No hay marcador en este momento.", "alert.no_category": "No hay categoría.", "alert.no_category_entry": "No hay artículos en esta categoria.", @@ -813,6 +849,8 @@ var translations = map[string]string{ "error.settings_mandatory_fields": "Los campos de nombre de usuario, tema, idioma y zona horaria son obligatorios.", "error.feed_mandatory_fields": "Los campos de URL y categoría son obligatorios.", "error.user_mandatory_fields": "El nombre de usuario es obligatorio.", + "error.api_key_already_exists": "Esta clave API ya existe.", + "error.unable_to_create_api_key": "No se puede crear esta clave API.", "form.feed.label.title": "Título", "form.feed.label.site_url": "URL del sitio", "form.feed.label.feed_url": "URL de la fuente", @@ -862,6 +900,7 @@ var translations = map[string]string{ "form.integration.nunux_keeper_activate": "Guardar artículos a Nunux Keeper", "form.integration.nunux_keeper_endpoint": "Extremo de API de Nunux Keeper", "form.integration.nunux_keeper_api_key": "Clave de API de Nunux Keeper", + "form.api_key.label.description": "Etiqueta de clave API", "form.submit.loading": "Cargando...", "form.submit.saving": "Guardando...", "time_elapsed.not_yet": "todavía no", @@ -939,6 +978,8 @@ var translations = map[string]string{ "menu.add_user": "Ajouter un utilisateur", "menu.flush_history": "Supprimer l'historique", "menu.feed_entries": "Articles", + "menu.api_keys": "Clés d'API", + "menu.create_api_key": "Créer une nouvelle clé d'API", "search.label": "Recherche", "search.placeholder": "Recherche...", "pagination.next": "Suivant", @@ -1066,6 +1107,14 @@ var translations = map[string]string{ "page.sessions.table.user_agent": "Navigateur Web", "page.sessions.table.actions": "Actions", "page.sessions.table.current_session": "Session actuelle", + "page.api_keys.title": "Clés d'API", + "page.api_keys.table.description": "Description", + "page.api_keys.table.token": "Jeton", + "page.api_keys.table.last_used_at": "Dernière utilisation", + "page.api_keys.table.created_at": "Date de création", + "page.api_keys.table.actions": "Actions", + "page.api_keys.never_used": "Jamais utilisé", + "page.new_api_key.title": "Nouvelle clé d'API", "alert.no_bookmark": "Il n'y a aucun favoris pour le moment.", "alert.no_category": "Il n'y a aucune catégorie.", "alert.no_category_entry": "Il n'y a aucun article dans cette catégorie.", @@ -1103,6 +1152,8 @@ var translations = map[string]string{ "error.settings_mandatory_fields": "Le nom d'utilisateur, le thème, la langue et le fuseau horaire sont obligatoire.", "error.feed_mandatory_fields": "L'URL et la catégorie sont obligatoire.", "error.user_mandatory_fields": "Le nom d'utilisateur est obligatoire.", + "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.", "form.feed.label.title": "Titre", "form.feed.label.site_url": "URL du site web", "form.feed.label.feed_url": "URL du flux", @@ -1152,6 +1203,7 @@ var translations = map[string]string{ "form.integration.nunux_keeper_activate": "Sauvegarder les articles vers Nunux Keeper", "form.integration.nunux_keeper_endpoint": "URL de l'API de Nunux Keeper", "form.integration.nunux_keeper_api_key": "Clé d'API de Nunux Keeper", + "form.api_key.label.description": "Libellé de la clé d'API", "form.submit.loading": "Chargement...", "form.submit.saving": "Sauvegarde en cours...", "time_elapsed.not_yet": "pas encore", @@ -1249,6 +1301,8 @@ var translations = map[string]string{ "menu.add_user": "Aggiungi utente", "menu.flush_history": "Svuota la cronologia", "menu.feed_entries": "Articoli", + "menu.api_keys": "Chiavi API", + "menu.create_api_key": "Crea una nuova chiave API", "search.label": "Cerca", "search.placeholder": "Cerca...", "pagination.next": "Successivo", @@ -1376,6 +1430,14 @@ var translations = map[string]string{ "page.sessions.table.user_agent": "User Agent", "page.sessions.table.actions": "Azioni", "page.sessions.table.current_session": "Sessione corrente", + "page.api_keys.title": "Chiavi API", + "page.api_keys.table.description": "Descrizione", + "page.api_keys.table.token": "Gettone", + "page.api_keys.table.last_used_at": "Ultimo uso", + "page.api_keys.table.created_at": "Data di creazione", + "page.api_keys.table.actions": "Azioni", + "page.api_keys.never_used": "Mai usato", + "page.new_api_key.title": "Nuova chiave API", "alert.no_bookmark": "Nessun preferito disponibile.", "alert.no_category": "Nessuna categoria disponibile.", "alert.no_category_entry": "Questa categoria non contiene alcun articolo.", @@ -1413,6 +1475,8 @@ var translations = map[string]string{ "error.settings_mandatory_fields": "Il nome utente, il tema, la lingua ed il fuso orario sono campi obbligatori.", "error.feed_mandatory_fields": "L'URL e la categoria sono obbligatori.", "error.user_mandatory_fields": "Il nome utente è obbligatorio.", + "error.api_key_already_exists": "Questa chiave API esiste già.", + "error.unable_to_create_api_key": "Impossibile creare questa chiave API.", "form.feed.label.title": "Titolo", "form.feed.label.site_url": "URL del sito", "form.feed.label.feed_url": "URL del feed", @@ -1462,6 +1526,7 @@ var translations = map[string]string{ "form.integration.nunux_keeper_activate": "Salva gli articoli su Nunux Keeper", "form.integration.nunux_keeper_endpoint": "Endpoint dell'API di Nunux Keeper", "form.integration.nunux_keeper_api_key": "API key dell'account Nunux Keeper", + "form.api_key.label.description": "Etichetta chiave API", "form.submit.loading": "Caricamento in corso...", "form.submit.saving": "Salvataggio in corso...", "time_elapsed.not_yet": "non ancora", @@ -1539,6 +1604,8 @@ var translations = map[string]string{ "menu.add_user": "ユーザーを追加", "menu.flush_history": "履歴を更新", "menu.feed_entries": "記事一覧", + "menu.api_keys": "APIキー", + "menu.create_api_key": "新しいAPIキーを作成する", "search.label": "検索", "search.placeholder": "…を検索", "pagination.next": "次", @@ -1666,6 +1733,14 @@ var translations = map[string]string{ "page.sessions.table.user_agent": "User Agent", "page.sessions.table.actions": "アクション", "page.sessions.table.current_session": "現在のセッション", + "page.api_keys.title": "APIキー", + "page.api_keys.table.description": "説明", + "page.api_keys.table.token": "トークン", + "page.api_keys.table.last_used_at": "最終使用", + "page.api_keys.table.created_at": "作成日", + "page.api_keys.table.actions": "アクション", + "page.api_keys.never_used": "使われたことがない", + "page.new_api_key.title": "新しいAPIキー", "alert.no_bookmark": "現在星付きはありません。", "alert.no_category": "カテゴリが存在しません。", "alert.no_category_entry": "このカテゴリには記事がありません。", @@ -1703,6 +1778,8 @@ var translations = map[string]string{ "error.settings_mandatory_fields": "ユーザー名、テーマ、言語、タイムゾーンの全てが必要です。", "error.feed_mandatory_fields": "URL と カテゴリが必要です。", "error.user_mandatory_fields": "ユーザー名が必要です。", + "error.api_key_already_exists": "このAPIキーは既に存在します。", + "error.unable_to_create_api_key": "このAPIキーを作成できません。", "form.feed.label.title": "タイトル", "form.feed.label.site_url": "サイト URL", "form.feed.label.feed_url": "フィード URL", @@ -1752,6 +1829,7 @@ var translations = map[string]string{ "form.integration.nunux_keeper_activate": "Nunux Keeper に記事を保存する", "form.integration.nunux_keeper_endpoint": "Nunux Keeper の API Endpoint", "form.integration.nunux_keeper_api_key": "Nunux Keeper の API key", + "form.api_key.label.description": "APIキーラベル", "form.submit.loading": "読み込み中…", "form.submit.saving": "保存中…", "time_elapsed.not_yet": "未来", @@ -1829,6 +1907,8 @@ var translations = map[string]string{ "menu.add_user": "Gebruiker toevoegen", "menu.flush_history": "Verwijder geschiedenis", "menu.feed_entries": "Lidwoord", + "menu.api_keys": "API-sleutels", + "menu.create_api_key": "Maak een nieuwe API-sleutel", "search.label": "Zoeken", "search.placeholder": "Zoeken...", "pagination.next": "Volgende", @@ -1956,6 +2036,14 @@ var translations = map[string]string{ "page.sessions.table.user_agent": "User-agent", "page.sessions.table.actions": "Acties", "page.sessions.table.current_session": "Huidige sessie", + "page.api_keys.title": "API-sleutels", + "page.api_keys.table.description": "Beschrijving", + "page.api_keys.table.token": "Blijk", + "page.api_keys.table.last_used_at": "Laatst gebruikt", + "page.api_keys.table.created_at": "Aanmaakdatum", + "page.api_keys.table.actions": "Acties", + "page.api_keys.never_used": "Nooit gebruikt", + "page.new_api_key.title": "Nieuwe API-sleutel", "alert.no_bookmark": "Er zijn op dit moment geen favorieten.", "alert.no_category": "Er zijn geen categorieën.", "alert.no_category_entry": "Deze categorie bevat geen feeds.", @@ -1993,6 +2081,8 @@ var translations = map[string]string{ "error.settings_mandatory_fields": "Gebruikersnaam, skin, taal en tijdzone zijn verplicht.", "error.feed_mandatory_fields": "The URL en de categorie zijn verplicht.", "error.user_mandatory_fields": "Gebruikersnaam is verplicht", + "error.api_key_already_exists": "This API Key already exists.", + "error.unable_to_create_api_key": "Kan deze API-sleutel niet maken.", "form.feed.label.title": "Naam", "form.feed.label.site_url": "Website URL", "form.feed.label.feed_url": "Feed URL", @@ -2042,6 +2132,7 @@ var translations = map[string]string{ "form.integration.nunux_keeper_activate": "Opslaan naar Nunux Keeper", "form.integration.nunux_keeper_endpoint": "Nunux Keeper URL", "form.integration.nunux_keeper_api_key": "Nunux Keeper API-sleutel", + "form.api_key.label.description": "API-sleutellabel", "form.submit.loading": "Laden...", "form.submit.saving": "Opslaag...", "time_elapsed.not_yet": "in de toekomst", @@ -2137,6 +2228,8 @@ var translations = map[string]string{ "menu.add_user": "Dodaj użytkownika", "menu.flush_history": "Usuń historię", "menu.feed_entries": "Artykuły", + "menu.api_keys": "Klucze API", + "menu.create_api_key": "Utwórz nowy klucz API", "search.label": "Szukaj", "search.placeholder": "Szukaj...", "pagination.next": "Następny", @@ -2266,6 +2359,14 @@ var translations = map[string]string{ "page.sessions.table.user_agent": "Agent użytkownika", "page.sessions.table.actions": "Działania", "page.sessions.table.current_session": "Bieżąca sesja", + "page.api_keys.title": "Klucze API", + "page.api_keys.table.description": "Opis", + "page.api_keys.table.token": "Znak", + "page.api_keys.table.last_used_at": "Ostatnio używane", + "page.api_keys.table.created_at": "Data utworzenia", + "page.api_keys.table.actions": "Działania", + "page.api_keys.never_used": "Nigdy nie używany", + "page.new_api_key.title": "Nowy klucz API", "alert.no_bookmark": "Obecnie nie ma żadnych zakładek.", "alert.no_category": "Nie ma żadnej kategorii!", "alert.no_category_entry": "W tej kategorii nie ma żadnych artykułów", @@ -2303,6 +2404,8 @@ var translations = map[string]string{ "error.settings_mandatory_fields": "Pola nazwy użytkownika, tematu, języka i strefy czasowej są obowiązkowe.", "error.feed_mandatory_fields": "URL i kategoria są obowiązkowe.", "error.user_mandatory_fields": "Nazwa użytkownika jest obowiązkowa.", + "error.api_key_already_exists": "Deze API-sleutel bestaat al.", + "error.unable_to_create_api_key": "Nie można utworzyć tego klucza API.", "form.feed.label.title": "Tytuł", "form.feed.label.site_url": "URL strony", "form.feed.label.feed_url": "URL kanału", @@ -2352,6 +2455,7 @@ var translations = map[string]string{ "form.integration.nunux_keeper_activate": "Zapisz artykuly do Nunux Keeper", "form.integration.nunux_keeper_endpoint": "Nunux Keeper URL", "form.integration.nunux_keeper_api_key": "Nunux Keeper API key", + "form.api_key.label.description": "Etykieta klucza API", "form.submit.loading": "Ładowanie...", "form.submit.saving": "Zapisywanie...", "time_elapsed.not_yet": "jeszcze nie", @@ -2453,6 +2557,8 @@ var translations = map[string]string{ "menu.add_user": "Добавить пользователя", "menu.flush_history": "Отчистить историю", "menu.feed_entries": "статьи", + "menu.api_keys": "API-ключи", + "menu.create_api_key": "Создать новый ключ API", "search.label": "Поиск", "search.placeholder": "Поиск…", "pagination.next": "Следующая", @@ -2582,6 +2688,14 @@ var translations = map[string]string{ "page.sessions.table.user_agent": "User Agent", "page.sessions.table.actions": "Действия", "page.sessions.table.current_session": "Текущая сессия", + "page.api_keys.title": "API-ключи", + "page.api_keys.table.description": "описание", + "page.api_keys.table.token": "знак", + "page.api_keys.table.last_used_at": "Последний раз был использован", + "page.api_keys.table.created_at": "Дата создания", + "page.api_keys.table.actions": "Действия", + "page.api_keys.never_used": "Никогда не использовался", + "page.new_api_key.title": "Новый ключ API", "alert.no_bookmark": "Нет закладок на данный момент.", "alert.no_category": "Категории отсутствуют.", "alert.no_category_entry": "В этой категории нет статей.", @@ -2619,6 +2733,8 @@ var translations = map[string]string{ "error.settings_mandatory_fields": "Имя пользователя, тема, язык и часовой пояс обязательны.", "error.feed_mandatory_fields": "URL и категория обязательны.", "error.user_mandatory_fields": "Имя пользователя обязательно.", + "error.api_key_already_exists": "Этот ключ API уже существует.", + "error.unable_to_create_api_key": "Невозможно создать этот ключ API.", "form.feed.label.title": "Название", "form.feed.label.site_url": "URL сайта", "form.feed.label.feed_url": "URL подписки", @@ -2668,6 +2784,7 @@ var translations = map[string]string{ "form.integration.nunux_keeper_activate": "Сохранять статьи в Nunux Keeper", "form.integration.nunux_keeper_endpoint": "Конечная точка Nunux Keeper API", "form.integration.nunux_keeper_api_key": "Nunux Keeper API key", + "form.api_key.label.description": "APIキーラベル", "form.submit.loading": "Загрузка…", "form.submit.saving": "Сохранение…", "time_elapsed.not_yet": "ещё нет", @@ -2751,6 +2868,8 @@ var translations = map[string]string{ "menu.add_user": "新建用户", "menu.flush_history": "清理历史", "menu.feed_entries": "文章", + "menu.api_keys": "API密钥", + "menu.create_api_key": "创建一个新的API密钥", "search.label": "搜索", "search.placeholder": "搜索…", "pagination.next": "下一页", @@ -2876,6 +2995,14 @@ var translations = map[string]string{ "page.sessions.table.user_agent": "User-Agent", "page.sessions.table.actions": "操作", "page.sessions.table.current_session": "当前会话", + "page.api_keys.title": "API密钥", + "page.api_keys.table.description": "描述", + "page.api_keys.table.token": "代币", + "page.api_keys.table.last_used_at": "最后使用", + "page.api_keys.table.created_at": "创立日期", + "page.api_keys.table.actions": "操作", + "page.api_keys.never_used": "没用过", + "page.new_api_key.title": "新的API密钥", "alert.no_bookmark": "目前没有书签", "alert.no_category": "目前没有分类", "alert.no_category_entry": "该分类下没有文章", @@ -2913,6 +3040,8 @@ var translations = map[string]string{ "error.settings_mandatory_fields": "必须填写用户名、主题、语言以及时区", "error.feed_mandatory_fields": "必须填写 URL 和分类", "error.user_mandatory_fields": "必须填写用户名", + "error.api_key_already_exists": "此API密钥已存在。", + "error.unable_to_create_api_key": "无法创建此API密钥。", "form.feed.label.title": "标题", "form.feed.label.site_url": "站点 URL", "form.feed.label.feed_url": "源 URL", @@ -2962,6 +3091,7 @@ var translations = map[string]string{ "form.integration.nunux_keeper_activate": "保存文章到 Nunux Keeper", "form.integration.nunux_keeper_endpoint": "Nunux Keeper API Endpoint", "form.integration.nunux_keeper_api_key": "Nunux Keeper API 密钥", + "form.api_key.label.description": "API密钥标签", "form.submit.loading": "载入中…", "form.submit.saving": "保存中…", "time_elapsed.not_yet": "尚未", @@ -3009,14 +3139,14 @@ var translations = map[string]string{ } var translationsChecksums = map[string]string{ - "de_DE": "2269a754f4af398fe6af44324eda8ed7daa708a11eb50f7bb0b779d6ed482ad8", - "en_US": "5256a170a5be7ba8e79ed0897475c416ce755797e9ab1173375dc5113515c2d8", - "es_ES": "19f48e44422712789a3736399e5d5fe9f88cc7fa3e4c228fdceec03f5d3666cd", - "fr_FR": "e6032bfec564e86f12182ea79f0ed61ec133ed0c04525571ab71e923cc5de276", - "it_IT": "39a466b969ffadf27e4bc3054ab36fe8b2bceb0d9c0a68d940d76a418d999073", - "ja_JP": "598e7257528a90125c14c5169663d44d2a7a0afb86354fe654bc68469216251d", - "nl_NL": "fc10720566f37e88da60add9eaefa6f79cb6b021e9f3c192e50dfc5720553d69", - "pl_PL": "fc99fbde29904f3680e95ed337e7d9b2c0755cc8137c2694d8b781c91007ae19", - "ru_RU": "a01fc70baedd9555370e29827ef8c9aba32a4fb8f07942feb7474bcac232a2fe", - "zh_CN": "3bd2c9841413c072d1977dc500d8adecef4f947b28f3a8d3e8d4f0e5c39584ad", + "de_DE": "75ccff01dcd27613e2d130c5b6abdb6bb2645029c93373c7b96d8754298002cd", + "en_US": "f6ac2959fbe86b273ca3cd95031741dbfc4db25e8b61d6b29b798a9faefae4c6", + "es_ES": "a3a494acf1864b2cc6573f9627e5bd2f07fa96a14a39619f310e87e66a4f2c01", + "fr_FR": "9162d348af1c6d30bb6f16bb85468d394a353e9def08cf77adc47404889e6e78", + "it_IT": "ad12b1282ed9b3d1a785f92af70c07f3d7aecf49e8a5d1f023742636b24a366b", + "ja_JP": "a9994611dc3b6a6dd763b6bd1c89bc7c5ec9985a04059f6c45342077d42a3e05", + "nl_NL": "54e9b6cd6758ee3e699028104f25704d6569e5ed8793ff17e817ad80f1ef7bd2", + "pl_PL": "6a95a4f7e8bce0d0d0e0f56d46e69b4577a44609d15511d9fa11c81cb981b5d7", + "ru_RU": "cb024cd742298206634be390a19b7371a797ab8484615a69af7d8fdbea9b58f8", + "zh_CN": "a5f32c5e4714bce8638f7fd19b6c3e54937d9ab00b08ab655076d7be35ef76bd", } diff --git a/locale/translations/de_DE.json b/locale/translations/de_DE.json index 7e0bfbc4..b1278038 100644 --- a/locale/translations/de_DE.json +++ b/locale/translations/de_DE.json @@ -44,6 +44,8 @@ "menu.add_user": "Benutzer anlegen", "menu.flush_history": "Verlauf leeren", "menu.feed_entries": "Artikel", + "menu.api_keys": "API-Schlüssel", + "menu.create_api_key": "Erstellen Sie einen neuen API-Schlüssel", "search.label": "Suche", "search.placeholder": "Suche...", "pagination.next": "Nächste", @@ -171,6 +173,14 @@ "page.sessions.table.user_agent": "Benutzeragent", "page.sessions.table.actions": "Aktionen", "page.sessions.table.current_session": "Aktuelle Sitzung", + "page.api_keys.title": "API-Schlüssel", + "page.api_keys.table.description": "Beschreibung", + "page.api_keys.table.token": "Zeichen", + "page.api_keys.table.last_used_at": "Zuletzt verwendeten", + "page.api_keys.table.created_at": "Erstellungsdatum", + "page.api_keys.table.actions": "Aktionen", + "page.api_keys.never_used": "Nie benutzt", + "page.new_api_key.title": "Neuer API-Schlüssel", "alert.no_bookmark": "Es existiert derzeit kein Lesezeichen.", "alert.no_category": "Es ist keine Kategorie vorhanden.", "alert.no_category_entry": "Es befindet sich kein Artikel in dieser Kategorie.", @@ -208,6 +218,8 @@ "error.settings_mandatory_fields": "Die Felder für Benutzername, Thema, Sprache und Zeitzone sind obligatorisch.", "error.feed_mandatory_fields": "Die URL und die Kategorie sind obligatorisch.", "error.user_mandatory_fields": "Der Benutzername ist obligatorisch.", + "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.", "form.feed.label.title": "Titel", "form.feed.label.site_url": "Webseite-URL", "form.feed.label.feed_url": "Abonnement-URL", @@ -257,6 +269,7 @@ "form.integration.nunux_keeper_activate": "Artikel in Nunux Keeper speichern", "form.integration.nunux_keeper_endpoint": "Nunux Keeper API-Endpunkt", "form.integration.nunux_keeper_api_key": "Nunux Keeper API-Schlüssel", + "form.api_key.label.description": "API-Schlüsselbezeichnung", "form.submit.loading": "Lade...", "form.submit.saving": "Speichern...", "time_elapsed.not_yet": "noch nicht", diff --git a/locale/translations/en_US.json b/locale/translations/en_US.json index a6349db5..bf516453 100644 --- a/locale/translations/en_US.json +++ b/locale/translations/en_US.json @@ -44,6 +44,8 @@ "menu.add_user": "Add user", "menu.flush_history": "Flush history", "menu.feed_entries": "Entries", + "menu.api_keys": "API Keys", + "menu.create_api_key": "Create a new API key", "search.label": "Search", "search.placeholder": "Search...", "pagination.next": "Next", @@ -171,6 +173,14 @@ "page.sessions.table.user_agent": "User Agent", "page.sessions.table.actions": "Actions", "page.sessions.table.current_session": "Current Session", + "page.api_keys.title": "API Keys", + "page.api_keys.table.description": "Description", + "page.api_keys.table.token": "Token", + "page.api_keys.table.last_used_at": "Last Used", + "page.api_keys.table.created_at": "Creation Date", + "page.api_keys.table.actions": "Actions", + "page.api_keys.never_used": "Never Used", + "page.new_api_key.title": "New API Key", "alert.no_bookmark": "There is no bookmark at the moment.", "alert.no_category": "There is no category.", "alert.no_category_entry": "There are no articles in this category.", @@ -208,6 +218,8 @@ "error.settings_mandatory_fields": "The username, theme, language and timezone fields are mandatory.", "error.feed_mandatory_fields": "The URL and the category are mandatory.", "error.user_mandatory_fields": "The username is mandatory.", + "error.api_key_already_exists": "This API Key already exists.", + "error.unable_to_create_api_key": "Unable to create this API Key.", "form.feed.label.title": "Title", "form.feed.label.site_url": "Site URL", "form.feed.label.feed_url": "Feed URL", @@ -257,6 +269,7 @@ "form.integration.nunux_keeper_activate": "Save articles to Nunux Keeper", "form.integration.nunux_keeper_endpoint": "Nunux Keeper API Endpoint", "form.integration.nunux_keeper_api_key": "Nunux Keeper API key", + "form.api_key.label.description": "API Key Label", "form.submit.loading": "Loading...", "form.submit.saving": "Saving...", "time_elapsed.not_yet": "not yet", diff --git a/locale/translations/es_ES.json b/locale/translations/es_ES.json index 7e672cd1..bc68e44e 100644 --- a/locale/translations/es_ES.json +++ b/locale/translations/es_ES.json @@ -44,6 +44,8 @@ "menu.add_user": "Agregar usuario", "menu.flush_history": "Borrar historial", "menu.feed_entries": "Artículos", + "menu.api_keys": "Claves API", + "menu.create_api_key": "Crear una nueva clave API", "search.label": "Buscar", "search.placeholder": "Búsqueda...", "pagination.next": "Siguiente", @@ -171,6 +173,14 @@ "page.sessions.table.user_agent": "Agente de usuario", "page.sessions.table.actions": "Acciones", "page.sessions.table.current_session": "Sesión actual", + "page.api_keys.title": "Claves API", + "page.api_keys.table.description": "Descripción", + "page.api_keys.table.token": "simbólico", + "page.api_keys.table.last_used_at": "Último utilizado", + "page.api_keys.table.created_at": "Fecha de creación", + "page.api_keys.table.actions": "Acciones", + "page.api_keys.never_used": "Nunca usado", + "page.new_api_key.title": "Nueva clave API", "alert.no_bookmark": "No hay marcador en este momento.", "alert.no_category": "No hay categoría.", "alert.no_category_entry": "No hay artículos en esta categoria.", @@ -208,6 +218,8 @@ "error.settings_mandatory_fields": "Los campos de nombre de usuario, tema, idioma y zona horaria son obligatorios.", "error.feed_mandatory_fields": "Los campos de URL y categoría son obligatorios.", "error.user_mandatory_fields": "El nombre de usuario es obligatorio.", + "error.api_key_already_exists": "Esta clave API ya existe.", + "error.unable_to_create_api_key": "No se puede crear esta clave API.", "form.feed.label.title": "Título", "form.feed.label.site_url": "URL del sitio", "form.feed.label.feed_url": "URL de la fuente", @@ -257,6 +269,7 @@ "form.integration.nunux_keeper_activate": "Guardar artículos a Nunux Keeper", "form.integration.nunux_keeper_endpoint": "Extremo de API de Nunux Keeper", "form.integration.nunux_keeper_api_key": "Clave de API de Nunux Keeper", + "form.api_key.label.description": "Etiqueta de clave API", "form.submit.loading": "Cargando...", "form.submit.saving": "Guardando...", "time_elapsed.not_yet": "todavía no", diff --git a/locale/translations/fr_FR.json b/locale/translations/fr_FR.json index c93323bb..860e475c 100644 --- a/locale/translations/fr_FR.json +++ b/locale/translations/fr_FR.json @@ -44,6 +44,8 @@ "menu.add_user": "Ajouter un utilisateur", "menu.flush_history": "Supprimer l'historique", "menu.feed_entries": "Articles", + "menu.api_keys": "Clés d'API", + "menu.create_api_key": "Créer une nouvelle clé d'API", "search.label": "Recherche", "search.placeholder": "Recherche...", "pagination.next": "Suivant", @@ -171,6 +173,14 @@ "page.sessions.table.user_agent": "Navigateur Web", "page.sessions.table.actions": "Actions", "page.sessions.table.current_session": "Session actuelle", + "page.api_keys.title": "Clés d'API", + "page.api_keys.table.description": "Description", + "page.api_keys.table.token": "Jeton", + "page.api_keys.table.last_used_at": "Dernière utilisation", + "page.api_keys.table.created_at": "Date de création", + "page.api_keys.table.actions": "Actions", + "page.api_keys.never_used": "Jamais utilisé", + "page.new_api_key.title": "Nouvelle clé d'API", "alert.no_bookmark": "Il n'y a aucun favoris pour le moment.", "alert.no_category": "Il n'y a aucune catégorie.", "alert.no_category_entry": "Il n'y a aucun article dans cette catégorie.", @@ -208,6 +218,8 @@ "error.settings_mandatory_fields": "Le nom d'utilisateur, le thème, la langue et le fuseau horaire sont obligatoire.", "error.feed_mandatory_fields": "L'URL et la catégorie sont obligatoire.", "error.user_mandatory_fields": "Le nom d'utilisateur est obligatoire.", + "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.", "form.feed.label.title": "Titre", "form.feed.label.site_url": "URL du site web", "form.feed.label.feed_url": "URL du flux", @@ -257,6 +269,7 @@ "form.integration.nunux_keeper_activate": "Sauvegarder les articles vers Nunux Keeper", "form.integration.nunux_keeper_endpoint": "URL de l'API de Nunux Keeper", "form.integration.nunux_keeper_api_key": "Clé d'API de Nunux Keeper", + "form.api_key.label.description": "Libellé de la clé d'API", "form.submit.loading": "Chargement...", "form.submit.saving": "Sauvegarde en cours...", "time_elapsed.not_yet": "pas encore", diff --git a/locale/translations/it_IT.json b/locale/translations/it_IT.json index 3cc11d0e..72ecf74e 100644 --- a/locale/translations/it_IT.json +++ b/locale/translations/it_IT.json @@ -44,6 +44,8 @@ "menu.add_user": "Aggiungi utente", "menu.flush_history": "Svuota la cronologia", "menu.feed_entries": "Articoli", + "menu.api_keys": "Chiavi API", + "menu.create_api_key": "Crea una nuova chiave API", "search.label": "Cerca", "search.placeholder": "Cerca...", "pagination.next": "Successivo", @@ -171,6 +173,14 @@ "page.sessions.table.user_agent": "User Agent", "page.sessions.table.actions": "Azioni", "page.sessions.table.current_session": "Sessione corrente", + "page.api_keys.title": "Chiavi API", + "page.api_keys.table.description": "Descrizione", + "page.api_keys.table.token": "Gettone", + "page.api_keys.table.last_used_at": "Ultimo uso", + "page.api_keys.table.created_at": "Data di creazione", + "page.api_keys.table.actions": "Azioni", + "page.api_keys.never_used": "Mai usato", + "page.new_api_key.title": "Nuova chiave API", "alert.no_bookmark": "Nessun preferito disponibile.", "alert.no_category": "Nessuna categoria disponibile.", "alert.no_category_entry": "Questa categoria non contiene alcun articolo.", @@ -208,6 +218,8 @@ "error.settings_mandatory_fields": "Il nome utente, il tema, la lingua ed il fuso orario sono campi obbligatori.", "error.feed_mandatory_fields": "L'URL e la categoria sono obbligatori.", "error.user_mandatory_fields": "Il nome utente è obbligatorio.", + "error.api_key_already_exists": "Questa chiave API esiste già.", + "error.unable_to_create_api_key": "Impossibile creare questa chiave API.", "form.feed.label.title": "Titolo", "form.feed.label.site_url": "URL del sito", "form.feed.label.feed_url": "URL del feed", @@ -257,6 +269,7 @@ "form.integration.nunux_keeper_activate": "Salva gli articoli su Nunux Keeper", "form.integration.nunux_keeper_endpoint": "Endpoint dell'API di Nunux Keeper", "form.integration.nunux_keeper_api_key": "API key dell'account Nunux Keeper", + "form.api_key.label.description": "Etichetta chiave API", "form.submit.loading": "Caricamento in corso...", "form.submit.saving": "Salvataggio in corso...", "time_elapsed.not_yet": "non ancora", diff --git a/locale/translations/ja_JP.json b/locale/translations/ja_JP.json index 4ed4f8ba..1f8f6a13 100644 --- a/locale/translations/ja_JP.json +++ b/locale/translations/ja_JP.json @@ -44,6 +44,8 @@ "menu.add_user": "ユーザーを追加", "menu.flush_history": "履歴を更新", "menu.feed_entries": "記事一覧", + "menu.api_keys": "APIキー", + "menu.create_api_key": "新しいAPIキーを作成する", "search.label": "検索", "search.placeholder": "…を検索", "pagination.next": "次", @@ -171,6 +173,14 @@ "page.sessions.table.user_agent": "User Agent", "page.sessions.table.actions": "アクション", "page.sessions.table.current_session": "現在のセッション", + "page.api_keys.title": "APIキー", + "page.api_keys.table.description": "説明", + "page.api_keys.table.token": "トークン", + "page.api_keys.table.last_used_at": "最終使用", + "page.api_keys.table.created_at": "作成日", + "page.api_keys.table.actions": "アクション", + "page.api_keys.never_used": "使われたことがない", + "page.new_api_key.title": "新しいAPIキー", "alert.no_bookmark": "現在星付きはありません。", "alert.no_category": "カテゴリが存在しません。", "alert.no_category_entry": "このカテゴリには記事がありません。", @@ -208,6 +218,8 @@ "error.settings_mandatory_fields": "ユーザー名、テーマ、言語、タイムゾーンの全てが必要です。", "error.feed_mandatory_fields": "URL と カテゴリが必要です。", "error.user_mandatory_fields": "ユーザー名が必要です。", + "error.api_key_already_exists": "このAPIキーは既に存在します。", + "error.unable_to_create_api_key": "このAPIキーを作成できません。", "form.feed.label.title": "タイトル", "form.feed.label.site_url": "サイト URL", "form.feed.label.feed_url": "フィード URL", @@ -257,6 +269,7 @@ "form.integration.nunux_keeper_activate": "Nunux Keeper に記事を保存する", "form.integration.nunux_keeper_endpoint": "Nunux Keeper の API Endpoint", "form.integration.nunux_keeper_api_key": "Nunux Keeper の API key", + "form.api_key.label.description": "APIキーラベル", "form.submit.loading": "読み込み中…", "form.submit.saving": "保存中…", "time_elapsed.not_yet": "未来", diff --git a/locale/translations/nl_NL.json b/locale/translations/nl_NL.json index 03039aea..d5c2ba50 100644 --- a/locale/translations/nl_NL.json +++ b/locale/translations/nl_NL.json @@ -44,6 +44,8 @@ "menu.add_user": "Gebruiker toevoegen", "menu.flush_history": "Verwijder geschiedenis", "menu.feed_entries": "Lidwoord", + "menu.api_keys": "API-sleutels", + "menu.create_api_key": "Maak een nieuwe API-sleutel", "search.label": "Zoeken", "search.placeholder": "Zoeken...", "pagination.next": "Volgende", @@ -171,6 +173,14 @@ "page.sessions.table.user_agent": "User-agent", "page.sessions.table.actions": "Acties", "page.sessions.table.current_session": "Huidige sessie", + "page.api_keys.title": "API-sleutels", + "page.api_keys.table.description": "Beschrijving", + "page.api_keys.table.token": "Blijk", + "page.api_keys.table.last_used_at": "Laatst gebruikt", + "page.api_keys.table.created_at": "Aanmaakdatum", + "page.api_keys.table.actions": "Acties", + "page.api_keys.never_used": "Nooit gebruikt", + "page.new_api_key.title": "Nieuwe API-sleutel", "alert.no_bookmark": "Er zijn op dit moment geen favorieten.", "alert.no_category": "Er zijn geen categorieën.", "alert.no_category_entry": "Deze categorie bevat geen feeds.", @@ -208,6 +218,8 @@ "error.settings_mandatory_fields": "Gebruikersnaam, skin, taal en tijdzone zijn verplicht.", "error.feed_mandatory_fields": "The URL en de categorie zijn verplicht.", "error.user_mandatory_fields": "Gebruikersnaam is verplicht", + "error.api_key_already_exists": "This API Key already exists.", + "error.unable_to_create_api_key": "Kan deze API-sleutel niet maken.", "form.feed.label.title": "Naam", "form.feed.label.site_url": "Website URL", "form.feed.label.feed_url": "Feed URL", @@ -257,6 +269,7 @@ "form.integration.nunux_keeper_activate": "Opslaan naar Nunux Keeper", "form.integration.nunux_keeper_endpoint": "Nunux Keeper URL", "form.integration.nunux_keeper_api_key": "Nunux Keeper API-sleutel", + "form.api_key.label.description": "API-sleutellabel", "form.submit.loading": "Laden...", "form.submit.saving": "Opslaag...", "time_elapsed.not_yet": "in de toekomst", diff --git a/locale/translations/pl_PL.json b/locale/translations/pl_PL.json index 7158dd7d..3a5ed6d1 100644 --- a/locale/translations/pl_PL.json +++ b/locale/translations/pl_PL.json @@ -44,6 +44,8 @@ "menu.add_user": "Dodaj użytkownika", "menu.flush_history": "Usuń historię", "menu.feed_entries": "Artykuły", + "menu.api_keys": "Klucze API", + "menu.create_api_key": "Utwórz nowy klucz API", "search.label": "Szukaj", "search.placeholder": "Szukaj...", "pagination.next": "Następny", @@ -173,6 +175,14 @@ "page.sessions.table.user_agent": "Agent użytkownika", "page.sessions.table.actions": "Działania", "page.sessions.table.current_session": "Bieżąca sesja", + "page.api_keys.title": "Klucze API", + "page.api_keys.table.description": "Opis", + "page.api_keys.table.token": "Znak", + "page.api_keys.table.last_used_at": "Ostatnio używane", + "page.api_keys.table.created_at": "Data utworzenia", + "page.api_keys.table.actions": "Działania", + "page.api_keys.never_used": "Nigdy nie używany", + "page.new_api_key.title": "Nowy klucz API", "alert.no_bookmark": "Obecnie nie ma żadnych zakładek.", "alert.no_category": "Nie ma żadnej kategorii!", "alert.no_category_entry": "W tej kategorii nie ma żadnych artykułów", @@ -210,6 +220,8 @@ "error.settings_mandatory_fields": "Pola nazwy użytkownika, tematu, języka i strefy czasowej są obowiązkowe.", "error.feed_mandatory_fields": "URL i kategoria są obowiązkowe.", "error.user_mandatory_fields": "Nazwa użytkownika jest obowiązkowa.", + "error.api_key_already_exists": "Deze API-sleutel bestaat al.", + "error.unable_to_create_api_key": "Nie można utworzyć tego klucza API.", "form.feed.label.title": "Tytuł", "form.feed.label.site_url": "URL strony", "form.feed.label.feed_url": "URL kanału", @@ -259,6 +271,7 @@ "form.integration.nunux_keeper_activate": "Zapisz artykuly do Nunux Keeper", "form.integration.nunux_keeper_endpoint": "Nunux Keeper URL", "form.integration.nunux_keeper_api_key": "Nunux Keeper API key", + "form.api_key.label.description": "Etykieta klucza API", "form.submit.loading": "Ładowanie...", "form.submit.saving": "Zapisywanie...", "time_elapsed.not_yet": "jeszcze nie", diff --git a/locale/translations/ru_RU.json b/locale/translations/ru_RU.json index a9bb260f..67885eed 100644 --- a/locale/translations/ru_RU.json +++ b/locale/translations/ru_RU.json @@ -44,6 +44,8 @@ "menu.add_user": "Добавить пользователя", "menu.flush_history": "Отчистить историю", "menu.feed_entries": "статьи", + "menu.api_keys": "API-ключи", + "menu.create_api_key": "Создать новый ключ API", "search.label": "Поиск", "search.placeholder": "Поиск…", "pagination.next": "Следующая", @@ -173,6 +175,14 @@ "page.sessions.table.user_agent": "User Agent", "page.sessions.table.actions": "Действия", "page.sessions.table.current_session": "Текущая сессия", + "page.api_keys.title": "API-ключи", + "page.api_keys.table.description": "описание", + "page.api_keys.table.token": "знак", + "page.api_keys.table.last_used_at": "Последний раз был использован", + "page.api_keys.table.created_at": "Дата создания", + "page.api_keys.table.actions": "Действия", + "page.api_keys.never_used": "Никогда не использовался", + "page.new_api_key.title": "Новый ключ API", "alert.no_bookmark": "Нет закладок на данный момент.", "alert.no_category": "Категории отсутствуют.", "alert.no_category_entry": "В этой категории нет статей.", @@ -210,6 +220,8 @@ "error.settings_mandatory_fields": "Имя пользователя, тема, язык и часовой пояс обязательны.", "error.feed_mandatory_fields": "URL и категория обязательны.", "error.user_mandatory_fields": "Имя пользователя обязательно.", + "error.api_key_already_exists": "Этот ключ API уже существует.", + "error.unable_to_create_api_key": "Невозможно создать этот ключ API.", "form.feed.label.title": "Название", "form.feed.label.site_url": "URL сайта", "form.feed.label.feed_url": "URL подписки", @@ -259,6 +271,7 @@ "form.integration.nunux_keeper_activate": "Сохранять статьи в Nunux Keeper", "form.integration.nunux_keeper_endpoint": "Конечная точка Nunux Keeper API", "form.integration.nunux_keeper_api_key": "Nunux Keeper API key", + "form.api_key.label.description": "APIキーラベル", "form.submit.loading": "Загрузка…", "form.submit.saving": "Сохранение…", "time_elapsed.not_yet": "ещё нет", diff --git a/locale/translations/zh_CN.json b/locale/translations/zh_CN.json index e6ddd3b2..e96aef18 100644 --- a/locale/translations/zh_CN.json +++ b/locale/translations/zh_CN.json @@ -44,6 +44,8 @@ "menu.add_user": "新建用户", "menu.flush_history": "清理历史", "menu.feed_entries": "文章", + "menu.api_keys": "API密钥", + "menu.create_api_key": "创建一个新的API密钥", "search.label": "搜索", "search.placeholder": "搜索…", "pagination.next": "下一页", @@ -169,6 +171,14 @@ "page.sessions.table.user_agent": "User-Agent", "page.sessions.table.actions": "操作", "page.sessions.table.current_session": "当前会话", + "page.api_keys.title": "API密钥", + "page.api_keys.table.description": "描述", + "page.api_keys.table.token": "代币", + "page.api_keys.table.last_used_at": "最后使用", + "page.api_keys.table.created_at": "创立日期", + "page.api_keys.table.actions": "操作", + "page.api_keys.never_used": "没用过", + "page.new_api_key.title": "新的API密钥", "alert.no_bookmark": "目前没有书签", "alert.no_category": "目前没有分类", "alert.no_category_entry": "该分类下没有文章", @@ -206,6 +216,8 @@ "error.settings_mandatory_fields": "必须填写用户名、主题、语言以及时区", "error.feed_mandatory_fields": "必须填写 URL 和分类", "error.user_mandatory_fields": "必须填写用户名", + "error.api_key_already_exists": "此API密钥已存在。", + "error.unable_to_create_api_key": "无法创建此API密钥。", "form.feed.label.title": "标题", "form.feed.label.site_url": "站点 URL", "form.feed.label.feed_url": "源 URL", @@ -255,6 +267,7 @@ "form.integration.nunux_keeper_activate": "保存文章到 Nunux Keeper", "form.integration.nunux_keeper_endpoint": "Nunux Keeper API Endpoint", "form.integration.nunux_keeper_api_key": "Nunux Keeper API 密钥", + "form.api_key.label.description": "API密钥标签", "form.submit.loading": "载入中…", "form.submit.saving": "保存中…", "time_elapsed.not_yet": "尚未", diff --git a/model/api_key.go b/model/api_key.go new file mode 100644 index 00000000..479c429e --- /dev/null +++ b/model/api_key.go @@ -0,0 +1,33 @@ +// Copyright 2020 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 ( + "time" + + "miniflux.app/crypto" +) + +// APIKey represents an application API key. +type APIKey struct { + ID int64 + UserID int64 + Token string + Description string + LastUsedAt *time.Time + CreatedAt time.Time +} + +// NewAPIKey initializes a new APIKey. +func NewAPIKey(userID int64, description string) *APIKey { + return &APIKey{ + UserID: userID, + Token: crypto.GenerateRandomString(32), + Description: description, + } +} + +// APIKeys represents a collection of API Key. +type APIKeys []*APIKey diff --git a/storage/api_key.go b/storage/api_key.go new file mode 100644 index 00000000..00db91df --- /dev/null +++ b/storage/api_key.go @@ -0,0 +1,104 @@ +// Copyright 2020 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 storage // import "miniflux.app/storage" + +import ( + "fmt" + + "miniflux.app/model" +) + +// APIKeyExists checks if an API Key with the same description exists. +func (s *Storage) APIKeyExists(userID int64, description string) bool { + var result bool + query := `SELECT true FROM api_keys WHERE user_id=$1 AND lower(description)=lower($2) LIMIT 1` + s.db.QueryRow(query, userID, description).Scan(&result) + return result +} + +// SetAPIKeyUsedTimestamp updates the last used date of an API Key. +func (s *Storage) SetAPIKeyUsedTimestamp(userID int64, token string) error { + query := `UPDATE api_keys SET last_used_at=now() WHERE user_id=$1 and token=$2` + _, err := s.db.Exec(query, userID, token) + if err != nil { + return fmt.Errorf(`store: unable to update last used date for API key: %v`, err) + } + + return nil +} + +// APIKeys returns all API Keys that belongs to the given user. +func (s *Storage) APIKeys(userID int64) (model.APIKeys, error) { + query := ` + SELECT + id, user_id, token, description, last_used_at, created_at + FROM + api_keys + WHERE + user_id=$1 + ORDER BY description ASC + ` + rows, err := s.db.Query(query, userID) + if err != nil { + return nil, fmt.Errorf(`store: unable to fetch API Keys: %v`, err) + } + defer rows.Close() + + apiKeys := make(model.APIKeys, 0) + for rows.Next() { + var apiKey model.APIKey + if err := rows.Scan( + &apiKey.ID, + &apiKey.UserID, + &apiKey.Token, + &apiKey.Description, + &apiKey.LastUsedAt, + &apiKey.CreatedAt, + ); err != nil { + return nil, fmt.Errorf(`store: unable to fetch API Key row: %v`, err) + } + + apiKeys = append(apiKeys, &apiKey) + } + + return apiKeys, nil +} + +// CreateAPIKey inserts a new API key. +func (s *Storage) CreateAPIKey(apiKey *model.APIKey) error { + query := ` + INSERT INTO api_keys + (user_id, token, description) + VALUES + ($1, $2, $3) + RETURNING + id, created_at + ` + err := s.db.QueryRow( + query, + apiKey.UserID, + apiKey.Token, + apiKey.Description, + ).Scan( + &apiKey.ID, + &apiKey.CreatedAt, + ) + if err != nil { + return fmt.Errorf(`store: unable to create category: %v`, err) + } + + return nil +} + +// RemoveAPIKey deletes an API Key. +func (s *Storage) RemoveAPIKey(userID, keyID int64) error { + query := `DELETE FROM api_keys WHERE id = $1 AND user_id = $2` + _, err := s.db.Exec(query, keyID, userID) + if err != nil { + return fmt.Errorf(`store: unable to remove this API Key: %v`, err) + } + + return nil +} diff --git a/storage/user.go b/storage/user.go index 5c146442..b182a568 100644 --- a/storage/user.go +++ b/storage/user.go @@ -253,6 +253,30 @@ func (s *Storage) UserByExtraField(field, value string) (*model.User, error) { return s.fetchUser(query, field, value) } +// UserByAPIKey returns a User from an API Key. +func (s *Storage) UserByAPIKey(token string) (*model.User, error) { + query := ` + SELECT + u.id, + u.username, + u.is_admin, + u.theme, + u.language, + u.timezone, + u.entry_direction, + u.keyboard_shortcuts, + u.last_login_at, + u.extra + FROM + users u + LEFT JOIN + api_keys ON api_keys.user_id=u.id + WHERE + api_keys.token = $1 + ` + return s.fetchUser(query, token) +} + func (s *Storage) fetchUser(query string, args ...interface{}) (*model.User, error) { var extra hstore.Hstore diff --git a/template/common.go b/template/common.go index cfc2d6a3..545e0482 100644 --- a/template/common.go +++ b/template/common.go @@ -7,7 +7,7 @@ var templateCommonMap = map[string]string{