feat/apikey-alias-authorship #2
@@ -5,7 +5,12 @@ FRONTEND_HOST="${FRONTEND_HOST:-http://localhost:80}"
|
||||
KC_CLIENT_ID="${KC_CLIENT_ID:-labdev}"
|
||||
KC_HOST="${KC_HOST:-https://login.hangman-lab.top}"
|
||||
KC_REALM="${KC_REALM:-Hangman-Lab}"
|
||||
DEBUG="${DEBUG:false}"
|
||||
# Note: ${DEBUG:-false} (correct default syntax). The old ${DEBUG:false}
|
||||
# produced an empty value when DEBUG was unset -> invalid config.json.
|
||||
DEBUG="${DEBUG:-false}"
|
||||
# DEBUG is emitted unquoted as a JSON boolean — guarantee it is exactly
|
||||
# true/false so config.json can never be invalid JSON.
|
||||
case "$DEBUG" in true|false) ;; *) DEBUG=false ;; esac
|
||||
rm -f /usr/share/nginx/html/config.js
|
||||
|
||||
|
||||
|
||||
@@ -2,7 +2,8 @@ import React, { useEffect, useState } from "react";
|
||||
import {Link, useParams} from "react-router-dom";
|
||||
import "katex/dist/katex.min.css";
|
||||
import "./MarkdownContent.css";
|
||||
import { Settings2, Pencil } from "lucide-react";
|
||||
import { Settings2, Pencil, User, Clock, History } from "lucide-react";
|
||||
import { formatDateTime } from "../../lib/utils";
|
||||
import MarkdownView from "./MarkdownView";
|
||||
import PatchCards from "./PatchCards";
|
||||
import PermissionGuard from "../PermissionGuard";
|
||||
@@ -85,6 +86,23 @@ const MarkdownContent = () => {
|
||||
</div>
|
||||
</PermissionGuard>
|
||||
</div>
|
||||
<div className="-mt-3 mb-6 flex flex-wrap items-center gap-x-5 gap-y-1 font-mono text-xs text-muted-foreground">
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<User className="h-3.5 w-3.5 text-secondary" />
|
||||
{markdown.author || "—"}
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<Clock className="h-3.5 w-3.5" />
|
||||
created {formatDateTime(markdown.created_at)}
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<History className="h-3.5 w-3.5" />
|
||||
updated {formatDateTime(markdown.updated_at)}
|
||||
{markdown.last_modified_by
|
||||
? ` by ${markdown.last_modified_by}`
|
||||
: ""}
|
||||
</span>
|
||||
</div>
|
||||
<MarkdownView content={parseMarkdownContent(markdown.content)} template={template}/>
|
||||
<PatchCards markdownId={id} />
|
||||
<MarkdownSettingModal
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
import { Plus, Pencil, Trash2, Save, Layers } from "lucide-react";
|
||||
import { Plus, Pencil, Trash2, Save, Layers, User, Clock } from "lucide-react";
|
||||
import { formatDateTime } from "../../lib/utils";
|
||||
import MarkdownView from "./MarkdownView";
|
||||
import PermissionGuard from "../PermissionGuard";
|
||||
import {
|
||||
@@ -137,6 +138,22 @@ const PatchCards = ({ markdownId }) => {
|
||||
</div>
|
||||
</PermissionGuard>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 border-b border-border/40 px-5 py-1.5 font-mono text-[11px] text-muted-foreground">
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<User className="h-3 w-3 text-secondary" />
|
||||
{patch.author || "—"}
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<Clock className="h-3 w-3" />
|
||||
{formatDateTime(patch.created_at)}
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-1">
|
||||
edited {formatDateTime(patch.updated_at)}
|
||||
{patch.last_modified_by
|
||||
? ` by ${patch.last_modified_by}`
|
||||
: ""}
|
||||
</span>
|
||||
</div>
|
||||
<div className="px-5 py-4">
|
||||
<MarkdownView content={{ markdown: patch.content }} />
|
||||
</div>
|
||||
|
||||
@@ -11,14 +11,16 @@ import {
|
||||
import { Button } from '../ui/button';
|
||||
import { Input, Label } from '../ui/input';
|
||||
|
||||
const AVAILABLE_ROLES = ['guest', 'creator', 'admin'];
|
||||
// Must match the backend allowlist (admin|creator|user).
|
||||
const AVAILABLE_ROLES = ['user', 'creator', 'admin'];
|
||||
|
||||
const SELECT_CLASS =
|
||||
"flex h-9 w-full rounded-md border border-input bg-background/60 px-3 py-1 text-sm text-foreground transition-colors focus-visible:outline-none focus-visible:border-primary/60 focus-visible:ring-2 focus-visible:ring-ring/40 disabled:cursor-not-allowed disabled:opacity-50";
|
||||
|
||||
const ApiKeyCreationModal = ({ isOpen, onClose }) => {
|
||||
const [alias, setAlias] = useState('');
|
||||
const [name, setName] = useState('');
|
||||
const [roles, setRoles] = useState(['guest']);
|
||||
const [roles, setRoles] = useState(['user']);
|
||||
const [generatedKey, setGeneratedKey] = useState(null);
|
||||
const createApiKeyMutation = useCreateApiKey();
|
||||
|
||||
@@ -44,12 +46,17 @@ const ApiKeyCreationModal = ({ isOpen, onClose }) => {
|
||||
};
|
||||
|
||||
const handleGenerate = async () => {
|
||||
if (!alias.trim()) {
|
||||
alert('Alias is required');
|
||||
return;
|
||||
}
|
||||
if (!name.trim()) {
|
||||
alert('API key name is required');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await createApiKeyMutation.mutateAsync({
|
||||
alias: alias.trim(),
|
||||
name: name.trim(),
|
||||
roles: roles
|
||||
});
|
||||
@@ -61,7 +68,7 @@ const ApiKeyCreationModal = ({ isOpen, onClose }) => {
|
||||
};
|
||||
|
||||
const handleCopy = () => {
|
||||
navigator.clipboard.writeText(generatedKey)
|
||||
navigator.clipboard.writeText(generatedKey.key)
|
||||
.then(() => alert('API key copied to clipboard'))
|
||||
.catch(err => console.error('failed to copy api key:', err));
|
||||
};
|
||||
@@ -80,6 +87,20 @@ const ApiKeyCreationModal = ({ isOpen, onClose }) => {
|
||||
</DialogHeader>
|
||||
{!generatedKey ? (
|
||||
<div className="space-y-5">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="api-key-alias">Alias</Label>
|
||||
<Input
|
||||
id="api-key-alias"
|
||||
type="text"
|
||||
placeholder="unique alias (reuse to renew)"
|
||||
value={alias}
|
||||
onChange={(e) => setAlias(e.target.value)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Unique. Using an existing alias renews that
|
||||
key (same key string, validity reset).
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="api-key-name">Name</Label>
|
||||
<Input
|
||||
@@ -130,7 +151,9 @@ const ApiKeyCreationModal = ({ isOpen, onClose }) => {
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg border border-secondary/40 bg-secondary/10 px-4 py-3 font-mono text-sm text-secondary">
|
||||
Please copy your API key immediately! It will only be displayed once!
|
||||
{generatedKey.renewed
|
||||
? "Key renewed — same key string, validity reset 15 days."
|
||||
: "Please copy your API key immediately! It will only be displayed once!"}
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="generated-api-key">Your API Key</Label>
|
||||
@@ -154,7 +177,7 @@ const ApiKeyCreationModal = ({ isOpen, onClose }) => {
|
||||
{!generatedKey && (
|
||||
<Button
|
||||
onClick={handleGenerate}
|
||||
disabled={createApiKeyMutation.isLoading || !name.trim()}
|
||||
disabled={createApiKeyMutation.isLoading || !alias.trim() || !name.trim()}
|
||||
>
|
||||
<KeyRound className="h-4 w-4" /> Generate
|
||||
</Button>
|
||||
|
||||
@@ -5,3 +5,17 @@ import { twMerge } from "tailwind-merge";
|
||||
export function cn(...inputs) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
/** Format a backend datetime string for display; '—' when missing/invalid. */
|
||||
export function formatDateTime(value) {
|
||||
if (!value) return "—";
|
||||
const d = new Date(value);
|
||||
if (isNaN(d.getTime())) return "—";
|
||||
return d.toLocaleString(undefined, {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5,7 +5,11 @@ module.exports = {
|
||||
entry: './src/index.js',
|
||||
output: {
|
||||
path: path.resolve(__dirname, './dist'),
|
||||
filename: 'bundle.js',
|
||||
// Content-hashed filename: index.html (injected by HtmlWebpackPlugin,
|
||||
// and not edge-cached) points at a unique URL each build, so a new
|
||||
// deploy is picked up immediately — no stale CDN/browser bundle.
|
||||
filename: '[name].[contenthash].js',
|
||||
chunkFilename: '[name].[contenthash].js',
|
||||
publicPath: '/',
|
||||
clean: true,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user