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(),
 | 
			
		||||
	}
 | 
			
		||||
	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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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…
	
	Add table
		Add a link
		
	
		Reference in a new issue