Compare commits
No commits in common. "1a865da6561e8fac9c5cde0383f4a2203eea14f0" and "8e8e8946384479d97f27a34a7f7a1e7af7a53696" have entirely different histories.
1a865da656
...
8e8e894638
12 changed files with 68 additions and 108 deletions
|
|
@ -4,10 +4,15 @@ from markdown import markdown
|
||||||
from pydantic import BaseModel, ConfigDict, field_validator
|
from pydantic import BaseModel, ConfigDict, field_validator
|
||||||
|
|
||||||
from .dav.webdav import WebDAV
|
from .dav.webdav import WebDAV
|
||||||
from .settings import SETTINGS, Credentials
|
from .settings import SETTINGS
|
||||||
from .transformed_string import TransformedString
|
from .transformed_string import TransformedString
|
||||||
|
|
||||||
|
|
||||||
|
class User(BaseModel):
|
||||||
|
name: str
|
||||||
|
password: str
|
||||||
|
|
||||||
|
|
||||||
class Site(BaseModel):
|
class Site(BaseModel):
|
||||||
model_config = ConfigDict(validate_default=True)
|
model_config = ConfigDict(validate_default=True)
|
||||||
|
|
||||||
|
|
@ -55,7 +60,7 @@ class Image(BaseModel):
|
||||||
|
|
||||||
class Config(BaseModel):
|
class Config(BaseModel):
|
||||||
# Login-Daten für Admin-Modus
|
# Login-Daten für Admin-Modus
|
||||||
admin: Credentials
|
admin: User
|
||||||
|
|
||||||
# Lösungswort
|
# Lösungswort
|
||||||
solution: TransformedString
|
solution: TransformedString
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ class WebDAV:
|
||||||
_webdav_client = WebDAVclient(
|
_webdav_client = WebDAVclient(
|
||||||
{
|
{
|
||||||
"webdav_hostname": SETTINGS.webdav.url,
|
"webdav_hostname": SETTINGS.webdav.url,
|
||||||
"webdav_login": SETTINGS.webdav.auth.username,
|
"webdav_login": SETTINGS.webdav.username,
|
||||||
"webdav_password": SETTINGS.webdav.auth.password,
|
"webdav_password": SETTINGS.webdav.password,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,6 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
T = TypeVar("T")
|
T = TypeVar("T")
|
||||||
|
|
||||||
|
|
||||||
class Credentials(BaseModel):
|
|
||||||
username: str = ""
|
|
||||||
password: str = ""
|
|
||||||
|
|
||||||
|
|
||||||
class DavSettings(BaseModel):
|
class DavSettings(BaseModel):
|
||||||
"""
|
"""
|
||||||
Connection to a DAV server.
|
Connection to a DAV server.
|
||||||
|
|
@ -21,10 +16,8 @@ class DavSettings(BaseModel):
|
||||||
path: str = "/remote.php/webdav"
|
path: str = "/remote.php/webdav"
|
||||||
prefix: str = "/advent22"
|
prefix: str = "/advent22"
|
||||||
|
|
||||||
auth: Credentials = Credentials(
|
username: str = "advent22_user"
|
||||||
username="advent22_user",
|
password: str = "password"
|
||||||
password="password",
|
|
||||||
)
|
|
||||||
|
|
||||||
cache_ttl: int = 60 * 10
|
cache_ttl: int = 60 * 10
|
||||||
config_filename: str = "config.toml"
|
config_filename: str = "config.toml"
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ async def user_is_admin(
|
||||||
|
|
||||||
username_correct = secrets.compare_digest(
|
username_correct = secrets.compare_digest(
|
||||||
credentials.username.lower(),
|
credentials.username.lower(),
|
||||||
cfg.admin.username.lower(),
|
cfg.admin.name.lower(),
|
||||||
)
|
)
|
||||||
password_correct = secrets.compare_digest(
|
password_correct = secrets.compare_digest(
|
||||||
credentials.password,
|
credentials.password,
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,7 @@ from pydantic import BaseModel
|
||||||
|
|
||||||
from advent22_api.core.helpers import EventDates
|
from advent22_api.core.helpers import EventDates
|
||||||
|
|
||||||
from ..core.calendar_config import (
|
from ..core.calendar_config import CalendarConfig, DoorsSaved, get_calendar_config
|
||||||
CalendarConfig,
|
|
||||||
DoorsSaved,
|
|
||||||
get_calendar_config,
|
|
||||||
)
|
|
||||||
from ..core.config import Config, Image, get_config
|
from ..core.config import Config, Image, get_config
|
||||||
from ..core.depends import (
|
from ..core.depends import (
|
||||||
TTFont,
|
TTFont,
|
||||||
|
|
@ -18,7 +14,7 @@ from ..core.depends import (
|
||||||
get_all_parts,
|
get_all_parts,
|
||||||
get_all_ttfonts,
|
get_all_ttfonts,
|
||||||
)
|
)
|
||||||
from ..core.settings import SETTINGS, Credentials, RedisSettings
|
from ..core.settings import SETTINGS, RedisSettings
|
||||||
from ._security import require_admin, user_is_admin
|
from ._security import require_admin, user_is_admin
|
||||||
|
|
||||||
router = APIRouter(prefix="/admin", tags=["admin"])
|
router = APIRouter(prefix="/admin", tags=["admin"])
|
||||||
|
|
@ -174,16 +170,24 @@ async def put_doors(
|
||||||
await cal_cfg.change(cfg)
|
await cal_cfg.change(cfg)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/credentials/{name}")
|
@router.get("/dav_credentials")
|
||||||
async def get_credentials(
|
async def get_dav_credentials(
|
||||||
name: str,
|
_: None = Depends(require_admin),
|
||||||
|
) -> tuple[str, str]:
|
||||||
|
"""
|
||||||
|
Zugangsdaten für WebDAV
|
||||||
|
"""
|
||||||
|
|
||||||
|
return SETTINGS.webdav.username, SETTINGS.webdav.password
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/ui_credentials")
|
||||||
|
async def get_ui_credentials(
|
||||||
_: None = Depends(require_admin),
|
_: None = Depends(require_admin),
|
||||||
cfg: Config = Depends(get_config),
|
cfg: Config = Depends(get_config),
|
||||||
) -> Credentials:
|
) -> tuple[str, str]:
|
||||||
|
"""
|
||||||
|
Zugangsdaten für Admin-UI
|
||||||
|
"""
|
||||||
|
|
||||||
if name == "dav":
|
return cfg.admin.name, cfg.admin.password
|
||||||
return SETTINGS.webdav.auth
|
|
||||||
elif name == "ui":
|
|
||||||
return cfg.admin
|
|
||||||
else:
|
|
||||||
return Credentials()
|
|
||||||
|
|
|
||||||
|
|
@ -2,19 +2,15 @@
|
||||||
<ConfigView />
|
<ConfigView />
|
||||||
<CalendarAssistant />
|
<CalendarAssistant />
|
||||||
<DoorMapEditor />
|
<DoorMapEditor />
|
||||||
<BulmaDrawer header="Vorschau" :opening="store.update" refreshable>
|
<BulmaDrawer header="Vorschau">
|
||||||
<UserView />
|
<UserView />
|
||||||
</BulmaDrawer>
|
</BulmaDrawer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { advent22Store } from "@/lib/store";
|
|
||||||
|
|
||||||
import UserView from "../UserView.vue";
|
import UserView from "../UserView.vue";
|
||||||
import BulmaDrawer from "../bulma/Drawer.vue";
|
import BulmaDrawer from "../bulma/Drawer.vue";
|
||||||
import CalendarAssistant from "./CalendarAssistant.vue";
|
import CalendarAssistant from "./CalendarAssistant.vue";
|
||||||
import ConfigView from "./ConfigView.vue";
|
import ConfigView from "./ConfigView.vue";
|
||||||
import DoorMapEditor from "./DoorMapEditor.vue";
|
import DoorMapEditor from "./DoorMapEditor.vue";
|
||||||
|
|
||||||
const store = advent22Store();
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -139,15 +139,12 @@
|
||||||
|
|
||||||
<dt>Zugangsdaten</dt>
|
<dt>Zugangsdaten</dt>
|
||||||
<dd class="is-family-monospace">
|
<dd class="is-family-monospace">
|
||||||
<BulmaSecret
|
<BulmaSecret @load="load_dav_credentials">
|
||||||
@show="load_credentials(creds.dav, 'admin/credentials/dav')"
|
|
||||||
@hide="clear_credentials(creds.dav)"
|
|
||||||
>
|
|
||||||
<span class="tag is-danger">user</span>
|
<span class="tag is-danger">user</span>
|
||||||
{{ creds.dav.username }}
|
{{ dav_credentials[0] }}
|
||||||
<br />
|
<br />
|
||||||
<span class="tag is-danger">pass</span>
|
<span class="tag is-danger">pass</span>
|
||||||
{{ creds.dav.password }}
|
{{ dav_credentials[1] }}
|
||||||
</BulmaSecret>
|
</BulmaSecret>
|
||||||
</dd>
|
</dd>
|
||||||
|
|
||||||
|
|
@ -170,15 +167,12 @@
|
||||||
|
|
||||||
<dt>UI-Admin</dt>
|
<dt>UI-Admin</dt>
|
||||||
<dd class="is-family-monospace">
|
<dd class="is-family-monospace">
|
||||||
<BulmaSecret
|
<BulmaSecret @load="load_ui_credentials">
|
||||||
@show="load_credentials(creds.ui, 'admin/credentials/ui')"
|
|
||||||
@hide="clear_credentials(creds.ui)"
|
|
||||||
>
|
|
||||||
<span class="tag is-danger">user</span>
|
<span class="tag is-danger">user</span>
|
||||||
{{ creds.ui.username }}
|
{{ ui_credentials[0] }}
|
||||||
<br />
|
<br />
|
||||||
<span class="tag is-danger">pass</span>
|
<span class="tag is-danger">pass</span>
|
||||||
{{ creds.ui.password }}
|
{{ ui_credentials[1] }}
|
||||||
</BulmaSecret>
|
</BulmaSecret>
|
||||||
</dd>
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
@ -243,16 +237,8 @@ const admin_config_model = ref<AdminConfigModel>({
|
||||||
});
|
});
|
||||||
|
|
||||||
const doors = ref<DoorSaved[]>([]);
|
const doors = ref<DoorSaved[]>([]);
|
||||||
const creds = ref<Record<string, Credentials>>({
|
const dav_credentials = ref<Credentials>(["", ""]);
|
||||||
dav: {
|
const ui_credentials = ref<Credentials>(["", ""]);
|
||||||
username: "",
|
|
||||||
password: "",
|
|
||||||
},
|
|
||||||
ui: {
|
|
||||||
username: "",
|
|
||||||
password: "",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
function fmt_puzzle_date(name: keyof AdminConfigModel["puzzle"]): string {
|
function fmt_puzzle_date(name: keyof AdminConfigModel["puzzle"]): string {
|
||||||
const iso_date = admin_config_model.value.puzzle[name];
|
const iso_date = admin_config_model.value.puzzle[name];
|
||||||
|
|
@ -271,26 +257,22 @@ async function on_open(): Promise<void> {
|
||||||
void store_update; // discard value
|
void store_update; // discard value
|
||||||
admin_config_model.value = new_admin_config_model;
|
admin_config_model.value = new_admin_config_model;
|
||||||
doors.value = new_doors;
|
doors.value = new_doors;
|
||||||
|
|
||||||
clear_credentials(creds.value.dav);
|
|
||||||
clear_credentials(creds.value.ui);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function load_credentials(
|
async function load_dav_credentials(): Promise<void> {
|
||||||
creds: Credentials,
|
|
||||||
endpoint: string,
|
|
||||||
): Promise<void> {
|
|
||||||
try {
|
try {
|
||||||
const new_creds = await API.request<Credentials>(endpoint);
|
dav_credentials.value = await API.request<Credentials>(
|
||||||
|
"admin/dav_credentials",
|
||||||
creds.username = new_creds.username;
|
);
|
||||||
creds.password = new_creds.password;
|
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
|
|
||||||
function clear_credentials(creds: Credentials): void {
|
async function load_ui_credentials(): Promise<void> {
|
||||||
creds.username = "";
|
try {
|
||||||
creds.password = "";
|
ui_credentials.value = await API.request<Credentials>(
|
||||||
|
"admin/ui_credentials",
|
||||||
|
);
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@
|
||||||
<span v-else>***</span>
|
<span v-else>***</span>
|
||||||
<BulmaButton
|
<BulmaButton
|
||||||
:class="`is-small is-${record.color} ml-2`"
|
:class="`is-small is-${record.color} ml-2`"
|
||||||
:icon="['fas', record.icon]"
|
:icon="['fas', `${record.icon}`]"
|
||||||
:busy="state === 'pending'"
|
:busy="state === 'clicked'"
|
||||||
@click="on_click"
|
@click="on_click"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -16,37 +16,24 @@ import { computed, ref } from "vue";
|
||||||
import BulmaButton from "./Button.vue";
|
import BulmaButton from "./Button.vue";
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(event: "show"): void;
|
(event: "load"): void;
|
||||||
(event: "hide"): void;
|
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
type State = "hidden" | "pending" | "visible";
|
type State = "hidden" | "clicked" | "visible";
|
||||||
const state = ref<State>("hidden");
|
const state = ref<State>("hidden");
|
||||||
|
|
||||||
const state_map: Record<State, { color: string; icon: string; next: State }> = {
|
const state_map: Record<State, { color: string; icon: string; next: State }> = {
|
||||||
hidden: { color: "primary", icon: "eye-slash", next: "pending" },
|
hidden: { color: "primary", icon: "eye-slash", next: "clicked" },
|
||||||
pending: { color: "warning", icon: "eye-slash", next: "visible" },
|
clicked: { color: "warning", icon: "eye-slash", next: "visible" },
|
||||||
visible: { color: "danger", icon: "eye", next: "hidden" },
|
visible: { color: "danger", icon: "eye", next: "hidden" },
|
||||||
} as const;
|
} as const;
|
||||||
const record = computed(() => state_map[state.value] ?? state_map.hidden);
|
const record = computed(() => state_map[state.value] ?? state_map.hidden);
|
||||||
|
|
||||||
let pending_timeout: number | undefined;
|
|
||||||
|
|
||||||
function on_click(): void {
|
function on_click(): void {
|
||||||
state.value = record.value.next;
|
state.value = record.value.next;
|
||||||
|
|
||||||
if (state.value === "hidden") {
|
|
||||||
emit("hide");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.value === "pending") {
|
|
||||||
pending_timeout = window.setTimeout(() => (state.value = "hidden"), 2500);
|
|
||||||
} else {
|
|
||||||
window.clearTimeout(pending_timeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.value === "visible") {
|
if (state.value === "visible") {
|
||||||
emit("show");
|
emit("load");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -44,17 +44,13 @@ export class API {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static get creds(): AxiosBasicCredentials {
|
public static get creds(): AxiosBasicCredentials {
|
||||||
const stored_auth = JSON.parse(localStorage.getItem(this.creds_key) ?? "");
|
const auth_json = localStorage.getItem(this.creds_key);
|
||||||
if (
|
if (auth_json !== null) {
|
||||||
stored_auth !== null &&
|
return JSON.parse(auth_json);
|
||||||
Object.hasOwn(stored_auth, "username") &&
|
} else {
|
||||||
Object.hasOwn(stored_auth, "password")
|
|
||||||
) {
|
|
||||||
return stored_auth;
|
|
||||||
}
|
|
||||||
|
|
||||||
return { username: "", password: "" };
|
return { username: "", password: "" };
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static get_axios_config({
|
private static get_axios_config({
|
||||||
endpoint,
|
endpoint,
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,4 @@ export interface ImageData {
|
||||||
data_url: string;
|
data_url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Credentials {
|
export type Credentials = [username: string, password: string];
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ export class Rectangle {
|
||||||
}
|
}
|
||||||
|
|
||||||
public update(corner_1?: Vector2D, corner_2?: Vector2D): Rectangle {
|
public update(corner_1?: Vector2D, corner_2?: Vector2D): Rectangle {
|
||||||
return new Rectangle(corner_1 ?? this.corner_1, corner_2 ?? this.corner_2);
|
return new Rectangle(corner_1 || this.corner_1, corner_2 || this.corner_2);
|
||||||
}
|
}
|
||||||
|
|
||||||
public move(vector: Vector2D): Rectangle {
|
public move(vector: Vector2D): Rectangle {
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ export const advent22Store = defineStore({
|
||||||
const favicon = await API.request<ImageData>("user/favicon");
|
const favicon = await API.request<ImageData>("user/favicon");
|
||||||
|
|
||||||
const link: HTMLLinkElement =
|
const link: HTMLLinkElement =
|
||||||
document.querySelector("link[rel*='icon']") ??
|
document.querySelector("link[rel*='icon']") ||
|
||||||
document.createElement("link");
|
document.createElement("link");
|
||||||
link.rel = "shortcut icon";
|
link.rel = "shortcut icon";
|
||||||
link.type = "image/x-icon";
|
link.type = "image/x-icon";
|
||||||
|
|
@ -111,12 +111,12 @@ export const advent22Store = defineStore({
|
||||||
},
|
},
|
||||||
|
|
||||||
async login(creds: Credentials): Promise<boolean> {
|
async login(creds: Credentials): Promise<boolean> {
|
||||||
API.creds = creds;
|
API.creds = { username: creds[0], password: creds[1] };
|
||||||
return await this.update_is_admin();
|
return await this.update_is_admin();
|
||||||
},
|
},
|
||||||
|
|
||||||
logout() {
|
logout() {
|
||||||
API.creds = null;
|
API.creds = { username: "", password: "" };
|
||||||
this.is_admin = false;
|
this.is_admin = false;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue