Feature: API Tokens (#848)
* initial commit * fix last_used timestamp --------- Co-authored-by: Christian Beutel <>
This commit is contained in:
58
db/main.go
58
db/main.go
@@ -136,6 +136,8 @@ func setupEventHandlers(app *pocketbase.PocketBase, client meilisearch.ServiceMa
|
|||||||
|
|
||||||
app.OnRecordsListRequest("feed", "profile_feed").BindFunc(listFeedHandler())
|
app.OnRecordsListRequest("feed", "profile_feed").BindFunc(listFeedHandler())
|
||||||
|
|
||||||
|
app.OnRecordCreate("api_tokens").BindFunc(createAPITokenHandler())
|
||||||
|
|
||||||
app.OnRecordCreateRequest().BindFunc(sanitizeHTML())
|
app.OnRecordCreateRequest().BindFunc(sanitizeHTML())
|
||||||
app.OnRecordUpdateRequest().BindFunc(sanitizeHTML())
|
app.OnRecordUpdateRequest().BindFunc(sanitizeHTML())
|
||||||
|
|
||||||
@@ -955,6 +957,22 @@ func listFeedHandler() func(e *core.RecordsListRequestEvent) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createAPITokenHandler() func(e *core.RecordEvent) error {
|
||||||
|
return func(e *core.RecordEvent) error {
|
||||||
|
rawToken := "wanderer_key_" + security.RandomString(32)
|
||||||
|
|
||||||
|
hashedKey := security.SHA256(rawToken)
|
||||||
|
|
||||||
|
e.Record.Set("token", hashedKey)
|
||||||
|
|
||||||
|
// Temporarily store rawToken so we can display it once to the user
|
||||||
|
e.Record.WithCustomData(true)
|
||||||
|
e.Record.Set("rawToken", rawToken)
|
||||||
|
|
||||||
|
return e.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func onBeforeServeHandler(client meilisearch.ServiceManager) func(se *core.ServeEvent) error {
|
func onBeforeServeHandler(client meilisearch.ServiceManager) func(se *core.ServeEvent) error {
|
||||||
return func(se *core.ServeEvent) error {
|
return func(se *core.ServeEvent) error {
|
||||||
registerRoutes(se, client)
|
registerRoutes(se, client)
|
||||||
@@ -1009,6 +1027,46 @@ func registerRoutes(se *core.ServeEvent, client meilisearch.ServiceManager) {
|
|||||||
return e.JSON(http.StatusOK, map[string]string{"status": "ok"})
|
return e.JSON(http.StatusOK, map[string]string{"status": "ok"})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
se.Router.POST("/auth/token", func(e *core.RequestEvent) error {
|
||||||
|
var data struct {
|
||||||
|
APIToken string `json:"api_token"`
|
||||||
|
}
|
||||||
|
if err := e.BindBody(&data); err != nil {
|
||||||
|
return apis.NewBadRequestError("Failed to read request data", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
hashedAPIToken := security.SHA256(data.APIToken)
|
||||||
|
|
||||||
|
tokenRecord, err := e.App.FindFirstRecordByFilter(
|
||||||
|
"api_tokens",
|
||||||
|
"token = {:hash}",
|
||||||
|
map[string]any{"hash": hashedAPIToken},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return apis.NewNotFoundError("Invalid or revoked API token", nil)
|
||||||
|
}
|
||||||
|
if !tokenRecord.GetDateTime("expiration").IsZero() &&
|
||||||
|
tokenRecord.GetDateTime("expiration").Time().Before(time.Now()) {
|
||||||
|
return apis.NewBadRequestError("Key has expired", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenRecord.Set("last_used", time.Now())
|
||||||
|
if err := e.App.Save(tokenRecord); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
userRecord, _ := e.App.FindRecordById("users", tokenRecord.GetString("user"))
|
||||||
|
token, err := userRecord.NewAuthToken()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return e.JSON(http.StatusOK, map[string]any{
|
||||||
|
"token": token,
|
||||||
|
"record": userRecord,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
se.Router.GET("/search/token", func(e *core.RequestEvent) error {
|
se.Router.GET("/search/token", func(e *core.RequestEvent) error {
|
||||||
searchRules := map[string]interface{}{
|
searchRules := map[string]interface{}{
|
||||||
"lists": map[string]string{"filter": "public = true"},
|
"lists": map[string]string{"filter": "public = true"},
|
||||||
|
|||||||
138
db/migrations/1772293922_created_api_tokens.go
Normal file
138
db/migrations/1772293922_created_api_tokens.go
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
m "github.com/pocketbase/pocketbase/migrations"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
m.Register(func(app core.App) error {
|
||||||
|
jsonData := `{
|
||||||
|
"createRule": "user = @request.auth.id",
|
||||||
|
"deleteRule": "user = @request.auth.id",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"autogeneratePattern": "[a-z0-9]{15}",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text3208210256",
|
||||||
|
"max": 15,
|
||||||
|
"min": 15,
|
||||||
|
"name": "id",
|
||||||
|
"pattern": "^[a-z0-9]+$",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": true,
|
||||||
|
"required": true,
|
||||||
|
"system": true,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "text1579384326",
|
||||||
|
"max": 0,
|
||||||
|
"min": 0,
|
||||||
|
"name": "name",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": true,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"autogeneratePattern": "",
|
||||||
|
"hidden": true,
|
||||||
|
"id": "text1597481275",
|
||||||
|
"max": 64,
|
||||||
|
"min": 64,
|
||||||
|
"name": "token",
|
||||||
|
"pattern": "",
|
||||||
|
"presentable": false,
|
||||||
|
"primaryKey": false,
|
||||||
|
"required": true,
|
||||||
|
"system": false,
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "date617435213",
|
||||||
|
"max": "",
|
||||||
|
"min": "",
|
||||||
|
"name": "expiration",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "date4016875332",
|
||||||
|
"max": "",
|
||||||
|
"min": "",
|
||||||
|
"name": "last_used",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cascadeDelete": false,
|
||||||
|
"collectionId": "_pb_users_auth_",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "relation2375276105",
|
||||||
|
"maxSelect": 1,
|
||||||
|
"minSelect": 0,
|
||||||
|
"name": "user",
|
||||||
|
"presentable": false,
|
||||||
|
"required": true,
|
||||||
|
"system": false,
|
||||||
|
"type": "relation"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "autodate2990389176",
|
||||||
|
"name": "created",
|
||||||
|
"onCreate": true,
|
||||||
|
"onUpdate": false,
|
||||||
|
"presentable": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "autodate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "autodate3332085495",
|
||||||
|
"name": "updated",
|
||||||
|
"onCreate": true,
|
||||||
|
"onUpdate": true,
|
||||||
|
"presentable": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "autodate"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"id": "pbc_3525142174",
|
||||||
|
"indexes": [],
|
||||||
|
"listRule": "user = @request.auth.id",
|
||||||
|
"name": "api_tokens",
|
||||||
|
"system": false,
|
||||||
|
"type": "base",
|
||||||
|
"updateRule": null,
|
||||||
|
"viewRule": "user = @request.auth.id"
|
||||||
|
}`
|
||||||
|
|
||||||
|
collection := &core.Collection{}
|
||||||
|
if err := json.Unmarshal([]byte(jsonData), &collection); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.Save(collection)
|
||||||
|
}, func(app core.App) error {
|
||||||
|
collection, err := app.FindCollectionByNameOrId("pbc_3525142174")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.Delete(collection)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -54,8 +54,30 @@ const auth: Handle = async ({ event, resolve }) => {
|
|||||||
const pb = new PocketBase(envPub.PUBLIC_POCKETBASE_URL)
|
const pb = new PocketBase(envPub.PUBLIC_POCKETBASE_URL)
|
||||||
const url = new URL(event.request.url);
|
const url = new URL(event.request.url);
|
||||||
|
|
||||||
// load the store data from the request cookie string
|
// Handle API token based auth for API requests
|
||||||
pb.authStore.loadFromCookie(event.request.headers.get('cookie') || '')
|
if (event.request.headers.has("Authorization") && url.pathname.startsWith("/api")) {
|
||||||
|
const authHeader = event.request.headers.get("Authorization") as string;
|
||||||
|
const apiToken = authHeader.replace("Bearer ", "");
|
||||||
|
if (apiToken.startsWith("wanderer_key")) {
|
||||||
|
try {
|
||||||
|
const authData = await pb.send("/auth/token", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
api_token: apiToken
|
||||||
|
}),
|
||||||
|
fetch: event.fetch,
|
||||||
|
})
|
||||||
|
pb.authStore.save(authData.token, authData.record)
|
||||||
|
} catch (e) {
|
||||||
|
throw error(500, "Failed to verify API token " + e)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// load the store data from the request cookie string
|
||||||
|
pb.authStore.loadFromCookie(event.request.headers.get('cookie') || '')
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const secure = event.url.protocol === "https:"
|
const secure = event.url.protocol === "https:"
|
||||||
let meiliCookie = event.cookies.get('meilisearch_token');
|
let meiliCookie = event.cookies.get('meilisearch_token');
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
value?: string | Date;
|
value?: string | Date;
|
||||||
label?: string;
|
label?: string;
|
||||||
error?: string | string[] | null;
|
error?: string | string[] | null;
|
||||||
|
min?: string | number;
|
||||||
|
max?: string | number;
|
||||||
onchange?: ChangeEventHandler<HTMLInputElement>;
|
onchange?: ChangeEventHandler<HTMLInputElement>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,7 +17,9 @@
|
|||||||
value = $bindable(),
|
value = $bindable(),
|
||||||
label = "",
|
label = "",
|
||||||
error = "",
|
error = "",
|
||||||
onchange
|
min,
|
||||||
|
max,
|
||||||
|
onchange,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -32,6 +36,8 @@
|
|||||||
class:border-red-400={(error?.length ?? 0) > 0}
|
class:border-red-400={(error?.length ?? 0) > 0}
|
||||||
class:bg-input-background-error={(error?.length ?? 0) > 0}
|
class:bg-input-background-error={(error?.length ?? 0) > 0}
|
||||||
type="date"
|
type="date"
|
||||||
|
{min}
|
||||||
|
{max}
|
||||||
bind:value
|
bind:value
|
||||||
{onchange}
|
{onchange}
|
||||||
/>
|
/>
|
||||||
|
|||||||
88
web/src/lib/components/settings/api_token_modal.svelte
Normal file
88
web/src/lib/components/settings/api_token_modal.svelte
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Modal from "$lib/components/base/modal.svelte";
|
||||||
|
import { APITokenCreateSchema } from "$lib/models/api/api_token_schema";
|
||||||
|
import type { APIToken } from "$lib/models/api_token";
|
||||||
|
import { validator } from "@felte/validator-zod";
|
||||||
|
import { createForm } from "felte";
|
||||||
|
import { _ } from "svelte-i18n";
|
||||||
|
import { z } from "zod";
|
||||||
|
import Datepicker from "../base/datepicker.svelte";
|
||||||
|
import TextField from "../base/text_field.svelte";
|
||||||
|
import Toggle from "../base/toggle.svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onsave?: (token: APIToken) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { onsave }: Props = $props();
|
||||||
|
|
||||||
|
let modal: Modal;
|
||||||
|
|
||||||
|
let expires: boolean = $state(false);
|
||||||
|
|
||||||
|
const today = new Date();
|
||||||
|
today.setDate(today.getDate() + 1);
|
||||||
|
const tomorrow = today.toISOString().split("T")[0];
|
||||||
|
|
||||||
|
export function openModal() {
|
||||||
|
modal.openModal();
|
||||||
|
setFields("name", "");
|
||||||
|
setFields("expiration", undefined);
|
||||||
|
setErrors("name", []);
|
||||||
|
setErrors("expiration", []);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { form, errors, setFields, setErrors } = createForm<
|
||||||
|
z.infer<typeof APITokenCreateSchema>
|
||||||
|
>({
|
||||||
|
initialValues: {
|
||||||
|
name: "",
|
||||||
|
expiration: "",
|
||||||
|
},
|
||||||
|
extend: validator({
|
||||||
|
schema: APITokenCreateSchema.extend({
|
||||||
|
user: z.string().optional(),
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
onSubmit: async (form) => {
|
||||||
|
onsave?.(form);
|
||||||
|
modal.closeModal!();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
id="api-token-modal"
|
||||||
|
size="md:min-w-md"
|
||||||
|
title={$_("generate-new-token")}
|
||||||
|
bind:this={modal}
|
||||||
|
>
|
||||||
|
{#snippet content()}
|
||||||
|
<form class="space-y-4" id="api-token-form" use:form>
|
||||||
|
<TextField label={$_("name")} name="name" error={$errors.name}
|
||||||
|
></TextField>
|
||||||
|
<Toggle label={$_("expires")} bind:value={expires}></Toggle>
|
||||||
|
{#if expires}
|
||||||
|
<Datepicker
|
||||||
|
label={$_("expiration")}
|
||||||
|
name="expiration"
|
||||||
|
error={$errors.expiration}
|
||||||
|
min={tomorrow}
|
||||||
|
></Datepicker>
|
||||||
|
{/if}
|
||||||
|
</form>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet footer()}
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<button class="btn-secondary" onclick={() => modal.closeModal()}
|
||||||
|
>{$_("cancel")}</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
class="btn-primary"
|
||||||
|
type="submit"
|
||||||
|
form="api-token-form"
|
||||||
|
name="save">{$_("save")}</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/snippet}</Modal
|
||||||
|
>
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Modal from "$lib/components/base/modal.svelte";
|
||||||
|
import { _ } from "svelte-i18n";
|
||||||
|
import TextField from "../base/text_field.svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
token: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { token = $bindable() }: Props = $props();
|
||||||
|
|
||||||
|
let modal: Modal;
|
||||||
|
let copied: boolean = $state(false);
|
||||||
|
|
||||||
|
export function openModal() {
|
||||||
|
copied = false;
|
||||||
|
modal.openModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeModal() {
|
||||||
|
token = null;
|
||||||
|
modal.closeModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyTokenToClipboard() {
|
||||||
|
if (!token) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
navigator.clipboard.writeText(token);
|
||||||
|
copied = true;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
id="api-token-success-modal"
|
||||||
|
size="md:min-w-md"
|
||||||
|
title={$_("new-token-generated")}
|
||||||
|
bind:this={modal}
|
||||||
|
>
|
||||||
|
{#snippet content()}
|
||||||
|
<p class="max-w-lg">
|
||||||
|
Please copy your new API token now. For your security, we cannot
|
||||||
|
show it to you again. If you lose this token, you will need to
|
||||||
|
delete it and generate a new one.
|
||||||
|
</p>
|
||||||
|
<div class="flex items-center gap-x-4 flex-nowrap">
|
||||||
|
<div class="flex-1">
|
||||||
|
<TextField value={token ?? ""} disabled></TextField>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="btn-secondary"
|
||||||
|
onclick={() => copyTokenToClipboard()}
|
||||||
|
aria-label="copy token to clipboard"
|
||||||
|
><i class="fa {copied ? 'fa-check' : 'fa-clipboard'}"
|
||||||
|
></i></button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
{#snippet footer()}
|
||||||
|
<button class="btn-primary" onclick={() => modal.closeModal()}
|
||||||
|
>{$_("close")}</button
|
||||||
|
>
|
||||||
|
{/snippet}</Modal
|
||||||
|
>
|
||||||
@@ -24,6 +24,8 @@
|
|||||||
"amenity": "",
|
"amenity": "",
|
||||||
"ammenity": "Einrichtung",
|
"ammenity": "Einrichtung",
|
||||||
"api-documentation": "API Dokumentation",
|
"api-documentation": "API Dokumentation",
|
||||||
|
"api-tokens": "",
|
||||||
|
"api-tokens-hint": "",
|
||||||
"apply-user-settings": "",
|
"apply-user-settings": "",
|
||||||
"attraction": "Sehenswürdigkeit",
|
"attraction": "Sehenswürdigkeit",
|
||||||
"author": "Autor",
|
"author": "Autor",
|
||||||
@@ -139,10 +141,12 @@
|
|||||||
"entry": "Eintrag",
|
"entry": "Eintrag",
|
||||||
"error-copying-trail": "Fehler beim Kopieren der Route",
|
"error-copying-trail": "Fehler beim Kopieren der Route",
|
||||||
"error-creating-user": "Fehler beim Erstellen des Nutzers",
|
"error-creating-user": "Fehler beim Erstellen des Nutzers",
|
||||||
|
"error-deleting-token": "",
|
||||||
"error-disabling-strava-integration": "Fehler beim Deaktivieren der Stravaintegration",
|
"error-disabling-strava-integration": "Fehler beim Deaktivieren der Stravaintegration",
|
||||||
"error-during-login": "Fehler beim Login",
|
"error-during-login": "Fehler beim Login",
|
||||||
"error-during-password-reset": "Email zum Zurücksetzen des Passworts konnte nicht versandt werden",
|
"error-during-password-reset": "Email zum Zurücksetzen des Passworts konnte nicht versandt werden",
|
||||||
"error-exporting-trail": "Fehler beim Exportieren der Route",
|
"error-exporting-trail": "Fehler beim Exportieren der Route",
|
||||||
|
"error-generating-token": "",
|
||||||
"error-liking-trail": "Error liking trail",
|
"error-liking-trail": "Error liking trail",
|
||||||
"error-logging-in-to-komoot": "Fehler bei der Anmeldung bei komoot",
|
"error-logging-in-to-komoot": "Fehler bei der Anmeldung bei komoot",
|
||||||
"error-posting-comment": "Fehler beim Posten des Kommentars",
|
"error-posting-comment": "Fehler beim Posten des Kommentars",
|
||||||
@@ -155,6 +159,8 @@
|
|||||||
"error-updating-strava-integration": "Fehler bei Aktualisierung der komoot-Integration",
|
"error-updating-strava-integration": "Fehler bei Aktualisierung der komoot-Integration",
|
||||||
"est-duration": "Gesch. Dauer",
|
"est-duration": "Gesch. Dauer",
|
||||||
"everyone-with-the-link": "Jeder mit dem Link",
|
"everyone-with-the-link": "Jeder mit dem Link",
|
||||||
|
"expiration": "",
|
||||||
|
"expires": "",
|
||||||
"explore": "Erkunden",
|
"explore": "Erkunden",
|
||||||
"explore-some-trails": "Erkunde einige Routen",
|
"explore-some-trails": "Erkunde einige Routen",
|
||||||
"export": "Exportieren",
|
"export": "Exportieren",
|
||||||
@@ -183,6 +189,7 @@
|
|||||||
"from-url": "Aus einer URL",
|
"from-url": "Aus einer URL",
|
||||||
"garage": "Auto Reparatur",
|
"garage": "Auto Reparatur",
|
||||||
"gas-station": "Tankstelle",
|
"gas-station": "Tankstelle",
|
||||||
|
"generate-new-token": "",
|
||||||
"german": "Deutsch",
|
"german": "Deutsch",
|
||||||
"get-position-from-exif": "Koordinaten aus EXIF Daten",
|
"get-position-from-exif": "Koordinaten aus EXIF Daten",
|
||||||
"get-started": "Los geht’s",
|
"get-started": "Los geht’s",
|
||||||
@@ -222,6 +229,7 @@
|
|||||||
"keep-original": "",
|
"keep-original": "",
|
||||||
"keep-private": "Ohne Veröffentlichung fortfahren",
|
"keep-private": "Ohne Veröffentlichung fortfahren",
|
||||||
"language": "Sprache",
|
"language": "Sprache",
|
||||||
|
"last-used": "",
|
||||||
"latitude": "Breitengrad",
|
"latitude": "Breitengrad",
|
||||||
"layer": "{n, plural, =1 {Ebene} other {Ebenen}}",
|
"layer": "{n, plural, =1 {Ebene} other {Ebenen}}",
|
||||||
"license": "Lizenz",
|
"license": "Lizenz",
|
||||||
@@ -266,13 +274,16 @@
|
|||||||
"n-years-ago": "vor {n} Jahren",
|
"n-years-ago": "vor {n} Jahren",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"near": "Nahe",
|
"near": "Nahe",
|
||||||
|
"never": "",
|
||||||
"new-list": "Neue Liste",
|
"new-list": "Neue Liste",
|
||||||
"new-password": "Neues Passwort",
|
"new-password": "Neues Passwort",
|
||||||
"new-password-error": "Fehler beim Speichern des Passworts",
|
"new-password-error": "Fehler beim Speichern des Passworts",
|
||||||
"new-password-success": "Neues Passwort wurde gesetzt",
|
"new-password-success": "Neues Passwort wurde gesetzt",
|
||||||
"new-password-text": "Wähle ein neues Passwort",
|
"new-password-text": "Wähle ein neues Passwort",
|
||||||
|
"new-token-generated": "",
|
||||||
"new-trail": "Neue Route",
|
"new-trail": "Neue Route",
|
||||||
"no-account": "Du hast noch kein Konto?",
|
"no-account": "Du hast noch kein Konto?",
|
||||||
|
"no-api-tokens": "",
|
||||||
"no-comments-so-far": "Bisher keine Kommentare",
|
"no-comments-so-far": "Bisher keine Kommentare",
|
||||||
"no-data": "Keine Daten",
|
"no-data": "Keine Daten",
|
||||||
"no-description-for-now": "Noch keine Beschreibung",
|
"no-description-for-now": "Noch keine Beschreibung",
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
"amenity": "Amenity",
|
"amenity": "Amenity",
|
||||||
"ammenity": "",
|
"ammenity": "",
|
||||||
"api-documentation": "API Documentation",
|
"api-documentation": "API Documentation",
|
||||||
|
"api-tokens": "API Tokens",
|
||||||
|
"api-tokens-hint": "API Tokens can be used to grant 3rd party applications access to your wanderer account.",
|
||||||
"apply-user-settings": "Apply user settings",
|
"apply-user-settings": "Apply user settings",
|
||||||
"attraction": "Attraction",
|
"attraction": "Attraction",
|
||||||
"author": "Author",
|
"author": "Author",
|
||||||
@@ -139,10 +141,12 @@
|
|||||||
"entry": "Entry",
|
"entry": "Entry",
|
||||||
"error-copying-trail": "Error copying trail",
|
"error-copying-trail": "Error copying trail",
|
||||||
"error-creating-user": "Error creating user",
|
"error-creating-user": "Error creating user",
|
||||||
|
"error-deleting-token": "Error deleting token",
|
||||||
"error-disabling-strava-integration": "Error disabling strava integration",
|
"error-disabling-strava-integration": "Error disabling strava integration",
|
||||||
"error-during-login": "Error during login",
|
"error-during-login": "Error during login",
|
||||||
"error-during-password-reset": "Unable to send password reset email",
|
"error-during-password-reset": "Unable to send password reset email",
|
||||||
"error-exporting-trail": "Error exporting trail",
|
"error-exporting-trail": "Error exporting trail",
|
||||||
|
"error-generating-token": "Error generating token",
|
||||||
"error-liking-trail": "Error liking trail",
|
"error-liking-trail": "Error liking trail",
|
||||||
"error-logging-in-to-komoot": "Error logging in to komoot",
|
"error-logging-in-to-komoot": "Error logging in to komoot",
|
||||||
"error-posting-comment": "Error posting comment",
|
"error-posting-comment": "Error posting comment",
|
||||||
@@ -155,6 +159,8 @@
|
|||||||
"error-updating-strava-integration": "Error updating komoot integration",
|
"error-updating-strava-integration": "Error updating komoot integration",
|
||||||
"est-duration": "Est. duration",
|
"est-duration": "Est. duration",
|
||||||
"everyone-with-the-link": "Everyone with the link",
|
"everyone-with-the-link": "Everyone with the link",
|
||||||
|
"expiration": "Expiration",
|
||||||
|
"expires": "Expires",
|
||||||
"explore": "Explore",
|
"explore": "Explore",
|
||||||
"explore-some-trails": "Explore some trails",
|
"explore-some-trails": "Explore some trails",
|
||||||
"export": "Export",
|
"export": "Export",
|
||||||
@@ -183,6 +189,7 @@
|
|||||||
"from-url": "From URL",
|
"from-url": "From URL",
|
||||||
"garage": "Garage",
|
"garage": "Garage",
|
||||||
"gas-station": "Gas station",
|
"gas-station": "Gas station",
|
||||||
|
"generate-new-token": "Generate new token",
|
||||||
"german": "German",
|
"german": "German",
|
||||||
"get-position-from-exif": "Get coordinates from EXIF data",
|
"get-position-from-exif": "Get coordinates from EXIF data",
|
||||||
"get-started": "Get started",
|
"get-started": "Get started",
|
||||||
@@ -222,6 +229,7 @@
|
|||||||
"keep-original": "Keep original",
|
"keep-original": "Keep original",
|
||||||
"keep-private": "Keep private",
|
"keep-private": "Keep private",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
|
"last-used": "Last used",
|
||||||
"latitude": "Latitude",
|
"latitude": "Latitude",
|
||||||
"layer": "{n, plural, =1 {Layer} other {Layers}}",
|
"layer": "{n, plural, =1 {Layer} other {Layers}}",
|
||||||
"license": "License",
|
"license": "License",
|
||||||
@@ -266,13 +274,16 @@
|
|||||||
"n-years-ago": "{n} years ago",
|
"n-years-ago": "{n} years ago",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"near": "Near",
|
"near": "Near",
|
||||||
|
"never": "Never",
|
||||||
"new-list": "New List",
|
"new-list": "New List",
|
||||||
"new-password": "New password",
|
"new-password": "New password",
|
||||||
"new-password-error": "Error setting new password",
|
"new-password-error": "Error setting new password",
|
||||||
"new-password-success": "The new password has been set",
|
"new-password-success": "The new password has been set",
|
||||||
"new-password-text": "Set a new password",
|
"new-password-text": "Set a new password",
|
||||||
|
"new-token-generated": "New API Token generated",
|
||||||
"new-trail": "New Trail",
|
"new-trail": "New Trail",
|
||||||
"no-account": "Don't have an account?",
|
"no-account": "Don't have an account?",
|
||||||
|
"no-api-tokens": "You have no API Tokens",
|
||||||
"no-comments-so-far": "No comments so far",
|
"no-comments-so-far": "No comments so far",
|
||||||
"no-data": "No data",
|
"no-data": "No data",
|
||||||
"no-description-for-now": "No description for now",
|
"no-description-for-now": "No description for now",
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
"amenity": "",
|
"amenity": "",
|
||||||
"ammenity": "Servicios",
|
"ammenity": "Servicios",
|
||||||
"api-documentation": "Documentación API",
|
"api-documentation": "Documentación API",
|
||||||
|
"api-tokens": "",
|
||||||
|
"api-tokens-hint": "",
|
||||||
"apply-user-settings": "",
|
"apply-user-settings": "",
|
||||||
"attraction": "Atracción",
|
"attraction": "Atracción",
|
||||||
"author": "Autor",
|
"author": "Autor",
|
||||||
@@ -37,6 +39,7 @@
|
|||||||
"basic-info": "Información básica",
|
"basic-info": "Información básica",
|
||||||
"basque": "Vasco",
|
"basque": "Vasco",
|
||||||
"before": "Antes",
|
"before": "Antes",
|
||||||
|
"behavior": "",
|
||||||
"bicycle-parking": "Aparcamiento de bicicletas",
|
"bicycle-parking": "Aparcamiento de bicicletas",
|
||||||
"bicycle-rental": "Alquiler de bicicletas",
|
"bicycle-rental": "Alquiler de bicicletas",
|
||||||
"bicycle-shop": "Tienda de bicicletas",
|
"bicycle-shop": "Tienda de bicicletas",
|
||||||
@@ -138,10 +141,12 @@
|
|||||||
"entry": "Entrada",
|
"entry": "Entrada",
|
||||||
"error-copying-trail": "",
|
"error-copying-trail": "",
|
||||||
"error-creating-user": "Error creando el usuario",
|
"error-creating-user": "Error creando el usuario",
|
||||||
|
"error-deleting-token": "",
|
||||||
"error-disabling-strava-integration": "Error al desactivar la integración de strava",
|
"error-disabling-strava-integration": "Error al desactivar la integración de strava",
|
||||||
"error-during-login": "Error durante el acceso",
|
"error-during-login": "Error durante el acceso",
|
||||||
"error-during-password-reset": "Imposible enviar la contraseña de restablecimiento al correo electrónico",
|
"error-during-password-reset": "Imposible enviar la contraseña de restablecimiento al correo electrónico",
|
||||||
"error-exporting-trail": "Error exportando la ruta",
|
"error-exporting-trail": "Error exportando la ruta",
|
||||||
|
"error-generating-token": "",
|
||||||
"error-liking-trail": "Error al dar \"me gusta\" a la ruta",
|
"error-liking-trail": "Error al dar \"me gusta\" a la ruta",
|
||||||
"error-logging-in-to-komoot": "Error al iniciar sesión en komoot",
|
"error-logging-in-to-komoot": "Error al iniciar sesión en komoot",
|
||||||
"error-posting-comment": "Error publicando el comentario",
|
"error-posting-comment": "Error publicando el comentario",
|
||||||
@@ -154,6 +159,8 @@
|
|||||||
"error-updating-strava-integration": "Error al actualizar la integración con komoot",
|
"error-updating-strava-integration": "Error al actualizar la integración con komoot",
|
||||||
"est-duration": "Duración estimada",
|
"est-duration": "Duración estimada",
|
||||||
"everyone-with-the-link": "Cualquier persona con el enlace",
|
"everyone-with-the-link": "Cualquier persona con el enlace",
|
||||||
|
"expiration": "",
|
||||||
|
"expires": "",
|
||||||
"explore": "Explora",
|
"explore": "Explora",
|
||||||
"explore-some-trails": "Explora alguna ruta",
|
"explore-some-trails": "Explora alguna ruta",
|
||||||
"export": "Exportar",
|
"export": "Exportar",
|
||||||
@@ -182,6 +189,7 @@
|
|||||||
"from-url": "Desde URL",
|
"from-url": "Desde URL",
|
||||||
"garage": "Garaje",
|
"garage": "Garaje",
|
||||||
"gas-station": "Gasolinera",
|
"gas-station": "Gasolinera",
|
||||||
|
"generate-new-token": "",
|
||||||
"german": "Alemán",
|
"german": "Alemán",
|
||||||
"get-position-from-exif": "Obtener las coordenadas de los datos EXIF",
|
"get-position-from-exif": "Obtener las coordenadas de los datos EXIF",
|
||||||
"get-started": "Iniciar",
|
"get-started": "Iniciar",
|
||||||
@@ -221,6 +229,7 @@
|
|||||||
"keep-original": "",
|
"keep-original": "",
|
||||||
"keep-private": "Keep private",
|
"keep-private": "Keep private",
|
||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
|
"last-used": "",
|
||||||
"latitude": "Latitud",
|
"latitude": "Latitud",
|
||||||
"layer": "{n, plural, one {}=1 {Lista} other {Listas}}",
|
"layer": "{n, plural, one {}=1 {Lista} other {Listas}}",
|
||||||
"license": "Licencia",
|
"license": "Licencia",
|
||||||
@@ -265,13 +274,16 @@
|
|||||||
"n-years-ago": "hace {n} años",
|
"n-years-ago": "hace {n} años",
|
||||||
"name": "Nombre",
|
"name": "Nombre",
|
||||||
"near": "Cerca",
|
"near": "Cerca",
|
||||||
|
"never": "",
|
||||||
"new-list": "Nueva lista",
|
"new-list": "Nueva lista",
|
||||||
"new-password": "Nueva contraseña",
|
"new-password": "Nueva contraseña",
|
||||||
"new-password-error": "Error estableciendo la nueva contraseña",
|
"new-password-error": "Error estableciendo la nueva contraseña",
|
||||||
"new-password-success": "La nueva contraseña ha sido configurada",
|
"new-password-success": "La nueva contraseña ha sido configurada",
|
||||||
"new-password-text": "Configura una nueva contraseña",
|
"new-password-text": "Configura una nueva contraseña",
|
||||||
|
"new-token-generated": "",
|
||||||
"new-trail": "Nueva Ruta",
|
"new-trail": "Nueva Ruta",
|
||||||
"no-account": "¿No tienes una cuenta?",
|
"no-account": "¿No tienes una cuenta?",
|
||||||
|
"no-api-tokens": "",
|
||||||
"no-comments-so-far": "Ningún comentario todavía",
|
"no-comments-so-far": "Ningún comentario todavía",
|
||||||
"no-data": "No datos",
|
"no-data": "No datos",
|
||||||
"no-description-for-now": "Ninguna descripción de momento",
|
"no-description-for-now": "Ninguna descripción de momento",
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
"amenity": "Altimetria",
|
"amenity": "Altimetria",
|
||||||
"ammenity": "",
|
"ammenity": "",
|
||||||
"api-documentation": "API dokumentazioa",
|
"api-documentation": "API dokumentazioa",
|
||||||
|
"api-tokens": "",
|
||||||
|
"api-tokens-hint": "",
|
||||||
"apply-user-settings": "",
|
"apply-user-settings": "",
|
||||||
"attraction": "Erakarmena",
|
"attraction": "Erakarmena",
|
||||||
"author": "Egilea",
|
"author": "Egilea",
|
||||||
@@ -37,6 +39,7 @@
|
|||||||
"basic-info": "Oinarrizko informazioa",
|
"basic-info": "Oinarrizko informazioa",
|
||||||
"basque": "Euskara",
|
"basque": "Euskara",
|
||||||
"before": "Aurretik",
|
"before": "Aurretik",
|
||||||
|
"behavior": "",
|
||||||
"bicycle-parking": "Bizikleta-parkina",
|
"bicycle-parking": "Bizikleta-parkina",
|
||||||
"bicycle-rental": "Bizikleta-alokairua",
|
"bicycle-rental": "Bizikleta-alokairua",
|
||||||
"bicycle-shop": "Bizikleta-denda",
|
"bicycle-shop": "Bizikleta-denda",
|
||||||
@@ -138,10 +141,12 @@
|
|||||||
"entry": "Sarrera",
|
"entry": "Sarrera",
|
||||||
"error-copying-trail": "",
|
"error-copying-trail": "",
|
||||||
"error-creating-user": "Errorea erabiltzailea sortzen",
|
"error-creating-user": "Errorea erabiltzailea sortzen",
|
||||||
|
"error-deleting-token": "",
|
||||||
"error-disabling-strava-integration": "Errorea stravarekin integrazioa desaktibatzean",
|
"error-disabling-strava-integration": "Errorea stravarekin integrazioa desaktibatzean",
|
||||||
"error-during-login": "Errorea sartzean",
|
"error-during-login": "Errorea sartzean",
|
||||||
"error-during-password-reset": "Ezin izan da pasahitza berrezartzeko mezua bidali",
|
"error-during-password-reset": "Ezin izan da pasahitza berrezartzeko mezua bidali",
|
||||||
"error-exporting-trail": "Errorea ibilbidea esportatzean",
|
"error-exporting-trail": "Errorea ibilbidea esportatzean",
|
||||||
|
"error-generating-token": "",
|
||||||
"error-liking-trail": "Errorea ibilbidea atsegitean",
|
"error-liking-trail": "Errorea ibilbidea atsegitean",
|
||||||
"error-logging-in-to-komoot": "Errorea komoot-en login egitean",
|
"error-logging-in-to-komoot": "Errorea komoot-en login egitean",
|
||||||
"error-posting-comment": "Errorea iruzkina egitean",
|
"error-posting-comment": "Errorea iruzkina egitean",
|
||||||
@@ -154,6 +159,8 @@
|
|||||||
"error-updating-strava-integration": "Errorea komoot integrazioa eguneratzean",
|
"error-updating-strava-integration": "Errorea komoot integrazioa eguneratzean",
|
||||||
"est-duration": "Ustezko iraupena",
|
"est-duration": "Ustezko iraupena",
|
||||||
"everyone-with-the-link": "Esteka duen edonor",
|
"everyone-with-the-link": "Esteka duen edonor",
|
||||||
|
"expiration": "",
|
||||||
|
"expires": "",
|
||||||
"explore": "Arakatu",
|
"explore": "Arakatu",
|
||||||
"explore-some-trails": "Arakatu ibilbide batzuk",
|
"explore-some-trails": "Arakatu ibilbide batzuk",
|
||||||
"export": "Esportatu",
|
"export": "Esportatu",
|
||||||
@@ -182,6 +189,7 @@
|
|||||||
"from-url": "URL batetik",
|
"from-url": "URL batetik",
|
||||||
"garage": "Garajea",
|
"garage": "Garajea",
|
||||||
"gas-station": "Gasolindegia",
|
"gas-station": "Gasolindegia",
|
||||||
|
"generate-new-token": "",
|
||||||
"german": "Alemaniera",
|
"german": "Alemaniera",
|
||||||
"get-position-from-exif": "Lortu koordenatuak EXIF datuetatik",
|
"get-position-from-exif": "Lortu koordenatuak EXIF datuetatik",
|
||||||
"get-started": "Hasi",
|
"get-started": "Hasi",
|
||||||
@@ -221,6 +229,7 @@
|
|||||||
"keep-original": "",
|
"keep-original": "",
|
||||||
"keep-private": "Keep private",
|
"keep-private": "Keep private",
|
||||||
"language": "Hizkuntza",
|
"language": "Hizkuntza",
|
||||||
|
"last-used": "",
|
||||||
"latitude": "Latitudea",
|
"latitude": "Latitudea",
|
||||||
"layer": "{n, plural, one {}=1 {geruza} other {geruza}}",
|
"layer": "{n, plural, one {}=1 {geruza} other {geruza}}",
|
||||||
"license": "Lizentzia",
|
"license": "Lizentzia",
|
||||||
@@ -265,13 +274,16 @@
|
|||||||
"n-years-ago": "Orain dela {n} urte",
|
"n-years-ago": "Orain dela {n} urte",
|
||||||
"name": "Izena",
|
"name": "Izena",
|
||||||
"near": "Gertu",
|
"near": "Gertu",
|
||||||
|
"never": "",
|
||||||
"new-list": "Zerrenda berria",
|
"new-list": "Zerrenda berria",
|
||||||
"new-password": "Pasahitz berria",
|
"new-password": "Pasahitz berria",
|
||||||
"new-password-error": "Errorea pasahitza berria ezartzean",
|
"new-password-error": "Errorea pasahitza berria ezartzean",
|
||||||
"new-password-success": "Pasahitz berria ezarri da",
|
"new-password-success": "Pasahitz berria ezarri da",
|
||||||
"new-password-text": "Pasahitz berria sortu",
|
"new-password-text": "Pasahitz berria sortu",
|
||||||
|
"new-token-generated": "",
|
||||||
"new-trail": "Ibilaldi berria",
|
"new-trail": "Ibilaldi berria",
|
||||||
"no-account": "Ez duzu konturik?",
|
"no-account": "Ez duzu konturik?",
|
||||||
|
"no-api-tokens": "",
|
||||||
"no-comments-so-far": "Ez dago iruzkinik",
|
"no-comments-so-far": "Ez dago iruzkinik",
|
||||||
"no-data": "Ez dago daturik",
|
"no-data": "Ez dago daturik",
|
||||||
"no-description-for-now": "Ez dago deskribapenik",
|
"no-description-for-now": "Ez dago deskribapenik",
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
"amenity": "",
|
"amenity": "",
|
||||||
"ammenity": "Aménagement",
|
"ammenity": "Aménagement",
|
||||||
"api-documentation": "Documentation API",
|
"api-documentation": "Documentation API",
|
||||||
|
"api-tokens": "",
|
||||||
|
"api-tokens-hint": "",
|
||||||
"apply-user-settings": "",
|
"apply-user-settings": "",
|
||||||
"attraction": "Attraction",
|
"attraction": "Attraction",
|
||||||
"author": "Auteur",
|
"author": "Auteur",
|
||||||
@@ -37,6 +39,7 @@
|
|||||||
"basic-info": "Informations de base",
|
"basic-info": "Informations de base",
|
||||||
"basque": "Basque",
|
"basque": "Basque",
|
||||||
"before": "Avant le",
|
"before": "Avant le",
|
||||||
|
"behavior": "",
|
||||||
"bicycle-parking": "Parking vélo",
|
"bicycle-parking": "Parking vélo",
|
||||||
"bicycle-rental": "Location de vélos",
|
"bicycle-rental": "Location de vélos",
|
||||||
"bicycle-shop": "Magasin de vélos",
|
"bicycle-shop": "Magasin de vélos",
|
||||||
@@ -138,10 +141,12 @@
|
|||||||
"entry": "Entrée",
|
"entry": "Entrée",
|
||||||
"error-copying-trail": "",
|
"error-copying-trail": "",
|
||||||
"error-creating-user": "Erreur durant la création de l'utilisateur",
|
"error-creating-user": "Erreur durant la création de l'utilisateur",
|
||||||
|
"error-deleting-token": "",
|
||||||
"error-disabling-strava-integration": "Erreur lors de la désactivation de l'intégration Strava",
|
"error-disabling-strava-integration": "Erreur lors de la désactivation de l'intégration Strava",
|
||||||
"error-during-login": "Erreur durant la connexion",
|
"error-during-login": "Erreur durant la connexion",
|
||||||
"error-during-password-reset": "Impossible d'envoyer l'e-mail de réinitialisation du mot de passe",
|
"error-during-password-reset": "Impossible d'envoyer l'e-mail de réinitialisation du mot de passe",
|
||||||
"error-exporting-trail": "Erreur lors de l'export de l'itinéraire",
|
"error-exporting-trail": "Erreur lors de l'export de l'itinéraire",
|
||||||
|
"error-generating-token": "",
|
||||||
"error-liking-trail": "Erreur de like de l'itinéraire",
|
"error-liking-trail": "Erreur de like de l'itinéraire",
|
||||||
"error-logging-in-to-komoot": "Erreur de connexion à Komoot",
|
"error-logging-in-to-komoot": "Erreur de connexion à Komoot",
|
||||||
"error-posting-comment": "Erreur lors de la publication du commentaire",
|
"error-posting-comment": "Erreur lors de la publication du commentaire",
|
||||||
@@ -154,6 +159,8 @@
|
|||||||
"error-updating-strava-integration": "Erreur lors de la mise à jour de l'intégration Komoot",
|
"error-updating-strava-integration": "Erreur lors de la mise à jour de l'intégration Komoot",
|
||||||
"est-duration": "Temps estimé",
|
"est-duration": "Temps estimé",
|
||||||
"everyone-with-the-link": "Tout le monde avec ce lien",
|
"everyone-with-the-link": "Tout le monde avec ce lien",
|
||||||
|
"expiration": "",
|
||||||
|
"expires": "",
|
||||||
"explore": "Explorer",
|
"explore": "Explorer",
|
||||||
"explore-some-trails": "Explorer les itinéraires",
|
"explore-some-trails": "Explorer les itinéraires",
|
||||||
"export": "Exporter",
|
"export": "Exporter",
|
||||||
@@ -182,6 +189,7 @@
|
|||||||
"from-url": "Depuis une URL",
|
"from-url": "Depuis une URL",
|
||||||
"garage": "Garage",
|
"garage": "Garage",
|
||||||
"gas-station": "Station-service",
|
"gas-station": "Station-service",
|
||||||
|
"generate-new-token": "",
|
||||||
"german": "Allemand",
|
"german": "Allemand",
|
||||||
"get-position-from-exif": "Obtenir les coordonnées à partir des données EXIF",
|
"get-position-from-exif": "Obtenir les coordonnées à partir des données EXIF",
|
||||||
"get-started": "C'est parti",
|
"get-started": "C'est parti",
|
||||||
@@ -221,6 +229,7 @@
|
|||||||
"keep-original": "",
|
"keep-original": "",
|
||||||
"keep-private": "Keep private",
|
"keep-private": "Keep private",
|
||||||
"language": "Langue",
|
"language": "Langue",
|
||||||
|
"last-used": "",
|
||||||
"latitude": "Latitude",
|
"latitude": "Latitude",
|
||||||
"layer": "{n, plural, =1 {Calque} other {Calques}}",
|
"layer": "{n, plural, =1 {Calque} other {Calques}}",
|
||||||
"license": "Licence",
|
"license": "Licence",
|
||||||
@@ -265,13 +274,16 @@
|
|||||||
"n-years-ago": "il y a {n} ans",
|
"n-years-ago": "il y a {n} ans",
|
||||||
"name": "Nom de l'itinéraire",
|
"name": "Nom de l'itinéraire",
|
||||||
"near": "À proximité de",
|
"near": "À proximité de",
|
||||||
|
"never": "",
|
||||||
"new-list": "Nouvelle liste",
|
"new-list": "Nouvelle liste",
|
||||||
"new-password": "Nouveau mot de passe",
|
"new-password": "Nouveau mot de passe",
|
||||||
"new-password-error": "Erreur lors de l'enregistrement du nouveau mot de passe",
|
"new-password-error": "Erreur lors de l'enregistrement du nouveau mot de passe",
|
||||||
"new-password-success": "Le nouveau mot de passe a été enregistré",
|
"new-password-success": "Le nouveau mot de passe a été enregistré",
|
||||||
"new-password-text": "Définir un nouveau mot de passe",
|
"new-password-text": "Définir un nouveau mot de passe",
|
||||||
|
"new-token-generated": "",
|
||||||
"new-trail": "Nouvel itinéraire",
|
"new-trail": "Nouvel itinéraire",
|
||||||
"no-account": "Pas encore de compte ?",
|
"no-account": "Pas encore de compte ?",
|
||||||
|
"no-api-tokens": "",
|
||||||
"no-comments-so-far": "Aucun commentaire pour l'instant",
|
"no-comments-so-far": "Aucun commentaire pour l'instant",
|
||||||
"no-data": "Pas de données",
|
"no-data": "Pas de données",
|
||||||
"no-description-for-now": "Pas de description pour le moment",
|
"no-description-for-now": "Pas de description pour le moment",
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
"amenity": "Amenity",
|
"amenity": "Amenity",
|
||||||
"ammenity": "",
|
"ammenity": "",
|
||||||
"api-documentation": "API Dokumentáció",
|
"api-documentation": "API Dokumentáció",
|
||||||
|
"api-tokens": "",
|
||||||
|
"api-tokens-hint": "",
|
||||||
"apply-user-settings": "",
|
"apply-user-settings": "",
|
||||||
"attraction": "Attraction",
|
"attraction": "Attraction",
|
||||||
"author": "Author",
|
"author": "Author",
|
||||||
@@ -37,6 +39,7 @@
|
|||||||
"basic-info": "Alap információk",
|
"basic-info": "Alap információk",
|
||||||
"basque": "Basque",
|
"basque": "Basque",
|
||||||
"before": "Before",
|
"before": "Before",
|
||||||
|
"behavior": "",
|
||||||
"bicycle-parking": "Bicycle Parking",
|
"bicycle-parking": "Bicycle Parking",
|
||||||
"bicycle-rental": "Bicycle Rental",
|
"bicycle-rental": "Bicycle Rental",
|
||||||
"bicycle-shop": "Bicycle Shop",
|
"bicycle-shop": "Bicycle Shop",
|
||||||
@@ -138,10 +141,12 @@
|
|||||||
"entry": "Bejegyzés",
|
"entry": "Bejegyzés",
|
||||||
"error-copying-trail": "",
|
"error-copying-trail": "",
|
||||||
"error-creating-user": "Hiba felhasználó hozzáadása közben",
|
"error-creating-user": "Hiba felhasználó hozzáadása közben",
|
||||||
|
"error-deleting-token": "",
|
||||||
"error-disabling-strava-integration": "Error disabling strava integration",
|
"error-disabling-strava-integration": "Error disabling strava integration",
|
||||||
"error-during-login": "Hiba bejelentkezés közben",
|
"error-during-login": "Hiba bejelentkezés közben",
|
||||||
"error-during-password-reset": "Unable to send password reset email",
|
"error-during-password-reset": "Unable to send password reset email",
|
||||||
"error-exporting-trail": "Error exporting trail",
|
"error-exporting-trail": "Error exporting trail",
|
||||||
|
"error-generating-token": "",
|
||||||
"error-liking-trail": "Error liking trail",
|
"error-liking-trail": "Error liking trail",
|
||||||
"error-logging-in-to-komoot": "Error logging in to komoot",
|
"error-logging-in-to-komoot": "Error logging in to komoot",
|
||||||
"error-posting-comment": "Error posting comment",
|
"error-posting-comment": "Error posting comment",
|
||||||
@@ -154,6 +159,8 @@
|
|||||||
"error-updating-strava-integration": "Error updating komoot integration",
|
"error-updating-strava-integration": "Error updating komoot integration",
|
||||||
"est-duration": "Becsült időtartam",
|
"est-duration": "Becsült időtartam",
|
||||||
"everyone-with-the-link": "Everyone with the link",
|
"everyone-with-the-link": "Everyone with the link",
|
||||||
|
"expiration": "",
|
||||||
|
"expires": "",
|
||||||
"explore": "Felfedezés",
|
"explore": "Felfedezés",
|
||||||
"explore-some-trails": "Fedezzen fel néhány ösvényt",
|
"explore-some-trails": "Fedezzen fel néhány ösvényt",
|
||||||
"export": "Export",
|
"export": "Export",
|
||||||
@@ -182,6 +189,7 @@
|
|||||||
"from-url": "From URL",
|
"from-url": "From URL",
|
||||||
"garage": "Garage",
|
"garage": "Garage",
|
||||||
"gas-station": "Gas station",
|
"gas-station": "Gas station",
|
||||||
|
"generate-new-token": "",
|
||||||
"german": "Német",
|
"german": "Német",
|
||||||
"get-position-from-exif": "Get coordinates from EXIF data",
|
"get-position-from-exif": "Get coordinates from EXIF data",
|
||||||
"get-started": "Get started",
|
"get-started": "Get started",
|
||||||
@@ -221,6 +229,7 @@
|
|||||||
"keep-original": "",
|
"keep-original": "",
|
||||||
"keep-private": "Keep private",
|
"keep-private": "Keep private",
|
||||||
"language": "Nyelf",
|
"language": "Nyelf",
|
||||||
|
"last-used": "",
|
||||||
"latitude": "Szélesség",
|
"latitude": "Szélesség",
|
||||||
"layer": "{n, plural, =1 {Layer} other {Layers}}",
|
"layer": "{n, plural, =1 {Layer} other {Layers}}",
|
||||||
"license": "License",
|
"license": "License",
|
||||||
@@ -265,13 +274,16 @@
|
|||||||
"n-years-ago": "{n} years ago",
|
"n-years-ago": "{n} years ago",
|
||||||
"name": "Név",
|
"name": "Név",
|
||||||
"near": "Közelben",
|
"near": "Közelben",
|
||||||
|
"never": "",
|
||||||
"new-list": "Új lista",
|
"new-list": "Új lista",
|
||||||
"new-password": "New password",
|
"new-password": "New password",
|
||||||
"new-password-error": "Error setting new password",
|
"new-password-error": "Error setting new password",
|
||||||
"new-password-success": "The new password has been set",
|
"new-password-success": "The new password has been set",
|
||||||
"new-password-text": "Set a new password",
|
"new-password-text": "Set a new password",
|
||||||
|
"new-token-generated": "",
|
||||||
"new-trail": "Új útvonal",
|
"new-trail": "Új útvonal",
|
||||||
"no-account": "Nincs még fiókja?",
|
"no-account": "Nincs még fiókja?",
|
||||||
|
"no-api-tokens": "",
|
||||||
"no-comments-so-far": "No comments so far",
|
"no-comments-so-far": "No comments so far",
|
||||||
"no-data": "No data",
|
"no-data": "No data",
|
||||||
"no-description-for-now": "No description for now",
|
"no-description-for-now": "No description for now",
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
"amenity": "Amenity",
|
"amenity": "Amenity",
|
||||||
"ammenity": "",
|
"ammenity": "",
|
||||||
"api-documentation": "Documentazione API",
|
"api-documentation": "Documentazione API",
|
||||||
|
"api-tokens": "",
|
||||||
|
"api-tokens-hint": "",
|
||||||
"apply-user-settings": "",
|
"apply-user-settings": "",
|
||||||
"attraction": "Attraction",
|
"attraction": "Attraction",
|
||||||
"author": "Autore",
|
"author": "Autore",
|
||||||
@@ -37,6 +39,7 @@
|
|||||||
"basic-info": "Informazioni di base",
|
"basic-info": "Informazioni di base",
|
||||||
"basque": "Basque",
|
"basque": "Basque",
|
||||||
"before": "Prima",
|
"before": "Prima",
|
||||||
|
"behavior": "",
|
||||||
"bicycle-parking": "Bicycle Parking",
|
"bicycle-parking": "Bicycle Parking",
|
||||||
"bicycle-rental": "Bicycle Rental",
|
"bicycle-rental": "Bicycle Rental",
|
||||||
"bicycle-shop": "Bicycle Shop",
|
"bicycle-shop": "Bicycle Shop",
|
||||||
@@ -138,10 +141,12 @@
|
|||||||
"entry": "Voce",
|
"entry": "Voce",
|
||||||
"error-copying-trail": "",
|
"error-copying-trail": "",
|
||||||
"error-creating-user": "Errore nella creazione dell'utente",
|
"error-creating-user": "Errore nella creazione dell'utente",
|
||||||
|
"error-deleting-token": "",
|
||||||
"error-disabling-strava-integration": "Error disabling strava integration",
|
"error-disabling-strava-integration": "Error disabling strava integration",
|
||||||
"error-during-login": "Errore durante il login",
|
"error-during-login": "Errore durante il login",
|
||||||
"error-during-password-reset": "Impossibile inviare email per ripristinare la password",
|
"error-during-password-reset": "Impossibile inviare email per ripristinare la password",
|
||||||
"error-exporting-trail": "Errore durante l'esportazione del percorso",
|
"error-exporting-trail": "Errore durante l'esportazione del percorso",
|
||||||
|
"error-generating-token": "",
|
||||||
"error-liking-trail": "Error liking trail",
|
"error-liking-trail": "Error liking trail",
|
||||||
"error-logging-in-to-komoot": "Error logging in to komoot",
|
"error-logging-in-to-komoot": "Error logging in to komoot",
|
||||||
"error-posting-comment": "Errore pubblicando il commento",
|
"error-posting-comment": "Errore pubblicando il commento",
|
||||||
@@ -154,6 +159,8 @@
|
|||||||
"error-updating-strava-integration": "Error updating komoot integration",
|
"error-updating-strava-integration": "Error updating komoot integration",
|
||||||
"est-duration": "Durata stimata",
|
"est-duration": "Durata stimata",
|
||||||
"everyone-with-the-link": "Everyone with the link",
|
"everyone-with-the-link": "Everyone with the link",
|
||||||
|
"expiration": "",
|
||||||
|
"expires": "",
|
||||||
"explore": "Esplora",
|
"explore": "Esplora",
|
||||||
"explore-some-trails": "Esplora alcuni percorsi",
|
"explore-some-trails": "Esplora alcuni percorsi",
|
||||||
"export": "Esporta",
|
"export": "Esporta",
|
||||||
@@ -182,6 +189,7 @@
|
|||||||
"from-url": "From URL",
|
"from-url": "From URL",
|
||||||
"garage": "Garage",
|
"garage": "Garage",
|
||||||
"gas-station": "Gas station",
|
"gas-station": "Gas station",
|
||||||
|
"generate-new-token": "",
|
||||||
"german": "Tedesco",
|
"german": "Tedesco",
|
||||||
"get-position-from-exif": "Ottieni posizione da dati EXIF",
|
"get-position-from-exif": "Ottieni posizione da dati EXIF",
|
||||||
"get-started": "Get started",
|
"get-started": "Get started",
|
||||||
@@ -221,6 +229,7 @@
|
|||||||
"keep-original": "",
|
"keep-original": "",
|
||||||
"keep-private": "Keep private",
|
"keep-private": "Keep private",
|
||||||
"language": "Lingua",
|
"language": "Lingua",
|
||||||
|
"last-used": "",
|
||||||
"latitude": "Latitudine",
|
"latitude": "Latitudine",
|
||||||
"layer": "{n, plural, =1 {Layer} other {Layers}}",
|
"layer": "{n, plural, =1 {Layer} other {Layers}}",
|
||||||
"license": "Licenza",
|
"license": "Licenza",
|
||||||
@@ -265,13 +274,16 @@
|
|||||||
"n-years-ago": "{n} anni fa",
|
"n-years-ago": "{n} anni fa",
|
||||||
"name": "Nome",
|
"name": "Nome",
|
||||||
"near": "Vicino",
|
"near": "Vicino",
|
||||||
|
"never": "",
|
||||||
"new-list": "Nuova lista",
|
"new-list": "Nuova lista",
|
||||||
"new-password": "Nuova password",
|
"new-password": "Nuova password",
|
||||||
"new-password-error": "Errore configurando la nuova password",
|
"new-password-error": "Errore configurando la nuova password",
|
||||||
"new-password-success": "La nuova password è stata configurata",
|
"new-password-success": "La nuova password è stata configurata",
|
||||||
"new-password-text": "Definire una nuova password",
|
"new-password-text": "Definire una nuova password",
|
||||||
|
"new-token-generated": "",
|
||||||
"new-trail": "Nuovo percorso",
|
"new-trail": "Nuovo percorso",
|
||||||
"no-account": "Non hai ancora un account?",
|
"no-account": "Non hai ancora un account?",
|
||||||
|
"no-api-tokens": "",
|
||||||
"no-comments-so-far": "Nessun commento per il momento",
|
"no-comments-so-far": "Nessun commento per il momento",
|
||||||
"no-data": "Nessun dato",
|
"no-data": "Nessun dato",
|
||||||
"no-description-for-now": "Nessuna descrizione per il momento",
|
"no-description-for-now": "Nessuna descrizione per il momento",
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
"amenity": "Amenity",
|
"amenity": "Amenity",
|
||||||
"ammenity": "Voorziening",
|
"ammenity": "Voorziening",
|
||||||
"api-documentation": "API-documentatie",
|
"api-documentation": "API-documentatie",
|
||||||
|
"api-tokens": "",
|
||||||
|
"api-tokens-hint": "",
|
||||||
"apply-user-settings": "",
|
"apply-user-settings": "",
|
||||||
"attraction": "Attractie",
|
"attraction": "Attractie",
|
||||||
"author": "Auteur",
|
"author": "Auteur",
|
||||||
@@ -37,6 +39,7 @@
|
|||||||
"basic-info": "Algemene informatie",
|
"basic-info": "Algemene informatie",
|
||||||
"basque": "Baskisch",
|
"basque": "Baskisch",
|
||||||
"before": "Voor",
|
"before": "Voor",
|
||||||
|
"behavior": "",
|
||||||
"bicycle-parking": "Fietsenstalling",
|
"bicycle-parking": "Fietsenstalling",
|
||||||
"bicycle-rental": "Fietsverhuur",
|
"bicycle-rental": "Fietsverhuur",
|
||||||
"bicycle-shop": "Fietsenwinkel",
|
"bicycle-shop": "Fietsenwinkel",
|
||||||
@@ -138,10 +141,12 @@
|
|||||||
"entry": "Item",
|
"entry": "Item",
|
||||||
"error-copying-trail": "",
|
"error-copying-trail": "",
|
||||||
"error-creating-user": "Fout bij aanmaken gebruiker",
|
"error-creating-user": "Fout bij aanmaken gebruiker",
|
||||||
|
"error-deleting-token": "",
|
||||||
"error-disabling-strava-integration": "Fout bij het uitschakelen van Strava-integratie",
|
"error-disabling-strava-integration": "Fout bij het uitschakelen van Strava-integratie",
|
||||||
"error-during-login": "Het inloggen is mislukt",
|
"error-during-login": "Het inloggen is mislukt",
|
||||||
"error-during-password-reset": "Kan geen e-mail voor wachtwoordherstel verzenden",
|
"error-during-password-reset": "Kan geen e-mail voor wachtwoordherstel verzenden",
|
||||||
"error-exporting-trail": "Fout bij exporteren van parcours",
|
"error-exporting-trail": "Fout bij exporteren van parcours",
|
||||||
|
"error-generating-token": "",
|
||||||
"error-liking-trail": "Fout bij het \"leuk vinden\" van route",
|
"error-liking-trail": "Fout bij het \"leuk vinden\" van route",
|
||||||
"error-logging-in-to-komoot": "Fout tijdens inloggen in Komoot",
|
"error-logging-in-to-komoot": "Fout tijdens inloggen in Komoot",
|
||||||
"error-posting-comment": "Fout bij het plaatsen van een reactie",
|
"error-posting-comment": "Fout bij het plaatsen van een reactie",
|
||||||
@@ -154,6 +159,8 @@
|
|||||||
"error-updating-strava-integration": "Fout bij bijwerken van Komoot integratie",
|
"error-updating-strava-integration": "Fout bij bijwerken van Komoot integratie",
|
||||||
"est-duration": "Geschatte duur",
|
"est-duration": "Geschatte duur",
|
||||||
"everyone-with-the-link": "Iedereen met de link",
|
"everyone-with-the-link": "Iedereen met de link",
|
||||||
|
"expiration": "",
|
||||||
|
"expires": "",
|
||||||
"explore": "Verkennen",
|
"explore": "Verkennen",
|
||||||
"explore-some-trails": "Verken enkele routes",
|
"explore-some-trails": "Verken enkele routes",
|
||||||
"export": "Exporteer",
|
"export": "Exporteer",
|
||||||
@@ -182,6 +189,7 @@
|
|||||||
"from-url": "Van URL",
|
"from-url": "Van URL",
|
||||||
"garage": "Garage",
|
"garage": "Garage",
|
||||||
"gas-station": "Benzinestation",
|
"gas-station": "Benzinestation",
|
||||||
|
"generate-new-token": "",
|
||||||
"german": "Duits",
|
"german": "Duits",
|
||||||
"get-position-from-exif": "Coördinaten ophalen uit EXIF-gegevens",
|
"get-position-from-exif": "Coördinaten ophalen uit EXIF-gegevens",
|
||||||
"get-started": "Aan de slag",
|
"get-started": "Aan de slag",
|
||||||
@@ -221,6 +229,7 @@
|
|||||||
"keep-original": "",
|
"keep-original": "",
|
||||||
"keep-private": "Keep private",
|
"keep-private": "Keep private",
|
||||||
"language": "Taal",
|
"language": "Taal",
|
||||||
|
"last-used": "",
|
||||||
"latitude": "Breedtegraad",
|
"latitude": "Breedtegraad",
|
||||||
"layer": "{n, plural, =1 {Layer} other {Layers}}",
|
"layer": "{n, plural, =1 {Layer} other {Layers}}",
|
||||||
"license": "Licentie",
|
"license": "Licentie",
|
||||||
@@ -265,13 +274,16 @@
|
|||||||
"n-years-ago": "{n} jaren geleden",
|
"n-years-ago": "{n} jaren geleden",
|
||||||
"name": "Naam",
|
"name": "Naam",
|
||||||
"near": "Nabij",
|
"near": "Nabij",
|
||||||
|
"never": "",
|
||||||
"new-list": "Nieuwe lijst",
|
"new-list": "Nieuwe lijst",
|
||||||
"new-password": "Nieuw wachtwoord",
|
"new-password": "Nieuw wachtwoord",
|
||||||
"new-password-error": "Fout bij het instellen van een nieuw wachtwoord",
|
"new-password-error": "Fout bij het instellen van een nieuw wachtwoord",
|
||||||
"new-password-success": "Het nieuwe wachtwoord is ingesteld.",
|
"new-password-success": "Het nieuwe wachtwoord is ingesteld.",
|
||||||
"new-password-text": "Stel nieuw wachtwoord in",
|
"new-password-text": "Stel nieuw wachtwoord in",
|
||||||
|
"new-token-generated": "",
|
||||||
"new-trail": "Nieuwe Route",
|
"new-trail": "Nieuwe Route",
|
||||||
"no-account": "Heb je nog geen account?",
|
"no-account": "Heb je nog geen account?",
|
||||||
|
"no-api-tokens": "",
|
||||||
"no-comments-so-far": "Tot nu toe geen opmerkingen",
|
"no-comments-so-far": "Tot nu toe geen opmerkingen",
|
||||||
"no-data": "Geen data",
|
"no-data": "Geen data",
|
||||||
"no-description-for-now": "Voorlopig geen beschrijving",
|
"no-description-for-now": "Voorlopig geen beschrijving",
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
"amenity": "Amenity",
|
"amenity": "Amenity",
|
||||||
"ammenity": "",
|
"ammenity": "",
|
||||||
"api-documentation": "Dokumentacja API",
|
"api-documentation": "Dokumentacja API",
|
||||||
|
"api-tokens": "",
|
||||||
|
"api-tokens-hint": "",
|
||||||
"apply-user-settings": "",
|
"apply-user-settings": "",
|
||||||
"attraction": "Attraction",
|
"attraction": "Attraction",
|
||||||
"author": "Autor",
|
"author": "Autor",
|
||||||
@@ -37,6 +39,7 @@
|
|||||||
"basic-info": "Podstawowe informacje",
|
"basic-info": "Podstawowe informacje",
|
||||||
"basque": "Basque",
|
"basque": "Basque",
|
||||||
"before": "Przed",
|
"before": "Przed",
|
||||||
|
"behavior": "",
|
||||||
"bicycle-parking": "Bicycle Parking",
|
"bicycle-parking": "Bicycle Parking",
|
||||||
"bicycle-rental": "Bicycle Rental",
|
"bicycle-rental": "Bicycle Rental",
|
||||||
"bicycle-shop": "Bicycle Shop",
|
"bicycle-shop": "Bicycle Shop",
|
||||||
@@ -138,10 +141,12 @@
|
|||||||
"entry": "Pozycja",
|
"entry": "Pozycja",
|
||||||
"error-copying-trail": "",
|
"error-copying-trail": "",
|
||||||
"error-creating-user": "Błąd tworzenia użytkownika",
|
"error-creating-user": "Błąd tworzenia użytkownika",
|
||||||
|
"error-deleting-token": "",
|
||||||
"error-disabling-strava-integration": "Błąd przy wyłączaniu integracji strava",
|
"error-disabling-strava-integration": "Błąd przy wyłączaniu integracji strava",
|
||||||
"error-during-login": "Błąd podczas logowania",
|
"error-during-login": "Błąd podczas logowania",
|
||||||
"error-during-password-reset": "Nie udało się wysłać e-maila z resetowaniem hasła",
|
"error-during-password-reset": "Nie udało się wysłać e-maila z resetowaniem hasła",
|
||||||
"error-exporting-trail": "Błąd podczas eksportowania szlaku",
|
"error-exporting-trail": "Błąd podczas eksportowania szlaku",
|
||||||
|
"error-generating-token": "",
|
||||||
"error-liking-trail": "Error liking trail",
|
"error-liking-trail": "Error liking trail",
|
||||||
"error-logging-in-to-komoot": "Błąd zapisu do komoot",
|
"error-logging-in-to-komoot": "Błąd zapisu do komoot",
|
||||||
"error-posting-comment": "Błąd wysyłania komentarza",
|
"error-posting-comment": "Błąd wysyłania komentarza",
|
||||||
@@ -154,6 +159,8 @@
|
|||||||
"error-updating-strava-integration": "Błąd aktualizacji integracji kamoot",
|
"error-updating-strava-integration": "Błąd aktualizacji integracji kamoot",
|
||||||
"est-duration": "Szacowany czas",
|
"est-duration": "Szacowany czas",
|
||||||
"everyone-with-the-link": "Everyone with the link",
|
"everyone-with-the-link": "Everyone with the link",
|
||||||
|
"expiration": "",
|
||||||
|
"expires": "",
|
||||||
"explore": "Eksploruj",
|
"explore": "Eksploruj",
|
||||||
"explore-some-trails": "Eksploruj różne szlaki",
|
"explore-some-trails": "Eksploruj różne szlaki",
|
||||||
"export": "Eksportuj",
|
"export": "Eksportuj",
|
||||||
@@ -182,6 +189,7 @@
|
|||||||
"from-url": "Z URL",
|
"from-url": "Z URL",
|
||||||
"garage": "Garage",
|
"garage": "Garage",
|
||||||
"gas-station": "Gas station",
|
"gas-station": "Gas station",
|
||||||
|
"generate-new-token": "",
|
||||||
"german": "Niemiecki",
|
"german": "Niemiecki",
|
||||||
"get-position-from-exif": "Odczytaj współrzędne z danych EXIF",
|
"get-position-from-exif": "Odczytaj współrzędne z danych EXIF",
|
||||||
"get-started": "Get started",
|
"get-started": "Get started",
|
||||||
@@ -221,6 +229,7 @@
|
|||||||
"keep-original": "",
|
"keep-original": "",
|
||||||
"keep-private": "Keep private",
|
"keep-private": "Keep private",
|
||||||
"language": "Język",
|
"language": "Język",
|
||||||
|
"last-used": "",
|
||||||
"latitude": "Szerokość",
|
"latitude": "Szerokość",
|
||||||
"layer": "{n, plural, =1 {Layer} other {Layers}}",
|
"layer": "{n, plural, =1 {Layer} other {Layers}}",
|
||||||
"license": "Licencja",
|
"license": "Licencja",
|
||||||
@@ -265,13 +274,16 @@
|
|||||||
"n-years-ago": "{n} lat temu",
|
"n-years-ago": "{n} lat temu",
|
||||||
"name": "Nazwa",
|
"name": "Nazwa",
|
||||||
"near": "Blisko",
|
"near": "Blisko",
|
||||||
|
"never": "",
|
||||||
"new-list": "Nowa Lista",
|
"new-list": "Nowa Lista",
|
||||||
"new-password": "Nowe hasło",
|
"new-password": "Nowe hasło",
|
||||||
"new-password-error": "Błąd ustawiania nowego hasła",
|
"new-password-error": "Błąd ustawiania nowego hasła",
|
||||||
"new-password-success": "Nowe hasło zostało ustawione",
|
"new-password-success": "Nowe hasło zostało ustawione",
|
||||||
"new-password-text": "Ustaw nowe hasło",
|
"new-password-text": "Ustaw nowe hasło",
|
||||||
|
"new-token-generated": "",
|
||||||
"new-trail": "Nowy szlak",
|
"new-trail": "Nowy szlak",
|
||||||
"no-account": "Nie masz konta?",
|
"no-account": "Nie masz konta?",
|
||||||
|
"no-api-tokens": "",
|
||||||
"no-comments-so-far": "Nie ma jeszcze komentarzy",
|
"no-comments-so-far": "Nie ma jeszcze komentarzy",
|
||||||
"no-data": "Brak danych",
|
"no-data": "Brak danych",
|
||||||
"no-description-for-now": "Nie ma jeszcze opisu",
|
"no-description-for-now": "Nie ma jeszcze opisu",
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
"amenity": "Amenity",
|
"amenity": "Amenity",
|
||||||
"ammenity": "",
|
"ammenity": "",
|
||||||
"api-documentation": "Documentação da API",
|
"api-documentation": "Documentação da API",
|
||||||
|
"api-tokens": "",
|
||||||
|
"api-tokens-hint": "",
|
||||||
"apply-user-settings": "",
|
"apply-user-settings": "",
|
||||||
"attraction": "Attraction",
|
"attraction": "Attraction",
|
||||||
"author": "Author",
|
"author": "Author",
|
||||||
@@ -37,6 +39,7 @@
|
|||||||
"basic-info": "Informações básicas",
|
"basic-info": "Informações básicas",
|
||||||
"basque": "Basque",
|
"basque": "Basque",
|
||||||
"before": "Antes",
|
"before": "Antes",
|
||||||
|
"behavior": "",
|
||||||
"bicycle-parking": "Bicycle Parking",
|
"bicycle-parking": "Bicycle Parking",
|
||||||
"bicycle-rental": "Bicycle Rental",
|
"bicycle-rental": "Bicycle Rental",
|
||||||
"bicycle-shop": "Bicycle Shop",
|
"bicycle-shop": "Bicycle Shop",
|
||||||
@@ -138,10 +141,12 @@
|
|||||||
"entry": "Entrada",
|
"entry": "Entrada",
|
||||||
"error-copying-trail": "",
|
"error-copying-trail": "",
|
||||||
"error-creating-user": "Erro ao criar utilizador",
|
"error-creating-user": "Erro ao criar utilizador",
|
||||||
|
"error-deleting-token": "",
|
||||||
"error-disabling-strava-integration": "Error disabling strava integration",
|
"error-disabling-strava-integration": "Error disabling strava integration",
|
||||||
"error-during-login": "Erro durante o ‘login’",
|
"error-during-login": "Erro durante o ‘login’",
|
||||||
"error-during-password-reset": "Unable to send password reset email",
|
"error-during-password-reset": "Unable to send password reset email",
|
||||||
"error-exporting-trail": "Erro na exportação do percurso",
|
"error-exporting-trail": "Erro na exportação do percurso",
|
||||||
|
"error-generating-token": "",
|
||||||
"error-liking-trail": "Error liking trail",
|
"error-liking-trail": "Error liking trail",
|
||||||
"error-logging-in-to-komoot": "Error logging in to komoot",
|
"error-logging-in-to-komoot": "Error logging in to komoot",
|
||||||
"error-posting-comment": "Error posting comment",
|
"error-posting-comment": "Error posting comment",
|
||||||
@@ -154,6 +159,8 @@
|
|||||||
"error-updating-strava-integration": "Error updating komoot integration",
|
"error-updating-strava-integration": "Error updating komoot integration",
|
||||||
"est-duration": "Duração prevista",
|
"est-duration": "Duração prevista",
|
||||||
"everyone-with-the-link": "Everyone with the link",
|
"everyone-with-the-link": "Everyone with the link",
|
||||||
|
"expiration": "",
|
||||||
|
"expires": "",
|
||||||
"explore": "Explorar",
|
"explore": "Explorar",
|
||||||
"explore-some-trails": "Explore algumas trilhas",
|
"explore-some-trails": "Explore algumas trilhas",
|
||||||
"export": "Exportar",
|
"export": "Exportar",
|
||||||
@@ -182,6 +189,7 @@
|
|||||||
"from-url": "From URL",
|
"from-url": "From URL",
|
||||||
"garage": "Garage",
|
"garage": "Garage",
|
||||||
"gas-station": "Gas station",
|
"gas-station": "Gas station",
|
||||||
|
"generate-new-token": "",
|
||||||
"german": "Alemão",
|
"german": "Alemão",
|
||||||
"get-position-from-exif": "Obter coordenadas dos dados EXIF",
|
"get-position-from-exif": "Obter coordenadas dos dados EXIF",
|
||||||
"get-started": "Get started",
|
"get-started": "Get started",
|
||||||
@@ -221,6 +229,7 @@
|
|||||||
"keep-original": "",
|
"keep-original": "",
|
||||||
"keep-private": "Keep private",
|
"keep-private": "Keep private",
|
||||||
"language": "Língua",
|
"language": "Língua",
|
||||||
|
"last-used": "",
|
||||||
"latitude": "Latitude",
|
"latitude": "Latitude",
|
||||||
"layer": "{n, plural, =1 {Layer} other {Layers}}",
|
"layer": "{n, plural, =1 {Layer} other {Layers}}",
|
||||||
"license": "Licença",
|
"license": "Licença",
|
||||||
@@ -265,13 +274,16 @@
|
|||||||
"n-years-ago": "{n} anos atrás",
|
"n-years-ago": "{n} anos atrás",
|
||||||
"name": "Nome",
|
"name": "Nome",
|
||||||
"near": "Próximo",
|
"near": "Próximo",
|
||||||
|
"never": "",
|
||||||
"new-list": "Nova lista",
|
"new-list": "Nova lista",
|
||||||
"new-password": "Nova senha",
|
"new-password": "Nova senha",
|
||||||
"new-password-error": "Error setting new password",
|
"new-password-error": "Error setting new password",
|
||||||
"new-password-success": "The new password has been set",
|
"new-password-success": "The new password has been set",
|
||||||
"new-password-text": "Set a new password",
|
"new-password-text": "Set a new password",
|
||||||
|
"new-token-generated": "",
|
||||||
"new-trail": "Nova trilha",
|
"new-trail": "Nova trilha",
|
||||||
"no-account": "Não tem uma conta?",
|
"no-account": "Não tem uma conta?",
|
||||||
|
"no-api-tokens": "",
|
||||||
"no-comments-so-far": "No comments so far",
|
"no-comments-so-far": "No comments so far",
|
||||||
"no-data": "Sem dados",
|
"no-data": "Sem dados",
|
||||||
"no-description-for-now": "No description for now",
|
"no-description-for-now": "No description for now",
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
"amenity": "Amenity",
|
"amenity": "Amenity",
|
||||||
"ammenity": "",
|
"ammenity": "",
|
||||||
"api-documentation": "Документация API",
|
"api-documentation": "Документация API",
|
||||||
|
"api-tokens": "",
|
||||||
|
"api-tokens-hint": "",
|
||||||
"apply-user-settings": "",
|
"apply-user-settings": "",
|
||||||
"attraction": "Attraction",
|
"attraction": "Attraction",
|
||||||
"author": "Автор",
|
"author": "Автор",
|
||||||
@@ -37,6 +39,7 @@
|
|||||||
"basic-info": "Основная информация",
|
"basic-info": "Основная информация",
|
||||||
"basque": "Basque",
|
"basque": "Basque",
|
||||||
"before": "До",
|
"before": "До",
|
||||||
|
"behavior": "",
|
||||||
"bicycle-parking": "Велосипедная парковка",
|
"bicycle-parking": "Велосипедная парковка",
|
||||||
"bicycle-rental": "Прокат велосипедов",
|
"bicycle-rental": "Прокат велосипедов",
|
||||||
"bicycle-shop": "Веломагазин",
|
"bicycle-shop": "Веломагазин",
|
||||||
@@ -138,10 +141,12 @@
|
|||||||
"entry": "Запись",
|
"entry": "Запись",
|
||||||
"error-copying-trail": "",
|
"error-copying-trail": "",
|
||||||
"error-creating-user": "Ошибка создания пользователя",
|
"error-creating-user": "Ошибка создания пользователя",
|
||||||
|
"error-deleting-token": "",
|
||||||
"error-disabling-strava-integration": "Ошибка отключения Strava",
|
"error-disabling-strava-integration": "Ошибка отключения Strava",
|
||||||
"error-during-login": "Ошибка входа",
|
"error-during-login": "Ошибка входа",
|
||||||
"error-during-password-reset": "Не удалось отправить email сброса пароля",
|
"error-during-password-reset": "Не удалось отправить email сброса пароля",
|
||||||
"error-exporting-trail": "Ошибка экспорта трека",
|
"error-exporting-trail": "Ошибка экспорта трека",
|
||||||
|
"error-generating-token": "",
|
||||||
"error-liking-trail": "Error liking trail",
|
"error-liking-trail": "Error liking trail",
|
||||||
"error-logging-in-to-komoot": "Ошибка входа в Komoot",
|
"error-logging-in-to-komoot": "Ошибка входа в Komoot",
|
||||||
"error-posting-comment": "Ошибка отправки комментария",
|
"error-posting-comment": "Ошибка отправки комментария",
|
||||||
@@ -154,6 +159,8 @@
|
|||||||
"error-updating-strava-integration": "Ошибка обновления Strava",
|
"error-updating-strava-integration": "Ошибка обновления Strava",
|
||||||
"est-duration": "Продолжительность",
|
"est-duration": "Продолжительность",
|
||||||
"everyone-with-the-link": "Everyone with the link",
|
"everyone-with-the-link": "Everyone with the link",
|
||||||
|
"expiration": "",
|
||||||
|
"expires": "",
|
||||||
"explore": "Изучить",
|
"explore": "Изучить",
|
||||||
"explore-some-trails": "Изучите треки",
|
"explore-some-trails": "Изучите треки",
|
||||||
"export": "Экспорт",
|
"export": "Экспорт",
|
||||||
@@ -182,6 +189,7 @@
|
|||||||
"from-url": "По ссылке",
|
"from-url": "По ссылке",
|
||||||
"garage": "Гараж",
|
"garage": "Гараж",
|
||||||
"gas-station": "Gas station",
|
"gas-station": "Gas station",
|
||||||
|
"generate-new-token": "",
|
||||||
"german": "Немецкий",
|
"german": "Немецкий",
|
||||||
"get-position-from-exif": "Координаты из EXIF",
|
"get-position-from-exif": "Координаты из EXIF",
|
||||||
"get-started": "Get started",
|
"get-started": "Get started",
|
||||||
@@ -221,6 +229,7 @@
|
|||||||
"keep-original": "",
|
"keep-original": "",
|
||||||
"keep-private": "Keep private",
|
"keep-private": "Keep private",
|
||||||
"language": "Язык",
|
"language": "Язык",
|
||||||
|
"last-used": "",
|
||||||
"latitude": "Широта",
|
"latitude": "Широта",
|
||||||
"layer": "{n, plural, =1 {Layer} other {Layers}}",
|
"layer": "{n, plural, =1 {Layer} other {Layers}}",
|
||||||
"license": "Лицензия",
|
"license": "Лицензия",
|
||||||
@@ -265,13 +274,16 @@
|
|||||||
"n-years-ago": "{n} лет назад",
|
"n-years-ago": "{n} лет назад",
|
||||||
"name": "Название",
|
"name": "Название",
|
||||||
"near": "Рядом",
|
"near": "Рядом",
|
||||||
|
"never": "",
|
||||||
"new-list": "Новый список",
|
"new-list": "Новый список",
|
||||||
"new-password": "Новый пароль",
|
"new-password": "Новый пароль",
|
||||||
"new-password-error": "Ошибка обновления пароля",
|
"new-password-error": "Ошибка обновления пароля",
|
||||||
"new-password-success": "Пароль успешно изменен",
|
"new-password-success": "Пароль успешно изменен",
|
||||||
"new-password-text": "Установить новый пароль",
|
"new-password-text": "Установить новый пароль",
|
||||||
|
"new-token-generated": "",
|
||||||
"new-trail": "Новый трек",
|
"new-trail": "Новый трек",
|
||||||
"no-account": "Нет аккаунта?",
|
"no-account": "Нет аккаунта?",
|
||||||
|
"no-api-tokens": "",
|
||||||
"no-comments-so-far": "Пока нет комментариев",
|
"no-comments-so-far": "Пока нет комментариев",
|
||||||
"no-data": "Нет данных",
|
"no-data": "Нет данных",
|
||||||
"no-description-for-now": "Пока нет описания",
|
"no-description-for-now": "Пока нет описания",
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
"amenity": "友好性",
|
"amenity": "友好性",
|
||||||
"ammenity": "",
|
"ammenity": "",
|
||||||
"api-documentation": "API 文档",
|
"api-documentation": "API 文档",
|
||||||
|
"api-tokens": "",
|
||||||
|
"api-tokens-hint": "",
|
||||||
"apply-user-settings": "",
|
"apply-user-settings": "",
|
||||||
"attraction": "景点",
|
"attraction": "景点",
|
||||||
"author": "作者",
|
"author": "作者",
|
||||||
@@ -37,6 +39,7 @@
|
|||||||
"basic-info": "基本信息",
|
"basic-info": "基本信息",
|
||||||
"basque": "Basque",
|
"basque": "Basque",
|
||||||
"before": "之前",
|
"before": "之前",
|
||||||
|
"behavior": "",
|
||||||
"bicycle-parking": "自行车停车场",
|
"bicycle-parking": "自行车停车场",
|
||||||
"bicycle-rental": "自行车租车",
|
"bicycle-rental": "自行车租车",
|
||||||
"bicycle-shop": "自行车店",
|
"bicycle-shop": "自行车店",
|
||||||
@@ -138,10 +141,12 @@
|
|||||||
"entry": "日程",
|
"entry": "日程",
|
||||||
"error-copying-trail": "",
|
"error-copying-trail": "",
|
||||||
"error-creating-user": "创建用户错误",
|
"error-creating-user": "创建用户错误",
|
||||||
|
"error-deleting-token": "",
|
||||||
"error-disabling-strava-integration": "禁用strava集成时出错",
|
"error-disabling-strava-integration": "禁用strava集成时出错",
|
||||||
"error-during-login": "登录错误",
|
"error-during-login": "登录错误",
|
||||||
"error-during-password-reset": "无法发送密码重置邮件",
|
"error-during-password-reset": "无法发送密码重置邮件",
|
||||||
"error-exporting-trail": "导出路线失败",
|
"error-exporting-trail": "导出路线失败",
|
||||||
|
"error-generating-token": "",
|
||||||
"error-liking-trail": "赞轨迹时出错",
|
"error-liking-trail": "赞轨迹时出错",
|
||||||
"error-logging-in-to-komoot": "登录到 komoot 时出错",
|
"error-logging-in-to-komoot": "登录到 komoot 时出错",
|
||||||
"error-posting-comment": "发布评论时出错",
|
"error-posting-comment": "发布评论时出错",
|
||||||
@@ -154,6 +159,8 @@
|
|||||||
"error-updating-strava-integration": "更新 komoot 集成出错",
|
"error-updating-strava-integration": "更新 komoot 集成出错",
|
||||||
"est-duration": "预计时长",
|
"est-duration": "预计时长",
|
||||||
"everyone-with-the-link": "Everyone with the link",
|
"everyone-with-the-link": "Everyone with the link",
|
||||||
|
"expiration": "",
|
||||||
|
"expires": "",
|
||||||
"explore": "探索",
|
"explore": "探索",
|
||||||
"explore-some-trails": "探索行程",
|
"explore-some-trails": "探索行程",
|
||||||
"export": "导出",
|
"export": "导出",
|
||||||
@@ -182,6 +189,7 @@
|
|||||||
"from-url": "从 URL",
|
"from-url": "从 URL",
|
||||||
"garage": "车库",
|
"garage": "车库",
|
||||||
"gas-station": "加油站",
|
"gas-station": "加油站",
|
||||||
|
"generate-new-token": "",
|
||||||
"german": "德语",
|
"german": "德语",
|
||||||
"get-position-from-exif": "从EXIF数据获取坐标",
|
"get-position-from-exif": "从EXIF数据获取坐标",
|
||||||
"get-started": "Get started",
|
"get-started": "Get started",
|
||||||
@@ -221,6 +229,7 @@
|
|||||||
"keep-original": "",
|
"keep-original": "",
|
||||||
"keep-private": "Keep private",
|
"keep-private": "Keep private",
|
||||||
"language": "语言",
|
"language": "语言",
|
||||||
|
"last-used": "",
|
||||||
"latitude": "纬度",
|
"latitude": "纬度",
|
||||||
"layer": "{n, plural, =1 {层} other {层}}",
|
"layer": "{n, plural, =1 {层} other {层}}",
|
||||||
"license": "开源协议",
|
"license": "开源协议",
|
||||||
@@ -265,13 +274,16 @@
|
|||||||
"n-years-ago": "{n} 年前",
|
"n-years-ago": "{n} 年前",
|
||||||
"name": "名称",
|
"name": "名称",
|
||||||
"near": "附近",
|
"near": "附近",
|
||||||
|
"never": "",
|
||||||
"new-list": "新列表",
|
"new-list": "新列表",
|
||||||
"new-password": "新密码",
|
"new-password": "新密码",
|
||||||
"new-password-error": "设置新密码时出错",
|
"new-password-error": "设置新密码时出错",
|
||||||
"new-password-success": "新密码已设置",
|
"new-password-success": "新密码已设置",
|
||||||
"new-password-text": "设置新密码",
|
"new-password-text": "设置新密码",
|
||||||
|
"new-token-generated": "",
|
||||||
"new-trail": "创建新路线",
|
"new-trail": "创建新路线",
|
||||||
"no-account": "还未注册?",
|
"no-account": "还未注册?",
|
||||||
|
"no-api-tokens": "",
|
||||||
"no-comments-so-far": "到目前为止没有评论",
|
"no-comments-so-far": "到目前为止没有评论",
|
||||||
"no-data": "无数据",
|
"no-data": "无数据",
|
||||||
"no-description-for-now": "暂无描述",
|
"no-description-for-now": "暂无描述",
|
||||||
|
|||||||
11
web/src/lib/models/api/api_token_schema.ts
Normal file
11
web/src/lib/models/api/api_token_schema.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { z, ZodType } from "zod";
|
||||||
|
import type { APIToken } from "../api_token";
|
||||||
|
|
||||||
|
const APITokenCreateSchema = z.object({
|
||||||
|
name: z.string().min(1, "required"),
|
||||||
|
expiration: z.string().refine((val) => !isNaN(Date.parse(val)), "invalid-date").refine((val) => Date.parse(val) > new Date().getTime(), "date-in-past").optional(),
|
||||||
|
user: z.string().length(15),
|
||||||
|
|
||||||
|
}) satisfies ZodType<APIToken>
|
||||||
|
|
||||||
|
export { APITokenCreateSchema }
|
||||||
13
web/src/lib/models/api_token.ts
Normal file
13
web/src/lib/models/api_token.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
export class APIToken {
|
||||||
|
id?: string;
|
||||||
|
name: string;
|
||||||
|
expiration?: string;
|
||||||
|
last_used?: string;
|
||||||
|
user?: string;
|
||||||
|
|
||||||
|
constructor(name: string, expiration?: string) {
|
||||||
|
this.name = name;
|
||||||
|
this.expiration = expiration;
|
||||||
|
}
|
||||||
|
}
|
||||||
59
web/src/lib/stores/api_token_store.ts
Normal file
59
web/src/lib/stores/api_token_store.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { get } from "svelte/store";
|
||||||
|
import { currentUser } from "./user_store";
|
||||||
|
import type { APIToken } from "$lib/models/api_token";
|
||||||
|
import { APIError } from "$lib/util/api_util";
|
||||||
|
import type { ListResult } from "pocketbase";
|
||||||
|
|
||||||
|
export async function api_tokens_index(f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
|
||||||
|
|
||||||
|
const r = await f(`/api/v1/api-token`, {
|
||||||
|
method: 'GET',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!r.ok) {
|
||||||
|
const response = await r.json();
|
||||||
|
throw new APIError(r.status, response.message, response.detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchedTokens: ListResult<APIToken> = await r.json();
|
||||||
|
|
||||||
|
return fetchedTokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function api_tokens_create(apiToken: APIToken) {
|
||||||
|
const user = get(currentUser)
|
||||||
|
if (!user) {
|
||||||
|
throw Error("Unauthenticated")
|
||||||
|
}
|
||||||
|
|
||||||
|
apiToken.user = user.id
|
||||||
|
|
||||||
|
let r = await fetch('/api/v1/api-token', {
|
||||||
|
method: 'PUT',
|
||||||
|
body: JSON.stringify(apiToken),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!r.ok) {
|
||||||
|
const response = await r.json();
|
||||||
|
throw new APIError(r.status, response.message, response.detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
const model: APIToken & { rawToken: string } = await r.json();
|
||||||
|
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function api_tokens_delete(token: APIToken) {
|
||||||
|
const r = await fetch('/api/v1/api-token/' + token.id, {
|
||||||
|
method: 'DELETE',
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!r.ok) {
|
||||||
|
const response = await r.json();
|
||||||
|
throw new APIError(r.status, response.message, response.detail)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return await r.json();
|
||||||
|
|
||||||
|
}
|
||||||
@@ -41,6 +41,7 @@ export enum Collection {
|
|||||||
trails_bounding_box = "trails_bounding_box",
|
trails_bounding_box = "trails_bounding_box",
|
||||||
trails_filter = "trails_filter",
|
trails_filter = "trails_filter",
|
||||||
users_anonymous = "users_anonymous",
|
users_anonymous = "users_anonymous",
|
||||||
|
api_tokens = "api_tokens"
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
25
web/src/routes/api/v1/api-token/+server.ts
Normal file
25
web/src/routes/api/v1/api-token/+server.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import type { APIToken } from '$lib/models/api_token';
|
||||||
|
import { create } from '$lib/util/api_util';
|
||||||
|
import { Collection, handleError, list } from '$lib/util/api_util';
|
||||||
|
import { json, type RequestEvent } from '@sveltejs/kit';
|
||||||
|
import { APITokenCreateSchema } from "$lib/models/api/api_token_schema";
|
||||||
|
|
||||||
|
export async function GET(event: RequestEvent) {
|
||||||
|
try {
|
||||||
|
const r = await list<APIToken>(event, Collection.api_tokens);
|
||||||
|
|
||||||
|
return json(r)
|
||||||
|
} catch (e: any) {
|
||||||
|
return handleError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export async function PUT(event: RequestEvent) {
|
||||||
|
try {
|
||||||
|
const r = await create<APIToken>(event, APITokenCreateSchema, Collection.api_tokens)
|
||||||
|
return json(r);
|
||||||
|
} catch (e) {
|
||||||
|
return handleError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
11
web/src/routes/api/v1/api-token/[id]/+server.ts
Normal file
11
web/src/routes/api/v1/api-token/[id]/+server.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Collection, handleError, remove } from "$lib/util/api_util";
|
||||||
|
import { json, type RequestEvent } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
export async function DELETE(event: RequestEvent) {
|
||||||
|
try {
|
||||||
|
const r = await remove(event, Collection.api_tokens)
|
||||||
|
return json(r);
|
||||||
|
} catch (e: any) {
|
||||||
|
return handleError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from "$app/navigation";
|
import { goto, invalidateAll } from "$app/navigation";
|
||||||
import { page } from "$app/state";
|
import { page } from "$app/state";
|
||||||
|
import Button from "$lib/components/base/button.svelte";
|
||||||
import ConfirmModal from "$lib/components/confirm_modal.svelte";
|
import ConfirmModal from "$lib/components/confirm_modal.svelte";
|
||||||
|
import ApiTokenModal from "$lib/components/settings/api_token_modal.svelte";
|
||||||
|
import ApiTokenSuccessModal from "$lib/components/settings/api_token_success_modal.svelte";
|
||||||
import EmailModal from "$lib/components/settings/email_modal.svelte";
|
import EmailModal from "$lib/components/settings/email_modal.svelte";
|
||||||
import PasswordModal from "$lib/components/settings/password_modal.svelte";
|
import PasswordModal from "$lib/components/settings/password_modal.svelte";
|
||||||
|
import type { APIToken } from "$lib/models/api_token";
|
||||||
|
import {
|
||||||
|
api_tokens_create,
|
||||||
|
api_tokens_delete,
|
||||||
|
} from "$lib/stores/api_token_store";
|
||||||
import { show_toast } from "$lib/stores/toast_store.svelte";
|
import { show_toast } from "$lib/stores/toast_store.svelte";
|
||||||
import {
|
import {
|
||||||
currentUser,
|
currentUser,
|
||||||
@@ -14,6 +22,8 @@
|
|||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
|
|
||||||
|
let { data } = $props();
|
||||||
|
|
||||||
const settings = page.data.settings;
|
const settings = page.data.settings;
|
||||||
|
|
||||||
let selectedLanguage = "en";
|
let selectedLanguage = "en";
|
||||||
@@ -24,6 +34,11 @@
|
|||||||
let confirmModal: ConfirmModal;
|
let confirmModal: ConfirmModal;
|
||||||
let emailModal: EmailModal;
|
let emailModal: EmailModal;
|
||||||
let passwordModal: PasswordModal;
|
let passwordModal: PasswordModal;
|
||||||
|
let tokenModal: ApiTokenModal;
|
||||||
|
let tokenSuccessModal: ApiTokenSuccessModal;
|
||||||
|
|
||||||
|
let tokenLoading: boolean = $state(false);
|
||||||
|
let rawAPIToken: string | null = $state(null);
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
citySearchQuery = settings?.location?.name ?? "";
|
citySearchQuery = settings?.location?.name ?? "";
|
||||||
@@ -74,6 +89,37 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function generateAPIToken(token: APIToken) {
|
||||||
|
try {
|
||||||
|
tokenLoading = true;
|
||||||
|
const tokenResponse = await api_tokens_create(token);
|
||||||
|
rawAPIToken = tokenResponse.rawToken;
|
||||||
|
tokenSuccessModal.openModal();
|
||||||
|
await invalidateAll();
|
||||||
|
} catch (e) {
|
||||||
|
show_toast({
|
||||||
|
text: $_("error-generating-token"),
|
||||||
|
icon: "close",
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
tokenLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteAPIToken(token: APIToken) {
|
||||||
|
try {
|
||||||
|
const tokenResponse = await api_tokens_delete(token);
|
||||||
|
await invalidateAll();
|
||||||
|
} catch (e) {
|
||||||
|
show_toast({
|
||||||
|
text: $_("error-deleting-token"),
|
||||||
|
icon: "close",
|
||||||
|
type: "error",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -91,6 +137,79 @@
|
|||||||
<button class="btn-secondary" onclick={() => passwordModal.openModal()}
|
<button class="btn-secondary" onclick={() => passwordModal.openModal()}
|
||||||
>{$_("change-password")}</button
|
>{$_("change-password")}</button
|
||||||
>
|
>
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<h4 class="text-xl font-medium">{$_("api-tokens")}</h4>
|
||||||
|
<Button
|
||||||
|
secondary
|
||||||
|
onclick={() => tokenModal.openModal()}
|
||||||
|
loading={tokenLoading}
|
||||||
|
><i class="fa fa-plus mr-2"></i>
|
||||||
|
{$_("generate-new-token")}</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<p class="mt-3">{$_("api-tokens-hint")}</p>
|
||||||
|
{#if data.apiTokens.totalItems == 0}
|
||||||
|
<p class="text-center pt-8 pb-2 text-gray-500">
|
||||||
|
{$_("no-api-tokens")}
|
||||||
|
</p>
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class="border border-input-border rounded-xl overflow-clip mt-4"
|
||||||
|
>
|
||||||
|
<table class="api-token-table w-full table-auto">
|
||||||
|
<thead class="text-left">
|
||||||
|
<tr class="text-sm bg-secondary-hover">
|
||||||
|
<th>{$_("name")}</th>
|
||||||
|
<th>{$_("expiration")}</th>
|
||||||
|
<th>{$_("last-used")}</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{#each data.apiTokens.items as token}
|
||||||
|
<tr class="border-t border-input-border">
|
||||||
|
<td>{token.name}</td>
|
||||||
|
<td
|
||||||
|
>{token.expiration
|
||||||
|
? new Date(
|
||||||
|
token.expiration,
|
||||||
|
).toLocaleDateString(undefined, {
|
||||||
|
month: "long",
|
||||||
|
day: "2-digit",
|
||||||
|
year: "numeric",
|
||||||
|
timeZone: "UTC",
|
||||||
|
})
|
||||||
|
: $_("never")}</td
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
>{token.last_used
|
||||||
|
? new Date(
|
||||||
|
token.last_used,
|
||||||
|
).toLocaleTimeString(undefined, {
|
||||||
|
month: "2-digit",
|
||||||
|
day: "2-digit",
|
||||||
|
year: "numeric",
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
})
|
||||||
|
: "-"}</td
|
||||||
|
>
|
||||||
|
<td
|
||||||
|
><button
|
||||||
|
onclick={() =>
|
||||||
|
deleteAPIToken(token)}
|
||||||
|
aria-label="delete api token"
|
||||||
|
><i class="fa fa-trash"></i></button
|
||||||
|
></td
|
||||||
|
>
|
||||||
|
</tr>
|
||||||
|
{/each}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<h4 class="text-xl text-red-400 font-medium">
|
<h4 class="text-xl text-red-400 font-medium">
|
||||||
{$_("danger-zone")}
|
{$_("danger-zone")}
|
||||||
@@ -108,13 +227,22 @@
|
|||||||
onsave={updateEmail}
|
onsave={updateEmail}
|
||||||
bind:this={emailModal}
|
bind:this={emailModal}
|
||||||
></EmailModal>
|
></EmailModal>
|
||||||
<PasswordModal
|
<PasswordModal onsave={updatePassword} bind:this={passwordModal}
|
||||||
onsave={updatePassword}
|
|
||||||
bind:this={passwordModal}
|
|
||||||
></PasswordModal>
|
></PasswordModal>
|
||||||
|
<ApiTokenModal onsave={generateAPIToken} bind:this={tokenModal}
|
||||||
|
></ApiTokenModal>
|
||||||
|
<ApiTokenSuccessModal bind:token={rawAPIToken} bind:this={tokenSuccessModal}
|
||||||
|
></ApiTokenSuccessModal>
|
||||||
{/if}
|
{/if}
|
||||||
<ConfirmModal
|
<ConfirmModal
|
||||||
text={$_("account-delete-confirm")}
|
text={$_("account-delete-confirm")}
|
||||||
bind:this={confirmModal}
|
bind:this={confirmModal}
|
||||||
onconfirm={deleteAccount}
|
onconfirm={deleteAccount}
|
||||||
></ConfirmModal>
|
></ConfirmModal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.api-token-table th,
|
||||||
|
td {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
7
web/src/routes/settings/account/+page.ts
Normal file
7
web/src/routes/settings/account/+page.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { api_tokens_index } from "$lib/stores/api_token_store";
|
||||||
|
import { type Load } from "@sveltejs/kit";
|
||||||
|
|
||||||
|
export const load: Load = async ({ params, fetch }) => {
|
||||||
|
const apiTokens = await api_tokens_index(fetch)
|
||||||
|
return { apiTokens: apiTokens }
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user