Add per-application API Keys
This commit is contained in:
parent
d1afe13a1c
commit
25cc0d2447
35 changed files with 940 additions and 71 deletions
|
@ -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")
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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{}
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import (
|
|||
"miniflux.app/logger"
|
||||
)
|
||||
|
||||
const schemaVersion = 26
|
||||
const schemaVersion = 27
|
||||
|
||||
// Migrate executes database migrations.
|
||||
func Migrate(db *sql.DB) {
|
||||
|
|
|
@ -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",
|
||||
|
|
10
database/sql/schema_version_27.sql
Normal file
10
database/sql/schema_version_27.sql
Normal file
|
@ -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)
|
||||
);
|
|
@ -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",
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "未来",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "ещё нет",
|
||||
|
|
|
@ -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": "尚未",
|
||||
|
|
33
model/api_key.go
Normal file
33
model/api_key.go
Normal file
|
@ -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
|
104
storage/api_key.go
Normal file
104
storage/api_key.go
Normal file
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@ var templateCommonMap = map[string]string{
|
|||
<div class="pagination">
|
||||
<div class="pagination-prev">
|
||||
{{ if .prevEntry }}
|
||||
<a href="{{ .prevEntryRoute }}{{ if .searchQuery }}?q={{ .searchQuery }}{{ end }}" title="{{ .prevEntry.Title }}" data-page="previous">{{ t "pagination.previous" }}</a>
|
||||
<a href="{{ .prevEntryRoute }}{{ if .searchQuery }}?q={{ .searchQuery }}{{ end }}" title="{{ .prevEntry.Title }}" data-page="previous" rel="prev">{{ t "pagination.previous" }}</a>
|
||||
{{ else }}
|
||||
{{ t "pagination.previous" }}
|
||||
{{ end }}
|
||||
|
@ -15,13 +15,14 @@ var templateCommonMap = map[string]string{
|
|||
|
||||
<div class="pagination-next">
|
||||
{{ if .nextEntry }}
|
||||
<a href="{{ .nextEntryRoute }}{{ if .searchQuery }}?q={{ .searchQuery }}{{ end }}" title="{{ .nextEntry.Title }}" data-page="next">{{ t "pagination.next" }}</a>
|
||||
<a href="{{ .nextEntryRoute }}{{ if .searchQuery }}?q={{ .searchQuery }}{{ end }}" title="{{ .nextEntry.Title }}" data-page="next" rel="next">{{ t "pagination.next" }}</a>
|
||||
{{ else }}
|
||||
{{ t "pagination.next" }}
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}`,
|
||||
{{ end }}
|
||||
`,
|
||||
"feed_list": `{{ define "feed_list" }}
|
||||
<div class="items">
|
||||
{{ range .feeds }}
|
||||
|
@ -311,7 +312,7 @@ var templateCommonMap = map[string]string{
|
|||
<div class="pagination">
|
||||
<div class="pagination-prev">
|
||||
{{ if .ShowPrev }}
|
||||
<a href="{{ .Route }}{{ if gt .PrevOffset 0 }}?offset={{ .PrevOffset }}{{ if .SearchQuery }}&q={{ .SearchQuery }}{{ end }}{{ else }}{{ if .SearchQuery }}?q={{ .SearchQuery }}{{ end }}{{ end }}" data-page="previous">{{ t "pagination.previous" }}</a>
|
||||
<a href="{{ .Route }}{{ if gt .PrevOffset 0 }}?offset={{ .PrevOffset }}{{ if .SearchQuery }}&q={{ .SearchQuery }}{{ end }}{{ else }}{{ if .SearchQuery }}?q={{ .SearchQuery }}{{ end }}{{ end }}" data-page="previous" rel="prev">{{ t "pagination.previous" }}</a>
|
||||
{{ else }}
|
||||
{{ t "pagination.previous" }}
|
||||
{{ end }}
|
||||
|
@ -319,7 +320,7 @@ var templateCommonMap = map[string]string{
|
|||
|
||||
<div class="pagination-next">
|
||||
{{ if .ShowNext }}
|
||||
<a href="{{ .Route }}?offset={{ .NextOffset }}{{ if .SearchQuery }}&q={{ .SearchQuery }}{{ end }}" data-page="next">{{ t "pagination.next" }}</a>
|
||||
<a href="{{ .Route }}?offset={{ .NextOffset }}{{ if .SearchQuery }}&q={{ .SearchQuery }}{{ end }}" data-page="next" rel="next">{{ t "pagination.next" }}</a>
|
||||
{{ else }}
|
||||
{{ t "pagination.next" }}
|
||||
{{ end }}
|
||||
|
@ -335,6 +336,9 @@ var templateCommonMap = map[string]string{
|
|||
<li>
|
||||
<a href="{{ route "integrations" }}">{{ t "menu.integrations" }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ route "apiKeys" }}">{{ t "menu.api_keys" }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ route "sessions" }}">{{ t "menu.sessions" }}</a>
|
||||
</li>
|
||||
|
@ -342,9 +346,6 @@ var templateCommonMap = map[string]string{
|
|||
<li>
|
||||
<a href="{{ route "users" }}">{{ t "menu.users" }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ route "createUser" }}">{{ t "menu.add_user" }}</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
<li>
|
||||
<a href="{{ route "about" }}">{{ t "menu.about" }}</a>
|
||||
|
@ -354,11 +355,11 @@ var templateCommonMap = map[string]string{
|
|||
}
|
||||
|
||||
var templateCommonMapChecksums = map[string]string{
|
||||
"entry_pagination": "4faa91e2eae150c5e4eab4d258e039dfdd413bab7602f0009360e6d52898e353",
|
||||
"entry_pagination": "cdca9cf12586e41e5355190b06d9168f57f77b85924d1e63b13524bc15abcbf6",
|
||||
"feed_list": "db406e7cb81292ce1d974d63f63270384a286848b2e74fe36bf711b4eb5717dd",
|
||||
"feed_menu": "318d8662dda5ca9dfc75b909c8461e79c86fb5082df1428f67aaf856f19f4b50",
|
||||
"item_meta": "d046305e8935ecd8643a94d28af384df29e40fc7ce334123cd057a6522bac23f",
|
||||
"layout": "a1f67b8908745ee4f9cee6f7bbbb0b242d4dcc101207ad4a9d67242b45683299",
|
||||
"pagination": "3386e90c6e1230311459e9a484629bc5d5bf39514a75ef2e73bbbc61142f7abb",
|
||||
"settings_menu": "78e5a487ede18610b23db74184dab023170f9e083cc0625bc2c874d1eea1a4ce",
|
||||
"pagination": "7b61288e86283c4cf0dc83bcbf8bf1c00c7cb29e60201c8c0b633b2450d2911f",
|
||||
"settings_menu": "e2b777630c0efdbc529800303c01d6744ed3af80ec505ac5a5b3f99c9b989156",
|
||||
}
|
||||
|
|
72
template/html/api_keys.html
Normal file
72
template/html/api_keys.html
Normal file
|
@ -0,0 +1,72 @@
|
|||
{{ define "title"}}{{ t "page.api_keys.title" }}{{ end }}
|
||||
|
||||
{{ define "content"}}
|
||||
<section class="page-header">
|
||||
<h1>{{ t "page.api_keys.title" }}</h1>
|
||||
{{ template "settings_menu" dict "user" .user }}
|
||||
</section>
|
||||
|
||||
{{ if .apiKeys }}
|
||||
{{ range .apiKeys }}
|
||||
<table>
|
||||
<tr>
|
||||
<th class="column-25">{{ t "page.api_keys.table.description" }}</th>
|
||||
<td>{{ .Description }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ t "page.api_keys.table.token" }}</th>
|
||||
<td>{{ .Token }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ t "page.api_keys.table.last_used_at" }}</th>
|
||||
<td>
|
||||
{{ if .LastUsedAt }}
|
||||
<time datetime="{{ isodate .LastUsedAt }}" title="{{ isodate .LastUsedAt }}">{{ elapsed $.user.Timezone .LastUsedAt }}</time>
|
||||
{{ else }}
|
||||
{{ t "page.api_keys.never_used" }}
|
||||
{{ end }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ t "page.api_keys.table.created_at" }}</th>
|
||||
<td>
|
||||
<time datetime="{{ isodate .CreatedAt }}" title="{{ isodate .CreatedAt }}">{{ elapsed $.user.Timezone .CreatedAt }}</time>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ t "page.api_keys.table.actions" }}</th>
|
||||
<td>
|
||||
<a href="#"
|
||||
data-confirm="true"
|
||||
data-label-question="{{ t "confirm.question" }}"
|
||||
data-label-yes="{{ t "confirm.yes" }}"
|
||||
data-label-no="{{ t "confirm.no" }}"
|
||||
data-label-loading="{{ t "confirm.loading" }}"
|
||||
data-url="{{ route "removeAPIKey" "keyID" .ID }}">{{ t "action.remove" }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<br>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
<h3>{{ t "page.integration.miniflux_api" }}</h3>
|
||||
<div class="panel">
|
||||
<ul>
|
||||
<li>
|
||||
{{ t "page.integration.miniflux_api_endpoint" }} = <strong>{{ baseURL }}/v1/</strong>
|
||||
</li>
|
||||
<li>
|
||||
{{ t "page.integration.miniflux_api_username" }} = <strong>{{ .user.Username }}</strong>
|
||||
</li>
|
||||
<li>
|
||||
{{ t "page.integration.miniflux_api_password" }} = <strong>{{ t "page.integration.miniflux_api_password_value" }}</strong>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<a href="{{ route "createAPIKey" }}" class="button button-primary">{{ t "menu.create_api_key" }}</a>
|
||||
</p>
|
||||
|
||||
{{ end }}
|
|
@ -6,6 +6,9 @@
|
|||
<li>
|
||||
<a href="{{ route "integrations" }}">{{ t "menu.integrations" }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ route "apiKeys" }}">{{ t "menu.api_keys" }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ route "sessions" }}">{{ t "menu.sessions" }}</a>
|
||||
</li>
|
||||
|
@ -13,9 +16,6 @@
|
|||
<li>
|
||||
<a href="{{ route "users" }}">{{ t "menu.users" }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{{ route "createUser" }}">{{ t "menu.add_user" }}</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
<li>
|
||||
<a href="{{ route "about" }}">{{ t "menu.about" }}</a>
|
||||
|
|
23
template/html/create_api_key.html
Normal file
23
template/html/create_api_key.html
Normal file
|
@ -0,0 +1,23 @@
|
|||
{{ define "title"}}{{ t "page.new_api_key.title" }}{{ end }}
|
||||
|
||||
{{ define "content"}}
|
||||
<section class="page-header">
|
||||
<h1>{{ t "page.new_api_key.title" }}</h1>
|
||||
{{ template "settings_menu" dict "user" .user }}
|
||||
</section>
|
||||
|
||||
<form action="{{ route "saveAPIKey" }}" method="post" autocomplete="off">
|
||||
<input type="hidden" name="csrf" value="{{ .csrf }}">
|
||||
|
||||
{{ if .errorMessage }}
|
||||
<div class="alert alert-error">{{ t .errorMessage }}</div>
|
||||
{{ end }}
|
||||
|
||||
<label for="form-description">{{ t "form.api_key.label.description" }}</label>
|
||||
<input type="text" name="description" id="form-description" value="{{ .form.Description }}" required autofocus>
|
||||
|
||||
<div class="buttons">
|
||||
<button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.save" }}</button> {{ t "action.or" }} <a href="{{ route "apiKeys" }}">{{ t "action.cancel" }}</a>
|
||||
</div>
|
||||
</form>
|
||||
{{ end }}
|
|
@ -117,21 +117,6 @@
|
|||
</div>
|
||||
</form>
|
||||
|
||||
<h3>{{ t "page.integration.miniflux_api" }}</h3>
|
||||
<div class="panel">
|
||||
<ul>
|
||||
<li>
|
||||
{{ t "page.integration.miniflux_api_endpoint" }} = <strong>{{ baseURL }}/v1/</strong>
|
||||
</li>
|
||||
<li>
|
||||
{{ t "page.integration.miniflux_api_username" }} = <strong>{{ .user.Username }}</strong>
|
||||
</li>
|
||||
<li>
|
||||
{{ t "page.integration.miniflux_api_password" }} = <strong>{{ t "page.integration.miniflux_api_password_value" }}</strong>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h3>{{ t "page.integration.bookmarklet" }}</h3>
|
||||
<div class="panel">
|
||||
<p>{{ t "page.integration.bookmarklet.help" }}</p>
|
||||
|
|
|
@ -42,6 +42,11 @@
|
|||
{{ end }}
|
||||
{{ end }}
|
||||
</table>
|
||||
<br>
|
||||
{{ end }}
|
||||
|
||||
<p>
|
||||
<a href="{{ route "createUser" }}" class="button button-primary">{{ t "menu.add_user" }}</a>
|
||||
</p>
|
||||
|
||||
{{ end }}
|
||||
|
|
|
@ -92,6 +92,79 @@ var templateViewsMap = map[string]string{
|
|||
</form>
|
||||
{{ end }}
|
||||
|
||||
{{ end }}
|
||||
`,
|
||||
"api_keys": `{{ define "title"}}{{ t "page.api_keys.title" }}{{ end }}
|
||||
|
||||
{{ define "content"}}
|
||||
<section class="page-header">
|
||||
<h1>{{ t "page.api_keys.title" }}</h1>
|
||||
{{ template "settings_menu" dict "user" .user }}
|
||||
</section>
|
||||
|
||||
{{ if .apiKeys }}
|
||||
{{ range .apiKeys }}
|
||||
<table>
|
||||
<tr>
|
||||
<th class="column-25">{{ t "page.api_keys.table.description" }}</th>
|
||||
<td>{{ .Description }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ t "page.api_keys.table.token" }}</th>
|
||||
<td>{{ .Token }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ t "page.api_keys.table.last_used_at" }}</th>
|
||||
<td>
|
||||
{{ if .LastUsedAt }}
|
||||
<time datetime="{{ isodate .LastUsedAt }}" title="{{ isodate .LastUsedAt }}">{{ elapsed $.user.Timezone .LastUsedAt }}</time>
|
||||
{{ else }}
|
||||
{{ t "page.api_keys.never_used" }}
|
||||
{{ end }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ t "page.api_keys.table.created_at" }}</th>
|
||||
<td>
|
||||
<time datetime="{{ isodate .CreatedAt }}" title="{{ isodate .CreatedAt }}">{{ elapsed $.user.Timezone .CreatedAt }}</time>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{{ t "page.api_keys.table.actions" }}</th>
|
||||
<td>
|
||||
<a href="#"
|
||||
data-confirm="true"
|
||||
data-label-question="{{ t "confirm.question" }}"
|
||||
data-label-yes="{{ t "confirm.yes" }}"
|
||||
data-label-no="{{ t "confirm.no" }}"
|
||||
data-label-loading="{{ t "confirm.loading" }}"
|
||||
data-url="{{ route "removeAPIKey" "keyID" .ID }}">{{ t "action.remove" }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<br>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
<h3>{{ t "page.integration.miniflux_api" }}</h3>
|
||||
<div class="panel">
|
||||
<ul>
|
||||
<li>
|
||||
{{ t "page.integration.miniflux_api_endpoint" }} = <strong>{{ baseURL }}/v1/</strong>
|
||||
</li>
|
||||
<li>
|
||||
{{ t "page.integration.miniflux_api_username" }} = <strong>{{ .user.Username }}</strong>
|
||||
</li>
|
||||
<li>
|
||||
{{ t "page.integration.miniflux_api_password" }} = <strong>{{ t "page.integration.miniflux_api_password_value" }}</strong>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
<a href="{{ route "createAPIKey" }}" class="button button-primary">{{ t "menu.create_api_key" }}</a>
|
||||
</p>
|
||||
|
||||
{{ end }}
|
||||
`,
|
||||
"bookmark_entries": `{{ define "title"}}{{ t "page.starred.title" }} ({{ .total }}){{ end }}
|
||||
|
@ -317,6 +390,30 @@ var templateViewsMap = map[string]string{
|
|||
</div>
|
||||
</form>
|
||||
{{ end }}
|
||||
`,
|
||||
"create_api_key": `{{ define "title"}}{{ t "page.new_api_key.title" }}{{ end }}
|
||||
|
||||
{{ define "content"}}
|
||||
<section class="page-header">
|
||||
<h1>{{ t "page.new_api_key.title" }}</h1>
|
||||
{{ template "settings_menu" dict "user" .user }}
|
||||
</section>
|
||||
|
||||
<form action="{{ route "saveAPIKey" }}" method="post" autocomplete="off">
|
||||
<input type="hidden" name="csrf" value="{{ .csrf }}">
|
||||
|
||||
{{ if .errorMessage }}
|
||||
<div class="alert alert-error">{{ t .errorMessage }}</div>
|
||||
{{ end }}
|
||||
|
||||
<label for="form-description">{{ t "form.api_key.label.description" }}</label>
|
||||
<input type="text" name="description" id="form-description" value="{{ .form.Description }}" required autofocus>
|
||||
|
||||
<div class="buttons">
|
||||
<button type="submit" class="button button-primary" data-label-loading="{{ t "form.submit.saving" }}">{{ t "action.save" }}</button> {{ t "action.or" }} <a href="{{ route "apiKeys" }}">{{ t "action.cancel" }}</a>
|
||||
</div>
|
||||
</form>
|
||||
{{ end }}
|
||||
`,
|
||||
"create_category": `{{ define "title"}}{{ t "page.new_category.title" }}{{ end }}
|
||||
|
||||
|
@ -992,21 +1089,6 @@ var templateViewsMap = map[string]string{
|
|||
</div>
|
||||
</form>
|
||||
|
||||
<h3>{{ t "page.integration.miniflux_api" }}</h3>
|
||||
<div class="panel">
|
||||
<ul>
|
||||
<li>
|
||||
{{ t "page.integration.miniflux_api_endpoint" }} = <strong>{{ baseURL }}/v1/</strong>
|
||||
</li>
|
||||
<li>
|
||||
{{ t "page.integration.miniflux_api_username" }} = <strong>{{ .user.Username }}</strong>
|
||||
</li>
|
||||
<li>
|
||||
{{ t "page.integration.miniflux_api_password" }} = <strong>{{ t "page.integration.miniflux_api_password_value" }}</strong>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<h3>{{ t "page.integration.bookmarklet" }}</h3>
|
||||
<div class="panel">
|
||||
<p>{{ t "page.integration.bookmarklet.help" }}</p>
|
||||
|
@ -1302,8 +1384,13 @@ var templateViewsMap = map[string]string{
|
|||
{{ end }}
|
||||
{{ end }}
|
||||
</table>
|
||||
<br>
|
||||
{{ end }}
|
||||
|
||||
<p>
|
||||
<a href="{{ route "createUser" }}" class="button button-primary">{{ t "menu.add_user" }}</a>
|
||||
</p>
|
||||
|
||||
{{ end }}
|
||||
`,
|
||||
}
|
||||
|
@ -1311,11 +1398,13 @@ var templateViewsMap = map[string]string{
|
|||
var templateViewsMapChecksums = map[string]string{
|
||||
"about": "4035658497363d7af7f79be83190404eb21ec633fe8ec636bdfc219d9fc78cfc",
|
||||
"add_subscription": "0dbea93b6fc07423fa066122ad960c69616b829533371a2dbadec1e22d4f1ae0",
|
||||
"api_keys": "27d401b31a72881d5232486ba17eb47edaf5246eaedce81de88698c15ebb2284",
|
||||
"bookmark_entries": "65588da78665699dd3f287f68325e9777d511f1a57fee4131a5bb6d00bb68df8",
|
||||
"categories": "2c5dd0ed6355bd5acc393bbf6117d20458b5581aab82036008324f6bbbe2af75",
|
||||
"category_entries": "dee7b9cd60c6c46f01dd4289940679df31c1fce28ce4aa7249fa459023e1eeb4",
|
||||
"category_feeds": "527c2ffbc4fcec775071424ba1022ae003525dba53a28cc41f48fb7b30aa984b",
|
||||
"choose_subscription": "84c9730cadd78e6ee5a6b4c499aab33acddb4324ac01924d33387543eec4d702",
|
||||
"create_api_key": "5f74d4e92a6684927f5305096378c8be278159a5cd88ce652c7be3280a7d1685",
|
||||
"create_category": "6b22b5ce51abf4e225e23a79f81be09a7fb90acb265e93a8faf9446dff74018d",
|
||||
"create_user": "9b73a55233615e461d1f07d99ad1d4d3b54532588ab960097ba3e090c85aaf3a",
|
||||
"edit_category": "b1c0b38f1b714c5d884edcd61e5b5295a5f1c8b71c469b35391e4dcc97cc6d36",
|
||||
|
@ -1326,11 +1415,11 @@ var templateViewsMapChecksums = map[string]string{
|
|||
"feeds": "ec7d3fa96735bd8422ba69ef0927dcccddc1cc51327e0271f0312d3f881c64fd",
|
||||
"history_entries": "87e17d39de70eb3fdbc4000326283be610928758eae7924e4b08dcb446f3b6a9",
|
||||
"import": "1b59b3bd55c59fcbc6fbb346b414dcdd26d1b4e0c307e437bb58b3f92ef01ad1",
|
||||
"integrations": "6104ff6ff3ac3c1ae5e850c78250aab6e99e2342a337589f3848459fa333766a",
|
||||
"integrations": "30329452743b35c668278f519245fd9be05c1726856e0384ba542f7c307f2788",
|
||||
"login": "0657174d13229bb6d0bc470ccda06bb1f15c1af65c86b20b41ffa5c819eef0cc",
|
||||
"search_entries": "274950d03298c24f3942e209c0faed580a6d57be9cf76a6c236175a7e766ac6a",
|
||||
"sessions": "5d5c677bddbd027e0b0c9f7a0dd95b66d9d95b4e130959f31fb955b926c2201c",
|
||||
"settings": "56f7c06f24eef317353582b0191aa9a5985f46ed755accf97e723ceb4bba4469",
|
||||
"unread_entries": "e38f7ffce17dfad3151b08cd33771a2cefe8ca9db42df04fc98bd1d675dd6075",
|
||||
"users": "17d0b7c760557e20f888d83d6a1b0d4506dab071a593cc42080ec0dbf16adf9e",
|
||||
"users": "d7ff52efc582bbad10504f4a04fa3adcc12d15890e45dff51cac281e0c446e45",
|
||||
}
|
||||
|
|
34
ui/api_key_create.go
Normal file
34
ui/api_key_create.go
Normal file
|
@ -0,0 +1,34 @@
|
|||
// 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 ui // import "miniflux.app/ui"
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"miniflux.app/http/request"
|
||||
"miniflux.app/http/response/html"
|
||||
"miniflux.app/ui/form"
|
||||
"miniflux.app/ui/session"
|
||||
"miniflux.app/ui/view"
|
||||
)
|
||||
|
||||
func (h *handler) showCreateAPIKeyPage(w http.ResponseWriter, r *http.Request) {
|
||||
sess := session.New(h.store, request.SessionID(r))
|
||||
view := view.New(h.tpl, r, sess)
|
||||
|
||||
user, err := h.store.UserByID(request.UserID(r))
|
||||
if err != nil {
|
||||
html.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
view.Set("form", &form.APIKeyForm{})
|
||||
view.Set("menu", "settings")
|
||||
view.Set("user", user)
|
||||
view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
|
||||
view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
|
||||
|
||||
html.OK(w, r, view.Render("create_api_key"))
|
||||
}
|
39
ui/api_key_list.go
Normal file
39
ui/api_key_list.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
// 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 ui // import "miniflux.app/ui"
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"miniflux.app/http/request"
|
||||
"miniflux.app/http/response/html"
|
||||
"miniflux.app/ui/session"
|
||||
"miniflux.app/ui/view"
|
||||
)
|
||||
|
||||
func (h *handler) showAPIKeysPage(w http.ResponseWriter, r *http.Request) {
|
||||
sess := session.New(h.store, request.SessionID(r))
|
||||
view := view.New(h.tpl, r, sess)
|
||||
|
||||
user, err := h.store.UserByID(request.UserID(r))
|
||||
if err != nil {
|
||||
html.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
apiKeys, err := h.store.APIKeys(user.ID)
|
||||
if err != nil {
|
||||
html.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
view.Set("apiKeys", apiKeys)
|
||||
view.Set("menu", "settings")
|
||||
view.Set("user", user)
|
||||
view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
|
||||
view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
|
||||
|
||||
html.OK(w, r, view.Render("api_keys"))
|
||||
}
|
24
ui/api_key_remove.go
Normal file
24
ui/api_key_remove.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
// 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 ui // import "miniflux.app/ui"
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"miniflux.app/http/request"
|
||||
"miniflux.app/http/response/html"
|
||||
"miniflux.app/http/route"
|
||||
"miniflux.app/logger"
|
||||
)
|
||||
|
||||
func (h *handler) removeAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||
keyID := request.RouteInt64Param(r, "keyID")
|
||||
err := h.store.RemoveAPIKey(request.UserID(r), keyID)
|
||||
if err != nil {
|
||||
logger.Error("[UI:RemoveAPIKey] %v", err)
|
||||
}
|
||||
|
||||
html.Redirect(w, r, route.Path(h.router, "apiKeys"))
|
||||
}
|
58
ui/api_key_save.go
Normal file
58
ui/api_key_save.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
// 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 ui // import "miniflux.app/ui"
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"miniflux.app/http/request"
|
||||
"miniflux.app/http/response/html"
|
||||
"miniflux.app/http/route"
|
||||
"miniflux.app/logger"
|
||||
"miniflux.app/model"
|
||||
"miniflux.app/ui/form"
|
||||
"miniflux.app/ui/session"
|
||||
"miniflux.app/ui/view"
|
||||
)
|
||||
|
||||
func (h *handler) saveAPIKey(w http.ResponseWriter, r *http.Request) {
|
||||
user, err := h.store.UserByID(request.UserID(r))
|
||||
if err != nil {
|
||||
html.ServerError(w, r, err)
|
||||
return
|
||||
}
|
||||
|
||||
apiKeyForm := form.NewAPIKeyForm(r)
|
||||
|
||||
sess := session.New(h.store, request.SessionID(r))
|
||||
view := view.New(h.tpl, r, sess)
|
||||
view.Set("form", apiKeyForm)
|
||||
view.Set("menu", "settings")
|
||||
view.Set("user", user)
|
||||
view.Set("countUnread", h.store.CountUnreadEntries(user.ID))
|
||||
view.Set("countErrorFeeds", h.store.CountErrorFeeds(user.ID))
|
||||
|
||||
if err := apiKeyForm.Validate(); err != nil {
|
||||
view.Set("errorMessage", err.Error())
|
||||
html.OK(w, r, view.Render("create_api_key"))
|
||||
return
|
||||
}
|
||||
|
||||
if h.store.APIKeyExists(user.ID, apiKeyForm.Description) {
|
||||
view.Set("errorMessage", "error.api_key_already_exists")
|
||||
html.OK(w, r, view.Render("create_api_key"))
|
||||
return
|
||||
}
|
||||
|
||||
apiKey := model.NewAPIKey(user.ID, apiKeyForm.Description)
|
||||
if err = h.store.CreateAPIKey(apiKey); err != nil {
|
||||
logger.Error("[UI:SaveAPIKey] %v", err)
|
||||
view.Set("errorMessage", "error.unable_to_create_api_key")
|
||||
html.OK(w, r, view.Render("create_api_key"))
|
||||
return
|
||||
}
|
||||
|
||||
html.Redirect(w, r, route.Path(h.router, "apiKeys"))
|
||||
}
|
32
ui/form/api_key.go
Normal file
32
ui/form/api_key.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
// 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 form // import "miniflux.app/ui/form"
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"miniflux.app/errors"
|
||||
)
|
||||
|
||||
// APIKeyForm represents the API Key form.
|
||||
type APIKeyForm struct {
|
||||
Description string
|
||||
}
|
||||
|
||||
// Validate makes sure the form values are valid.
|
||||
func (a APIKeyForm) Validate() error {
|
||||
if a.Description == "" {
|
||||
return errors.NewLocalizedError("error.fields_mandatory")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewAPIKeyForm returns a new APIKeyForm.
|
||||
func NewAPIKeyForm(r *http.Request) *APIKeyForm {
|
||||
return &APIKeyForm{
|
||||
Description: r.FormValue("description"),
|
||||
}
|
||||
}
|
6
ui/ui.go
6
ui/ui.go
|
@ -109,6 +109,12 @@ func Serve(router *mux.Router, store *storage.Storage, pool *worker.Pool, feedHa
|
|||
uiRouter.HandleFunc("/sessions", handler.showSessionsPage).Name("sessions").Methods("GET")
|
||||
uiRouter.HandleFunc("/sessions/{sessionID}/remove", handler.removeSession).Name("removeSession").Methods("POST")
|
||||
|
||||
// API Keys pages.
|
||||
uiRouter.HandleFunc("/keys", handler.showAPIKeysPage).Name("apiKeys").Methods("GET")
|
||||
uiRouter.HandleFunc("/keys/{keyID}/remove", handler.removeAPIKey).Name("removeAPIKey").Methods("POST")
|
||||
uiRouter.HandleFunc("/keys/create", handler.showCreateAPIKeyPage).Name("createAPIKey").Methods("GET")
|
||||
uiRouter.HandleFunc("/keys/save", handler.saveAPIKey).Name("saveAPIKey").Methods("POST")
|
||||
|
||||
// OPML pages.
|
||||
uiRouter.HandleFunc("/export", handler.exportFeeds).Name("export").Methods("GET")
|
||||
uiRouter.HandleFunc("/import", handler.showImportPage).Name("import").Methods("GET")
|
||||
|
|
Loading…
Reference in a new issue