Add svelte-heros icon library
- Install svelte-heros for consistent Heroicons usage - Replace inline SVGs for password toggle with Eye/EyeOff icons - Fix USE_EMBEDDED mode to serve frontend from filesystem
This commit is contained in:
parent
8b2d2649ac
commit
46d9ff26dd
5 changed files with 49 additions and 45 deletions
|
|
@ -4,15 +4,22 @@ import (
|
|||
"embed"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
)
|
||||
|
||||
//go:embed all:embed
|
||||
var Files embed.FS
|
||||
|
||||
func FrontendFileSystem() http.FileSystem {
|
||||
return http.FS(Files)
|
||||
func FrontendFileSystem() (http.FileSystem, string) {
|
||||
if os.Getenv("USE_EMBEDDED") == "false" {
|
||||
return http.Dir("../frontend/build"), ""
|
||||
}
|
||||
return http.FS(Files), "embed/"
|
||||
}
|
||||
|
||||
func FrontendFS() fs.FS {
|
||||
if os.Getenv("USE_EMBEDDED") == "false" {
|
||||
return os.DirFS("../frontend/build")
|
||||
}
|
||||
return Files
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ func main() {
|
|||
|
||||
// Get the embedded filesystem containing the frontend.
|
||||
// The frontend is compiled into the binary during build time.
|
||||
frontendFS := FrontendFileSystem()
|
||||
frontendFS, frontendPrefix := FrontendFileSystem()
|
||||
|
||||
// Set Gin mode based on environment.
|
||||
// USE_EMBEDDED=false means development mode (e.g., via air hot-reloader).
|
||||
|
|
@ -142,31 +142,31 @@ func main() {
|
|||
// Set up SPA fallback for non-API routes.
|
||||
// This must be the last route to catch all unmatched paths.
|
||||
engine.NoRoute(func(c *gin.Context) {
|
||||
path := c.Request.URL.Path
|
||||
reqPath := c.Request.URL.Path
|
||||
|
||||
// If it's an API path, return 404.
|
||||
// The API router should have handled /api/* routes.
|
||||
if strings.HasPrefix(path, "/api/") {
|
||||
if strings.HasPrefix(reqPath, "/api/") {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "not found"})
|
||||
return
|
||||
}
|
||||
|
||||
// Serve SvelteKit hashed assets at /_app/*
|
||||
if strings.HasPrefix(path, "/_app/") {
|
||||
assetPath := "embed/_app/" + strings.TrimPrefix(path, "/_app/")
|
||||
serveGinStaticFile(c, frontendFS, assetPath)
|
||||
if strings.HasPrefix(reqPath, "/_app/") {
|
||||
filePath := frontendPrefix + "_app/" + strings.TrimPrefix(reqPath, "/_app/")
|
||||
serveGinStaticFile(c, frontendFS, filePath)
|
||||
return
|
||||
}
|
||||
|
||||
// Serve favicon
|
||||
if path == "/favicon.png" {
|
||||
serveGinStaticFile(c, frontendFS, "embed/favicon.png")
|
||||
if reqPath == "/favicon.png" {
|
||||
serveGinStaticFile(c, frontendFS, frontendPrefix+"favicon.png")
|
||||
return
|
||||
}
|
||||
|
||||
// For all other paths, serve the SPA index.html.
|
||||
// This allows client-side routing (e.g., /domains/example.org).
|
||||
serveGinStaticFile(c, frontendFS, "embed/index.html")
|
||||
serveGinStaticFile(c, frontendFS, frontendPrefix+"index.html")
|
||||
})
|
||||
|
||||
// Build the address string for binding: "IP:PORT"
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@
|
|||
"daisyui": "^5.5.19",
|
||||
"postcss": "^8.5.8",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-heros": "^8.0.1",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^5.0.0",
|
||||
|
|
@ -289,6 +290,8 @@
|
|||
|
||||
"svelte": ["svelte@5.55.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.6.4", "esm-env": "^1.2.1", "esrap": "^2.2.2", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-SThllKq6TRMBwPtat7ASnm/9CDXnIhBR0NPGw0ujn2DVYx9rVwsPZxDaDQcYGdUz/3BYVsCzdq7pZarRQoGvtw=="],
|
||||
|
||||
"svelte-heros": ["svelte-heros@8.0.1", "", { "peerDependencies": { "svelte": "^5.0.0" } }, "sha512-67rDThxmdbjvJxKDqDUnkTsmyI5JtXcS0S0Ryg/EmTlHscFyJNgJyEARX5/xt3ONsZA0i6+dWnq+EX5jH9Wq1Q=="],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.2.2", "", {}, "sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q=="],
|
||||
|
||||
"tapable": ["tapable@2.3.2", "", {}, "sha512-1MOpMXuhGzGL5TTCZFItxCc0AARf1EZFQkGqMm7ERKj8+Hgr5oLvJOVFcC+lRmR8hCe2S3jC4T5D7Vg/d7/fhA=="],
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@
|
|||
"daisyui": "^5.5.19",
|
||||
"postcss": "^8.5.8",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-heros": "^8.0.1",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "^5.0.0",
|
||||
"vite": "^5.0.0"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import { Eye, EyeOff } from 'svelte-heros';
|
||||
|
||||
interface User {
|
||||
id: number;
|
||||
|
|
@ -14,9 +15,8 @@
|
|||
let localPart = $state('');
|
||||
let newUser = $state({ password: '', quota: 0 });
|
||||
let createError = $state('');
|
||||
let createdPassword = $state('');
|
||||
let showCreatedPassword = $state(false);
|
||||
let domainName = $state('');
|
||||
let showPassword = $state(false);
|
||||
|
||||
async function loadUsers(name: string) {
|
||||
try {
|
||||
|
|
@ -37,10 +37,9 @@
|
|||
|
||||
function openModal() {
|
||||
createError = '';
|
||||
createdPassword = '';
|
||||
showCreatedPassword = false;
|
||||
localPart = '';
|
||||
newUser = { password: '', quota: 0 };
|
||||
showPassword = false;
|
||||
newUser = { password: generatePasswordStr(), quota: 0 };
|
||||
dialogEl?.showModal();
|
||||
}
|
||||
|
||||
|
|
@ -48,7 +47,7 @@
|
|||
dialogEl?.close();
|
||||
}
|
||||
|
||||
function generatePassword() {
|
||||
function generatePasswordStr(): string {
|
||||
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*';
|
||||
let password = '';
|
||||
const array = new Uint8Array(16);
|
||||
|
|
@ -56,7 +55,7 @@
|
|||
for (let i = 0; i < 16; i++) {
|
||||
password += chars[array[i] % chars.length];
|
||||
}
|
||||
newUser.password = password;
|
||||
return password;
|
||||
}
|
||||
|
||||
async function createUser() {
|
||||
|
|
@ -70,14 +69,13 @@
|
|||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({ email, password: newUser.password || undefined, quota: newUser.quota })
|
||||
body: JSON.stringify({ email, password: newUser.password, quota: newUser.quota })
|
||||
});
|
||||
const data = await res.json();
|
||||
if (res.ok) {
|
||||
createdPassword = data.password || '';
|
||||
showCreatedPassword = true;
|
||||
await loadUsers(domainName);
|
||||
closeModal();
|
||||
} else {
|
||||
const data = await res.json();
|
||||
createError = data.error || 'Failed to create user';
|
||||
}
|
||||
} catch (e) {
|
||||
|
|
@ -87,7 +85,7 @@
|
|||
}
|
||||
|
||||
async function copyPassword() {
|
||||
await navigator.clipboard.writeText(createdPassword);
|
||||
await navigator.clipboard.writeText(newUser.password);
|
||||
}
|
||||
|
||||
async function deleteUser(id: number) {
|
||||
|
|
@ -211,18 +209,26 @@
|
|||
</div>
|
||||
</fieldset>
|
||||
<fieldset class="fieldset mt-4">
|
||||
<legend class="fieldset-legend">Password (leave empty to generate)</legend>
|
||||
<legend class="fieldset-legend">Password</legend>
|
||||
<div class="join w-full">
|
||||
<input
|
||||
type={showCreatedPassword ? 'text' : 'password'}
|
||||
class="input input-bordered join-item flex-1"
|
||||
type={showPassword ? 'text' : 'password'}
|
||||
class="input input-bordered join-item flex-1 font-mono"
|
||||
bind:value={newUser.password}
|
||||
required
|
||||
/>
|
||||
<button type="button" class="btn join-item" onclick={() => showCreatedPassword = !showCreatedPassword}>
|
||||
{showCreatedPassword ? '🙈' : '👁️'}
|
||||
<button type="button" class="btn join-item" onclick={() => showPassword = !showPassword}>
|
||||
{#if showPassword}
|
||||
<EyeOff class="h-5 w-5" />
|
||||
{:else}
|
||||
<Eye class="h-5 w-5" />
|
||||
{/if}
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary join-item" onclick={generatePassword}>
|
||||
Generate
|
||||
<button type="button" class="btn join-item" onclick={() => navigator.clipboard.writeText(newUser.password)}>
|
||||
Copy
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary join-item" onclick={() => newUser.password = generatePasswordStr()}>
|
||||
Regenerate
|
||||
</button>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
|
@ -235,27 +241,14 @@
|
|||
min="0"
|
||||
/>
|
||||
</fieldset>
|
||||
{#if showCreatedPassword && createdPassword}
|
||||
<div class="alert alert-success mt-4">
|
||||
<div class="flex-1">
|
||||
<label class="text-sm font-medium">User created! Copy password:</label>
|
||||
<div class="flex items-center gap-2 mt-1">
|
||||
<code class="flex-1 bg-base-200 p-2 rounded text-sm break-all">{createdPassword}</code>
|
||||
<button type="button" class="btn btn-sm" onclick={copyPassword}>
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if createError}
|
||||
<div class="alert alert-error mt-4">
|
||||
<span>{createError}</span>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="modal-action">
|
||||
<button type="button" class="btn" onclick={closeModal}>Close</button>
|
||||
<button type="submit" class="btn btn-primary" disabled={showCreatedPassword}>Create</button>
|
||||
<button type="button" class="btn" onclick={closeModal}>Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">Create</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue