Config UI: improve sponsor token flow (#22989)
Co-authored-by: andig <andi@evcc.io> Co-authored-by: andig <cpuidle@gmail.com>
This commit is contained in:
parent
a539dd7452
commit
4163748e50
19 changed files with 289 additions and 69 deletions
|
|
@ -33,15 +33,16 @@
|
|||
v-if="$hiddenFeatures()"
|
||||
test-id="generalconfig-sponsoring"
|
||||
:label="$t('config.sponsor.title')"
|
||||
:text="sponsorStatus.name || '---'"
|
||||
:text-class="sponsorStatus.cssClass"
|
||||
:text="sponsorStatus.title"
|
||||
:text-class="sponsorStatus.textClass"
|
||||
modal-id="sponsorModal"
|
||||
experimental
|
||||
>
|
||||
<template #text-prefix>
|
||||
<span
|
||||
v-if="sponsorStatus.expiresSoon && sponsorStatus.name"
|
||||
class="d-inline-block me-1 p-1 rounded-circle bg-warning rounded-circle"
|
||||
v-if="sponsorStatus.badgeClass"
|
||||
class="d-inline-block me-1 p-1 rounded-circle"
|
||||
:class="sponsorStatus.badgeClass"
|
||||
></span>
|
||||
</template>
|
||||
</GeneralConfigEntry>
|
||||
|
|
@ -79,6 +80,9 @@ export default {
|
|||
name: "GeneralConfig",
|
||||
components: { TitleModal, GeneralConfigEntry },
|
||||
mixins: [formatter],
|
||||
props: {
|
||||
sponsorError: Boolean,
|
||||
},
|
||||
emits: ["site-changed"],
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -103,12 +107,27 @@ export default {
|
|||
sponsorStatus() {
|
||||
const sponsor = store.state?.sponsor || {};
|
||||
const { name, expiresSoon } = sponsor;
|
||||
let cssClass = "";
|
||||
let textClass = "";
|
||||
let badgeClass = "";
|
||||
let title = name;
|
||||
if (name) {
|
||||
cssClass = expiresSoon ? "text-warning" : "text-primary";
|
||||
if (expiresSoon) {
|
||||
textClass = "text-warning";
|
||||
badgeClass = "bg-warning";
|
||||
} else {
|
||||
badgeClass = "bg-primary";
|
||||
}
|
||||
} else {
|
||||
title = "---";
|
||||
}
|
||||
|
||||
return { name, expiresSoon, cssClass };
|
||||
if (this.sponsorError) {
|
||||
textClass = "text-danger";
|
||||
badgeClass = "bg-danger";
|
||||
title = this.$t("config.sponsor.invalid");
|
||||
}
|
||||
|
||||
return { title, expiresSoon, textClass, badgeClass };
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
class="d-flex justify-content-between order-2 order-sm-1 gap-2 flex-grow-1 flex-sm-grow-0"
|
||||
>
|
||||
<button
|
||||
v-if="!disableCancel"
|
||||
type="button"
|
||||
class="btn btn-link text-muted btn-cancel"
|
||||
data-bs-dismiss="modal"
|
||||
|
|
@ -77,6 +78,7 @@ export default {
|
|||
errorMessage: String,
|
||||
docs: String,
|
||||
endpoint: String,
|
||||
disableCancel: Boolean,
|
||||
disableRemove: Boolean,
|
||||
noButtons: Boolean,
|
||||
transformReadValues: Function,
|
||||
|
|
|
|||
|
|
@ -8,54 +8,77 @@
|
|||
:transform-read-values="transformReadValues"
|
||||
data-testid="sponsor-modal"
|
||||
size="lg"
|
||||
:no-buttons="!showForm"
|
||||
:disable-remove="!hasToken"
|
||||
:no-buttons="notUiEditable"
|
||||
:disable-remove="!hasUiToken"
|
||||
disable-cancel
|
||||
@changed="$emit('changed')"
|
||||
@open="showForm = false"
|
||||
>
|
||||
<template #default="{ values }">
|
||||
<SponsorTokenExpires v-bind="sponsor" />
|
||||
<div class="mt-4 mb-3">
|
||||
<Sponsor v-bind="sponsor" />
|
||||
</div>
|
||||
<div v-if="!showForm" class="d-flex gap-1 justify-content-between flex-wrap">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-link text-muted text-truncate"
|
||||
@click="showForm = !showForm"
|
||||
>
|
||||
{{ sponsorTokenLabel }}
|
||||
</button>
|
||||
<a
|
||||
v-if="!hasToken"
|
||||
class="btn btn-link text-muted text-truncate"
|
||||
:href="trialTokenLink"
|
||||
target="_blank"
|
||||
>
|
||||
{{ $t("config.sponsor.trialToken") }}
|
||||
</a>
|
||||
</div>
|
||||
<div v-else>
|
||||
<hr />
|
||||
<FormRow
|
||||
<hr class="my-4" />
|
||||
<div v-if="showTokenForm">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<label for="sponsorToken" class="fw-bold my-2">{{
|
||||
$t("config.sponsor.enterYourToken")
|
||||
}}</label>
|
||||
<button
|
||||
v-if="hasUiToken"
|
||||
type="button"
|
||||
class="btn btn-link btn-sm text-nowrap text-muted"
|
||||
@click="
|
||||
editMode = false;
|
||||
values.token = '';
|
||||
"
|
||||
>
|
||||
{{ $t("config.general.cancel") }}
|
||||
</button>
|
||||
</div>
|
||||
<textarea
|
||||
id="sponsorToken"
|
||||
:label="sponsorTokenLabel"
|
||||
:help="
|
||||
$t('config.sponsor.descriptionToken', { url: 'https://sponsor.evcc.io/' })
|
||||
"
|
||||
docs-link="/docs/sponsorship#trial"
|
||||
class="mt-4"
|
||||
>
|
||||
<textarea
|
||||
id="sponsorToken"
|
||||
v-model="values.token"
|
||||
required
|
||||
rows="5"
|
||||
spellcheck="false"
|
||||
v-model="values.token"
|
||||
class="form-control mb-1"
|
||||
required
|
||||
rows="5"
|
||||
spellcheck="false"
|
||||
@paste="(event) => handlePaste(event, values)"
|
||||
/>
|
||||
<small
|
||||
>{{ $t("config.sponsor.descriptionToken", { url: "sponsor.evcc.io" }) }}
|
||||
<a :href="trialTokenLink" target="_blank">
|
||||
{{ $t("config.sponsor.trialToken") }}.</a
|
||||
>
|
||||
</small>
|
||||
</div>
|
||||
<div v-else-if="token">
|
||||
<label for="existingToken" class="fw-bold my-2">{{
|
||||
$t("config.sponsor.yourToken")
|
||||
}}</label>
|
||||
<div class="d-flex align-items-start gap-2 text-muted">
|
||||
<input
|
||||
id="existingToken"
|
||||
:value="token"
|
||||
disabled
|
||||
rows="1"
|
||||
class="form-control"
|
||||
@paste="(event) => handlePaste(event, values)"
|
||||
:class="{ 'is-invalid': error }"
|
||||
/>
|
||||
</FormRow>
|
||||
<span v-if="fromYaml" class="text-muted text-nowrap align-self-center ms-2">
|
||||
{{ $t("config.sponsor.viaYaml") }}
|
||||
</span>
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="btn btn-link text-nowrap"
|
||||
:class="error ? 'text-danger' : 'text-muted'"
|
||||
@click="editMode = true"
|
||||
>
|
||||
{{ $t("config.sponsor.changeToken") }}
|
||||
</button>
|
||||
</div>
|
||||
<SponsorTokenExpires v-bind="sponsor" />
|
||||
</div>
|
||||
</template>
|
||||
</JsonModal>
|
||||
|
|
@ -63,31 +86,42 @@
|
|||
|
||||
<script>
|
||||
import JsonModal from "./JsonModal.vue";
|
||||
import FormRow from "./FormRow.vue";
|
||||
import Sponsor, { VICTRON_DEVICE } from "../Savings/Sponsor.vue";
|
||||
import Sponsor from "../Savings/Sponsor.vue";
|
||||
import SponsorTokenExpires from "../Savings/SponsorTokenExpires.vue";
|
||||
import store from "@/store";
|
||||
import { docsPrefix } from "@/i18n";
|
||||
import { cleanYaml } from "@/utils/cleanYaml";
|
||||
export default {
|
||||
name: "SponsorModal",
|
||||
components: { FormRow, JsonModal, Sponsor, SponsorTokenExpires },
|
||||
components: { JsonModal, Sponsor, SponsorTokenExpires },
|
||||
props: {
|
||||
error: Boolean,
|
||||
},
|
||||
emits: ["changed"],
|
||||
data: () => ({
|
||||
showForm: false,
|
||||
editMode: false,
|
||||
}),
|
||||
computed: {
|
||||
sponsor() {
|
||||
return store?.state?.sponsor;
|
||||
},
|
||||
hasToken() {
|
||||
const name = this.sponsor?.name || "";
|
||||
return name !== "" && name !== VICTRON_DEVICE;
|
||||
token() {
|
||||
return this.sponsor?.token;
|
||||
},
|
||||
sponsorTokenLabel() {
|
||||
return this.hasToken
|
||||
? this.$t("config.sponsor.changeToken")
|
||||
: this.$t("config.sponsor.addToken");
|
||||
fromYaml() {
|
||||
return this.sponsor?.fromYaml;
|
||||
},
|
||||
name() {
|
||||
return this.sponsor?.name || "";
|
||||
},
|
||||
showTokenForm() {
|
||||
return this.editMode || !this.token;
|
||||
},
|
||||
notUiEditable() {
|
||||
return !!this.name && this.fromYaml;
|
||||
},
|
||||
hasUiToken() {
|
||||
return this.token && !this.fromYaml;
|
||||
},
|
||||
trialTokenLink() {
|
||||
return `${docsPrefix()}/docs/sponsorship#trial`;
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ export default defineComponent({
|
|||
name: String,
|
||||
expiresAt: String,
|
||||
expiresSoon: Boolean,
|
||||
fromYaml: Boolean,
|
||||
},
|
||||
computed: {
|
||||
isTrial() {
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export default defineComponent({
|
|||
expiresSoon: Boolean,
|
||||
expiresAt: String,
|
||||
name: String,
|
||||
fromYaml: Boolean,
|
||||
},
|
||||
computed: {
|
||||
inXDays() {
|
||||
|
|
|
|||
|
|
@ -58,5 +58,6 @@ TokenExpires.args = {
|
|||
name: "Sponsor",
|
||||
expiresAt: new Date().toISOString(),
|
||||
expiresSoon: true,
|
||||
fromYaml: false,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -248,6 +248,8 @@ export interface Sponsor {
|
|||
name: string;
|
||||
expiresAt: string;
|
||||
expiresSoon: boolean;
|
||||
token?: string;
|
||||
fromYaml: boolean;
|
||||
}
|
||||
|
||||
export interface Notification {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,10 @@
|
|||
<ExperimentalBanner v-else-if="$hiddenFeatures()" />
|
||||
|
||||
<h2 class="my-4 mt-5">{{ $t("config.section.general") }}</h2>
|
||||
<GeneralConfig @site-changed="siteChanged" />
|
||||
<GeneralConfig
|
||||
:sponsor-error="hasClassError('sponsorship')"
|
||||
@site-changed="siteChanged"
|
||||
/>
|
||||
|
||||
<div v-if="$hiddenFeatures()">
|
||||
<h2 class="my-4">{{ $t("config.section.loadpoints") }} 🧪</h2>
|
||||
|
|
@ -367,7 +370,7 @@
|
|||
<MqttModal @changed="loadDirty" />
|
||||
<NetworkModal @changed="loadDirty" />
|
||||
<ControlModal @changed="loadDirty" />
|
||||
<SponsorModal @changed="loadDirty" />
|
||||
<SponsorModal :error="hasClassError('sponsorship')" @changed="loadDirty" />
|
||||
<HemsModal @changed="yamlChanged" />
|
||||
<MessagingModal @changed="yamlChanged" />
|
||||
<TariffsModal @changed="yamlChanged" />
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ func NewFromConfig(ctx context.Context, typ string, other map[string]interface{}
|
|||
|
||||
v, err := factory(ctx, other)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("cannot create charger type '%s': %w", typ, err)
|
||||
return nil, fmt.Errorf("cannot create charger type '%s': %w", typ, err)
|
||||
}
|
||||
|
||||
return v, err
|
||||
return v, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -491,6 +491,7 @@ func configureSponsorship(token string) (err error) {
|
|||
if token, err = settings.String(keys.SponsorToken); err != nil {
|
||||
return err
|
||||
}
|
||||
sponsor.SetFromYaml(false) // from database
|
||||
}
|
||||
|
||||
// TODO migrate settings
|
||||
|
|
|
|||
|
|
@ -429,14 +429,18 @@
|
|||
"addToken": "Token eingeben",
|
||||
"changeToken": "Token ändern",
|
||||
"description": "Das Sponsoring-Modell hilft uns, das Projekt zu pflegen und nachhaltig neue und spannende Funktionen zu entwickeln. Als Sponsor erhältst du Zugriff auf alle Wallbox-Implementierungen.",
|
||||
"descriptionToken": "Du erhältst das Token von {url}. Wir bieten auch ein Testtoken für Tests an.",
|
||||
"descriptionToken": "Als Sponsor erhältst du dein Token auf {url}. Zum Ausprobieren gibts ein ",
|
||||
"enterYourToken": "Token eingeben",
|
||||
"error": "Das Sponsortoken ist ungültig.",
|
||||
"invalid": "ungültig",
|
||||
"labelToken": "Sponsortoken",
|
||||
"title": "Sponsoring",
|
||||
"tokenRequired": "Du musst ein Sponsortoken konfigurieren, bevor du dieses Gerät anlegen kannst.",
|
||||
"tokenRequiredLearnMore": "Mehr erfahren.",
|
||||
"tokenRequiredShort": "Kein Sponsortoken konfiguriert.",
|
||||
"trialToken": "Testtoken"
|
||||
"trialToken": "Testtoken",
|
||||
"viaYaml": "über evcc.yaml",
|
||||
"yourToken": "Dein Token"
|
||||
},
|
||||
"system": {
|
||||
"backupRestore": {
|
||||
|
|
|
|||
|
|
@ -429,14 +429,18 @@
|
|||
"addToken": "Enter token",
|
||||
"changeToken": "Change token",
|
||||
"description": "The sponsoring model helps us to maintain the project and sustainably build new and exciting features. As a sponsor you get access to all charger implementations.",
|
||||
"descriptionToken": "You get the token from {url}. We also offer a trial token for testing.",
|
||||
"descriptionToken": "Sponsors find their token on {url}. For getting-started, we offer a ",
|
||||
"enterYourToken": "Enter your token",
|
||||
"error": "The sponsor token is not valid.",
|
||||
"invalid": "invalid",
|
||||
"labelToken": "Sponsor token",
|
||||
"title": "Sponsorship",
|
||||
"tokenRequired": "You must configure a sponsor token before you can create this device.",
|
||||
"tokenRequiredLearnMore": "Learn more.",
|
||||
"tokenRequiredShort": "No sponsor token configured.",
|
||||
"trialToken": "Trial token"
|
||||
"trialToken": "Trial token",
|
||||
"viaYaml": "via evcc.yaml",
|
||||
"yourToken": "Your token"
|
||||
},
|
||||
"system": {
|
||||
"backupRestore": {
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ func updateSponsortokenHandler(w http.ResponseWriter, r *http.Request) {
|
|||
jsonError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
sponsor.SetFromYaml(false)
|
||||
}
|
||||
|
||||
// TODO find better place
|
||||
|
|
|
|||
|
|
@ -25,8 +25,8 @@ func NewFromConfig(ctx context.Context, typ string, other map[string]interface{}
|
|||
|
||||
v, err := factory(ctx, other)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("cannot create tariff type '%s': %w", typ, err)
|
||||
return nil, fmt.Errorf("cannot create tariff type '%s': %w", typ, err)
|
||||
}
|
||||
|
||||
return v, err
|
||||
return v, nil
|
||||
}
|
||||
|
|
|
|||
17
tests/sponsor.evcc.yaml
Normal file
17
tests/sponsor.evcc.yaml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# expired sponsor token
|
||||
sponsortoken: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJldmNjLmlvIiwic3ViIjoidHJpYWwiLCJleHAiOjE3NTQ5OTI4MDAsImlhdCI6MTc1MzY5NjgwMCwic3BlIjp0cnVlLCJzcmMiOiJtYSJ9.XKa5DHT-icCM9awcX4eS8feW0J_KIjsx2IxjcRRQOcQ
|
||||
|
||||
site:
|
||||
title: Sponsor Test
|
||||
|
||||
loadpoints:
|
||||
- title: Carport
|
||||
charger: easee_charger
|
||||
|
||||
chargers:
|
||||
- name: easee_charger
|
||||
type: template
|
||||
template: easee
|
||||
user: test@example.org
|
||||
password: none
|
||||
charger: EH123456
|
||||
80
tests/sponsor.spec.ts
Normal file
80
tests/sponsor.spec.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { start, stop, baseUrl } from "./evcc";
|
||||
import { enableExperimental } from "./utils";
|
||||
|
||||
test.use({ baseURL: baseUrl() });
|
||||
|
||||
const shortToken = (t: string) => t.substring(0, 6) + "......." + t.substring(t.length - 6);
|
||||
|
||||
const EXPIRED_TOKEN =
|
||||
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJldmNjLmlvIiwic3ViIjoidHJpYWwiLCJleHAiOjE3NTQ5OTI4MDAsImlhdCI6MTc1MzY5NjgwMCwic3BlIjp0cnVlLCJzcmMiOiJtYSJ9.XKa5DHT-icCM9awcX4eS8feW0J_KIjsx2IxjcRRQOcQ";
|
||||
const SHORT_TOKEN = shortToken(EXPIRED_TOKEN);
|
||||
|
||||
test.afterEach(async () => {
|
||||
await stop();
|
||||
});
|
||||
|
||||
test.describe("sponsor token", () => {
|
||||
test("token from YAML config", async ({ page }) => {
|
||||
await start("sponsor.evcc.yaml");
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
|
||||
// Check fatal error
|
||||
await expect(page.getByTestId("fatal-error")).toContainText("sponsorship");
|
||||
|
||||
// Open sponsor modal
|
||||
const sponsorEntry = page.getByTestId("generalconfig-sponsoring");
|
||||
await expect(sponsorEntry).toContainText("invalid");
|
||||
await sponsorEntry.getByRole("button", { name: "Edit" }).click();
|
||||
|
||||
const modal = page.getByTestId("sponsor-modal");
|
||||
const tokenInput = modal.getByRole("textbox", { name: "Your token" });
|
||||
|
||||
await expect(tokenInput).toHaveValue(SHORT_TOKEN);
|
||||
await expect(tokenInput).toHaveClass(/is-invalid/);
|
||||
await expect(modal.getByText("via evcc.yaml")).toBeVisible();
|
||||
await expect(modal.getByRole("button", { name: "Remove" })).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("token from database config", async ({ page }) => {
|
||||
await start(undefined, "sponsor.sql");
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
|
||||
// Open sponsor modal
|
||||
await page
|
||||
.getByTestId("generalconfig-sponsoring")
|
||||
.getByRole("button", { name: "Edit" })
|
||||
.click();
|
||||
|
||||
const modal = page.getByTestId("sponsor-modal");
|
||||
await expect(modal.getByText("via evcc.yaml")).not.toBeVisible();
|
||||
|
||||
// Click change button to reveal textarea
|
||||
await modal.getByRole("button", { name: "Change token" }).click();
|
||||
|
||||
await expect(modal.getByRole("textbox", { name: "Enter your token" })).toBeVisible();
|
||||
await expect(modal.getByRole("button", { name: "Remove" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("insert token in new installation", async ({ page }) => {
|
||||
await start();
|
||||
await page.goto("/#/config");
|
||||
await enableExperimental(page);
|
||||
|
||||
// Open sponsor modal and enter token
|
||||
await page
|
||||
.getByTestId("generalconfig-sponsoring")
|
||||
.getByRole("button", { name: "Edit" })
|
||||
.click();
|
||||
|
||||
const modal = page.getByTestId("sponsor-modal");
|
||||
const textarea = modal.getByRole("textbox", { name: "Enter your token" });
|
||||
|
||||
await textarea.fill(EXPIRED_TOKEN);
|
||||
// Try to save to trigger validation
|
||||
await modal.getByRole("button", { name: "Save" }).click();
|
||||
await expect(modal).toContainText("token is expired");
|
||||
});
|
||||
});
|
||||
26
tests/sponsor.sql
Normal file
26
tests/sponsor.sql
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
BEGIN;
|
||||
|
||||
CREATE TABLE `settings` (
|
||||
`key` text
|
||||
, `value` text
|
||||
, PRIMARY KEY(`key`)
|
||||
);
|
||||
|
||||
CREATE TABLE `configs` (
|
||||
`id` integer PRIMARY KEY AUTOINCREMENT
|
||||
, `class` integer
|
||||
, `type` text
|
||||
, `title` text
|
||||
, `icon` text
|
||||
, `product` text
|
||||
, `value` text
|
||||
);
|
||||
|
||||
-- expired sponsor token
|
||||
INSERT INTO settings("key", value) VALUES('sponsorToken', 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJldmNjLmlvIiwic3ViIjoidHJpYWwiLCJleHAiOjE3NTQ5OTI4MDAsImlhdCI6MTc1MzY5NjgwMCwic3BlIjp0cnVlLCJzcmMiOiJtYSJ9.XKa5DHT-icCM9awcX4eS8feW0J_KIjsx2IxjcRRQOcQ');
|
||||
|
||||
-- loadpoint with charger that requires sponsorship
|
||||
INSERT INTO configs(id, class, type, title, icon, product, value) VALUES(3, 1, 'template', '', '', 'Easee Home', '{"charger":"EH123456","password":"none","template":"easee","timeout":"20s","user":"test@example.org"}');
|
||||
INSERT INTO configs(id, class, type, title, icon, product, value) VALUES(4, 5, '', '', '', '', '{"charger":"db:3","circuit":"","meter":"","phasesConfigured":0,"soc":{"poll":{"mode":"charging","interval":3600000000000},"estimate":true},"thresholds":{"enable":{"delay":60000000000,"threshold":0},"disable":{"delay":180000000000,"threshold":0}},"title":"Carport","vehicle":""}');
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -15,6 +15,7 @@ import (
|
|||
var (
|
||||
mu sync.RWMutex
|
||||
Subject, Token string
|
||||
fromYaml bool = true
|
||||
ExpiresAt time.Time
|
||||
)
|
||||
|
||||
|
|
@ -35,6 +36,13 @@ func IsAuthorizedForApi() bool {
|
|||
return IsAuthorized() && Subject != unavailable && Token != ""
|
||||
}
|
||||
|
||||
// SetFromYaml sets whether the token comes from YAML config or database
|
||||
func SetFromYaml(val bool) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
fromYaml = val
|
||||
}
|
||||
|
||||
// check and set sponsorship token
|
||||
func ConfigureSponsorship(token string) error {
|
||||
mu.Lock()
|
||||
|
|
@ -52,6 +60,8 @@ func ConfigureSponsorship(token string) error {
|
|||
}
|
||||
}
|
||||
|
||||
Token = token
|
||||
|
||||
conn, err := cloud.Connection()
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -66,7 +76,6 @@ func ConfigureSponsorship(token string) error {
|
|||
if err == nil && res.Authorized {
|
||||
Subject = res.Subject
|
||||
ExpiresAt = res.ExpiresAt.AsTime()
|
||||
Token = token
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
|
@ -81,14 +90,27 @@ func ConfigureSponsorship(token string) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// redactToken returns a redacted version of the token showing only start and end characters
|
||||
func redactToken(token string) string {
|
||||
if len(token) <= 12 {
|
||||
return ""
|
||||
}
|
||||
return token[:6] + "......." + token[len(token)-6:]
|
||||
}
|
||||
|
||||
type sponsorStatus struct {
|
||||
Name string `json:"name"`
|
||||
ExpiresAt time.Time `json:"expiresAt,omitempty"`
|
||||
ExpiresSoon bool `json:"expiresSoon,omitempty"`
|
||||
Token string `json:"token,omitempty"`
|
||||
FromYaml bool `json:"fromYaml"`
|
||||
}
|
||||
|
||||
// Status returns the sponsorship status
|
||||
func Status() sponsorStatus {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
|
||||
var expiresSoon bool
|
||||
if d := time.Until(ExpiresAt); d < 30*24*time.Hour && d > 0 {
|
||||
expiresSoon = true
|
||||
|
|
@ -98,5 +120,7 @@ func Status() sponsorStatus {
|
|||
Name: Subject,
|
||||
ExpiresAt: ExpiresAt,
|
||||
ExpiresSoon: expiresSoon,
|
||||
Token: redactToken(Token),
|
||||
FromYaml: fromYaml,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,8 +46,8 @@ func NewFromConfig(ctx context.Context, typ string, other map[string]interface{}
|
|||
|
||||
v, err := factory(ctx, cc.Other)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("cannot create vehicle type '%s': %w", typ, err)
|
||||
return nil, fmt.Errorf("cannot create vehicle type '%s': %w", typ, err)
|
||||
}
|
||||
|
||||
return v, err
|
||||
return v, nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue