first
This commit is contained in:
commit
f4be03ceba
1826 changed files with 830924 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
build/
|
||||
71
Makefile
Normal file
71
Makefile
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
.PHONY: all dev-frontend dev-backend build build-frontend build-backend clean test lint
|
||||
|
||||
# Variables
|
||||
APP_NAME := imc-vibe
|
||||
FRONTEND_DIR := frontend
|
||||
BACKEND_DIR := backend
|
||||
BUILD_DIR := build
|
||||
|
||||
# Default target
|
||||
all: build
|
||||
|
||||
# Build everything (frontend + backend + embed)
|
||||
build: build-frontend build-backend
|
||||
|
||||
# Build frontend only
|
||||
build-frontend:
|
||||
@echo "Building frontend..."
|
||||
cd $(FRONTEND_DIR) && bun install --frozen-lockfile
|
||||
cd $(FRONTEND_DIR) && bun run build
|
||||
@echo "Frontend built successfully"
|
||||
|
||||
# Build backend only (copies frontend build into embed directory)
|
||||
build-backend: build-frontend
|
||||
@echo "Copying frontend to embed directory..."
|
||||
rm -rf $(BACKEND_DIR)/cmd/server/embed
|
||||
cp -r $(FRONTEND_DIR)/build $(BACKEND_DIR)/cmd/server/embed
|
||||
@echo "Building backend..."
|
||||
cd $(BACKEND_DIR) && go mod tidy
|
||||
cd $(BACKEND_DIR) && go build -ldflags "-s -w" -o ../$(BUILD_DIR)/$(APP_NAME) ./cmd/server
|
||||
@echo "Backend built successfully"
|
||||
|
||||
# Development targets
|
||||
dev-frontend:
|
||||
@echo "Starting frontend dev server..."
|
||||
cd $(FRONTEND_DIR) && bun run dev
|
||||
|
||||
dev-backend:
|
||||
@echo "Starting backend dev server..."
|
||||
cd $(BACKEND_DIR) && USE_EMBEDDED=false go run ./cmd/server
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
@echo "Cleaning..."
|
||||
rm -rf $(BUILD_DIR)
|
||||
rm -rf $(FRONTEND_DIR)/build
|
||||
rm -rf $(FRONTEND_DIR)/.svelte-kit
|
||||
rm -rf $(BACKEND_DIR)/cmd/server/embed
|
||||
cd $(BACKEND_DIR) && go clean
|
||||
@echo "Cleaned successfully"
|
||||
|
||||
# Test
|
||||
test:
|
||||
cd $(BACKEND_DIR) && go test ./...
|
||||
|
||||
# Lint
|
||||
lint:
|
||||
cd $(BACKEND_DIR) && go vet ./...
|
||||
|
||||
# Help
|
||||
help:
|
||||
@echo "Available targets:"
|
||||
@echo " all - Build frontend and backend (default)"
|
||||
@echo " build - Build frontend and backend"
|
||||
@echo " build-frontend - Build frontend only"
|
||||
@echo " build-backend - Build backend only"
|
||||
@echo " dev-frontend - Start frontend dev server"
|
||||
@echo " dev-backend - Start backend dev server"
|
||||
@echo " clean - Remove build artifacts"
|
||||
@echo " test - Run backend tests"
|
||||
@echo " lint - Run Go vet"
|
||||
@echo " help - Show this help"
|
||||
8
backend/.env
Normal file
8
backend/.env
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USER=mailadmin
|
||||
DB_PASSWORD=MAILADMIN-PASSWORD-HERE
|
||||
DB_NAME=mailserver
|
||||
JWT_SECRET=change-this-secret-in-production
|
||||
ADMIN_USER=admin
|
||||
ADMIN_PASSWORD=admin123
|
||||
19
backend/.env.example
Normal file
19
backend/.env.example
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Database
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USER=mailadmin
|
||||
DB_PASSWORD=your-password
|
||||
DB_NAME=mailserver
|
||||
|
||||
# App
|
||||
PORT=8080
|
||||
USE_EMBEDDED=true
|
||||
JWT_SECRET=change-this-to-a-random-secret
|
||||
|
||||
# Mail server paths
|
||||
MAIL_DATA_DIR=/var/vmail
|
||||
POSTQUEUE_PATH=/usr/sbin/postqueue
|
||||
POSTSUPER_PATH=/usr/sbin/postsuper
|
||||
DOVECOT_QUOTA_CMD=/usr/bin/doveadm
|
||||
JOURNALCTL_PATH=/usr/bin/journalctl
|
||||
RSPMAD_API=http://127.0.0.1:11334
|
||||
1
backend/cmd/server/embed/_app/env.js
Normal file
1
backend/cmd/server/embed/_app/env.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const env={}
|
||||
|
|
@ -0,0 +1 @@
|
|||
.loading.svelte-12qhfyh{min-height:100vh;display:flex;align-items:center;justify-content:center;font-size:1.2rem;color:#666}body{margin:0;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,sans-serif;background:#f5f5f5}.app.svelte-12qhfyh{min-height:100vh}header.svelte-12qhfyh{background:#2c3e50;color:#fff;padding:.75rem 2rem;display:flex;align-items:center;gap:2rem;flex-wrap:wrap}h1.svelte-12qhfyh{margin:0;font-size:1.25rem}nav.svelte-12qhfyh{display:flex;gap:.5rem;flex:1}nav.svelte-12qhfyh a:where(.svelte-12qhfyh){color:#ecf0f1;text-decoration:none;padding:.5rem 1rem;border-radius:4px;font-size:.9rem;transition:background .2s}nav.svelte-12qhfyh a:where(.svelte-12qhfyh):hover{background:#34495e}main.svelte-12qhfyh{padding:2rem;max-width:1400px;margin:0 auto}.user-menu.svelte-12qhfyh{display:flex;align-items:center;gap:1rem;font-size:.9rem}.user-menu.svelte-12qhfyh span:where(.svelte-12qhfyh){color:#ecf0f1}.user-menu.svelte-12qhfyh a:where(.svelte-12qhfyh){color:#ecf0f1;text-decoration:none;padding:.375rem .75rem;border:1px solid #7f8c8d;border-radius:4px;font-size:.85rem}.user-menu.svelte-12qhfyh a:where(.svelte-12qhfyh):hover{background:#34495e}.user-menu.svelte-12qhfyh button:where(.svelte-12qhfyh){background:transparent;border:1px solid #7f8c8d;color:#ecf0f1;padding:.375rem .75rem;border-radius:4px;cursor:pointer;font-size:.85rem}.user-menu.svelte-12qhfyh button:where(.svelte-12qhfyh):hover{background:#34495e}
|
||||
|
|
@ -0,0 +1 @@
|
|||
.header.svelte-15uyvzd{display:flex;justify-content:space-between;align-items:center;margin-bottom:1.5rem;gap:1rem}.back.svelte-15uyvzd{color:#3498db;text-decoration:none;font-size:.9rem}.back.svelte-15uyvzd:hover{text-decoration:underline}.header.svelte-15uyvzd h2:where(.svelte-15uyvzd){flex:1;margin:0}table.svelte-15uyvzd{width:100%;background:#fff;border-radius:8px;overflow:hidden;box-shadow:0 2px 4px #0000001a}th.svelte-15uyvzd,td.svelte-15uyvzd{padding:1rem;text-align:left;border-bottom:1px solid #eee}th.svelte-15uyvzd{background:#f8f9fa;font-weight:600}button.svelte-15uyvzd{padding:.5rem 1rem;border:none;border-radius:4px;cursor:pointer;background:#3498db;color:#fff}button.danger.svelte-15uyvzd{background:#e74c3c}.modal-backdrop.svelte-15uyvzd{position:fixed;top:0;right:0;bottom:0;left:0;background:#00000080;border:none;cursor:pointer;z-index:100}.modal.svelte-15uyvzd{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:#fff;padding:2rem;border-radius:8px;min-width:400px;z-index:101}.modal.svelte-15uyvzd h3:where(.svelte-15uyvzd){margin-top:0}.modal.svelte-15uyvzd form:where(.svelte-15uyvzd){display:flex;flex-direction:column;gap:1rem}.modal.svelte-15uyvzd label:where(.svelte-15uyvzd){display:flex;flex-direction:column;gap:.5rem}.modal.svelte-15uyvzd input:where(.svelte-15uyvzd){padding:.5rem;border:1px solid #ddd;border-radius:4px}.actions.svelte-15uyvzd{display:flex;gap:1rem;justify-content:flex-end}
|
||||
|
|
@ -0,0 +1 @@
|
|||
.header.svelte-oa5z6t{display:flex;justify-content:space-between;align-items:center;margin-bottom:1.5rem;gap:1rem}.back.svelte-oa5z6t{color:#3498db;text-decoration:none;font-size:.9rem}.back.svelte-oa5z6t:hover{text-decoration:underline}.header.svelte-oa5z6t h2:where(.svelte-oa5z6t){flex:1;margin:0}table.svelte-oa5z6t{width:100%;background:#fff;border-radius:8px;overflow:hidden;box-shadow:0 2px 4px #0000001a}th.svelte-oa5z6t,td.svelte-oa5z6t{padding:1rem;text-align:left;border-bottom:1px solid #eee}th.svelte-oa5z6t{background:#f8f9fa;font-weight:600}button.svelte-oa5z6t{padding:.5rem 1rem;border:none;border-radius:4px;cursor:pointer;background:#3498db;color:#fff}button.danger.svelte-oa5z6t{background:#e74c3c}.modal-backdrop.svelte-oa5z6t{position:fixed;top:0;right:0;bottom:0;left:0;background:#00000080;border:none;cursor:pointer;z-index:100}.modal.svelte-oa5z6t{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:#fff;padding:2rem;border-radius:8px;min-width:400px;z-index:101}.modal.svelte-oa5z6t h3:where(.svelte-oa5z6t){margin-top:0}.modal.svelte-oa5z6t form:where(.svelte-oa5z6t){display:flex;flex-direction:column;gap:1rem}.modal.svelte-oa5z6t label:where(.svelte-oa5z6t){display:flex;flex-direction:column;gap:.5rem}.modal.svelte-oa5z6t input:where(.svelte-oa5z6t){padding:.5rem;border:1px solid #ddd;border-radius:4px}.actions.svelte-oa5z6t{display:flex;gap:1rem;justify-content:flex-end}
|
||||
|
|
@ -0,0 +1 @@
|
|||
.header.svelte-1lsf4ps{display:flex;justify-content:space-between;align-items:center;margin-bottom:1.5rem;flex-wrap:wrap;gap:1rem}.controls.svelte-1lsf4ps{display:flex;gap:1rem;align-items:center}.controls.svelte-1lsf4ps label:where(.svelte-1lsf4ps){display:flex;align-items:center;gap:.5rem}.controls.svelte-1lsf4ps select:where(.svelte-1lsf4ps),.controls.svelte-1lsf4ps input:where(.svelte-1lsf4ps){padding:.5rem;border:1px solid #ddd;border-radius:4px}button.svelte-1lsf4ps{padding:.5rem 1rem;border:none;border-radius:4px;cursor:pointer;background:#3498db;color:#fff}.log-container.svelte-1lsf4ps{background:#1a1a2e;color:#eee;padding:1rem;border-radius:8px;max-height:600px;overflow-y:auto;font-family:monospace;font-size:.85rem}.log-entry.svelte-1lsf4ps{padding:.25rem 0;border-bottom:1px solid #333}.log-entry.svelte-1lsf4ps:last-child{border-bottom:none}.timestamp.svelte-1lsf4ps{color:#888;margin-right:.5rem}.priority.svelte-1lsf4ps{margin-right:.5rem;font-weight:700}.log-entry.error.svelte-1lsf4ps .priority:where(.svelte-1lsf4ps){color:#e74c3c}.log-entry.warning.svelte-1lsf4ps .priority:where(.svelte-1lsf4ps){color:#f39c12}.log-entry.info.svelte-1lsf4ps .priority:where(.svelte-1lsf4ps){color:#3498db}.error.svelte-1lsf4ps{color:#e74c3c}
|
||||
|
|
@ -0,0 +1 @@
|
|||
.header.svelte-qegr5c{display:flex;justify-content:space-between;align-items:center;margin-bottom:1.5rem}table.svelte-qegr5c{width:100%;background:#fff;border-radius:8px;overflow:hidden;box-shadow:0 2px 4px #0000001a}th.svelte-qegr5c,td.svelte-qegr5c{padding:.75rem;text-align:left;border-bottom:1px solid #eee;font-size:.9rem}th.svelte-qegr5c{background:#f8f9fa;font-weight:600}.id.svelte-qegr5c{font-family:monospace;font-size:.8rem}.reason.svelte-qegr5c{color:#e74c3c}button.svelte-qegr5c{padding:.25rem .5rem;border:none;border-radius:4px;cursor:pointer;background:#3498db;color:#fff;font-size:.8rem;margin-right:.25rem}button.danger.svelte-qegr5c{background:#e74c3c}.error.svelte-qegr5c{color:#e74c3c}
|
||||
|
|
@ -0,0 +1 @@
|
|||
.stats-grid.svelte-1uha8ag{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:1.5rem;margin-top:2rem}.stat-card.svelte-1uha8ag{background:#fff;padding:1.5rem;border-radius:8px;box-shadow:0 2px 4px #0000001a;text-align:center}.stat-card.warning.svelte-1uha8ag{border:2px solid #e74c3c}.stat-card.svelte-1uha8ag h3:where(.svelte-1uha8ag){font-size:2.5rem;margin:0;color:#2c3e50}.stat-card.svelte-1uha8ag p:where(.svelte-1uha8ag){margin:.5rem 0 0;color:#7f8c8d}.warning.svelte-1uha8ag h3:where(.svelte-1uha8ag){color:#e74c3c}
|
||||
|
|
@ -0,0 +1 @@
|
|||
.page-container.svelte-imhhoo{min-height:100vh;background:#f5f7fa}.header.svelte-imhhoo{background:#fff;padding:1rem 2rem;box-shadow:0 2px 4px #0000001a}.header-content.svelte-imhhoo{max-width:800px;margin:0 auto;display:flex;justify-content:space-between;align-items:center}.header.svelte-imhhoo h1:where(.svelte-imhhoo){margin:0;font-size:1.5rem;color:#2c3e50}.user-info.svelte-imhhoo{display:flex;align-items:center;gap:1rem}.logout.svelte-imhhoo{padding:.5rem 1rem;background:#e74c3c;color:#fff;border:none;border-radius:4px;cursor:pointer}.content.svelte-imhhoo{max-width:500px;margin:2rem auto;padding:2rem;background:#fff;border-radius:12px;box-shadow:0 2px 8px #0000001a}.error.svelte-imhhoo{background:#fee;color:#c00;padding:.75rem;border-radius:4px;margin-bottom:1rem}.success.svelte-imhhoo{background:#efe;color:#060;padding:.75rem;border-radius:4px;margin-bottom:1rem}form.svelte-imhhoo{display:flex;flex-direction:column;gap:1rem}label.svelte-imhhoo{display:flex;flex-direction:column;gap:.5rem;font-weight:500;color:#555}input.svelte-imhhoo{padding:.75rem;border:1px solid #ddd;border-radius:6px;font-size:1rem}input.svelte-imhhoo:focus{outline:none;border-color:#667eea;box-shadow:0 0 0 3px #667eea1a}button.svelte-imhhoo{padding:.75rem;background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;border:none;border-radius:6px;font-size:1rem;font-weight:500;cursor:pointer;margin-top:.5rem}button.svelte-imhhoo:hover:not(:disabled){opacity:.9}button.svelte-imhhoo:disabled{opacity:.6;cursor:not-allowed}.links.svelte-imhhoo{margin-top:1.5rem;text-align:center}.links.svelte-imhhoo a:where(.svelte-imhhoo){color:#667eea;text-decoration:none}.links.svelte-imhhoo a:where(.svelte-imhhoo):hover{text-decoration:underline}
|
||||
|
|
@ -0,0 +1 @@
|
|||
.page-container.svelte-1s1ktxm{min-height:100vh;background:#f5f7fa}.header.svelte-1s1ktxm{background:#fff;padding:1rem 2rem;box-shadow:0 2px 4px #0000001a;display:flex;justify-content:space-between;align-items:center}.header.svelte-1s1ktxm h1:where(.svelte-1s1ktxm){margin:0;font-size:1.5rem;color:#2c3e50}.user-info.svelte-1s1ktxm{display:flex;align-items:center;gap:1rem}.logout.svelte-1s1ktxm{padding:.5rem 1rem;background:#e74c3c;color:#fff;border:none;border-radius:4px;cursor:pointer}.content.svelte-1s1ktxm{max-width:800px;margin:2rem auto;padding:0 1rem}.card.svelte-1s1ktxm{background:#fff;padding:1.5rem;border-radius:12px;box-shadow:0 2px 8px #0000001a;margin-bottom:1.5rem}.card.svelte-1s1ktxm h2:where(.svelte-1s1ktxm){margin:0 0 1rem;color:#2c3e50;font-size:1.2rem}.info-row.svelte-1s1ktxm{display:flex;padding:.75rem 0;border-bottom:1px solid #eee}.info-row.svelte-1s1ktxm:last-child{border-bottom:none}.label.svelte-1s1ktxm{font-weight:500;color:#666;width:120px}.value.svelte-1s1ktxm{color:#2c3e50}.actions.svelte-1s1ktxm{display:flex;gap:1rem;flex-wrap:wrap}.action-btn.svelte-1s1ktxm{padding:.75rem 1.5rem;background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;text-decoration:none;border-radius:6px;font-weight:500;transition:opacity .2s}.action-btn.svelte-1s1ktxm:hover{opacity:.9}
|
||||
|
|
@ -0,0 +1 @@
|
|||
.login-container.svelte-15sczaz{min-height:100vh;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#667eea,#764ba2);padding:1rem}.login-card.svelte-15sczaz{background:#fff;padding:2rem;border-radius:12px;box-shadow:0 10px 40px #0003;width:100%;max-width:400px}h1.svelte-15sczaz{text-align:center;margin:0 0 1rem;color:#2c3e50}.info.svelte-15sczaz{color:#666;text-align:center;margin-bottom:1.5rem;font-size:.9rem}.error.svelte-15sczaz{background:#fee;color:#c00;padding:.75rem;border-radius:4px;margin-bottom:1rem;font-size:.9rem}.success.svelte-15sczaz{background:#efe;color:#060;padding:1rem;border-radius:4px;margin-bottom:1rem;text-align:center}.success.svelte-15sczaz p:where(.svelte-15sczaz){margin:.5rem 0}form.svelte-15sczaz{display:flex;flex-direction:column;gap:1rem}label.svelte-15sczaz{display:flex;flex-direction:column;gap:.5rem;font-weight:500;color:#555}input.svelte-15sczaz{padding:.75rem;border:1px solid #ddd;border-radius:6px;font-size:1rem}input.svelte-15sczaz:focus{outline:none;border-color:#667eea;box-shadow:0 0 0 3px #667eea1a}button.svelte-15sczaz{padding:.75rem;background:linear-gradient(135deg,#667eea,#764ba2);color:#fff;border:none;border-radius:6px;font-size:1rem;font-weight:500;cursor:pointer;transition:opacity .2s}button.svelte-15sczaz:hover:not(:disabled){opacity:.9}button.svelte-15sczaz:disabled{opacity:.6;cursor:not-allowed}.links.svelte-15sczaz{margin-top:1.5rem;text-align:center;padding-top:1rem;border-top:1px solid #eee}.links.svelte-15sczaz a:where(.svelte-15sczaz){color:#667eea;text-decoration:none;font-size:.9rem}.links.svelte-15sczaz a:where(.svelte-15sczaz):hover{text-decoration:underline}
|
||||
|
|
@ -0,0 +1 @@
|
|||
.login-container.svelte-1i2smtp{min-height:100vh;display:flex;align-items:center;justify-content:center;background:linear-gradient(135deg,#2c3e50,#34495e)}.login-card.svelte-1i2smtp{background:#fff;padding:3rem;border-radius:12px;box-shadow:0 10px 40px #0003;width:100%;max-width:400px}h1.svelte-1i2smtp{margin:0;text-align:center;color:#2c3e50;font-size:2rem}.subtitle.svelte-1i2smtp{text-align:center;color:#7f8c8d;margin:.5rem 0 2rem}.error.svelte-1i2smtp{background:#fee;border:1px solid #fcc;color:#c00;padding:.75rem;border-radius:4px;margin-bottom:1rem;font-size:.9rem}form.svelte-1i2smtp{display:flex;flex-direction:column;gap:1.25rem}label.svelte-1i2smtp{display:flex;flex-direction:column;gap:.5rem;font-weight:500;color:#2c3e50}input.svelte-1i2smtp{padding:.75rem;border:1px solid #ddd;border-radius:6px;font-size:1rem;transition:border-color .2s}input.svelte-1i2smtp:focus{outline:none;border-color:#3498db}.forgot.svelte-1i2smtp{text-align:right;margin-top:-.5rem}.forgot.svelte-1i2smtp a:where(.svelte-1i2smtp){color:#3498db;text-decoration:none;font-size:.9rem}.forgot.svelte-1i2smtp a:where(.svelte-1i2smtp):hover{text-decoration:underline}button.svelte-1i2smtp{padding:.875rem;background:#3498db;color:#fff;border:none;border-radius:6px;font-size:1rem;font-weight:600;cursor:pointer;transition:background .2s;margin-top:.5rem}button.svelte-1i2smtp:hover:not(:disabled){background:#2980b9}button.svelte-1i2smtp:disabled{opacity:.7;cursor:not-allowed}
|
||||
|
|
@ -0,0 +1 @@
|
|||
.header.svelte-83awcy{display:flex;justify-content:space-between;align-items:center;margin-bottom:1.5rem}table.svelte-83awcy{width:100%;background:#fff;border-radius:8px;overflow:hidden;box-shadow:0 2px 4px #0000001a}th.svelte-83awcy,td.svelte-83awcy{padding:1rem;text-align:left;border-bottom:1px solid #eee}th.svelte-83awcy{background:#f8f9fa;font-weight:600}button.svelte-83awcy{padding:.5rem 1rem;border:none;border-radius:4px;cursor:pointer;background:#3498db;color:#fff}button.danger.svelte-83awcy{background:#e74c3c}.modal-backdrop.svelte-83awcy{position:fixed;top:0;right:0;bottom:0;left:0;background:#00000080;display:flex;align-items:center;justify-content:center;z-index:100}.modal.svelte-83awcy{background:#fff;padding:2rem;border-radius:8px;min-width:400px}.modal.svelte-83awcy form:where(.svelte-83awcy){display:flex;flex-direction:column;gap:1rem}.modal.svelte-83awcy label:where(.svelte-83awcy){display:flex;flex-direction:column;gap:.5rem}.modal.svelte-83awcy input:where(.svelte-83awcy){padding:.5rem;border:1px solid #ddd;border-radius:4px}.actions.svelte-83awcy{display:flex;gap:1rem;justify-content:flex-end}
|
||||
|
|
@ -0,0 +1 @@
|
|||
.header.svelte-lb8x40{margin-bottom:2rem}.back.svelte-lb8x40{display:inline-block;margin-bottom:1rem;color:#3498db;text-decoration:none}.back.svelte-lb8x40:hover{text-decoration:underline}.error.svelte-lb8x40{color:#e74c3c}.cards-grid.svelte-lb8x40{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:1.5rem}.card.svelte-lb8x40{background:#fff;padding:2rem;border-radius:8px;box-shadow:0 2px 4px #0000001a;text-align:center;text-decoration:none;color:inherit;transition:transform .2s,box-shadow .2s}.card.svelte-lb8x40:hover{transform:translateY(-2px);box-shadow:0 4px 8px #00000026}.card.svelte-lb8x40 h3:where(.svelte-lb8x40){font-size:3rem;margin:0;color:#3498db}.card.svelte-lb8x40 p:where(.svelte-lb8x40){margin:.5rem 0 0;color:#7f8c8d;font-size:1.1rem}
|
||||
|
|
@ -0,0 +1 @@
|
|||
var C=Object.defineProperty;var w=a=>{throw TypeError(a)};var D=(a,e,s)=>e in a?C(a,e,{enumerable:!0,configurable:!0,writable:!0,value:s}):a[e]=s;var A=(a,e,s)=>D(a,typeof e!="symbol"?e+"":e,s),x=(a,e,s)=>e.has(a)||w("Cannot "+s);var t=(a,e,s)=>(x(a,e,"read from private field"),s?s.call(a):e.get(a)),u=(a,e,s)=>e.has(a)?w("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(a):e.set(a,s),E=(a,e,s,i)=>(x(a,e,"write to private field"),i?i.call(a,s):e.set(a,s),s);import{U as N,_ as g,V as R,o as F,E as M,c as S,h as k,A as B,Z as j,G as z,w as G,v as I,a9 as P,x as U,y as V,j as Z,z as T}from"./DtGl34IE.js";var d,l,c,p,v,m,b;class q{constructor(e,s=!0){A(this,"anchor");u(this,d,new Map);u(this,l,new Map);u(this,c,new Map);u(this,p,new Set);u(this,v,!0);u(this,m,e=>{if(t(this,d).has(e)){var s=t(this,d).get(e),i=t(this,l).get(s);if(i)N(i),t(this,p).delete(s);else{var n=t(this,c).get(s);n&&(t(this,l).set(s,n.effect),t(this,c).delete(s),n.fragment.lastChild.remove(),this.anchor.before(n.fragment),i=n.effect)}for(const[f,h]of t(this,d)){if(t(this,d).delete(f),f===e)break;const r=t(this,c).get(h);r&&(g(r.effect),t(this,c).delete(h))}for(const[f,h]of t(this,l)){if(f===s||t(this,p).has(f))continue;const r=()=>{if(Array.from(t(this,d).values()).includes(f)){var _=document.createDocumentFragment();j(h,_),_.append(F()),t(this,c).set(f,{effect:h,fragment:_})}else g(h);t(this,p).delete(f),t(this,l).delete(f)};t(this,v)||!i?(t(this,p).add(f),R(h,r,!1)):r()}}});u(this,b,e=>{t(this,d).delete(e);const s=Array.from(t(this,d).values());for(const[i,n]of t(this,c))s.includes(i)||(g(n.effect),t(this,c).delete(i))});this.anchor=e,E(this,v,s)}ensure(e,s){var i=S,n=z();if(s&&!t(this,l).has(e)&&!t(this,c).has(e))if(n){var f=document.createDocumentFragment(),h=F();f.append(h),t(this,c).set(e,{effect:M(()=>s(h)),fragment:f})}else t(this,l).set(e,M(()=>s(this.anchor)));if(t(this,d).set(i,e),n){for(const[r,o]of t(this,l))r===e?i.unskip_effect(o):i.skip_effect(o);for(const[r,o]of t(this,c))r===e?i.unskip_effect(o.effect):i.skip_effect(o.effect);i.oncommit(t(this,m)),i.ondiscard(t(this,b))}else k&&(this.anchor=B),t(this,m).call(this,i)}}d=new WeakMap,l=new WeakMap,c=new WeakMap,p=new WeakMap,v=new WeakMap,m=new WeakMap,b=new WeakMap;function K(a,e,s=!1){var i;k&&(i=B,I());var n=new q(a),f=s?P:0;function h(r,o){if(k){var _=U(i);if(r!==parseInt(_.substring(1))){var y=V();Z(y),n.anchor=y,T(!1),n.ensure(r,o),T(!0);return}}n.ensure(r,o)}G(()=>{var r=!1;e((o,_=0)=>{r=!0,h(_,o)}),r||h(-1,null)},f)}export{q as B,K as i};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{s as c,g as l}from"./BX-b5t2l.js";import{n as a,m as o,g as b,t as d,d as p,s as g}from"./DtGl34IE.js";let s=!1,i=Symbol();function m(e,u,r){const n=r[u]??(r[u]={store:null,source:o(void 0),unsubscribe:a});if(n.store!==e&&!(i in r))if(n.unsubscribe(),n.store=e??null,e==null)n.source.v=void 0,n.unsubscribe=a;else{var t=!0;n.unsubscribe=c(e,f=>{t?n.source.v=f:g(n.source,f)}),t=!1}return e&&i in r?l(e):b(n.source)}function y(){const e={};function u(){d(()=>{for(var r in e)e[r].unsubscribe();p(e,i,{enumerable:!1,value:!0})})}return[e,u]}function N(e){var u=s;try{return s=!1,[e(),s]}finally{s=u}}export{y as a,N as c,m as s};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{s as e}from"./RKc8iKz_.js";const r=()=>{const s=e;return{page:{subscribe:s.page.subscribe},navigating:{subscribe:s.navigating.subscribe},updated:s.updated}},b={subscribe(s){return r().page.subscribe(s)}};export{b as p};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{n as o,u as a,ah as d}from"./DtGl34IE.js";function p(s,u,e){if(s==null)return u(void 0),o;const t=a(()=>s.subscribe(u,e));return t.unsubscribe?()=>t.unsubscribe():t}const i=[];function h(s,u=o){let e=null;const t=new Set;function c(r){if(d(s,r)&&(s=r,e)){const b=!i.length;for(const n of t)n[1](),i.push(n,s);if(b){for(let n=0;n<i.length;n+=2)i[n][0](i[n+1]);i.length=0}}}function f(r){c(r(s))}function l(r,b=o){const n=[r,b];return t.add(n),t.size===1&&(e=u(c,f)||o),r(s),()=>{t.delete(n),t.size===0&&e&&(e(),e=null)}}return{set:c,update:f,subscribe:l}}function _(s){let u;return p(s,e=>u=e)(),u}export{_ as g,p as s,h as w};
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
import{h as c}from"./DtGl34IE.js";const a=[...`
|
||||
\r\f \v\uFEFF`];function e(r,g,u){var f=r==null?"":""+r;if(g&&(f=f?f+" "+g:g),u){for(var t of Object.keys(u))if(u[t])f=f?f+" "+t:t;else if(f.length)for(var l=t.length,i=0;(i=f.indexOf(t,i))>=0;){var n=i+l;(i===0||a.includes(f[i-1]))&&(n===f.length||a.includes(f[n]))?f=(i===0?"":f.substring(0,i))+f.substring(n+1):i=n}}return f===""?null:f}function v(r,g,u,f,t,l){var i=r.__className;if(c||i!==u||i===void 0){var n=e(u,f,l);(!c||n!==r.getAttribute("class"))&&(n==null?r.removeAttribute("class"):r.className=n),r.__className=u}else if(l&&t!==l)for(var o in l){var s=!!l[o];(t==null||s!==!!t[o])&&r.classList.toggle(o,s)}return l}export{v as s};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{am as p,o as u,an as c,k as l,ao as E,ap as g,aq as w,h as d,A as s,ar as y,v as N,as as A,j as M,at as x}from"./DtGl34IE.js";var f;const i=((f=globalThis==null?void 0:globalThis.window)==null?void 0:f.trustedTypes)&&globalThis.window.trustedTypes.createPolicy("svelte-trusted-html",{createHTML:t=>t});function L(t){return(i==null?void 0:i.createHTML(t))??t}function b(t){var r=p("template");return r.innerHTML=L(t.replaceAll("<!>","<!---->")),r.content}function n(t,r){var e=c;e.nodes===null&&(e.nodes={start:t,end:r,a:null,t:null})}function R(t,r){var e=(r&g)!==0,m=(r&w)!==0,a,v=!t.startsWith("<!>");return()=>{if(d)return n(s,null),s;a===void 0&&(a=b(v?t:"<!>"+t),e||(a=l(a)));var o=m||E?document.importNode(a,!0):a.cloneNode(!0);if(e){var T=l(o),h=o.lastChild;n(T,h)}else n(o,o);return o}}function C(t=""){if(!d){var r=u(t+"");return n(r,r),r}var e=s;return e.nodeType!==A?(e.before(e=u()),M(e)):x(e),n(e,e),e}function I(){if(d)return n(s,null),s;var t=document.createDocumentFragment(),r=document.createComment(""),e=u();return t.append(r,e),n(r,e),t}function D(t,r){if(d){var e=c;(!(e.f&y)||e.nodes.end===null)&&(e.nodes.end=s),N();return}t!==null&&t.before(r)}const O="5";var _;typeof window<"u"&&((_=window.__svelte??(window.__svelte={})).v??(_.v=new Set)).add(O);export{D as a,n as b,I as c,R as f,C as t};
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
import{aa as g,ab as d,a8 as l,u as b,ac as i,ad as m,g as p,ae as v,af as h,ag as k}from"./DtGl34IE.js";function x(t=!1){const a=g,e=a.l.u;if(!e)return;let o=()=>v(a.s);if(t){let n=0,s={};const _=h(()=>{let c=!1;const r=a.s;for(const f in r)r[f]!==s[f]&&(s[f]=r[f],c=!0);return c&&n++,n});o=()=>p(_)}e.b.length&&d(()=>{u(a,o),i(e.b)}),l(()=>{const n=b(()=>e.m.map(m));return()=>{for(const s of n)typeof s=="function"&&s()}}),e.a.length&&l(()=>{u(a,o),i(e.a)})}function u(t,a){if(t.l.s)for(const e of t.l.s)p(e);a()}k();export{x as i};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{a8 as o,aa as t,ai as a,u as c}from"./DtGl34IE.js";function u(n){throw new Error("https://svelte.dev/e/lifecycle_outside_component")}function r(n){t===null&&u(),a&&t.l!==null?l(t).m.push(n):o(()=>{const e=c(n);if(typeof e=="function")return e})}function l(n){var e=n.l;return e.u??(e.u={a:[],b:[],m:[]})}export{r as o};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
import{l as b,c as v,b as m,u as _,r as i,h as y}from"./DtGl34IE.js";function k(e,l,u=l){var s=new WeakSet;b(e,"input",async r=>{var a=r?e.defaultValue:e.value;if(a=t(e)?o(a):a,u(a),v!==null&&s.add(v),await m(),a!==(a=l())){var d=e.selectionStart,c=e.selectionEnd,n=e.value.length;if(e.value=a??"",c!==null){var f=e.value.length;d===c&&c===n&&f>n?(e.selectionStart=f,e.selectionEnd=f):(e.selectionStart=d,e.selectionEnd=Math.min(c,f))}}}),(y&&e.defaultValue!==e.value||_(l)==null&&e.value)&&(u(t(e)?o(e.value):e.value),v!==null&&s.add(v)),i(()=>{var r=l();if(e===document.activeElement){var a=v;if(s.has(a))return}t(e)&&r===o(e.value)||e.type==="date"&&!r&&!e.value||r!==e.value&&(e.value=r??"")})}function t(e){var l=e.type;return l==="number"||l==="range"}function o(e){return e===""?null:+e}export{k as b};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{h as i,q as d,e as n,L as v,N as h,I as u,f as g,i as A}from"./DtGl34IE.js";const L=Symbol("is custom element"),N=Symbol("is html"),l=u?"link":"LINK";function S(r){if(i){var s=!1,e=()=>{if(!s){if(s=!0,r.hasAttribute("value")){var a=r.value;t(r,"value",null),r.value=a}if(r.hasAttribute("checked")){var o=r.checked;t(r,"checked",null),r.checked=o}}};r.__on_r=e,d(e),n()}}function t(r,s,e,a){var o=p(r);i&&(o[s]=r.getAttribute(s),s==="src"||s==="srcset"||s==="href"&&r.nodeName===l)||o[s]!==(o[s]=e)&&(s==="loading"&&(r[v]=e),e==null?r.removeAttribute(s):typeof e!="string"&&I(r).includes(s)?r[s]=e:r.setAttribute(s,e))}function p(r){return r.__attributes??(r.__attributes={[L]:r.nodeName.includes("-"),[N]:r.namespaceURI===h})}var c=new Map;function I(r){var s=r.getAttribute("is")||r.nodeName,e=c.get(s);if(e)return e;c.set(s,e=[]);for(var a,o=r,f=Element.prototype;f!==o;){a=A(o);for(var _ in a)a[_].set&&e.push(_);o=g(o)}return e}export{S as r,t as s};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{h as N,j as z,k as W,o as H,v as Z,w as $,g as L,x as j,H as ee,y as U,z as O,A as b,C as ne,B as re,D as X,c as fe,E as Y,F as ae,G as ie,J as le,K as ue,M as V,O as se,P as q,Q as oe,R as ve,m as te,S as T,T as de,U as J,V as K,W as y,X as ce,Y as pe,Z as ge,_ as he,$ as _e}from"./DtGl34IE.js";function Ce(e,a){return a}function Ee(e,a,l){for(var o=[],c=a.length,u,i=a.length,r=0;r<c;r++){let v=a[r];K(v,()=>{if(u){if(u.pending.delete(v),u.done.add(v),u.pending.size===0){var w=e.outrogroups;B(e,V(u.done)),w.delete(u),w.size===0&&(e.outrogroups=null)}}else i-=1},!1)}if(i===0){var t=o.length===0&&l!==null;if(t){var f=l,d=f.parentNode;pe(d),d.append(f),e.items.clear()}B(e,a,!t)}else u={pending:new Set(a),done:new Set},(e.outrogroups??(e.outrogroups=new Set)).add(u)}function B(e,a,l=!0){var o;if(e.pending.size>0){o=new Set;for(const i of e.pending.values())for(const r of i)o.add(e.items.get(r).e)}for(var c=0;c<a.length;c++){var u=a[c];if(o!=null&&o.has(u)){u.f|=T;const i=document.createDocumentFragment();ge(u,i)}else he(a[c],l)}}var G;function Ae(e,a,l,o,c,u=null){var i=e,r=new Map;{var t=e;i=N?z(W(t)):t.appendChild(H())}N&&Z();var f=null,d=le(()=>{var s=l();return ue(s)?s:s==null?[]:V(s)}),v,w=new Map,h=!0;function n(s){p.effect.f&de||(p.pending.delete(s),p.fallback=f,me(p,v,i,a,o),f!==null&&(v.length===0?f.f&T?(f.f^=T,D(f,null,i)):J(f):K(f,()=>{f=null})))}function C(s){p.pending.delete(s)}var I=$(()=>{v=L(d);var s=v.length;let k=!1;if(N){var R=j(i)===ee;R!==(s===0)&&(i=U(),z(i),O(!1),k=!0)}for(var _=new Set,A=fe,M=ie(),E=0;E<s;E+=1){N&&b.nodeType===ne&&b.data===re&&(i=b,k=!0,O(!1));var g=v[E],F=o(g,E),m=h?null:r.get(F);m?(m.v&&X(m.v,g),m.i&&X(m.i,E),M&&A.unskip_effect(m.e)):(m=Te(r,h?i:G??(G=H()),g,F,E,c,a,l),h||(m.e.f|=T),r.set(F,m)),_.add(F)}if(s===0&&u&&!f&&(h?f=Y(()=>u(i)):(f=Y(()=>u(G??(G=H()))),f.f|=T)),s>_.size&&ae(),N&&s>0&&z(U()),!h)if(w.set(A,_),M){for(const[P,Q]of r)_.has(P)||A.skip_effect(Q.e);A.oncommit(n),A.ondiscard(C)}else n(A);k&&O(!0),L(d)}),p={effect:I,items:r,pending:w,outrogroups:null,fallback:f};h=!1,N&&(i=b)}function x(e){for(;e!==null&&!(e.f&ce);)e=e.next;return e}function me(e,a,l,o,c){var E;var u=a.length,i=e.items,r=x(e.effect.first),t,f=null,d=[],v=[],w,h,n,C;for(C=0;C<u;C+=1){if(w=a[C],h=c(w,C),n=i.get(h).e,e.outrogroups!==null)for(const g of e.outrogroups)g.pending.delete(n),g.done.delete(n);if(n.f&y&&J(n),n.f&T)if(n.f^=T,n===r)D(n,null,l);else{var I=f?f.next:r;n===e.effect.last&&(e.effect.last=n.prev),n.prev&&(n.prev.next=n.next),n.next&&(n.next.prev=n.prev),S(e,f,n),S(e,n,I),D(n,I,l),f=n,d=[],v=[],r=x(f.next);continue}if(n!==r){if(t!==void 0&&t.has(n)){if(d.length<v.length){var p=v[0],s;f=p.prev;var k=d[0],R=d[d.length-1];for(s=0;s<d.length;s+=1)D(d[s],p,l);for(s=0;s<v.length;s+=1)t.delete(v[s]);S(e,k.prev,R.next),S(e,f,k),S(e,R,p),r=p,f=R,C-=1,d=[],v=[]}else t.delete(n),D(n,r,l),S(e,n.prev,n.next),S(e,n,f===null?e.effect.first:f.next),S(e,f,n),f=n;continue}for(d=[],v=[];r!==null&&r!==n;)(t??(t=new Set)).add(r),v.push(r),r=x(r.next);if(r===null)continue}n.f&T||d.push(n),f=n,r=x(n.next)}if(e.outrogroups!==null){for(const g of e.outrogroups)g.pending.size===0&&(B(e,V(g.done)),(E=e.outrogroups)==null||E.delete(g));e.outrogroups.size===0&&(e.outrogroups=null)}if(r!==null||t!==void 0){var _=[];if(t!==void 0)for(n of t)n.f&y||_.push(n);for(;r!==null;)!(r.f&y)&&r!==e.fallback&&_.push(r),r=x(r.next);var A=_.length;if(A>0){var M=u===0?l:null;Ee(e,_,M)}}}function Te(e,a,l,o,c,u,i,r){var t=i&oe?i&ve?q(l):te(l,!1,!1):null,f=i&se?q(c):null;return{v:t,i:f,e:Y(()=>(u(a,t??l,f??c,r),()=>{e.delete(o)}))}}function D(e,a,l){if(e.nodes)for(var o=e.nodes.start,c=e.nodes.end,u=a&&!(a.f&T)?a.nodes.start:l;o!==null;){var i=_e(o);if(u.before(o),o===c)return;o=i}}function S(e,a,l){a===null?e.effect.first=l:a.next=l,l===null?e.effect.last=a:l.prev=a}export{Ae as e,Ce as i};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
import{a as r}from"../chunks/RKc8iKz_.js";import{w as t}from"../chunks/CfgL3m1f.js";export{t as load_css,r as start};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{c as A,a as h,f as q}from"../chunks/CJv7FbHz.js";import{w as j,a9 as z,p as Q,a8 as U,s as v,g as r,a3 as x,a as V,a0 as B,a4 as n,a2 as _,a5 as i,a7 as W}from"../chunks/DtGl34IE.js";import{s as G,a as H}from"../chunks/B3ZK_jYJ.js";import{d as J,a as K,s as O}from"../chunks/n1byU5WW.js";import{B as X,i as C}from"../chunks/-EcHSJia.js";import{p as Y}from"../chunks/B7kAneMc.js";import{g as f}from"../chunks/RKc8iKz_.js";function E(u,o,...d){var m=new X(u);j(()=>{const l=o()??null;m.ensure(l,l&&(s=>l(s,...d)))},z)}var Z=q('<div class="loading svelte-12qhfyh">Loading...</div>'),$=q('<span class="svelte-12qhfyh"> </span>'),aa=q('<div class="app svelte-12qhfyh"><header class="svelte-12qhfyh"><h1 class="svelte-12qhfyh">IMC Vibe</h1> <nav class="svelte-12qhfyh"><a href="/" class="svelte-12qhfyh">Dashboard</a> <a href="/domains" class="svelte-12qhfyh">Domains</a> <a href="/users" class="svelte-12qhfyh">Users</a> <a href="/aliases" class="svelte-12qhfyh">Aliases</a> <a href="/queue" class="svelte-12qhfyh">Queue</a> <a href="/logs" class="svelte-12qhfyh">Logs</a></nav> <div class="user-menu svelte-12qhfyh"><!> <a href="/auth/change-password" class="svelte-12qhfyh">Change Password</a> <button class="svelte-12qhfyh">Logout</button></div></header> <main class="svelte-12qhfyh"><!></main></div>');function na(u,o){Q(o,!0);const d=()=>G(Y,"$page",m),[m,l]=H();let s=B(null),p=B(!0);U(()=>{d().url.pathname.startsWith("/auth/")?v(p,!1):I()});async function I(){var a;const e=localStorage.getItem("token");if(!e){f("/auth/login");return}try{const t=await fetch("/api/auth/me",{headers:{Authorization:`Bearer ${e}`}});if(!t.ok){f("/auth/login");return}const c=await t.json();if(v(s,c.data,!0),((a=r(s))==null?void 0:a.role)!=="admin"){f("/auth/dashboard");return}}catch{f("/auth/login")}finally{v(p,!1)}}function L(){localStorage.removeItem("token"),v(s,null),f("/auth/login")}var b=A(),S=x(b);{var T=e=>{var a=Z();h(e,a)},D=e=>{var a=aa(),t=n(a),c=_(n(t),4),k=n(c);{var M=g=>{var y=$(),R=n(y,!0);i(y),W(()=>O(R,r(s).username||r(s).email)),h(g,y)};C(k,g=>{r(s)&&g(M)})}var N=_(k,4);i(c),i(t);var w=_(t,2),P=n(w);E(P,()=>o.children),i(w),i(a),K("click",N,L),h(e,a)},F=e=>{var a=A(),t=x(a);E(t,()=>o.children),h(e,a)};C(S,e=>{var a;r(p)?e(T):((a=r(s))==null?void 0:a.role)==="admin"?e(D,1):e(F,-1)})}h(u,b),V(),l()}J(["click"]);export{na as component};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{a as h,f as g}from"../chunks/CJv7FbHz.js";import{i as l}from"../chunks/DFRBE_Uq.js";import{p as v,a3 as d,a7 as _,a as x,a4 as e,a5 as o,a2 as $}from"../chunks/DtGl34IE.js";import{s as p}from"../chunks/n1byU5WW.js";import{p as m}from"../chunks/CfgL3m1f.js";import{s as k}from"../chunks/RKc8iKz_.js";const b={get error(){return m.error},get status(){return m.status}};k.updated.check;const i=b;var E=g("<h1> </h1> <p> </p>",1);function B(f,n){v(n,!1),l();var t=E(),r=d(t),c=e(r,!0);o(r);var a=$(r,2),u=e(a,!0);o(a),_(()=>{var s;p(c,i.status),p(u,(s=i.error)==null?void 0:s.message)}),h(f,t),x()}export{B as component};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{a as m,f as y}from"../chunks/CJv7FbHz.js";import{p as Z,a0 as _,a1 as I,a8 as aa,s as i,a3 as N,a7 as D,a as ea,a2 as o,a4 as r,a5 as s,g as t,a6 as ta}from"../chunks/DtGl34IE.js";import{s as sa,a as oa}from"../chunks/B3ZK_jYJ.js";import{d as ra,a as w,s as F,e as la}from"../chunks/n1byU5WW.js";import{i as R}from"../chunks/-EcHSJia.js";import{e as ia,i as da}from"../chunks/ecAlPOj8.js";import{s as T,r as U}from"../chunks/e1WBZ7UL.js";import{b as q}from"../chunks/ayyTDfEa.js";import{p as na}from"../chunks/B7kAneMc.js";var va=y("<p>Loading...</p>"),ca=y("<p>No aliases configured yet.</p>"),ua=y('<tr><td class="svelte-15uyvzd"> </td><td class="svelte-15uyvzd"> </td><td class="svelte-15uyvzd"><button class="danger svelte-15uyvzd">Delete</button></td></tr>'),pa=y('<table class="svelte-15uyvzd"><thead><tr><th class="svelte-15uyvzd">Source</th><th class="svelte-15uyvzd">Destination</th><th class="svelte-15uyvzd">Actions</th></tr></thead><tbody></tbody></table>'),fa=y('<button class="modal-backdrop svelte-15uyvzd" aria-label="Close modal"></button> <div class="modal svelte-15uyvzd"><h3 class="svelte-15uyvzd">Add Alias</h3> <form class="svelte-15uyvzd"><label class="svelte-15uyvzd">Source (alias address): <input type="text" required="" class="svelte-15uyvzd"/></label> <label class="svelte-15uyvzd">Destination (forward to): <input type="text" placeholder="user@example.org" required="" class="svelte-15uyvzd"/></label> <div class="actions svelte-15uyvzd"><button type="button" class="svelte-15uyvzd">Cancel</button> <button type="submit" class="svelte-15uyvzd">Create</button></div></form></div>',1),ma=y('<div class="header svelte-15uyvzd"><a class="back svelte-15uyvzd"> </a> <h2 class="svelte-15uyvzd">Aliases</h2> <button class="svelte-15uyvzd">Add Alias</button></div> <!> <!>',1);function Aa(L,O){Z(O,!0);const B=()=>sa(na,"$page",J),[J,M]=oa();let A=_(I([])),S=_(!0),b=_(!1),p=_(I({source:"",destination:""})),l=_("");async function $(a){try{const e=await fetch(`/api/domains/${encodeURIComponent(a)}/aliases`);if(e.ok){const d=await e.json();i(A,d.data||[],!0)}}catch(e){console.error("Failed to load aliases:",e)}finally{i(S,!1)}}async function P(){try{const a=await fetch(`/api/domains/${encodeURIComponent(t(l))}/aliases`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(t(p))});if(a.ok)i(b,!1),i(p,{source:"",destination:""},!0),await $(t(l));else{const e=await a.json();alert(e.error||"Failed to create alias")}}catch(a){console.error("Failed to create alias:",a)}}async function G(a){if(confirm("Delete this alias?"))try{await fetch(`/api/domains/${encodeURIComponent(t(l))}/aliases/${a}`,{method:"DELETE"}),await $(t(l))}catch(e){console.error("Failed to delete alias:",e)}}aa(()=>{const a=B().params.name;a&&(i(l,a,!0),$(a))});var j=ma(),C=N(j),z=r(C),H=r(z);s(z);var K=o(z,4);s(C);var E=o(C,2);{var Q=a=>{var e=va();m(a,e)},V=a=>{var e=ca();m(a,e)},W=a=>{var e=pa(),d=o(r(e));ia(d,21,()=>t(A),da,(g,n)=>{var v=ua(),c=r(v),h=r(c,!0);s(c);var f=o(c),k=r(f,!0);s(f);var x=o(f),u=r(x);s(x),s(v),D(()=>{F(h,t(n).source),F(k,t(n).destination)}),w("click",u,()=>G(t(n).id)),m(g,v)}),s(d),s(e),m(a,e)};R(E,a=>{t(S)?a(Q):t(A).length===0?a(V,1):a(W,-1)})}var X=o(E,2);{var Y=a=>{var e=fa(),d=N(e),g=o(d,2),n=o(r(g),2),v=r(n),c=o(r(v));U(c),s(v);var h=o(v,2),f=o(r(h));U(f),s(h);var k=o(h,2),x=r(k);ta(2),s(k),s(n),s(g),D(()=>T(c,"placeholder",`info@${t(l)??""}`)),w("click",d,()=>i(b,!1)),la("submit",n,u=>{u.preventDefault(),P()}),q(c,()=>t(p).source,u=>t(p).source=u),q(f,()=>t(p).destination,u=>t(p).destination=u),w("click",x,()=>i(b,!1)),m(a,e)};R(X,a=>{t(b)&&a(Y)})}D(()=>{T(z,"href",`/domains/${t(l)??""}`),F(H,`← Back to ${t(l)??""}`)}),w("click",K,()=>i(b,!0)),m(L,j),ea(),M()}ra(["click"]);export{Aa as component};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{a as b,f as _}from"../chunks/CJv7FbHz.js";import{p as ot,a0 as w,a1 as M,a8 as rt,s as c,a3 as O,a7 as D,a as lt,a2 as s,a4 as r,a5 as o,g as a,a6 as it}from"../chunks/DtGl34IE.js";import{s as nt,a as dt}from"../chunks/B3ZK_jYJ.js";import{d as ct,a as $,s as q,e as vt}from"../chunks/n1byU5WW.js";import{i as Q}from"../chunks/-EcHSJia.js";import{e as ut,i as pt}from"../chunks/ecAlPOj8.js";import{s as R,r as E}from"../chunks/e1WBZ7UL.js";import{b as N}from"../chunks/ayyTDfEa.js";import{p as mt}from"../chunks/B7kAneMc.js";var ft=_("<p>Loading...</p>"),bt=_("<p>No users configured yet.</p>"),_t=_('<tr><td class="svelte-oa5z6t"> </td><td class="svelte-oa5z6t"> </td><td class="svelte-oa5z6t"> </td><td class="svelte-oa5z6t"><button class="danger svelte-oa5z6t">Delete</button></td></tr>'),ht=_('<table class="svelte-oa5z6t"><thead><tr><th class="svelte-oa5z6t">Email</th><th class="svelte-oa5z6t">Quota</th><th class="svelte-oa5z6t">Used</th><th class="svelte-oa5z6t">Actions</th></tr></thead><tbody></tbody></table>'),zt=_('<button class="modal-backdrop svelte-oa5z6t" aria-label="Close modal"></button> <div class="modal svelte-oa5z6t"><h3 class="svelte-oa5z6t">Add User</h3> <form class="svelte-oa5z6t"><label class="svelte-oa5z6t">Email: <input type="email" required="" class="svelte-oa5z6t"/></label> <label class="svelte-oa5z6t">Password: <input type="password" required="" class="svelte-oa5z6t"/></label> <label class="svelte-oa5z6t">Quota (bytes, 0 = default): <input type="number" min="0" class="svelte-oa5z6t"/></label> <div class="actions svelte-oa5z6t"><button type="button" class="svelte-oa5z6t">Cancel</button> <button type="submit" class="svelte-oa5z6t">Create</button></div></form></div>',1),gt=_('<div class="header svelte-oa5z6t"><a class="back svelte-oa5z6t"> </a> <h2 class="svelte-oa5z6t">Users</h2> <button class="svelte-oa5z6t">Add User</button></div> <!> <!>',1);function Bt(L,P){ot(P,!0);const S=()=>nt(mt,"$page",G),[G,J]=dt();let C=w(M([])),T=w(!0),h=w(!1),i=w(M({email:"",password:"",quota:0})),n=w("");async function F(t){try{const e=await fetch(`/api/domains/${encodeURIComponent(t)}/users`);if(e.ok){const v=await e.json();c(C,v.data||[],!0)}}catch(e){console.error("Failed to load users:",e)}finally{c(T,!1)}}async function K(){try{const t=await fetch(`/api/domains/${encodeURIComponent(a(n))}/users`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(a(i))});if(t.ok)c(h,!1),c(i,{email:"",password:"",quota:0},!0),await F(a(n));else{const e=await t.json();alert(e.error||"Failed to create user")}}catch(t){console.error("Failed to create user:",t)}}async function H(t){if(confirm("Delete this user? Mailbox will NOT be deleted."))try{await fetch(`/api/domains/${encodeURIComponent(a(n))}/users/${t}`,{method:"DELETE"}),await F(a(n))}catch(e){console.error("Failed to delete user:",e)}}function j(t){return t===0?"Default":t<1024?t+" B":t<1024*1024?(t/1024).toFixed(1)+" KB":t<1024*1024*1024?(t/1024/1024).toFixed(1)+" MB":(t/1024/1024/1024).toFixed(2)+" GB"}rt(()=>{const t=S().params.name;t&&(c(n,t,!0),F(t))});var A=gt(),B=O(A),y=r(B),V=r(y);o(y);var W=s(y,4);o(B);var I=s(B,2);{var X=t=>{var e=ft();b(t,e)},Y=t=>{var e=bt();b(t,e)},Z=t=>{var e=ht(),v=s(r(e));ut(v,21,()=>a(C),pt,(x,d)=>{var u=_t(),p=r(u),z=r(p,!0);o(p);var m=s(p),g=r(m,!0);o(m);var f=s(m),k=r(f,!0);o(f);var U=s(f),l=r(U);o(U),o(u),D((et,st)=>{q(z,a(d).email),q(g,et),q(k,st)},[()=>j(a(d).quota),()=>j(a(d).used_quota)]),$("click",l,()=>H(a(d).id)),b(x,u)}),o(v),o(e),b(t,e)};Q(I,t=>{a(T)?t(X):a(C).length===0?t(Y,1):t(Z,-1)})}var tt=s(I,2);{var at=t=>{var e=zt(),v=O(e),x=s(v,2),d=s(r(x),2),u=r(d),p=s(r(u));E(p),o(u);var z=s(u,2),m=s(r(z));E(m),o(z);var g=s(z,2),f=s(r(g));E(f),o(g);var k=s(g,2),U=r(k);it(2),o(k),o(d),o(x),D(()=>R(p,"placeholder",`user@${a(n)??""}`)),$("click",v,()=>c(h,!1)),vt("submit",d,l=>{l.preventDefault(),K()}),N(p,()=>a(i).email,l=>a(i).email=l),N(m,()=>a(i).password,l=>a(i).password=l),N(f,()=>a(i).quota,l=>a(i).quota=l),$("click",U,()=>c(h,!1)),b(t,e)};Q(tt,t=>{a(h)&&t(at)})}D(()=>{R(y,"href",`/domains/${a(n)??""}`),q(V,`← Back to ${a(n)??""}`)}),$("click",W,()=>c(h,!0)),b(L,A),lt(),J()}ct(["click"]);export{Bt as component};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{a as d,f as m}from"../chunks/CJv7FbHz.js";import{l as Z,c as A,aj as $,t as ee,K as ae,ak as re,al as te,p as se,a0 as g,a1 as oe,a8 as le,a3 as ie,g as s,a as ne,s as c,a2 as v,a4 as f,a5 as u,a7 as U}from"../chunks/DtGl34IE.js";import{d as ve,a as H,s as x}from"../chunks/n1byU5WW.js";import{i as ue}from"../chunks/-EcHSJia.js";import{e as fe,i as pe}from"../chunks/ecAlPOj8.js";import{r as _e}from"../chunks/e1WBZ7UL.js";import{s as ce}from"../chunks/BYrNiJ2z.js";import{b as de}from"../chunks/ayyTDfEa.js";function W(e,t,p=!1){if(e.multiple){if(t==null)return;if(!ae(t))return re();for(var o of e.options)o.selected=t.includes(h(o));return}for(o of e.options){var i=h(o);if(te(i,t)){o.selected=!0;return}}(!p||t!==void 0)&&(e.selectedIndex=-1)}function me(e){var t=new MutationObserver(()=>{W(e,e.__value)});t.observe(e,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["value"]}),ee(()=>{t.disconnect()})}function ge(e,t,p=t){var o=new WeakSet,i=!0;Z(e,"change",n=>{var _=n?"[selected]":":checked",l;if(e.multiple)l=[].map.call(e.querySelectorAll(_),h);else{var b=e.querySelector(_)??e.querySelector("option:not([disabled])");l=b&&h(b)}p(l),e.__value=l,A!==null&&o.add(A)}),$(()=>{var n=t();if(e===document.activeElement){var _=A;if(o.has(_))return}if(W(e,n,i),i&&n===void 0){var l=e.querySelector(":checked");l!==null&&(n=h(l),p(n))}e.__value=n,i=!1}),me(e)}function h(e){return"__value"in e?e.__value:e.value}var he=m("<p>Loading...</p>"),be=m('<p class="error svelte-1lsf4ps"> </p>'),ye=m("<p>No log entries found.</p>"),ke=m('<div><span class="timestamp svelte-1lsf4ps"> </span> <span class="priority svelte-1lsf4ps"> </span> <span class="message"> </span></div>'),we=m('<div class="log-container svelte-1lsf4ps"></div>'),xe=m('<div class="header svelte-1lsf4ps"><h2>Mail Logs</h2> <div class="controls svelte-1lsf4ps"><label class="svelte-1lsf4ps">Hours: <select class="svelte-1lsf4ps"><option>Last hour</option><option>Last 6 hours</option><option>Last 24 hours</option><option>Last 48 hours</option></select></label> <label class="svelte-1lsf4ps">Filter: <input type="text" placeholder="Filter logs..." class="svelte-1lsf4ps"/></label> <button class="svelte-1lsf4ps">Refresh</button></div></div> <!>',1);function Me(e,t){se(t,!0);let p=g(oe([])),o=g(!0),i=g(""),n=g(""),_=g(1);async function l(){c(o,!0),c(i,"");try{const a=await fetch(`/api/logs?hours=${s(_)}&filter=${encodeURIComponent(s(n))}`);if(a.ok){const r=await a.json();c(p,r.data||[],!0)}else c(i,"Failed to load logs")}catch(a){c(i,"Connection error"),console.error("Failed to load logs:",a)}finally{c(o,!1)}}function b(a){const r=a.toLowerCase();return r==="err"||r==="error"?"error":r==="warning"||r==="warn"?"warning":r==="info"?"info":""}le(()=>{l()});var K=xe(),L=ie(K),N=v(f(L),2),q=f(N),y=v(f(q)),F=f(y);F.value=F.__value=1;var S=v(F);S.value=S.__value=6;var C=v(S);C.value=C.__value=24;var O=v(C);O.value=O.__value=48,u(y),u(q);var j=v(q,2),E=v(f(j));_e(E),u(j);var z=v(j,2);u(N),u(L);var B=v(L,2);{var D=a=>{var r=he();d(a,r)},G=a=>{var r=be(),I=f(r,!0);u(r),U(()=>x(I,s(i))),d(a,r)},J=a=>{var r=ye();d(a,r)},Q=a=>{var r=we();fe(r,21,()=>s(p),pe,(I,k)=>{var w=ke(),M=f(w),T=f(M,!0);u(M);var R=v(M,2),V=f(R);u(R);var P=v(R,2),X=f(P,!0);u(P),u(w),U(Y=>{ce(w,1,`log-entry ${Y??""}`,"svelte-1lsf4ps"),x(T,s(k).timestamp),x(V,`[${s(k).priority??""}]`),x(X,s(k).message)},[()=>b(s(k).priority)]),d(I,w)}),u(r),d(a,r)};ue(B,a=>{s(o)?a(D):s(i)?a(G,1):s(p).length===0?a(J,2):a(Q,-1)})}H("change",y,l),ge(y,()=>s(_),a=>c(_,a)),H("keydown",E,a=>a.key==="Enter"&&l()),de(E,()=>s(n),a=>c(n,a)),H("click",z,l),d(e,K),ne()}ve(["change","keydown","click"]);export{Me as component};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{a as i,f as d}from"../chunks/CJv7FbHz.js";import{p as Y,a0 as w,a1 as Z,a8 as $,a3 as ee,a as te,s as u,a2 as o,a4 as s,a5 as r,g as a,a7 as z}from"../chunks/DtGl34IE.js";import{d as ae,a as D,s as l}from"../chunks/n1byU5WW.js";import{i as re}from"../chunks/-EcHSJia.js";import{e as se,i as oe}from"../chunks/ecAlPOj8.js";var ce=d("<p>Loading...</p>"),le=d('<p class="error svelte-qegr5c"> </p>'),ie=d("<p>Queue is empty.</p>"),ue=d('<tr><td class="id svelte-qegr5c"> </td><td class="svelte-qegr5c"> </td><td class="svelte-qegr5c"> </td><td class="svelte-qegr5c"> </td><td class="svelte-qegr5c"> </td><td class="reason svelte-qegr5c"> </td><td class="svelte-qegr5c"><button class="svelte-qegr5c">Requeue</button> <button class="danger svelte-qegr5c">Delete</button></td></tr>'),de=d('<table class="svelte-qegr5c"><thead><tr><th class="svelte-qegr5c">ID</th><th class="svelte-qegr5c">Sender</th><th class="svelte-qegr5c">Recipients</th><th class="svelte-qegr5c">Size</th><th class="svelte-qegr5c">Time</th><th class="svelte-qegr5c">Reason</th><th class="svelte-qegr5c">Actions</th></tr></thead><tbody></tbody></table>'),ve=d('<div class="header svelte-qegr5c"><h2>Mail Queue</h2> <button class="svelte-qegr5c">Refresh</button></div> <!>',1);function pe(B,E){Y(E,!0);let h=w(Z([])),g=w(!0),v=w("");async function n(){u(g,!0),u(v,"");try{const e=await fetch("/api/queue");if(e.ok){const t=await e.json();u(h,t.data||[],!0)}else u(v,"Failed to load queue")}catch(e){u(v,"Connection error"),console.error("Failed to load queue:",e)}finally{u(g,!1)}}async function T(e){try{await fetch(`/api/queue/${e}/requeue`,{method:"POST"}),await n()}catch(t){console.error("Failed to requeue:",t)}}async function j(e){if(confirm("Delete this message from queue?"))try{await fetch(`/api/queue/${e}`,{method:"DELETE"}),await n()}catch(t){console.error("Failed to delete from queue:",t)}}function L(e){return e<1024?e+" B":e<1024*1024?(e/1024).toFixed(1)+" KB":(e/1024/1024).toFixed(1)+" MB"}$(()=>{n()});var Q=ve(),q=ee(Q),M=o(s(q),2);r(q);var y=o(q,2);{var A=e=>{var t=ce();i(e,t)},C=e=>{var t=le(),f=s(t,!0);r(t),z(()=>l(f,a(v))),i(e,t)},I=e=>{var t=ie();i(e,t)},K=e=>{var t=de(),f=o(s(t));se(f,21,()=>a(h),oe,(O,c)=>{var p=ue(),_=s(p),P=s(_,!0);r(_);var m=o(_),G=s(m,!0);r(m);var b=o(m),H=s(b,!0);r(b);var x=o(b),J=s(x,!0);r(x);var F=o(x),N=s(F,!0);r(F);var k=o(F),U=s(k,!0);r(k);var R=o(k),S=s(R),V=o(S,2);r(R),r(p),z((W,X)=>{l(P,a(c).id),l(G,a(c).sender),l(H,W),l(J,X),l(N,a(c).time),l(U,a(c).reason)},[()=>a(c).recipients.join(", "),()=>L(a(c).size)]),D("click",S,()=>T(a(c).id)),D("click",V,()=>j(a(c).id)),i(O,p)}),r(f),r(t),i(e,t)};re(y,e=>{a(g)?e(A):a(v)?e(C,1):a(h).length===0?e(I,2):e(K,-1)})}D("click",M,n),i(B,Q),te()}ae(["click"]);export{pe as component};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{a as r,f as m}from"../chunks/CJv7FbHz.js";import{i as t}from"../chunks/DFRBE_Uq.js";import{o as i}from"../chunks/DR7h53-u.js";import{p as s,a as n}from"../chunks/DtGl34IE.js";import{g as f}from"../chunks/RKc8iKz_.js";var e=m("<p>Redirecting to domains...</p>");function l(o,a){s(a,!1),i(()=>{f("/domains")}),t();var p=e();r(o,p),n()}export{l as component};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{a as p,f as n}from"../chunks/CJv7FbHz.js";import{o as L}from"../chunks/DR7h53-u.js";import{p as M,a0 as b,a1 as Q,a2 as o,a3 as B,a as C,s as q,g as l,a4 as s,a5 as t,a6 as i,a7 as G}from"../chunks/DtGl34IE.js";import{s as v}from"../chunks/n1byU5WW.js";import{i as H}from"../chunks/-EcHSJia.js";import{s as I}from"../chunks/BYrNiJ2z.js";var J=n("<p>Loading...</p>"),K=n('<div class="stats-grid svelte-1uha8ag"><div class="stat-card svelte-1uha8ag"><h3 class="svelte-1uha8ag"> </h3> <p class="svelte-1uha8ag">Domains</p></div> <div class="stat-card svelte-1uha8ag"><h3 class="svelte-1uha8ag"> </h3> <p class="svelte-1uha8ag">Users</p></div> <div class="stat-card svelte-1uha8ag"><h3 class="svelte-1uha8ag"> </h3> <p class="svelte-1uha8ag">Aliases</p></div> <div><h3 class="svelte-1uha8ag"> </h3> <p class="svelte-1uha8ag">Queued Emails</p></div></div>'),N=n("<h2>Dashboard</h2> <!>",1);function X(w,z){M(z,!0);let r=b(Q({totalDomains:0,totalUsers:0,totalAliases:0,queueSize:0})),g=b(!0);L(async()=>{try{const a=await fetch("/api/stats");if(a.ok){const e=await a.json();q(r,e.data,!0)}}catch(a){console.error("Failed to fetch stats:",a)}finally{q(g,!1)}});var m=N(),A=o(B(m),2);{var S=a=>{var e=J();p(a,e)},U=a=>{var e=K(),h=s(e),f=s(h),j=s(f,!0);t(f),i(2),t(h);var c=o(h,2),_=s(c),k=s(_,!0);t(_),i(2),t(c);var u=o(c,2),x=s(u),E=s(x,!0);t(x),i(2),t(u);var d=o(u,2);let y;var D=s(d),F=s(D,!0);t(D),i(2),t(d),t(e),G(()=>{v(j,l(r).totalDomains),v(k,l(r).totalUsers),v(E,l(r).totalAliases),y=I(d,1,"stat-card svelte-1uha8ag",null,y,{warning:l(r).queueSize>10}),v(F,l(r).queueSize)}),p(a,e)};H(A,a=>{l(g)?a(S):a(U,-1)})}p(w,m),C()}export{X as component};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{a as r,f as m}from"../chunks/CJv7FbHz.js";import{i as t}from"../chunks/DFRBE_Uq.js";import{o as i}from"../chunks/DR7h53-u.js";import{p as s,a as n}from"../chunks/DtGl34IE.js";import{g as f}from"../chunks/RKc8iKz_.js";var e=m("<p>Redirecting to domains...</p>");function l(o,a){s(a,!1),i(()=>{f("/domains")}),t();var p=e();r(o,p),n()}export{l as component};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{a as p,f as g}from"../chunks/CJv7FbHz.js";import{o as W}from"../chunks/DR7h53-u.js";import{p as X,a7 as q,g as t,a as Y,a2 as i,a4 as o,a0 as v,a5 as r,a6 as Z,s}from"../chunks/DtGl34IE.js";import{d as aa,a as ea,e as ta,s as B}from"../chunks/n1byU5WW.js";import{i as I}from"../chunks/-EcHSJia.js";import{r as L}from"../chunks/e1WBZ7UL.js";import{b as N}from"../chunks/ayyTDfEa.js";import{g as f}from"../chunks/RKc8iKz_.js";var sa=g("<span>Logged in as <strong> </strong></span>"),oa=g('<div class="error svelte-imhhoo"> </div>'),ra=g('<div class="success svelte-imhhoo">Password changed successfully!</div>'),ia=g('<div class="page-container svelte-imhhoo"><header class="header svelte-imhhoo"><div class="header-content svelte-imhhoo"><h1 class="svelte-imhhoo">Change Password</h1> <div class="user-info svelte-imhhoo"><!> <button class="logout svelte-imhhoo">Logout</button></div></div></header> <div class="content svelte-imhhoo"><!> <!> <form class="svelte-imhhoo"><label class="svelte-imhhoo">Current Password <input type="password" placeholder="Enter current password" required="" class="svelte-imhhoo"/></label> <label class="svelte-imhhoo">New Password <input type="password" placeholder="Enter new password (min 8 characters)" required="" class="svelte-imhhoo"/></label> <label class="svelte-imhhoo">Confirm New Password <input type="password" placeholder="Confirm new password" required="" class="svelte-imhhoo"/></label> <button type="submit" class="svelte-imhhoo"> </button></form> <div class="links svelte-imhhoo"><a href="/auth/dashboard" class="svelte-imhhoo">Back to Dashboard</a></div></div></div>');function pa(F,J){X(J,!0);let c=null,u=v(""),h=v(""),m=v(""),l=v(""),z=v(!1),n=v(!1);W(async()=>{const a=localStorage.getItem("token");if(!a){await f("/auth/login");return}try{const e=await fetch("/api/auth/me",{headers:{Authorization:`Bearer ${a}`}});if(!e.ok){await f("/auth/login");return}c=(await e.json()).data}catch{await f("/auth/login")}});async function M(a){if(a.preventDefault(),s(l,""),t(h)!==t(m)){s(l,"Passwords do not match");return}if(t(h).length<8){s(l,"Password must be at least 8 characters");return}s(n,!0);try{const e=await fetch("/api/auth/change-password",{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${localStorage.getItem("token")}`},body:JSON.stringify({oldPassword:t(u),newPassword:t(h)})}),d=await e.json();if(!e.ok){s(l,d.error||"Failed to change password",!0);return}s(z,!0),s(u,""),s(h,""),s(m,"")}catch{s(l,"Connection error")}finally{s(n,!1)}}function G(){localStorage.removeItem("token"),f("/auth/login")}var w=ia(),b=o(w),A=o(b),D=i(o(A),2),E=o(D);{var H=a=>{var e=sa(),d=i(o(e)),V=o(d,!0);r(d),r(e),q(()=>B(V,c.email||c.username)),p(a,e)};I(E,a=>{c&&a(H)})}var K=i(E,2);r(D),r(A),r(b);var O=i(b,2),T=o(O);{var Q=a=>{var e=oa(),d=o(e,!0);r(e),q(()=>B(d,t(l))),p(a,e)};I(T,a=>{t(l)&&a(Q)})}var $=i(T,2);{var R=a=>{var e=ra();p(a,e)};I($,a=>{t(z)&&a(R)})}var _=i($,2),P=o(_),y=i(o(P));L(y),r(P);var k=i(P,2),C=i(o(k));L(C),r(k);var x=i(k,2),S=i(o(x));L(S),r(x);var j=i(x,2),U=o(j,!0);r(j),r(_),Z(2),r(O),r(w),q(()=>{y.disabled=t(n),C.disabled=t(n),S.disabled=t(n),j.disabled=t(n),B(U,t(n)?"Changing...":"Change Password")}),ea("click",K,G),ta("submit",_,M),N(y,()=>t(u),a=>s(u,a)),N(C,()=>t(h),a=>s(h,a)),N(S,()=>t(m),a=>s(m,a)),p(F,w),Y()}aa(["click"]);export{pa as component};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{a as n,f as c}from"../chunks/CJv7FbHz.js";import{o as H}from"../chunks/DR7h53-u.js";import{p as J,a as K,a2 as r,a4 as a,s as O,a5 as s,g as T,a0 as V,a7 as g,a3 as W,a6 as X}from"../chunks/DtGl34IE.js";import{d as Y,a as Z,s as i}from"../chunks/n1byU5WW.js";import{i as h}from"../chunks/-EcHSJia.js";import{g as d}from"../chunks/RKc8iKz_.js";var $=c("<span>Logged in as <strong> </strong></span>"),aa=c("<p>Loading...</p>"),sa=c('<div class="info-row svelte-1s1ktxm"><span class="label svelte-1s1ktxm">Quota:</span> <span class="value svelte-1s1ktxm"> </span></div>'),ta=c('<div class="card svelte-1s1ktxm"><h2 class="svelte-1s1ktxm">Account Information</h2> <div class="info-row svelte-1s1ktxm"><span class="label svelte-1s1ktxm">Email:</span> <span class="value svelte-1s1ktxm"> </span></div> <div class="info-row svelte-1s1ktxm"><span class="label svelte-1s1ktxm">Username:</span> <span class="value svelte-1s1ktxm"> </span></div> <div class="info-row svelte-1s1ktxm"><span class="label svelte-1s1ktxm">Role:</span> <span class="value svelte-1s1ktxm"> </span></div> <!></div> <div class="card svelte-1s1ktxm"><h2 class="svelte-1s1ktxm">Quick Actions</h2> <div class="actions svelte-1s1ktxm"><a href="/auth/change-password" class="action-btn svelte-1s1ktxm">Change Password</a></div></div>',1),ea=c('<div class="page-container svelte-1s1ktxm"><header class="header svelte-1s1ktxm"><h1 class="svelte-1s1ktxm">User Dashboard</h1> <div class="user-info svelte-1s1ktxm"><!> <button class="logout svelte-1s1ktxm">Logout</button></div></header> <div class="content svelte-1s1ktxm"><!></div></div>');function da(U,B){J(B,!0);let t=null,_=V(!0);H(async()=>{const e=localStorage.getItem("token");if(!e){await d("/auth/login");return}try{const o=await fetch("/api/auth/me",{headers:{Authorization:`Bearer ${e}`}});if(!o.ok){await d("/auth/login");return}if(t=(await o.json()).data,(t==null?void 0:t.role)==="admin"){await d("/");return}}catch{await d("/auth/login")}finally{O(_,!1)}});function M(){localStorage.removeItem("token"),d("/auth/login")}var m=ea(),p=a(m),w=r(a(p),2),b=a(w);{var N=e=>{var o=$(),l=r(a(o)),v=a(l,!0);s(l),s(o),g(()=>i(v,t.email||t.username)),n(e,o)};h(b,e=>{t&&e(N)})}var Q=r(b,2);s(w),s(p);var A=r(p,2),S=a(A);{var j=e=>{var o=aa();n(e,o)},z=e=>{var o=ta(),l=W(o),v=r(a(l),2),L=r(a(v),2),C=a(L,!0);s(L),s(v);var u=r(v,2),q=r(a(u),2),D=a(q,!0);s(q),s(u);var f=r(u,2),y=r(a(f),2),E=a(y,!0);s(y),s(f);var F=r(f,2);{var P=x=>{var k=sa(),I=r(a(k),2),R=a(I,!0);s(I),s(k),g(G=>i(R,G),[()=>t.quota>0?(t.quota/1024/1024).toFixed(0)+" MB":"Unlimited"]),n(x,k)};h(F,x=>{t.quota!==void 0&&x(P)})}s(l),X(2),g(()=>{i(C,t.email||"N/A"),i(D,t.username||"N/A"),i(E,t.role)}),n(e,o)};h(S,e=>{T(_)?e(j):t&&e(z,1)})}s(A),s(m),Z("click",Q,M),n(U,m),K()}Y(["click"]);export{da as component};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{a as v,f}from"../chunks/CJv7FbHz.js";import{a2 as c,a4 as i,g as s,a5 as o,a0 as d,a7 as y,a6 as k,a3 as E,s as r}from"../chunks/DtGl34IE.js";import{s as x,e as O}from"../chunks/n1byU5WW.js";import{i as w}from"../chunks/-EcHSJia.js";import{r as T}from"../chunks/e1WBZ7UL.js";import{b as D}from"../chunks/ayyTDfEa.js";import"../chunks/CfgL3m1f.js";var I=f('<div class="error svelte-15sczaz"> </div>'),J=f('<div class="success svelte-15sczaz"><p class="svelte-15sczaz">If an account exists with that email or username, a password reset link has been sent.</p> <p class="svelte-15sczaz">Please check your inbox.</p></div> <div class="links svelte-15sczaz"><a href="/auth/login" class="svelte-15sczaz">Back to Login</a></div>',1),N=f(`<p class="info svelte-15sczaz">Enter your email address or username and we'll send you a link to reset your password.</p> <form class="svelte-15sczaz"><label class="svelte-15sczaz">Email or Username <input type="text" placeholder="user@example.org or username" required="" class="svelte-15sczaz"/></label> <button type="submit" class="svelte-15sczaz"> </button></form> <div class="links svelte-15sczaz"><a href="/auth/login" class="svelte-15sczaz">Back to Login</a></div>`,1),U=f('<div class="login-container svelte-15sczaz"><div class="login-card svelte-15sczaz"><h1 class="svelte-15sczaz">Reset Password</h1> <!> <!></div></div>');function V(S){let u=d(""),l=d(""),b=d(!1),n=d(!1);async function L(e){e.preventDefault(),r(l,""),r(n,!0);try{const a=await fetch("/api/auth/forgot",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({identifier:s(u)})}),t=await a.json();if(!a.ok){r(l,t.error||"Request failed",!0);return}r(b,!0)}catch{r(l,"Connection error")}finally{r(n,!1)}}var m=U(),g=i(m),_=c(i(g),2);{var P=e=>{var a=I(),t=i(a,!0);o(a),y(()=>x(t,s(l))),v(e,a)};w(_,e=>{s(l)&&e(P)})}var R=c(_,2);{var j=e=>{var a=J();k(2),v(e,a)},q=e=>{var a=N(),t=c(E(a),2),p=i(t),z=c(i(p));T(z),o(p);var h=c(p,2),B=i(h,!0);o(h),o(t),k(2),y(()=>{z.disabled=s(n),h.disabled=s(n),x(B,s(n)?"Sending...":"Send Reset Link")}),O("submit",t,L),D(z,()=>s(u),C=>r(u,C)),v(e,a)};w(R,e=>{s(b)?e(j):e(q,-1)})}o(g),o(m),v(S,m)}export{V as component};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{a as y,f as j}from"../chunks/CJv7FbHz.js";import{p as A,a7 as S,g as t,a as D,a2 as i,a4 as s,s as r,a0 as p,a5 as o}from"../chunks/DtGl34IE.js";import{e as F,s as x}from"../chunks/n1byU5WW.js";import{i as J}from"../chunks/-EcHSJia.js";import{r as k}from"../chunks/e1WBZ7UL.js";import{b as C}from"../chunks/ayyTDfEa.js";import{g as I}from"../chunks/RKc8iKz_.js";var L=j('<div class="error svelte-1i2smtp"> </div>'),N=j('<div class="login-container svelte-1i2smtp"><div class="login-card svelte-1i2smtp"><h1 class="svelte-1i2smtp">IMC Vibe</h1> <p class="subtitle svelte-1i2smtp">Mail Server Administration</p> <!> <form class="svelte-1i2smtp"><label class="svelte-1i2smtp">Email or Username <input type="text" placeholder="user@example.org or username" required="" autocomplete="username" class="svelte-1i2smtp"/></label> <label class="svelte-1i2smtp">Password <input type="password" placeholder="Enter password" required="" autocomplete="current-password" class="svelte-1i2smtp"/></label> <div class="forgot svelte-1i2smtp"><a href="/auth/forgot" class="svelte-1i2smtp">Forgot password?</a></div> <button type="submit" class="svelte-1i2smtp"> </button></form></div></div>');function Q(q,E){A(E,!0);let m=p(""),d=p(""),l=p(""),n=p(!1);async function M(){r(n,!0),r(l,"");try{const e=await fetch("/api/auth/login",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({username:t(m),password:t(d)})}),a=await e.json();if(!e.ok){r(l,a.error||"Login failed",!0);return}localStorage.setItem("token",a.data.token),a.data.user.role==="admin"?I("/"):I("/auth/dashboard")}catch{r(l,"Connection error")}finally{r(n,!1)}}var v=N(),g=s(v),h=i(s(g),4);{var O=e=>{var a=L(),T=s(a,!0);o(a),S(()=>x(T,t(l))),y(e,a)};J(h,e=>{t(l)&&e(O)})}var u=i(h,2),c=s(u),_=i(s(c));k(_),o(c);var f=i(c,2),w=i(s(f));k(w),o(f);var b=i(f,4),P=s(b,!0);o(b),o(u),o(g),o(v),S(()=>{b.disabled=t(n),x(P,t(n)?"Signing in...":"Sign In")}),F("submit",u,e=>{e.preventDefault(),M()}),C(_,()=>t(m),e=>r(m,e)),C(w,()=>t(d),e=>r(d,e)),y(q,v),D()}export{Q as component};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{a as m,f}from"../chunks/CJv7FbHz.js";import{p as K,a0 as w,a1 as j,a8 as Q,a3 as R,a as V,s as c,a2 as r,a4 as s,a5 as e,g as o,a6 as W,a7 as X}from"../chunks/DtGl34IE.js";import{d as Y,a as h,e as Z,s as C}from"../chunks/n1byU5WW.js";import{i as L}from"../chunks/-EcHSJia.js";import{e as $,i as aa}from"../chunks/ecAlPOj8.js";import{r as ta,s as ea}from"../chunks/e1WBZ7UL.js";import{b as sa}from"../chunks/ayyTDfEa.js";var oa=f("<p>Loading...</p>"),ra=f("<p>No domains configured yet.</p>"),la=f('<tr><td class="svelte-83awcy"><a> </a></td><td class="svelte-83awcy"> </td><td class="svelte-83awcy"> </td><td class="svelte-83awcy"><button class="danger svelte-83awcy">Delete</button></td></tr>'),ia=f('<table class="svelte-83awcy"><thead><tr><th class="svelte-83awcy">Domain</th><th class="svelte-83awcy">Users</th><th class="svelte-83awcy">Aliases</th><th class="svelte-83awcy">Actions</th></tr></thead><tbody></tbody></table>'),ca=f('<div class="modal-backdrop svelte-83awcy"><div class="modal svelte-83awcy"><h3>Add Domain</h3> <form class="svelte-83awcy"><label class="svelte-83awcy">Domain name: <input type="text" placeholder="example.org" required="" class="svelte-83awcy"/></label> <div class="actions svelte-83awcy"><button type="button" class="svelte-83awcy">Cancel</button> <button type="submit" class="svelte-83awcy">Create</button></div></form></div></div>'),na=f('<div class="header svelte-83awcy"><h2>Domains</h2> <button class="svelte-83awcy">Add Domain</button></div> <!> <!>',1);function ha(N,O){K(O,!0);let _=w(j([])),A=w(!0),p=w(!1),b=w(j({name:""}));async function g(){try{const a=await fetch("/api/domains");if(a.ok){const t=await a.json();c(_,t.data||[],!0)}}catch(a){console.error("Failed to load domains:",a)}finally{c(A,!1)}}async function P(){try{(await fetch("/api/domains",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o(b))})).ok&&(c(p,!1),c(b,{name:""},!0),await g())}catch(a){console.error("Failed to create domain:",a)}}async function S(a){if(confirm("Delete this domain? Users and aliases will be deleted."))try{await fetch(`/api/domains/${a}`,{method:"DELETE"}),await g()}catch(t){console.error("Failed to delete domain:",t)}}Q(()=>{g()});var E=na(),D=R(E),U=r(s(D),2);e(D);var F=r(D,2);{var q=a=>{var t=oa();m(a,t)},J=a=>{var t=ra();m(a,t)},M=a=>{var t=ia(),n=r(s(t));$(n,21,()=>o(_),aa,(u,l)=>{var d=la(),v=s(d),y=s(v),i=s(y,!0);e(y),e(v);var k=r(v),G=s(k,!0);e(k);var x=r(k),H=s(x,!0);e(x);var T=r(x),I=s(T);e(T),e(d),X(()=>{ea(y,"href",`/domains/${o(l).name??""}/users`),C(i,o(l).name),C(G,o(l).userCount),C(H,o(l).aliasCount)}),h("click",I,()=>S(o(l).id)),m(u,d)}),e(n),e(t),m(a,t)};L(F,a=>{o(A)?a(q):o(_).length===0?a(J,1):a(M,-1)})}var z=r(F,2);{var B=a=>{var t=ca(),n=s(t),u=r(s(n),2),l=s(u),d=r(s(l));ta(d),e(l);var v=r(l,2),y=s(v);W(2),e(v),e(u),e(n),e(t),h("click",t,()=>c(p,!1)),h("click",n,i=>i.stopPropagation()),Z("submit",u,i=>{i.preventDefault(),P()}),sa(d,()=>o(b).name,i=>o(b).name=i),h("click",y,()=>c(p,!1)),m(a,t)};L(z,a=>{o(p)&&a(B)})}h("click",U,()=>c(p,!0)),m(N,E),V()}Y(["click"]);export{ha as component};
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{c as J,a as i,f as c}from"../chunks/CJv7FbHz.js";import{p as K,a8 as M,a3 as D,a as N,s as m,a0 as d,g as e,a4 as t,a5 as r,a7 as y,a2 as _,a6 as C}from"../chunks/DtGl34IE.js";import{s as O,a as P}from"../chunks/B3ZK_jYJ.js";import{s as p}from"../chunks/n1byU5WW.js";import{i as Q}from"../chunks/-EcHSJia.js";import{s as $}from"../chunks/e1WBZ7UL.js";import{p as S}from"../chunks/B7kAneMc.js";var T=c("<p>Loading...</p>"),V=c('<p class="error svelte-lb8x40"> </p>'),W=c('<div class="header svelte-lb8x40"><a href="/domains" class="back svelte-lb8x40">← Back to Domains</a> <h2> </h2></div> <div class="cards-grid svelte-lb8x40"><a class="card svelte-lb8x40"><h3 class="svelte-lb8x40"> </h3> <p class="svelte-lb8x40">Users</p></a> <a class="card svelte-lb8x40"><h3 class="svelte-lb8x40"> </h3> <p class="svelte-lb8x40">Aliases</p></a></div>',1),X=c("<p>Domain not found</p>");function oa(w,U){K(U,!0);const j=()=>O(S,"$page",A),[A,B]=P();let o=d(null),u=d(!0),n=d("");async function F(a){try{const s=await fetch(`/api/domains/${encodeURIComponent(a)}`);if(s.ok){const l=await s.json();m(o,l.data,!0)}else m(n,"Domain not found")}catch{m(n,"Failed to load domain")}finally{m(u,!1)}}M(()=>{const a=j().params.name;a&&F(a)});var h=J(),I=D(h);{var L=a=>{var s=T();i(a,s)},R=a=>{var s=V(),l=t(s,!0);r(s),y(()=>p(l,e(n))),i(a,s)},q=a=>{var s=W(),l=D(s),x=_(t(l),2),E=t(x,!0);r(x),r(l);var b=_(l,2),v=t(b),g=t(v),G=t(g,!0);r(g),C(2),r(v);var f=_(v,2),k=t(f),H=t(k,!0);r(k),C(2),r(f),r(b),y(()=>{p(E,e(o).name),$(v,"href",`/domains/${e(o).name??""}/users`),p(G,e(o).userCount),$(f,"href",`/domains/${e(o).name??""}/aliases`),p(H,e(o).aliasCount)}),i(a,s)},z=a=>{var s=X();i(a,s)};Q(I,a=>{e(u)?a(L):e(n)?a(R,1):e(o)?a(q,2):a(z,-1)})}i(w,h),N(),B()}export{oa as component};
|
||||
1
backend/cmd/server/embed/_app/version.json
Normal file
1
backend/cmd/server/embed/_app/version.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"version":"1774129207705"}
|
||||
4
backend/cmd/server/embed/favicon.png
Normal file
4
backend/cmd/server/embed/favicon.png
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<rect width="100" height="100" rx="20" fill="#2c3e50"/>
|
||||
<text x="50" y="65" font-size="50" text-anchor="middle" fill="white">V</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 208 B |
40
backend/cmd/server/embed/index.html
Normal file
40
backend/cmd/server/embed/index.html
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link href="/_app/immutable/entry/start.tprRzko2.js" rel="modulepreload">
|
||||
<link href="/_app/immutable/chunks/RKc8iKz_.js" rel="modulepreload">
|
||||
<link href="/_app/immutable/chunks/DtGl34IE.js" rel="modulepreload">
|
||||
<link href="/_app/immutable/chunks/BX-b5t2l.js" rel="modulepreload">
|
||||
<link href="/_app/immutable/chunks/CfgL3m1f.js" rel="modulepreload">
|
||||
<link href="/_app/immutable/chunks/DR7h53-u.js" rel="modulepreload">
|
||||
<link href="/_app/immutable/entry/app.BI_Sozkn.js" rel="modulepreload">
|
||||
<link href="/_app/immutable/chunks/n1byU5WW.js" rel="modulepreload">
|
||||
<link href="/_app/immutable/chunks/CJv7FbHz.js" rel="modulepreload">
|
||||
<link href="/_app/immutable/chunks/-EcHSJia.js" rel="modulepreload">
|
||||
<link href="/_app/immutable/chunks/B3ZK_jYJ.js" rel="modulepreload">
|
||||
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">
|
||||
<script>
|
||||
{
|
||||
__sveltekit_63i6zh = {
|
||||
base: ""
|
||||
};
|
||||
|
||||
const element = document.currentScript.parentElement;
|
||||
|
||||
Promise.all([
|
||||
import("/_app/immutable/entry/start.tprRzko2.js"),
|
||||
import("/_app/immutable/entry/app.BI_Sozkn.js")
|
||||
]).then(([kit, app]) => {
|
||||
kit.start(app, element);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
19
backend/cmd/server/frontend.go
Normal file
19
backend/cmd/server/frontend.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
//go:embed all:embed
|
||||
//go:embed all:embed/_app
|
||||
var Files embed.FS
|
||||
|
||||
func FrontendFileSystem() http.FileSystem {
|
||||
return http.FS(Files)
|
||||
}
|
||||
|
||||
func FrontendFS() fs.FS {
|
||||
return Files
|
||||
}
|
||||
180
backend/cmd/server/main.go
Normal file
180
backend/cmd/server/main.go
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/imc-vibe/backend/internal/api"
|
||||
"github.com/imc-vibe/backend/internal/auth"
|
||||
"github.com/imc-vibe/backend/internal/config"
|
||||
"github.com/imc-vibe/backend/internal/db"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg := config.Load()
|
||||
|
||||
database, err := db.Connect(cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect to database: %v", err)
|
||||
}
|
||||
|
||||
if err := database.InitSchema(); err != nil {
|
||||
log.Printf("Warning: Could not initialize schema: %v", err)
|
||||
}
|
||||
|
||||
if cfg.AdminUser != "" && cfg.AdminPassword != "" {
|
||||
hash, err := auth.HashPassword(cfg.AdminPassword)
|
||||
if err != nil {
|
||||
log.Printf("Warning: Failed to hash admin password: %v", err)
|
||||
} else {
|
||||
if err := database.EnsureAdminUser(cfg.AdminUser, hash); err != nil {
|
||||
log.Printf("Warning: Failed to create admin user: %v", err)
|
||||
} else {
|
||||
log.Printf("Admin user '%s' created or already exists", cfg.AdminUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEmbedded := os.Getenv("USE_EMBEDDED") != "false"
|
||||
port := cfg.Port
|
||||
|
||||
frontendFS := FrontendFileSystem()
|
||||
|
||||
router := http.NewServeMux()
|
||||
|
||||
router.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(`{"status":"ok"}`))
|
||||
})
|
||||
|
||||
apiRouter := api.New(database, cfg)
|
||||
router.Handle("/api/", apiRouter.Handler())
|
||||
|
||||
if useEmbedded {
|
||||
router.HandleFunc("/favicon.png", serveStaticFile(frontendFS, "embed/favicon.png"))
|
||||
router.Handle("/_app/", http.StripPrefix("/_app/", serveStaticPrefixed(frontendFS, "embed/_app/")))
|
||||
router.HandleFunc("/", serveSPA(frontendFS))
|
||||
log.Println("Using embedded frontend")
|
||||
} else {
|
||||
router.Handle("/", http.FileServer(http.Dir("../frontend/build")))
|
||||
log.Println("Using filesystem frontend")
|
||||
}
|
||||
|
||||
log.Printf("Server starting on http://localhost:%s", port)
|
||||
log.Fatal(http.ListenAndServe(":"+port, router))
|
||||
}
|
||||
|
||||
func serveStaticPrefixed(fsys http.FileSystem, prefix string) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
path := prefix + r.URL.Path
|
||||
log.Printf("Static file requested: /_app/%s -> %s", r.URL.Path, path)
|
||||
|
||||
file, err := fsys.Open(path)
|
||||
if err != nil {
|
||||
log.Printf("Error opening file %s: %v", path, err)
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
fi, err := file.Stat()
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if fi.IsDir() {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
contentType := mimeType(path)
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
http.ServeContent(w, r, fi.Name(), fi.ModTime(), file)
|
||||
}
|
||||
}
|
||||
|
||||
func serveStaticFile(fsys http.FileSystem, filename string) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
log.Printf("Static file requested: /%s", filename)
|
||||
|
||||
file, err := fsys.Open(filename)
|
||||
if err != nil {
|
||||
log.Printf("Error opening file %s: %v", filename, err)
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
fi, err := file.Stat()
|
||||
if err != nil {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
contentType := mimeType(filename)
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
http.ServeContent(w, r, fi.Name(), fi.ModTime(), file)
|
||||
}
|
||||
}
|
||||
|
||||
func serveSPA(fsys http.FileSystem) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
|
||||
if strings.HasPrefix(path, "/api/") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if path == "/" || path == "" {
|
||||
path = "embed/index.html"
|
||||
} else {
|
||||
path = "embed/index.html"
|
||||
}
|
||||
|
||||
file, err := fsys.Open(path)
|
||||
if err != nil {
|
||||
log.Printf("Error opening %s: %v", path, err)
|
||||
http.Error(w, "Not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
fi, _ := file.Stat()
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
http.ServeContent(w, r, fi.Name(), fi.ModTime(), file)
|
||||
}
|
||||
}
|
||||
|
||||
func mimeType(path string) string {
|
||||
switch {
|
||||
case strings.HasSuffix(path, ".html"):
|
||||
return "text/html; charset=utf-8"
|
||||
case strings.HasSuffix(path, ".css"):
|
||||
return "text/css"
|
||||
case strings.HasSuffix(path, ".js"):
|
||||
return "application/javascript"
|
||||
case strings.HasSuffix(path, ".json"):
|
||||
return "application/json"
|
||||
case strings.HasSuffix(path, ".png"):
|
||||
return "image/png"
|
||||
case strings.HasSuffix(path, ".jpg") || strings.HasSuffix(path, ".jpeg"):
|
||||
return "image/jpeg"
|
||||
case strings.HasSuffix(path, ".svg"):
|
||||
return "image/svg+xml"
|
||||
case strings.HasSuffix(path, ".ico"):
|
||||
return "image/x-icon"
|
||||
case strings.HasSuffix(path, ".woff"):
|
||||
return "font/woff"
|
||||
case strings.HasSuffix(path, ".woff2"):
|
||||
return "font/woff2"
|
||||
case strings.HasSuffix(path, ".ttf"):
|
||||
return "font/ttf"
|
||||
case strings.HasSuffix(path, ".eot"):
|
||||
return "application/vnd.ms-fontobject"
|
||||
default:
|
||||
return "text/plain"
|
||||
}
|
||||
}
|
||||
19
backend/go.mod
Normal file
19
backend/go.mod
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
module github.com/imc-vibe/backend
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||
github.com/joho/godotenv v1.5.1
|
||||
golang.org/x/crypto v0.49.0
|
||||
gorm.io/driver/mysql v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
)
|
||||
20
backend/go.sum
Normal file
20
backend/go.sum
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
|
||||
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
|
||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
160
backend/internal/api/handlers/aliases.go
Normal file
160
backend/internal/api/handlers/aliases.go
Normal file
|
|
@ -0,0 +1,160 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/imc-vibe/backend/internal/db"
|
||||
)
|
||||
|
||||
type AliasHandler struct {
|
||||
db *db.DB
|
||||
}
|
||||
|
||||
func NewAliasHandler(database *db.DB) *AliasHandler {
|
||||
return &AliasHandler{db: database}
|
||||
}
|
||||
|
||||
type CreateAliasRequest struct {
|
||||
Source string `json:"source"`
|
||||
Destination string `json:"destination"`
|
||||
}
|
||||
|
||||
func (h *AliasHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
Error(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
domainName := extractDomainNameFromPath(r.URL.Path)
|
||||
if domainName == "" {
|
||||
Error(w, http.StatusBadRequest, "domain name required")
|
||||
return
|
||||
}
|
||||
|
||||
authCtx := GetAuthContext(r)
|
||||
if authCtx == nil {
|
||||
Error(w, http.StatusUnauthorized, "authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
canAccess, _ := h.db.CanAccessDomain(authCtx.UserID, domainName, authCtx.IsAdmin())
|
||||
if !canAccess {
|
||||
Error(w, http.StatusForbidden, "access denied")
|
||||
return
|
||||
}
|
||||
|
||||
domain, err := h.db.GetDomainByName(domainName)
|
||||
if err != nil {
|
||||
Error(w, http.StatusNotFound, "domain not found")
|
||||
return
|
||||
}
|
||||
|
||||
aliases, err := h.db.GetAliasesByDomain(domain.ID)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "database error")
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, aliases)
|
||||
}
|
||||
|
||||
func (h *AliasHandler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
Error(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
domainName := extractDomainNameFromPath(r.URL.Path)
|
||||
if domainName == "" {
|
||||
Error(w, http.StatusBadRequest, "domain name required")
|
||||
return
|
||||
}
|
||||
|
||||
authCtx := GetAuthContext(r)
|
||||
if authCtx == nil {
|
||||
Error(w, http.StatusUnauthorized, "authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
canAccess, _ := h.db.CanAccessDomain(authCtx.UserID, domainName, authCtx.IsAdmin())
|
||||
if !canAccess {
|
||||
Error(w, http.StatusForbidden, "access denied")
|
||||
return
|
||||
}
|
||||
|
||||
domain, err := h.db.GetDomainByName(domainName)
|
||||
if err != nil {
|
||||
Error(w, http.StatusNotFound, "domain not found")
|
||||
return
|
||||
}
|
||||
|
||||
var req CreateAliasRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Source == "" || req.Destination == "" {
|
||||
Error(w, http.StatusBadRequest, "source and destination required")
|
||||
return
|
||||
}
|
||||
|
||||
alias, err := h.db.CreateAliasInDomain(req.Source, req.Destination, domain.ID)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "failed to create alias")
|
||||
return
|
||||
}
|
||||
|
||||
Created(w, alias)
|
||||
}
|
||||
|
||||
func (h *AliasHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete {
|
||||
Error(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
pathParts := strings.Split(r.URL.Path, "/")
|
||||
var domainName, idStr string
|
||||
for i, part := range pathParts {
|
||||
if part == "domains" && i+1 < len(pathParts) {
|
||||
domainName = pathParts[i+1]
|
||||
}
|
||||
if part == "aliases" && i+1 < len(pathParts) {
|
||||
idStr = pathParts[i+1]
|
||||
}
|
||||
}
|
||||
|
||||
if domainName == "" || idStr == "" {
|
||||
Error(w, http.StatusBadRequest, "invalid path")
|
||||
return
|
||||
}
|
||||
|
||||
authCtx := GetAuthContext(r)
|
||||
if authCtx == nil {
|
||||
Error(w, http.StatusUnauthorized, "authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
canAccess, _ := h.db.CanAccessDomain(authCtx.UserID, domainName, authCtx.IsAdmin())
|
||||
if !canAccess {
|
||||
Error(w, http.StatusForbidden, "access denied")
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, "invalid alias id")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.DeleteAlias(uint(id)); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "failed to delete alias")
|
||||
return
|
||||
}
|
||||
|
||||
NoContent(w)
|
||||
}
|
||||
269
backend/internal/api/handlers/auth.go
Normal file
269
backend/internal/api/handlers/auth.go
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/imc-vibe/backend/internal/auth"
|
||||
"github.com/imc-vibe/backend/internal/db"
|
||||
)
|
||||
|
||||
const MaxLoginAttempts = 5
|
||||
|
||||
type AuthHandler struct {
|
||||
db *db.DB
|
||||
jwtManager *auth.JWTManager
|
||||
}
|
||||
|
||||
func NewAuthHandler(database *db.DB, jwtManager *auth.JWTManager) *AuthHandler {
|
||||
return &AuthHandler{
|
||||
db: database,
|
||||
jwtManager: jwtManager,
|
||||
}
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type ChangePasswordRequest struct {
|
||||
OldPassword string `json:"oldPassword"`
|
||||
NewPassword string `json:"newPassword"`
|
||||
}
|
||||
|
||||
type ForgotPasswordRequest struct {
|
||||
Identifier string `json:"identifier"`
|
||||
}
|
||||
|
||||
type UserResponse struct {
|
||||
ID uint `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Role string `json:"role"`
|
||||
Domains []string `json:"domains"`
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
Error(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
var req LoginRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Username == "" || req.Password == "" {
|
||||
Error(w, http.StatusBadRequest, "username and password required")
|
||||
return
|
||||
}
|
||||
|
||||
ip := getClientIP(r)
|
||||
|
||||
if isLockedOut(ip, req.Username, h.db) {
|
||||
Error(w, http.StatusTooManyRequests, "too many failed attempts, try again later")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.db.GetImcUserByUsername(req.Username)
|
||||
if err != nil || user == nil || !auth.CheckPassword(req.Password, user.PasswordHash) {
|
||||
recordFailedAttempt(req.Username, ip, h.db)
|
||||
Error(w, http.StatusUnauthorized, "invalid credentials")
|
||||
return
|
||||
}
|
||||
|
||||
clearFailedAttempts(req.Username, ip, h.db)
|
||||
|
||||
domains, _ := h.db.GetUserAccessibleDomains(user.ID, user.Role == "admin")
|
||||
|
||||
domainNames := make([]string, len(domains))
|
||||
for i, d := range domains {
|
||||
domainNames[i] = d.Name
|
||||
}
|
||||
|
||||
token, err := h.jwtManager.GenerateToken(user.ID, user.Username, user.Role, 24*time.Hour)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "failed to generate token")
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, map[string]interface{}{
|
||||
"token": token,
|
||||
"user": UserResponse{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Role: user.Role,
|
||||
Domains: domainNames,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Me(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
Error(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
authCtx := GetAuthContext(r)
|
||||
if authCtx == nil {
|
||||
Error(w, http.StatusUnauthorized, "not authenticated")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.db.GetImcUserByID(authCtx.UserID)
|
||||
if err != nil || user == nil {
|
||||
Error(w, http.StatusNotFound, "user not found")
|
||||
return
|
||||
}
|
||||
|
||||
domains, _ := h.db.GetUserAccessibleDomains(user.ID, user.Role == "admin")
|
||||
|
||||
domainNames := make([]string, len(domains))
|
||||
for i, d := range domains {
|
||||
domainNames[i] = d.Name
|
||||
}
|
||||
|
||||
Success(w, UserResponse{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Role: user.Role,
|
||||
Domains: domainNames,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) ForgotPassword(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
Error(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
var req ForgotPasswordRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, http.StatusBadRequest, "invalid request")
|
||||
return
|
||||
}
|
||||
|
||||
req.Identifier = strings.TrimSpace(strings.ToLower(req.Identifier))
|
||||
if req.Identifier == "" {
|
||||
Error(w, http.StatusBadRequest, "identifier required")
|
||||
return
|
||||
}
|
||||
|
||||
_, err := h.db.GetImcUserByUsername(req.Identifier)
|
||||
if err == nil {
|
||||
Success(w, map[string]string{
|
||||
"message": "If the account exists, a password reset link will be sent",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, map[string]string{
|
||||
"message": "If the account exists, a password reset link will be sent",
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) ChangePassword(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
Error(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
authCtx := GetAuthContext(r)
|
||||
if authCtx == nil {
|
||||
Error(w, http.StatusUnauthorized, "authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
var req ChangePasswordRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, http.StatusBadRequest, "invalid request")
|
||||
return
|
||||
}
|
||||
|
||||
if req.OldPassword == "" || req.NewPassword == "" {
|
||||
Error(w, http.StatusBadRequest, "old and new password required")
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.NewPassword) < 8 {
|
||||
Error(w, http.StatusBadRequest, "password must be at least 8 characters")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.db.GetImcUserByID(authCtx.UserID)
|
||||
if err != nil || user == nil {
|
||||
Error(w, http.StatusNotFound, "user not found")
|
||||
return
|
||||
}
|
||||
|
||||
if !auth.CheckPassword(req.OldPassword, user.PasswordHash) {
|
||||
Error(w, http.StatusUnauthorized, "current password is incorrect")
|
||||
return
|
||||
}
|
||||
|
||||
newHash, err := auth.HashPassword(req.NewPassword)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "failed to hash password")
|
||||
return
|
||||
}
|
||||
|
||||
err = h.db.UpdateImcUserPassword(user.ID, newHash)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "failed to update password")
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, map[string]string{"message": "password updated successfully"})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) {
|
||||
Success(w, map[string]string{"message": "logged out"})
|
||||
}
|
||||
|
||||
func isLockedOut(ip, identifier string, database *db.DB) bool {
|
||||
var count int64
|
||||
cutoff := time.Now().Add(-15 * time.Minute)
|
||||
|
||||
database.Model(&db.ImcLoginAttempt{}).
|
||||
Where("ip_address = ? AND attempted_at > ? AND successful = false", ip, cutoff).
|
||||
Count(&count)
|
||||
|
||||
if count >= MaxLoginAttempts {
|
||||
return true
|
||||
}
|
||||
|
||||
database.Model(&db.ImcLoginAttempt{}).
|
||||
Where("email = ? AND attempted_at > ? AND successful = false", identifier, cutoff).
|
||||
Count(&count)
|
||||
|
||||
return count >= MaxLoginAttempts
|
||||
}
|
||||
|
||||
func recordFailedAttempt(identifier, ip string, database *db.DB) {
|
||||
database.Create(&db.ImcLoginAttempt{
|
||||
Email: identifier,
|
||||
IPAddress: ip,
|
||||
Successful: false,
|
||||
})
|
||||
}
|
||||
|
||||
func clearFailedAttempts(identifier, ip string, database *db.DB) {
|
||||
database.Model(&db.ImcLoginAttempt{}).
|
||||
Where("email = ? OR ip_address = ?", identifier, ip).
|
||||
Update("successful", true)
|
||||
}
|
||||
|
||||
func getClientIP(r *http.Request) string {
|
||||
forwarded := r.Header.Get("X-Forwarded-For")
|
||||
if forwarded != "" {
|
||||
return strings.Split(forwarded, ",")[0]
|
||||
}
|
||||
if idx := strings.LastIndex(r.RemoteAddr, ":"); idx != -1 {
|
||||
return r.RemoteAddr[:idx]
|
||||
}
|
||||
return r.RemoteAddr
|
||||
}
|
||||
316
backend/internal/api/handlers/domains.go
Normal file
316
backend/internal/api/handlers/domains.go
Normal file
|
|
@ -0,0 +1,316 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/imc-vibe/backend/internal/db"
|
||||
)
|
||||
|
||||
type DomainHandler struct {
|
||||
db *db.DB
|
||||
}
|
||||
|
||||
func NewDomainHandler(database *db.DB) *DomainHandler {
|
||||
return &DomainHandler{db: database}
|
||||
}
|
||||
|
||||
type CreateDomainRequest struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type DomainPermissions struct {
|
||||
DomainID uint `json:"domainId"`
|
||||
DomainName string `json:"domainName"`
|
||||
UserID uint `json:"userId"`
|
||||
CanManage bool `json:"canManage"`
|
||||
}
|
||||
|
||||
func (h *DomainHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
Error(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
authCtx := GetAuthContext(r)
|
||||
if authCtx == nil {
|
||||
Error(w, http.StatusUnauthorized, "authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
isAdmin := authCtx.IsAdmin()
|
||||
domains, err := h.db.GetUserAccessibleDomains(authCtx.UserID, isAdmin)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "database error")
|
||||
return
|
||||
}
|
||||
|
||||
domainStats := make([]db.DomainStats, len(domains))
|
||||
for i, d := range domains {
|
||||
var userCount, aliasCount int64
|
||||
h.db.Model(&db.User{}).Where("domain_id = ?", d.ID).Count(&userCount)
|
||||
h.db.Model(&db.Alias{}).Where("domain_id = ?", d.ID).Count(&aliasCount)
|
||||
domainStats[i] = db.DomainStats{
|
||||
ID: d.ID,
|
||||
Name: d.Name,
|
||||
UserCount: userCount,
|
||||
AliasCount: aliasCount,
|
||||
}
|
||||
}
|
||||
|
||||
Success(w, domainStats)
|
||||
}
|
||||
|
||||
func (h *DomainHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
Error(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
domainName := extractDomainName(r.URL.Path)
|
||||
if domainName == "" {
|
||||
Error(w, http.StatusBadRequest, "domain name required")
|
||||
return
|
||||
}
|
||||
|
||||
domain, err := h.db.GetDomainByName(domainName)
|
||||
if err != nil {
|
||||
Error(w, http.StatusNotFound, "domain not found")
|
||||
return
|
||||
}
|
||||
|
||||
authCtx := GetAuthContext(r)
|
||||
if authCtx == nil {
|
||||
Error(w, http.StatusUnauthorized, "authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
canAccess, _ := h.db.CanAccessDomain(authCtx.UserID, domainName, authCtx.IsAdmin())
|
||||
if !canAccess {
|
||||
Error(w, http.StatusForbidden, "access denied")
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, domain)
|
||||
}
|
||||
|
||||
func (h *DomainHandler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
Error(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
authCtx := GetAuthContext(r)
|
||||
if authCtx == nil || !authCtx.IsAdmin() {
|
||||
Error(w, http.StatusForbidden, "admin access required")
|
||||
return
|
||||
}
|
||||
|
||||
var req CreateDomainRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name == "" {
|
||||
Error(w, http.StatusBadRequest, "domain name required")
|
||||
return
|
||||
}
|
||||
|
||||
existing, err := h.db.GetDomainByName(req.Name)
|
||||
if err == nil && existing != nil {
|
||||
Error(w, http.StatusConflict, "domain already exists")
|
||||
return
|
||||
}
|
||||
|
||||
domain, err := h.db.CreateDomain(req.Name)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "failed to create domain")
|
||||
return
|
||||
}
|
||||
|
||||
Created(w, domain)
|
||||
}
|
||||
|
||||
func (h *DomainHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete {
|
||||
Error(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
authCtx := GetAuthContext(r)
|
||||
if authCtx == nil || !authCtx.IsAdmin() {
|
||||
Error(w, http.StatusForbidden, "admin access required")
|
||||
return
|
||||
}
|
||||
|
||||
domainName := extractDomainName(r.URL.Path)
|
||||
if domainName == "" {
|
||||
Error(w, http.StatusBadRequest, "domain name required")
|
||||
return
|
||||
}
|
||||
|
||||
domain, err := h.db.GetDomainByName(domainName)
|
||||
if err != nil {
|
||||
Error(w, http.StatusNotFound, "domain not found")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.DeleteDomain(domain.ID); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "failed to delete domain")
|
||||
return
|
||||
}
|
||||
|
||||
NoContent(w)
|
||||
}
|
||||
|
||||
func (h *DomainHandler) GetPermissions(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
Error(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
authCtx := GetAuthContext(r)
|
||||
if authCtx == nil {
|
||||
Error(w, http.StatusUnauthorized, "authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
if !authCtx.IsAdmin() {
|
||||
Error(w, http.StatusForbidden, "admin access required")
|
||||
return
|
||||
}
|
||||
|
||||
domainName := extractDomainName(r.URL.Path)
|
||||
if domainName == "" {
|
||||
Error(w, http.StatusBadRequest, "domain name required")
|
||||
return
|
||||
}
|
||||
|
||||
domain, err := h.db.GetDomainByName(domainName)
|
||||
if err != nil {
|
||||
Error(w, http.StatusNotFound, "domain not found")
|
||||
return
|
||||
}
|
||||
|
||||
users, err := h.db.GetUsersForDomain(domain.ID)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "database error")
|
||||
return
|
||||
}
|
||||
|
||||
permissions := make([]DomainPermissions, len(users))
|
||||
for i, u := range users {
|
||||
permissions[i] = DomainPermissions{
|
||||
DomainID: domain.ID,
|
||||
DomainName: domain.Name,
|
||||
UserID: u.ID,
|
||||
CanManage: true,
|
||||
}
|
||||
}
|
||||
|
||||
Success(w, permissions)
|
||||
}
|
||||
|
||||
func (h *DomainHandler) AddPermission(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
Error(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
authCtx := GetAuthContext(r)
|
||||
if authCtx == nil || !authCtx.IsAdmin() {
|
||||
Error(w, http.StatusForbidden, "admin access required")
|
||||
return
|
||||
}
|
||||
|
||||
domainName := extractDomainName(r.URL.Path)
|
||||
if domainName == "" {
|
||||
Error(w, http.StatusBadRequest, "domain name required")
|
||||
return
|
||||
}
|
||||
|
||||
domain, err := h.db.GetDomainByName(domainName)
|
||||
if err != nil {
|
||||
Error(w, http.StatusNotFound, "domain not found")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
UserID uint `json:"userId"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, http.StatusBadRequest, "invalid request")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.AddUserToDomain(req.UserID, domain.ID); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "failed to add user to domain")
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, map[string]string{"message": "user added to domain"})
|
||||
}
|
||||
|
||||
func (h *DomainHandler) RemovePermission(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete {
|
||||
Error(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
authCtx := GetAuthContext(r)
|
||||
if authCtx == nil || !authCtx.IsAdmin() {
|
||||
Error(w, http.StatusForbidden, "admin access required")
|
||||
return
|
||||
}
|
||||
|
||||
domainName := extractDomainName(r.URL.Path)
|
||||
if domainName == "" {
|
||||
Error(w, http.StatusBadRequest, "domain name required")
|
||||
return
|
||||
}
|
||||
|
||||
domain, err := h.db.GetDomainByName(domainName)
|
||||
if err != nil {
|
||||
Error(w, http.StatusNotFound, "domain not found")
|
||||
return
|
||||
}
|
||||
|
||||
userID := extractIDFromPath(r.URL.Path)
|
||||
|
||||
if err := h.db.RemoveUserFromDomain(userID, domain.ID); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "failed to remove user from domain")
|
||||
return
|
||||
}
|
||||
|
||||
NoContent(w)
|
||||
}
|
||||
|
||||
func extractDomainName(path string) string {
|
||||
parts := strings.Split(strings.TrimPrefix(path, "/api/"), "/")
|
||||
if len(parts) >= 2 && parts[1] != "" {
|
||||
return parts[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func extractIDFromPath(path string) uint {
|
||||
parts := strings.Split(path, "/")
|
||||
for i := len(parts) - 1; i >= 0; i-- {
|
||||
if idStr := parts[i]; idStr != "" {
|
||||
var id uint
|
||||
for _, c := range idStr {
|
||||
if c >= '0' && c <= '9' {
|
||||
id = id*10 + uint(c-'0')
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
if id > 0 {
|
||||
return id
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
38
backend/internal/api/handlers/logs.go
Normal file
38
backend/internal/api/handlers/logs.go
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/imc-vibe/backend/internal/mail"
|
||||
)
|
||||
|
||||
type LogsHandler struct{}
|
||||
|
||||
func NewLogsHandler() *LogsHandler {
|
||||
return &LogsHandler{}
|
||||
}
|
||||
|
||||
func (h *LogsHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
Error(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
hours := 1
|
||||
if h := r.URL.Query().Get("hours"); h != "" {
|
||||
if n, err := strconv.Atoi(h); err == nil && n > 0 {
|
||||
hours = n
|
||||
}
|
||||
}
|
||||
|
||||
filter := r.URL.Query().Get("filter")
|
||||
|
||||
entries, err := mail.GetLogs(hours, filter)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "failed to get logs")
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, entries)
|
||||
}
|
||||
72
backend/internal/api/handlers/middleware.go
Normal file
72
backend/internal/api/handlers/middleware.go
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/imc-vibe/backend/internal/auth"
|
||||
)
|
||||
|
||||
type contextKey string
|
||||
|
||||
const AuthContextKey contextKey = "auth"
|
||||
|
||||
func AuthMiddleware(jwtManager *auth.JWTManager, next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
http.Error(w, `{"error":"authorization header required"}`, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.SplitN(authHeader, " ", 2)
|
||||
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
|
||||
http.Error(w, `{"error":"invalid authorization header format"}`, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
claims, err := jwtManager.ValidateToken(parts[1])
|
||||
if err != nil {
|
||||
http.Error(w, `{"error":"invalid token"}`, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
authCtx := &auth.Context{
|
||||
UserID: claims.UserID,
|
||||
Username: claims.Username,
|
||||
Role: auth.Role(claims.Role),
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), AuthContextKey, authCtx)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
func GetAuthContext(r *http.Request) *auth.Context {
|
||||
if ctx := r.Context().Value(AuthContextKey); ctx != nil {
|
||||
if authCtx, ok := ctx.(*auth.Context); ok {
|
||||
return authCtx
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func RequireAdmin(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
authCtx := GetAuthContext(r)
|
||||
if authCtx == nil || !authCtx.IsAdmin() {
|
||||
http.Error(w, `{"error":"admin access required"}`, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func extractID(path string) string {
|
||||
parts := strings.Split(path, "/")
|
||||
if len(parts) > 0 {
|
||||
return parts[len(parts)-1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
58
backend/internal/api/handlers/queue.go
Normal file
58
backend/internal/api/handlers/queue.go
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/imc-vibe/backend/internal/mail"
|
||||
)
|
||||
|
||||
type QueueHandler struct{}
|
||||
|
||||
func NewQueueHandler() *QueueHandler {
|
||||
return &QueueHandler{}
|
||||
}
|
||||
|
||||
func (h *QueueHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
Error(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
entries, err := mail.GetQueue()
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "failed to get queue")
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, entries)
|
||||
}
|
||||
|
||||
func (h *QueueHandler) Requeue(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
Error(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
id := extractID(r.URL.Path)
|
||||
if err := mail.RequeueMail(id); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "failed to requeue mail")
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, map[string]string{"message": "mail requeued"})
|
||||
}
|
||||
|
||||
func (h *QueueHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete {
|
||||
Error(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
id := extractID(r.URL.Path)
|
||||
if err := mail.DeleteFromQueue(id); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "failed to delete from queue")
|
||||
return
|
||||
}
|
||||
|
||||
NoContent(w)
|
||||
}
|
||||
41
backend/internal/api/handlers/response.go
Normal file
41
backend/internal/api/handlers/response.go
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type Response struct {
|
||||
Data interface{} `json:"data"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Meta *Meta `json:"meta,omitempty"`
|
||||
}
|
||||
|
||||
type Meta struct {
|
||||
Total int `json:"total,omitempty"`
|
||||
Page int `json:"page,omitempty"`
|
||||
}
|
||||
|
||||
func JSON(w http.ResponseWriter, status int, data interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(Response{Data: data})
|
||||
}
|
||||
|
||||
func Error(w http.ResponseWriter, status int, message string) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(Response{Error: message})
|
||||
}
|
||||
|
||||
func Success(w http.ResponseWriter, data interface{}) {
|
||||
JSON(w, http.StatusOK, data)
|
||||
}
|
||||
|
||||
func Created(w http.ResponseWriter, data interface{}) {
|
||||
JSON(w, http.StatusCreated, data)
|
||||
}
|
||||
|
||||
func NoContent(w http.ResponseWriter) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
53
backend/internal/api/handlers/stats.go
Normal file
53
backend/internal/api/handlers/stats.go
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/imc-vibe/backend/internal/db"
|
||||
)
|
||||
|
||||
type StatsHandler struct {
|
||||
db *db.DB
|
||||
}
|
||||
|
||||
func NewStatsHandler(database *db.DB) *StatsHandler {
|
||||
return &StatsHandler{db: database}
|
||||
}
|
||||
|
||||
type Stats struct {
|
||||
TotalDomains int `json:"totalDomains"`
|
||||
TotalUsers int `json:"totalUsers"`
|
||||
TotalAliases int `json:"totalAliases"`
|
||||
QueueSize int `json:"queueSize"`
|
||||
}
|
||||
|
||||
func (h *StatsHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
Error(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
domains, err := h.db.GetAllDomains()
|
||||
if err != nil {
|
||||
domains = []db.DomainStats{}
|
||||
}
|
||||
|
||||
users, err := h.db.GetAllUsers()
|
||||
if err != nil {
|
||||
users = []db.User{}
|
||||
}
|
||||
|
||||
aliases, err := h.db.GetAllAliases()
|
||||
if err != nil {
|
||||
aliases = []db.AliasWithDomain{}
|
||||
}
|
||||
|
||||
stats := Stats{
|
||||
TotalDomains: len(domains),
|
||||
TotalUsers: len(users),
|
||||
TotalAliases: len(aliases),
|
||||
QueueSize: 0,
|
||||
}
|
||||
|
||||
Success(w, stats)
|
||||
}
|
||||
351
backend/internal/api/handlers/users.go
Normal file
351
backend/internal/api/handlers/users.go
Normal file
|
|
@ -0,0 +1,351 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/imc-vibe/backend/internal/auth"
|
||||
"github.com/imc-vibe/backend/internal/db"
|
||||
)
|
||||
|
||||
type UserHandler struct {
|
||||
db *db.DB
|
||||
}
|
||||
|
||||
func NewUserHandler(database *db.DB) *UserHandler {
|
||||
return &UserHandler{db: database}
|
||||
}
|
||||
|
||||
type CreateUserRequest struct {
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
Quota int64 `json:"quota"`
|
||||
}
|
||||
|
||||
type UpdateUserRequest struct {
|
||||
Password string `json:"password,omitempty"`
|
||||
Quota int64 `json:"quota,omitempty"`
|
||||
}
|
||||
|
||||
func (h *UserHandler) List(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
Error(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
domainName := extractDomainNameFromPath(r.URL.Path)
|
||||
if domainName == "" {
|
||||
Error(w, http.StatusBadRequest, "domain name required")
|
||||
return
|
||||
}
|
||||
|
||||
authCtx := GetAuthContext(r)
|
||||
if authCtx == nil {
|
||||
Error(w, http.StatusUnauthorized, "authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
canAccess, _ := h.db.CanAccessDomain(authCtx.UserID, domainName, authCtx.IsAdmin())
|
||||
if !canAccess {
|
||||
Error(w, http.StatusForbidden, "access denied")
|
||||
return
|
||||
}
|
||||
|
||||
domain, err := h.db.GetDomainByName(domainName)
|
||||
if err != nil {
|
||||
Error(w, http.StatusNotFound, "domain not found")
|
||||
return
|
||||
}
|
||||
|
||||
users, err := h.db.GetUsersByDomain(domain.ID)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "database error")
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, users)
|
||||
}
|
||||
|
||||
func (h *UserHandler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
Error(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/domains/"), "/")
|
||||
if len(parts) < 4 {
|
||||
Error(w, http.StatusBadRequest, "invalid path")
|
||||
return
|
||||
}
|
||||
domainName := parts[0]
|
||||
|
||||
authCtx := GetAuthContext(r)
|
||||
if authCtx == nil {
|
||||
Error(w, http.StatusUnauthorized, "authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
canAccess, _ := h.db.CanAccessDomain(authCtx.UserID, domainName, authCtx.IsAdmin())
|
||||
if !canAccess {
|
||||
Error(w, http.StatusForbidden, "access denied")
|
||||
return
|
||||
}
|
||||
|
||||
idStr := parts[3]
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, "invalid user id")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.db.GetUserByID(uint(id))
|
||||
if err != nil {
|
||||
Error(w, http.StatusNotFound, "user not found")
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, user)
|
||||
}
|
||||
|
||||
func (h *UserHandler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
Error(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
domainName := extractDomainNameFromPath(r.URL.Path)
|
||||
if domainName == "" {
|
||||
Error(w, http.StatusBadRequest, "domain name required")
|
||||
return
|
||||
}
|
||||
|
||||
authCtx := GetAuthContext(r)
|
||||
if authCtx == nil {
|
||||
Error(w, http.StatusUnauthorized, "authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
canAccess, _ := h.db.CanAccessDomain(authCtx.UserID, domainName, authCtx.IsAdmin())
|
||||
if !canAccess {
|
||||
Error(w, http.StatusForbidden, "access denied")
|
||||
return
|
||||
}
|
||||
|
||||
domain, err := h.db.GetDomainByName(domainName)
|
||||
if err != nil {
|
||||
Error(w, http.StatusNotFound, "domain not found")
|
||||
return
|
||||
}
|
||||
|
||||
var req CreateUserRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Email == "" || req.Password == "" {
|
||||
Error(w, http.StatusBadRequest, "email and password required")
|
||||
return
|
||||
}
|
||||
|
||||
existing, _ := h.db.GetUserByEmail(req.Email)
|
||||
if existing != nil {
|
||||
Error(w, http.StatusConflict, "user already exists")
|
||||
return
|
||||
}
|
||||
|
||||
passwordHash := "{BLF-CRYPT}" + req.Password
|
||||
|
||||
user, err := h.db.CreateUserInDomain(req.Email, passwordHash, req.Quota, domain.ID)
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "failed to create user")
|
||||
return
|
||||
}
|
||||
|
||||
Created(w, user)
|
||||
}
|
||||
|
||||
func (h *UserHandler) Update(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPut {
|
||||
Error(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/domains/"), "/")
|
||||
if len(parts) < 4 {
|
||||
Error(w, http.StatusBadRequest, "invalid path")
|
||||
return
|
||||
}
|
||||
domainName := parts[0]
|
||||
|
||||
authCtx := GetAuthContext(r)
|
||||
if authCtx == nil {
|
||||
Error(w, http.StatusUnauthorized, "authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
canAccess, _ := h.db.CanAccessDomain(authCtx.UserID, domainName, authCtx.IsAdmin())
|
||||
if !canAccess {
|
||||
Error(w, http.StatusForbidden, "access denied")
|
||||
return
|
||||
}
|
||||
|
||||
idStr := parts[3]
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, "invalid user id")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.db.GetUserByID(uint(id))
|
||||
if err != nil {
|
||||
Error(w, http.StatusNotFound, "user not found")
|
||||
return
|
||||
}
|
||||
|
||||
var req UpdateUserRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, http.StatusBadRequest, "invalid request body")
|
||||
return
|
||||
}
|
||||
|
||||
if req.Password != "" {
|
||||
passwordHash := "{BLF-CRYPT}" + req.Password
|
||||
if err := h.db.UpdateUserPassword(user.ID, passwordHash); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "failed to update password")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if req.Quota >= 0 {
|
||||
if err := h.db.UpdateUserQuota(user.ID, req.Quota); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "failed to update quota")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
Success(w, map[string]string{"message": "user updated"})
|
||||
}
|
||||
|
||||
func (h *UserHandler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodDelete {
|
||||
Error(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/domains/"), "/")
|
||||
if len(parts) < 4 {
|
||||
Error(w, http.StatusBadRequest, "invalid path")
|
||||
return
|
||||
}
|
||||
domainName := parts[0]
|
||||
|
||||
authCtx := GetAuthContext(r)
|
||||
if authCtx == nil {
|
||||
Error(w, http.StatusUnauthorized, "authentication required")
|
||||
return
|
||||
}
|
||||
|
||||
canAccess, _ := h.db.CanAccessDomain(authCtx.UserID, domainName, authCtx.IsAdmin())
|
||||
if !canAccess {
|
||||
Error(w, http.StatusForbidden, "access denied")
|
||||
return
|
||||
}
|
||||
|
||||
idStr := parts[3]
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, "invalid user id")
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.DeleteUser(uint(id)); err != nil {
|
||||
Error(w, http.StatusInternalServerError, "failed to delete user")
|
||||
return
|
||||
}
|
||||
|
||||
NoContent(w)
|
||||
}
|
||||
|
||||
func (h *UserHandler) ListAll(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
Error(w, http.StatusMethodNotAllowed, "method not allowed")
|
||||
return
|
||||
}
|
||||
|
||||
authCtx := GetAuthContext(r)
|
||||
if authCtx == nil || !authCtx.IsAdmin() {
|
||||
Error(w, http.StatusForbidden, "admin access required")
|
||||
return
|
||||
}
|
||||
|
||||
users, err := h.db.GetAllUsers()
|
||||
if err != nil {
|
||||
Error(w, http.StatusInternalServerError, "database error")
|
||||
return
|
||||
}
|
||||
|
||||
Success(w, users)
|
||||
}
|
||||
|
||||
func extractDomainNameFromPath(path string) string {
|
||||
parts := strings.Split(strings.TrimPrefix(path, "/api/"), "/")
|
||||
if len(parts) >= 2 && parts[0] == "domains" && parts[1] != "" {
|
||||
if idx := strings.Index(parts[1], "/"); idx > 0 {
|
||||
return parts[1][:idx]
|
||||
}
|
||||
return parts[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func requireAuth(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if GetAuthContext(r) == nil {
|
||||
http.Error(w, `{"error":"authentication required"}`, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func requireAdmin(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
authCtx := GetAuthContext(r)
|
||||
if authCtx == nil || !authCtx.IsAdmin() {
|
||||
http.Error(w, `{"error":"admin access required"}`, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func checkDomainAccess(database *db.DB) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
authCtx := GetAuthContext(r)
|
||||
if authCtx == nil {
|
||||
http.Error(w, `{"error":"authentication required"}`, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
domainName := extractDomainNameFromPath(r.URL.Path)
|
||||
if domainName == "" {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
canAccess, _ := database.CanAccessDomain(authCtx.UserID, domainName, authCtx.IsAdmin())
|
||||
if !canAccess {
|
||||
http.Error(w, `{"error":"access denied"}`, http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var _ = auth.RoleAdmin
|
||||
247
backend/internal/api/router.go
Normal file
247
backend/internal/api/router.go
Normal file
|
|
@ -0,0 +1,247 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/imc-vibe/backend/internal/api/handlers"
|
||||
"github.com/imc-vibe/backend/internal/auth"
|
||||
"github.com/imc-vibe/backend/internal/config"
|
||||
"github.com/imc-vibe/backend/internal/db"
|
||||
)
|
||||
|
||||
type Router struct {
|
||||
authHandler *handlers.AuthHandler
|
||||
domainHandler *handlers.DomainHandler
|
||||
userHandler *handlers.UserHandler
|
||||
aliasHandler *handlers.AliasHandler
|
||||
statsHandler *handlers.StatsHandler
|
||||
queueHandler *handlers.QueueHandler
|
||||
logsHandler *handlers.LogsHandler
|
||||
jwtManager *auth.JWTManager
|
||||
db *db.DB
|
||||
}
|
||||
|
||||
func New(database *db.DB, cfg *config.Config) *Router {
|
||||
jwtManager := auth.NewJWTManager(cfg.JWTSecret, "imc-vibe")
|
||||
|
||||
return &Router{
|
||||
authHandler: handlers.NewAuthHandler(database, jwtManager),
|
||||
domainHandler: handlers.NewDomainHandler(database),
|
||||
userHandler: handlers.NewUserHandler(database),
|
||||
aliasHandler: handlers.NewAliasHandler(database),
|
||||
statsHandler: handlers.NewStatsHandler(database),
|
||||
queueHandler: handlers.NewQueueHandler(),
|
||||
logsHandler: handlers.NewLogsHandler(),
|
||||
jwtManager: jwtManager,
|
||||
db: database,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) Handler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
path := req.URL.Path
|
||||
|
||||
// Only handle /api/* routes
|
||||
if !strings.HasPrefix(path, "/api/") {
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
// Public routes - no auth required
|
||||
switch path {
|
||||
case "/api/auth/login":
|
||||
r.authHandler.Login(w, req)
|
||||
return
|
||||
case "/api/auth/forgot":
|
||||
r.authHandler.ForgotPassword(w, req)
|
||||
return
|
||||
case "/api/auth/logout":
|
||||
r.authHandler.Logout(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
// Protected routes - auth required
|
||||
req = r.validateAuth(req)
|
||||
if req == nil {
|
||||
http.Error(w, `{"error":"not authenticated"}`, http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case path == "/api/auth/me":
|
||||
r.authHandler.Me(w, req)
|
||||
case path == "/api/auth/change-password":
|
||||
r.authHandler.ChangePassword(w, req)
|
||||
case path == "/api/stats":
|
||||
r.statsHandler.Get(w, req)
|
||||
case strings.HasPrefix(path, "/api/domains/"):
|
||||
if strings.Contains(strings.TrimPrefix(path, "/api/domains/"), "/users") {
|
||||
r.handleDomainUsers(w, req)
|
||||
} else if strings.Contains(strings.TrimPrefix(path, "/api/domains/"), "/aliases") {
|
||||
r.handleDomainAliases(w, req)
|
||||
} else {
|
||||
r.handleDomains(w, req)
|
||||
}
|
||||
case strings.HasPrefix(path, "/api/domains"):
|
||||
r.handleDomains(w, req)
|
||||
case strings.HasPrefix(path, "/api/queue"):
|
||||
r.handleQueue(w, req)
|
||||
case path == "/api/logs":
|
||||
r.logsHandler.List(w, req)
|
||||
default:
|
||||
http.NotFound(w, req)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Router) validateAuth(req *http.Request) *http.Request {
|
||||
authHeader := req.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
parts := strings.SplitN(authHeader, " ", 2)
|
||||
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
|
||||
return nil
|
||||
}
|
||||
|
||||
claims, err := r.jwtManager.ValidateToken(parts[1])
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
authCtx := &auth.Context{
|
||||
UserID: claims.UserID,
|
||||
Username: claims.Username,
|
||||
Role: auth.Role(claims.Role),
|
||||
}
|
||||
|
||||
ctx := context.WithValue(req.Context(), handlers.AuthContextKey, authCtx)
|
||||
return req.WithContext(ctx)
|
||||
}
|
||||
|
||||
func (r *Router) handleDomains(w http.ResponseWriter, req *http.Request) {
|
||||
switch req.Method {
|
||||
case http.MethodGet:
|
||||
if len(req.URL.Path) > len("/api/domains/") {
|
||||
r.domainHandler.Get(w, req)
|
||||
} else {
|
||||
r.domainHandler.List(w, req)
|
||||
}
|
||||
case http.MethodPost:
|
||||
r.domainHandler.Create(w, req)
|
||||
case http.MethodDelete:
|
||||
r.domainHandler.Delete(w, req)
|
||||
default:
|
||||
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) handleDomainUsers(w http.ResponseWriter, req *http.Request) {
|
||||
path := req.URL.Path
|
||||
parts := strings.Split(strings.TrimPrefix(path, "/api/domains/"), "/")
|
||||
if len(parts) < 2 || parts[1] != "users" {
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
domainName := parts[0]
|
||||
var idStr string
|
||||
if len(parts) >= 3 {
|
||||
idStr = parts[2]
|
||||
}
|
||||
|
||||
switch req.Method {
|
||||
case http.MethodGet:
|
||||
if idStr != "" {
|
||||
req.URL.Path = "/api/domains/" + domainName + "/users/" + idStr
|
||||
r.userHandler.Get(w, req)
|
||||
} else {
|
||||
req.URL.Path = "/api/domains/" + domainName + "/users"
|
||||
r.userHandler.List(w, req)
|
||||
}
|
||||
case http.MethodPost:
|
||||
req.URL.Path = "/api/domains/" + domainName + "/users"
|
||||
r.userHandler.Create(w, req)
|
||||
case http.MethodPut:
|
||||
if idStr != "" {
|
||||
req.URL.Path = "/api/domains/" + domainName + "/users/" + idStr
|
||||
r.userHandler.Update(w, req)
|
||||
} else {
|
||||
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
|
||||
}
|
||||
case http.MethodDelete:
|
||||
if idStr != "" {
|
||||
req.URL.Path = "/api/domains/" + domainName + "/users/" + idStr
|
||||
r.userHandler.Delete(w, req)
|
||||
} else {
|
||||
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
|
||||
}
|
||||
default:
|
||||
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) handleDomainAliases(w http.ResponseWriter, req *http.Request) {
|
||||
path := req.URL.Path
|
||||
parts := strings.Split(strings.TrimPrefix(path, "/api/domains/"), "/")
|
||||
if len(parts) < 2 || parts[1] != "aliases" {
|
||||
http.NotFound(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
domainName := parts[0]
|
||||
var idStr string
|
||||
if len(parts) >= 3 {
|
||||
idStr = parts[2]
|
||||
}
|
||||
|
||||
switch req.Method {
|
||||
case http.MethodGet:
|
||||
req.URL.Path = "/api/domains/" + domainName + "/aliases"
|
||||
r.aliasHandler.List(w, req)
|
||||
case http.MethodPost:
|
||||
req.URL.Path = "/api/domains/" + domainName + "/aliases"
|
||||
r.aliasHandler.Create(w, req)
|
||||
case http.MethodDelete:
|
||||
if idStr != "" {
|
||||
req.URL.Path = "/api/domains/" + domainName + "/aliases/" + idStr
|
||||
r.aliasHandler.Delete(w, req)
|
||||
} else {
|
||||
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
|
||||
}
|
||||
default:
|
||||
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) handleQueue(w http.ResponseWriter, req *http.Request) {
|
||||
path := req.URL.Path
|
||||
id := ""
|
||||
if len(path) > len("/api/queue/") {
|
||||
id = path[len("/api/queue/"):]
|
||||
}
|
||||
|
||||
switch req.Method {
|
||||
case http.MethodGet:
|
||||
r.queueHandler.List(w, req)
|
||||
case http.MethodPost:
|
||||
if id != "" {
|
||||
req.URL.Path = "/api/queue/" + id
|
||||
r.queueHandler.Requeue(w, req)
|
||||
} else {
|
||||
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
|
||||
}
|
||||
case http.MethodDelete:
|
||||
if id != "" {
|
||||
req.URL.Path = "/api/queue/" + id
|
||||
r.queueHandler.Delete(w, req)
|
||||
} else {
|
||||
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
|
||||
}
|
||||
default:
|
||||
http.Error(w, `{"error":"method not allowed"}`, http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
85
backend/internal/auth/jwt.go
Normal file
85
backend/internal/auth/jwt.go
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidToken = errors.New("invalid token")
|
||||
ErrExpiredToken = errors.New("token has expired")
|
||||
)
|
||||
|
||||
type Claims struct {
|
||||
UserID uint `json:"userId"`
|
||||
Username string `json:"username"`
|
||||
Role string `json:"role"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
type JWTManager struct {
|
||||
secretKey []byte
|
||||
issuer string
|
||||
}
|
||||
|
||||
func NewJWTManager(secretKey, issuer string) *JWTManager {
|
||||
return &JWTManager{
|
||||
secretKey: []byte(secretKey),
|
||||
issuer: issuer,
|
||||
}
|
||||
}
|
||||
|
||||
func (j *JWTManager) GenerateToken(userID uint, username, role string, duration time.Duration) (string, error) {
|
||||
claims := &Claims{
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
Role: role,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(duration)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
Issuer: j.issuer,
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString(j.secretKey)
|
||||
}
|
||||
|
||||
func (j *JWTManager) ValidateToken(tokenString string) (*Claims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
return j.secretKey, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, jwt.ErrTokenExpired) {
|
||||
return nil, ErrExpiredToken
|
||||
}
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(*Claims)
|
||||
if !ok || !token.Valid {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func HashPassword(password string) (string, error) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(hash), nil
|
||||
}
|
||||
|
||||
func CheckPassword(password, hash string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
18
backend/internal/auth/rbac.go
Normal file
18
backend/internal/auth/rbac.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package auth
|
||||
|
||||
type Role string
|
||||
|
||||
const (
|
||||
RoleAdmin Role = "admin"
|
||||
RoleUser Role = "user"
|
||||
)
|
||||
|
||||
type Context struct {
|
||||
UserID uint
|
||||
Username string
|
||||
Role Role
|
||||
}
|
||||
|
||||
func (c *Context) IsAdmin() bool {
|
||||
return c.Role == RoleAdmin
|
||||
}
|
||||
56
backend/internal/config/config.go
Normal file
56
backend/internal/config/config.go
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
DBHost string
|
||||
DBPort string
|
||||
DBUser string
|
||||
DBPassword string
|
||||
DBName string
|
||||
Port string
|
||||
UseEmbedded bool
|
||||
JWTSecret string
|
||||
AdminUser string
|
||||
AdminPassword string
|
||||
MailDataDir string
|
||||
PostqueuePath string
|
||||
PostsuperPath string
|
||||
DovecotQuotaCmd string
|
||||
JournalctlPath string
|
||||
RspamdAPI string
|
||||
}
|
||||
|
||||
func Load() *Config {
|
||||
godotenv.Load("backend/.env")
|
||||
|
||||
return &Config{
|
||||
DBHost: getEnv("DB_HOST", "localhost"),
|
||||
DBPort: getEnv("DB_PORT", "3306"),
|
||||
DBUser: getEnv("DB_USER", "mailadmin"),
|
||||
DBPassword: getEnv("DB_PASSWORD", ""),
|
||||
DBName: getEnv("DB_NAME", "mailserver"),
|
||||
Port: getEnv("PORT", "8080"),
|
||||
UseEmbedded: getEnv("USE_EMBEDDED", "true") == "true",
|
||||
JWTSecret: getEnv("JWT_SECRET", "change-this-secret"),
|
||||
AdminUser: getEnv("ADMIN_USER", ""),
|
||||
AdminPassword: getEnv("ADMIN_PASSWORD", ""),
|
||||
MailDataDir: getEnv("MAIL_DATA_DIR", "/var/vmail"),
|
||||
PostqueuePath: getEnv("POSTQUEUE_PATH", "/usr/sbin/postqueue"),
|
||||
PostsuperPath: getEnv("POSTSUPER_PATH", "/usr/sbin/postsuper"),
|
||||
DovecotQuotaCmd: getEnv("DOVECOT_QUOTA_CMD", "/usr/bin/doveadm"),
|
||||
JournalctlPath: getEnv("JOURNALCTL_PATH", "/usr/bin/journalctl"),
|
||||
RspamdAPI: getEnv("RSPAMD_API", "http://127.0.0.1:11334"),
|
||||
}
|
||||
}
|
||||
|
||||
func getEnv(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
70
backend/internal/db/aliases.go
Normal file
70
backend/internal/db/aliases.go
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
package db
|
||||
|
||||
func (d *DB) GetAllAliases() ([]AliasWithDomain, error) {
|
||||
var aliases []Alias
|
||||
if err := d.Preload("Domain").Find(&aliases).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]AliasWithDomain, len(aliases))
|
||||
for i, a := range aliases {
|
||||
result[i] = AliasWithDomain{
|
||||
Alias: a,
|
||||
DomainName: "",
|
||||
}
|
||||
if a.Domain != nil {
|
||||
result[i].DomainName = a.Domain.Name
|
||||
}
|
||||
}
|
||||
|
||||
if result == nil {
|
||||
result = []AliasWithDomain{}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (d *DB) GetAliasesByDomain(domainID uint) ([]Alias, error) {
|
||||
var aliases []Alias
|
||||
if err := d.Where("domain_id = ?", domainID).Find(&aliases).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if aliases == nil {
|
||||
aliases = []Alias{}
|
||||
}
|
||||
return aliases, nil
|
||||
}
|
||||
|
||||
func (d *DB) GetAliasByID(id uint) (*Alias, error) {
|
||||
var alias Alias
|
||||
if err := d.Preload("Domain").First(&alias, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &alias, nil
|
||||
}
|
||||
|
||||
func (d *DB) CreateAlias(source, destination string) (*Alias, error) {
|
||||
alias := Alias{
|
||||
Source: source,
|
||||
Destination: destination,
|
||||
}
|
||||
if err := d.Create(&alias).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &alias, nil
|
||||
}
|
||||
|
||||
func (d *DB) CreateAliasInDomain(source, destination string, domainID uint) (*Alias, error) {
|
||||
alias := Alias{
|
||||
DomainID: domainID,
|
||||
Source: source,
|
||||
Destination: destination,
|
||||
}
|
||||
if err := d.Create(&alias).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &alias, nil
|
||||
}
|
||||
|
||||
func (d *DB) DeleteAlias(id uint) error {
|
||||
return d.Delete(&Alias{}, id).Error
|
||||
}
|
||||
164
backend/internal/db/db.go
Normal file
164
backend/internal/db/db.go
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
package db
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
|
||||
"github.com/imc-vibe/backend/internal/config"
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
*gorm.DB
|
||||
}
|
||||
|
||||
type Domain struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"uniqueIndex;size:50;not null" json:"name"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Users []User `gorm:"foreignKey:DomainID" json:"users,omitempty"`
|
||||
Aliases []Alias `gorm:"foreignKey:DomainID" json:"aliases,omitempty"`
|
||||
}
|
||||
|
||||
func (Domain) TableName() string { return "virtual_domains" }
|
||||
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
DomainID uint `gorm:"not null" json:"domainId"`
|
||||
Email string `gorm:"uniqueIndex;size:100;not null" json:"email"`
|
||||
Password string `gorm:"size:150;not null" json:"-"`
|
||||
Quota int64 `gorm:"default:0" json:"quota"`
|
||||
Domain *Domain `gorm:"foreignKey:DomainID" json:"domain,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt,omitempty"`
|
||||
}
|
||||
|
||||
func (User) TableName() string { return "virtual_users" }
|
||||
|
||||
type Alias struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
DomainID uint `gorm:"not null" json:"domainId"`
|
||||
Source string `gorm:"size:100;not null" json:"source"`
|
||||
Destination string `gorm:"size:100;not null" json:"destination"`
|
||||
Domain *Domain `gorm:"foreignKey:DomainID" json:"domain,omitempty"`
|
||||
}
|
||||
|
||||
func (Alias) TableName() string { return "virtual_aliases" }
|
||||
|
||||
type ImcUser struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Username string `gorm:"uniqueIndex;size:100;not null" json:"username"`
|
||||
PasswordHash string `gorm:"size:255;not null" json:"-"`
|
||||
Role string `gorm:"type:enum('admin','user');default:'user'" json:"role"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Domains []ImcUserDomain `gorm:"foreignKey:UserID" json:"domains,omitempty"`
|
||||
}
|
||||
|
||||
func (ImcUser) TableName() string { return "imc_users" }
|
||||
|
||||
type ImcUserDomain struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `gorm:"not null" json:"userId"`
|
||||
DomainID uint `gorm:"not null" json:"domainId"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Domain *Domain `gorm:"foreignKey:DomainID" json:"domain,omitempty"`
|
||||
}
|
||||
|
||||
func (ImcUserDomain) TableName() string { return "imc_users2domains" }
|
||||
|
||||
type ImcLoginAttempt struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Email string `gorm:"size:100;not null" json:"email"`
|
||||
IPAddress string `gorm:"size:45;not null" json:"ipAddress"`
|
||||
AttemptedAt time.Time `json:"attemptedAt"`
|
||||
Successful bool `gorm:"default:false" json:"successful"`
|
||||
}
|
||||
|
||||
func (ImcLoginAttempt) TableName() string { return "imc_login_attempts" }
|
||||
|
||||
type DomainStats struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
UserCount int64 `json:"userCount"`
|
||||
AliasCount int64 `json:"aliasCount"`
|
||||
}
|
||||
|
||||
type AliasWithDomain struct {
|
||||
Alias
|
||||
DomainName string `json:"domainName"`
|
||||
}
|
||||
|
||||
type UserWithDomain struct {
|
||||
User
|
||||
DomainName string `json:"domainName"`
|
||||
}
|
||||
|
||||
func Connect(cfg *config.Config) (*DB, error) {
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true&charset=utf8mb4",
|
||||
cfg.DBUser, cfg.DBPassword, cfg.DBHost, cfg.DBPort, cfg.DBName)
|
||||
|
||||
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to database: %w", err)
|
||||
}
|
||||
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sqlDB.SetMaxOpenConns(25)
|
||||
sqlDB.SetMaxIdleConns(5)
|
||||
sqlDB.SetConnMaxLifetime(5 * time.Minute)
|
||||
|
||||
return &DB{db}, nil
|
||||
}
|
||||
|
||||
func (d *DB) InitSchema() error {
|
||||
imcUsersSQL := `
|
||||
CREATE TABLE IF NOT EXISTS imc_users (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(100) NOT NULL UNIQUE,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
role ENUM('admin','user') DEFAULT 'user',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_username (username),
|
||||
INDEX idx_role (role)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
`
|
||||
if err := d.Exec(imcUsersSQL).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
loginAttemptsSQL := `
|
||||
CREATE TABLE IF NOT EXISTS imc_login_attempts (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
email VARCHAR(100) NOT NULL,
|
||||
ip_address VARCHAR(45) NOT NULL,
|
||||
attempted_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
successful BOOLEAN DEFAULT FALSE,
|
||||
INDEX idx_email_time (email, attempted_at),
|
||||
INDEX idx_ip_time (ip_address, attempted_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
`
|
||||
if err := d.Exec(loginAttemptsSQL).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
users2DomainsSQL := `
|
||||
CREATE TABLE IF NOT EXISTS imc_users2domains (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT UNSIGNED NOT NULL,
|
||||
domain_id INT UNSIGNED NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY unique_user_domain (user_id, domain_id),
|
||||
INDEX idx_user_id (user_id),
|
||||
INDEX idx_domain_id (domain_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
`
|
||||
return d.Exec(users2DomainsSQL).Error
|
||||
}
|
||||
68
backend/internal/db/domains.go
Normal file
68
backend/internal/db/domains.go
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
package db
|
||||
|
||||
func (d *DB) GetAllDomains() ([]DomainStats, error) {
|
||||
var domains []Domain
|
||||
if err := d.Find(&domains).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stats := make([]DomainStats, len(domains))
|
||||
for i, dom := range domains {
|
||||
var userCount, aliasCount int64
|
||||
d.Model(&User{}).Where("domain_id = ?", dom.ID).Count(&userCount)
|
||||
d.Model(&Alias{}).Where("domain_id = ?", dom.ID).Count(&aliasCount)
|
||||
|
||||
stats[i] = DomainStats{
|
||||
ID: dom.ID,
|
||||
Name: dom.Name,
|
||||
UserCount: userCount,
|
||||
AliasCount: aliasCount,
|
||||
}
|
||||
}
|
||||
|
||||
if stats == nil {
|
||||
stats = []DomainStats{}
|
||||
}
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
func (d *DB) GetDomainByID(id uint) (*Domain, error) {
|
||||
var domain Domain
|
||||
if err := d.Where("id = ?", id).First(&domain).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &domain, nil
|
||||
}
|
||||
|
||||
func (d *DB) GetDomainByName(name string) (*Domain, error) {
|
||||
var domain Domain
|
||||
if err := d.Where("name = ?", name).First(&domain).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &domain, nil
|
||||
}
|
||||
|
||||
func (d *DB) CreateDomain(name string) (*Domain, error) {
|
||||
domain := Domain{Name: name}
|
||||
if err := d.Create(&domain).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &domain, nil
|
||||
}
|
||||
|
||||
func (d *DB) DeleteDomain(id uint) error {
|
||||
tx := d.Begin()
|
||||
defer func() { tx.Rollback() }()
|
||||
|
||||
if err := tx.Where("domain_id = ?", id).Delete(&User{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("domain_id = ?", id).Delete(&Alias{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Delete(&Domain{}, id).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tx.Commit().Error
|
||||
}
|
||||
122
backend/internal/db/imc_users.go
Normal file
122
backend/internal/db/imc_users.go
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
package db
|
||||
|
||||
func (d *DB) GetImcUserByUsername(username string) (*ImcUser, error) {
|
||||
var user ImcUser
|
||||
if err := d.Preload("Domains.Domain").Where("username = ?", username).First(&user).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (d *DB) GetImcUserByID(id uint) (*ImcUser, error) {
|
||||
var user ImcUser
|
||||
if err := d.Preload("Domains.Domain").First(&user, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (d *DB) CreateImcUser(username, passwordHash, role string) (*ImcUser, error) {
|
||||
user := ImcUser{
|
||||
Username: username,
|
||||
PasswordHash: passwordHash,
|
||||
Role: role,
|
||||
}
|
||||
if err := d.Create(&user).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (d *DB) UpdateImcUserPassword(id uint, passwordHash string) error {
|
||||
return d.Model(&ImcUser{}).Where("id = ?", id).Update("password_hash", passwordHash).Error
|
||||
}
|
||||
|
||||
func (d *DB) DeleteImcUser(id uint) error {
|
||||
return d.Delete(&ImcUser{}, id).Error
|
||||
}
|
||||
|
||||
func (d *DB) EnsureAdminUser(username, passwordHash string) error {
|
||||
var user ImcUser
|
||||
err := d.Where("username = ? AND role = ?", username, "admin").First(&user).Error
|
||||
if err == nil {
|
||||
return d.Model(&ImcUser{}).Where("id = ?", user.ID).Update("password_hash", passwordHash).Error
|
||||
}
|
||||
_, err = d.CreateImcUser(username, passwordHash, "admin")
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *DB) GetUserAccessibleDomains(userID uint, isAdmin bool) ([]Domain, error) {
|
||||
if isAdmin {
|
||||
var domains []Domain
|
||||
if err := d.Find(&domains).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return domains, nil
|
||||
}
|
||||
|
||||
var domains []Domain
|
||||
err := d.Table("imc_users2domains").
|
||||
Select("virtual_domains.*").
|
||||
Joins("JOIN virtual_domains ON virtual_domains.id = imc_users2domains.domain_id").
|
||||
Where("imc_users2domains.user_id = ?", userID).
|
||||
Find(&domains).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if domains == nil {
|
||||
domains = []Domain{}
|
||||
}
|
||||
return domains, nil
|
||||
}
|
||||
|
||||
func (d *DB) CanAccessDomain(userID uint, domainName string, isAdmin bool) (bool, error) {
|
||||
if isAdmin {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
var count int64
|
||||
err := d.Table("imc_users2domains").
|
||||
Select("COUNT(*)").
|
||||
Joins("JOIN virtual_domains ON virtual_domains.id = imc_users2domains.domain_id").
|
||||
Where("imc_users2domains.user_id = ? AND virtual_domains.name = ?", userID, domainName).
|
||||
Count(&count).Error
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (d *DB) AddUserToDomain(userID, domainID uint) error {
|
||||
ud := ImcUserDomain{
|
||||
UserID: userID,
|
||||
DomainID: domainID,
|
||||
}
|
||||
return d.Create(&ud).Error
|
||||
}
|
||||
|
||||
func (d *DB) RemoveUserFromDomain(userID, domainID uint) error {
|
||||
return d.Where("user_id = ? AND domain_id = ?", userID, domainID).Delete(&ImcUserDomain{}).Error
|
||||
}
|
||||
|
||||
func (d *DB) GetUsersForDomain(domainID uint) ([]ImcUser, error) {
|
||||
var users []ImcUser
|
||||
err := d.Table("imc_users2domains").
|
||||
Select("imc_users.*").
|
||||
Joins("JOIN imc_users ON imc_users.id = imc_users2domains.user_id").
|
||||
Where("imc_users2domains.domain_id = ?", domainID).
|
||||
Find(&users).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if users == nil {
|
||||
users = []ImcUser{}
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (d *DB) IsUserInDomain(userID, domainID uint) (bool, error) {
|
||||
var count int64
|
||||
err := d.Model(&ImcUserDomain{}).Where("user_id = ? AND domain_id = ?", userID, domainID).Count(&count).Error
|
||||
return count > 0, err
|
||||
}
|
||||
76
backend/internal/db/users.go
Normal file
76
backend/internal/db/users.go
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
package db
|
||||
|
||||
func (d *DB) GetAllUsers() ([]User, error) {
|
||||
var users []User
|
||||
if err := d.Preload("Domain").Find(&users).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if users == nil {
|
||||
users = []User{}
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (d *DB) GetUsersByDomain(domainID uint) ([]User, error) {
|
||||
var users []User
|
||||
if err := d.Where("domain_id = ?", domainID).Find(&users).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if users == nil {
|
||||
users = []User{}
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
func (d *DB) GetUserByID(id uint) (*User, error) {
|
||||
var user User
|
||||
if err := d.Preload("Domain").First(&user, id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (d *DB) GetUserByEmail(email string) (*User, error) {
|
||||
var user User
|
||||
if err := d.Where("email = ?", email).First(&user).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (d *DB) CreateUser(email, passwordHash string, quota int64) (*User, error) {
|
||||
user := User{
|
||||
Email: email,
|
||||
Password: passwordHash,
|
||||
Quota: quota,
|
||||
}
|
||||
if err := d.Create(&user).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (d *DB) CreateUserInDomain(email, passwordHash string, quota int64, domainID uint) (*User, error) {
|
||||
user := User{
|
||||
DomainID: domainID,
|
||||
Email: email,
|
||||
Password: passwordHash,
|
||||
Quota: quota,
|
||||
}
|
||||
if err := d.Create(&user).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (d *DB) UpdateUserPassword(id uint, passwordHash string) error {
|
||||
return d.Model(&User{}).Where("id = ?", id).Update("password", passwordHash).Error
|
||||
}
|
||||
|
||||
func (d *DB) UpdateUserQuota(id uint, quota int64) error {
|
||||
return d.Model(&User{}).Where("id = ?", id).Update("quota", quota).Error
|
||||
}
|
||||
|
||||
func (d *DB) DeleteUser(id uint) error {
|
||||
return d.Delete(&User{}, id).Error
|
||||
}
|
||||
88
backend/internal/mail/logs.go
Normal file
88
backend/internal/mail/logs.go
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
package mail
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LogEntry struct {
|
||||
Timestamp string `json:"timestamp"`
|
||||
Priority string `json:"priority"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func GetLogs(hours int, filter string) ([]LogEntry, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
args := []string{
|
||||
"-u", "postfix",
|
||||
"--since", formatDuration(hours),
|
||||
"--no-pager",
|
||||
"-o", "short=false",
|
||||
}
|
||||
|
||||
cmd := exec.CommandContext(ctx, "journalctl", args...)
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var entries []LogEntry
|
||||
scanner := bufio.NewScanner(&out)
|
||||
priorityRegex := regexp.MustCompile(`\[([^\]]+)\]`)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
if filter != "" && !strings.Contains(strings.ToLower(line), strings.ToLower(filter)) {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.SplitN(line, " ", 4)
|
||||
if len(parts) < 4 {
|
||||
continue
|
||||
}
|
||||
|
||||
timestamp := parts[0] + " " + parts[1]
|
||||
|
||||
matches := priorityRegex.FindStringSubmatch(parts[2])
|
||||
priority := "info"
|
||||
if len(matches) > 1 {
|
||||
priority = matches[1]
|
||||
} else if strings.Contains(parts[2], "err") {
|
||||
priority = "err"
|
||||
} else if strings.Contains(parts[2], "warning") || strings.Contains(parts[2], "warn") {
|
||||
priority = "warning"
|
||||
}
|
||||
|
||||
message := parts[3]
|
||||
|
||||
if filter != "" && !strings.Contains(strings.ToLower(message), strings.ToLower(filter)) {
|
||||
continue
|
||||
}
|
||||
|
||||
entries = append(entries, LogEntry{
|
||||
Timestamp: timestamp,
|
||||
Priority: priority,
|
||||
Message: message,
|
||||
})
|
||||
}
|
||||
|
||||
if entries == nil {
|
||||
entries = []LogEntry{}
|
||||
}
|
||||
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func formatDuration(hours int) string {
|
||||
return time.Now().Add(-time.Duration(hours) * time.Hour).Format("15:04:05 2006-01-02")
|
||||
}
|
||||
123
backend/internal/mail/queue.go
Normal file
123
backend/internal/mail/queue.go
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
package mail
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type QueueEntry struct {
|
||||
ID string `json:"id"`
|
||||
Sender string `json:"sender"`
|
||||
Recipients []string `json:"recipients"`
|
||||
Size int `json:"size"`
|
||||
Time string `json:"time"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
var queueLineRegex = regexp.MustCompile(`^([A-Fa-f0-9]+)\s+(\d+)\s+(\w+)\s+([A-Za-z]+\s+\d+\s+[\d:]+)\s+([^\s]+)\s+(.+)$`)
|
||||
|
||||
func GetQueue() ([]QueueEntry, error) {
|
||||
cmd := exec.Command("postqueue", "-p", "-j")
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var entries []QueueEntry
|
||||
scanner := bufio.NewScanner(&out)
|
||||
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
if line == "" || !strings.HasPrefix(line, "{") {
|
||||
continue
|
||||
}
|
||||
|
||||
var entry struct {
|
||||
QueueID string `json:"queueid"`
|
||||
Sender string `json:"sender"`
|
||||
Recipients []struct {
|
||||
Address string `json:"address"`
|
||||
} `json:"recipients"`
|
||||
Size int64 `json:"size"`
|
||||
Arrival string `json:"arrival_timestamp"`
|
||||
Delay string `json:"delay_reason"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(line), &entry); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
recipients := make([]string, len(entry.Recipients))
|
||||
for i, r := range entry.Recipients {
|
||||
recipients[i] = r.Address
|
||||
}
|
||||
|
||||
entries = append(entries, QueueEntry{
|
||||
ID: entry.QueueID,
|
||||
Sender: entry.Sender,
|
||||
Recipients: recipients,
|
||||
Size: int(entry.Size),
|
||||
Time: entry.Arrival,
|
||||
Reason: entry.Delay,
|
||||
})
|
||||
}
|
||||
|
||||
if entries == nil {
|
||||
entries = []QueueEntry{}
|
||||
}
|
||||
|
||||
return entries, nil
|
||||
}
|
||||
|
||||
func RequeueMail(id string) error {
|
||||
cmd := exec.Command("postqueue", "-f", "-v")
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
cmd.Stderr = &out
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeleteFromQueue(id string) error {
|
||||
cmd := exec.Command("postsuper", "-d", id)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func GetQueueCount() (int, error) {
|
||||
cmd := exec.Command("postqueue", "-p")
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
count := 0
|
||||
scanner := bufio.NewScanner(&out)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.Contains(line, " mail queue") {
|
||||
parts := strings.Fields(line)
|
||||
for i, p := range parts {
|
||||
if n, err := strconv.Atoi(p); err == nil && n > 0 {
|
||||
count = n
|
||||
if i > 0 && parts[i-1] == "in" {
|
||||
return n, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
||||
336
frontend/.svelte-kit/ambient.d.ts
vendored
Normal file
336
frontend/.svelte-kit/ambient.d.ts
vendored
Normal file
|
|
@ -0,0 +1,336 @@
|
|||
|
||||
// this file is generated — do not edit it
|
||||
|
||||
|
||||
/// <reference types="@sveltejs/kit" />
|
||||
|
||||
/**
|
||||
* This module provides access to environment variables that are injected _statically_ into your bundle at build time and are limited to _private_ access.
|
||||
*
|
||||
* | | Runtime | Build time |
|
||||
* | ------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
|
||||
* | Private | [`$env/dynamic/private`](https://svelte.dev/docs/kit/$env-dynamic-private) | [`$env/static/private`](https://svelte.dev/docs/kit/$env-static-private) |
|
||||
* | Public | [`$env/dynamic/public`](https://svelte.dev/docs/kit/$env-dynamic-public) | [`$env/static/public`](https://svelte.dev/docs/kit/$env-static-public) |
|
||||
*
|
||||
* Static environment variables are [loaded by Vite](https://vitejs.dev/guide/env-and-mode.html#env-files) from `.env` files and `process.env` at build time and then statically injected into your bundle at build time, enabling optimisations like dead code elimination.
|
||||
*
|
||||
* **_Private_ access:**
|
||||
*
|
||||
* - This module cannot be imported into client-side code
|
||||
* - This module only includes variables that _do not_ begin with [`config.kit.env.publicPrefix`](https://svelte.dev/docs/kit/configuration#env) _and do_ start with [`config.kit.env.privatePrefix`](https://svelte.dev/docs/kit/configuration#env) (if configured)
|
||||
*
|
||||
* For example, given the following build time environment:
|
||||
*
|
||||
* ```env
|
||||
* ENVIRONMENT=production
|
||||
* PUBLIC_BASE_URL=http://site.com
|
||||
* ```
|
||||
*
|
||||
* With the default `publicPrefix` and `privatePrefix`:
|
||||
*
|
||||
* ```ts
|
||||
* import { ENVIRONMENT, PUBLIC_BASE_URL } from '$env/static/private';
|
||||
*
|
||||
* console.log(ENVIRONMENT); // => "production"
|
||||
* console.log(PUBLIC_BASE_URL); // => throws error during build
|
||||
* ```
|
||||
*
|
||||
* The above values will be the same _even if_ different values for `ENVIRONMENT` or `PUBLIC_BASE_URL` are set at runtime, as they are statically replaced in your code with their build time values.
|
||||
*/
|
||||
declare module '$env/static/private' {
|
||||
export const SHELL: string;
|
||||
export const npm_command: string;
|
||||
export const LSCOLORS: string;
|
||||
export const SESSION_MANAGER: string;
|
||||
export const QT_ACCESSIBILITY: string;
|
||||
export const COLORTERM: string;
|
||||
export const XDG_CONFIG_DIRS: string;
|
||||
export const LESS: string;
|
||||
export const XDG_SESSION_PATH: string;
|
||||
export const GNOME_DESKTOP_SESSION_ID: string;
|
||||
export const GTK_IM_MODULE: string;
|
||||
export const QT_IM_MODULES: string;
|
||||
export const GNOME_KEYRING_CONTROL: string;
|
||||
export const LANGUAGE: string;
|
||||
export const NODE: string;
|
||||
export const SSH_AUTH_SOCK: string;
|
||||
export const AGENT: string;
|
||||
export const npm_config_local_prefix: string;
|
||||
export const XMODIFIERS: string;
|
||||
export const DESKTOP_SESSION: string;
|
||||
export const GTK_MODULES: string;
|
||||
export const XDG_SEAT: string;
|
||||
export const PWD: string;
|
||||
export const XDG_SESSION_DESKTOP: string;
|
||||
export const LOGNAME: string;
|
||||
export const XDG_SESSION_TYPE: string;
|
||||
export const GPG_AGENT_INFO: string;
|
||||
export const _: string;
|
||||
export const XAUTHORITY: string;
|
||||
export const XDG_GREETER_DATA_DIR: string;
|
||||
export const GDM_LANG: string;
|
||||
export const HOME: string;
|
||||
export const OPENCODE: string;
|
||||
export const LANG: string;
|
||||
export const LS_COLORS: string;
|
||||
export const XDG_CURRENT_DESKTOP: string;
|
||||
export const npm_package_version: string;
|
||||
export const VTE_VERSION: string;
|
||||
export const DEBEMAIL: string;
|
||||
export const XDG_SEAT_PATH: string;
|
||||
export const GNOME_TERMINAL_SCREEN: string;
|
||||
export const CLUTTER_IM_MODULE: string;
|
||||
export const MFLAGS: string;
|
||||
export const npm_lifecycle_script: string;
|
||||
export const XDG_SESSION_CLASS: string;
|
||||
export const MAKEFLAGS: string;
|
||||
export const TERM: string;
|
||||
export const npm_package_name: string;
|
||||
export const ZSH: string;
|
||||
export const GTK_OVERLAY_SCROLLING: string;
|
||||
export const LIBVIRT_DEFAULT_URI: string;
|
||||
export const USER: string;
|
||||
export const GNOME_TERMINAL_SERVICE: string;
|
||||
export const DISPLAY: string;
|
||||
export const npm_lifecycle_event: string;
|
||||
export const SHLVL: string;
|
||||
export const PAGER: string;
|
||||
export const MAKELEVEL: string;
|
||||
export const QT_IM_MODULE: string;
|
||||
export const XDG_VTNR: string;
|
||||
export const XDG_SESSION_ID: string;
|
||||
export const npm_config_user_agent: string;
|
||||
export const npm_execpath: string;
|
||||
export const XDG_RUNTIME_DIR: string;
|
||||
export const npm_package_json: string;
|
||||
export const BUN_INSTALL: string;
|
||||
export const EMAIL: string;
|
||||
export const GTK3_MODULES: string;
|
||||
export const XDG_DATA_DIRS: string;
|
||||
export const PATH: string;
|
||||
export const GDMSESSION: string;
|
||||
export const DBUS_SESSION_BUS_ADDRESS: string;
|
||||
export const THOR_MERGE: string;
|
||||
export const npm_node_execpath: string;
|
||||
export const OLDPWD: string;
|
||||
export const NODE_ENV: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* This module provides access to environment variables that are injected _statically_ into your bundle at build time and are _publicly_ accessible.
|
||||
*
|
||||
* | | Runtime | Build time |
|
||||
* | ------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
|
||||
* | Private | [`$env/dynamic/private`](https://svelte.dev/docs/kit/$env-dynamic-private) | [`$env/static/private`](https://svelte.dev/docs/kit/$env-static-private) |
|
||||
* | Public | [`$env/dynamic/public`](https://svelte.dev/docs/kit/$env-dynamic-public) | [`$env/static/public`](https://svelte.dev/docs/kit/$env-static-public) |
|
||||
*
|
||||
* Static environment variables are [loaded by Vite](https://vitejs.dev/guide/env-and-mode.html#env-files) from `.env` files and `process.env` at build time and then statically injected into your bundle at build time, enabling optimisations like dead code elimination.
|
||||
*
|
||||
* **_Public_ access:**
|
||||
*
|
||||
* - This module _can_ be imported into client-side code
|
||||
* - **Only** variables that begin with [`config.kit.env.publicPrefix`](https://svelte.dev/docs/kit/configuration#env) (which defaults to `PUBLIC_`) are included
|
||||
*
|
||||
* For example, given the following build time environment:
|
||||
*
|
||||
* ```env
|
||||
* ENVIRONMENT=production
|
||||
* PUBLIC_BASE_URL=http://site.com
|
||||
* ```
|
||||
*
|
||||
* With the default `publicPrefix` and `privatePrefix`:
|
||||
*
|
||||
* ```ts
|
||||
* import { ENVIRONMENT, PUBLIC_BASE_URL } from '$env/static/public';
|
||||
*
|
||||
* console.log(ENVIRONMENT); // => throws error during build
|
||||
* console.log(PUBLIC_BASE_URL); // => "http://site.com"
|
||||
* ```
|
||||
*
|
||||
* The above values will be the same _even if_ different values for `ENVIRONMENT` or `PUBLIC_BASE_URL` are set at runtime, as they are statically replaced in your code with their build time values.
|
||||
*/
|
||||
declare module '$env/static/public' {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* This module provides access to environment variables set _dynamically_ at runtime and that are limited to _private_ access.
|
||||
*
|
||||
* | | Runtime | Build time |
|
||||
* | ------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
|
||||
* | Private | [`$env/dynamic/private`](https://svelte.dev/docs/kit/$env-dynamic-private) | [`$env/static/private`](https://svelte.dev/docs/kit/$env-static-private) |
|
||||
* | Public | [`$env/dynamic/public`](https://svelte.dev/docs/kit/$env-dynamic-public) | [`$env/static/public`](https://svelte.dev/docs/kit/$env-static-public) |
|
||||
*
|
||||
* Dynamic environment variables are defined by the platform you're running on. For example if you're using [`adapter-node`](https://github.com/sveltejs/kit/tree/main/packages/adapter-node) (or running [`vite preview`](https://svelte.dev/docs/kit/cli)), this is equivalent to `process.env`.
|
||||
*
|
||||
* **_Private_ access:**
|
||||
*
|
||||
* - This module cannot be imported into client-side code
|
||||
* - This module includes variables that _do not_ begin with [`config.kit.env.publicPrefix`](https://svelte.dev/docs/kit/configuration#env) _and do_ start with [`config.kit.env.privatePrefix`](https://svelte.dev/docs/kit/configuration#env) (if configured)
|
||||
*
|
||||
* > [!NOTE] In `dev`, `$env/dynamic` includes environment variables from `.env`. In `prod`, this behavior will depend on your adapter.
|
||||
*
|
||||
* > [!NOTE] To get correct types, environment variables referenced in your code should be declared (for example in an `.env` file), even if they don't have a value until the app is deployed:
|
||||
* >
|
||||
* > ```env
|
||||
* > MY_FEATURE_FLAG=
|
||||
* > ```
|
||||
* >
|
||||
* > You can override `.env` values from the command line like so:
|
||||
* >
|
||||
* > ```sh
|
||||
* > MY_FEATURE_FLAG="enabled" npm run dev
|
||||
* > ```
|
||||
*
|
||||
* For example, given the following runtime environment:
|
||||
*
|
||||
* ```env
|
||||
* ENVIRONMENT=production
|
||||
* PUBLIC_BASE_URL=http://site.com
|
||||
* ```
|
||||
*
|
||||
* With the default `publicPrefix` and `privatePrefix`:
|
||||
*
|
||||
* ```ts
|
||||
* import { env } from '$env/dynamic/private';
|
||||
*
|
||||
* console.log(env.ENVIRONMENT); // => "production"
|
||||
* console.log(env.PUBLIC_BASE_URL); // => undefined
|
||||
* ```
|
||||
*/
|
||||
declare module '$env/dynamic/private' {
|
||||
export const env: {
|
||||
SHELL: string;
|
||||
npm_command: string;
|
||||
LSCOLORS: string;
|
||||
SESSION_MANAGER: string;
|
||||
QT_ACCESSIBILITY: string;
|
||||
COLORTERM: string;
|
||||
XDG_CONFIG_DIRS: string;
|
||||
LESS: string;
|
||||
XDG_SESSION_PATH: string;
|
||||
GNOME_DESKTOP_SESSION_ID: string;
|
||||
GTK_IM_MODULE: string;
|
||||
QT_IM_MODULES: string;
|
||||
GNOME_KEYRING_CONTROL: string;
|
||||
LANGUAGE: string;
|
||||
NODE: string;
|
||||
SSH_AUTH_SOCK: string;
|
||||
AGENT: string;
|
||||
npm_config_local_prefix: string;
|
||||
XMODIFIERS: string;
|
||||
DESKTOP_SESSION: string;
|
||||
GTK_MODULES: string;
|
||||
XDG_SEAT: string;
|
||||
PWD: string;
|
||||
XDG_SESSION_DESKTOP: string;
|
||||
LOGNAME: string;
|
||||
XDG_SESSION_TYPE: string;
|
||||
GPG_AGENT_INFO: string;
|
||||
_: string;
|
||||
XAUTHORITY: string;
|
||||
XDG_GREETER_DATA_DIR: string;
|
||||
GDM_LANG: string;
|
||||
HOME: string;
|
||||
OPENCODE: string;
|
||||
LANG: string;
|
||||
LS_COLORS: string;
|
||||
XDG_CURRENT_DESKTOP: string;
|
||||
npm_package_version: string;
|
||||
VTE_VERSION: string;
|
||||
DEBEMAIL: string;
|
||||
XDG_SEAT_PATH: string;
|
||||
GNOME_TERMINAL_SCREEN: string;
|
||||
CLUTTER_IM_MODULE: string;
|
||||
MFLAGS: string;
|
||||
npm_lifecycle_script: string;
|
||||
XDG_SESSION_CLASS: string;
|
||||
MAKEFLAGS: string;
|
||||
TERM: string;
|
||||
npm_package_name: string;
|
||||
ZSH: string;
|
||||
GTK_OVERLAY_SCROLLING: string;
|
||||
LIBVIRT_DEFAULT_URI: string;
|
||||
USER: string;
|
||||
GNOME_TERMINAL_SERVICE: string;
|
||||
DISPLAY: string;
|
||||
npm_lifecycle_event: string;
|
||||
SHLVL: string;
|
||||
PAGER: string;
|
||||
MAKELEVEL: string;
|
||||
QT_IM_MODULE: string;
|
||||
XDG_VTNR: string;
|
||||
XDG_SESSION_ID: string;
|
||||
npm_config_user_agent: string;
|
||||
npm_execpath: string;
|
||||
XDG_RUNTIME_DIR: string;
|
||||
npm_package_json: string;
|
||||
BUN_INSTALL: string;
|
||||
EMAIL: string;
|
||||
GTK3_MODULES: string;
|
||||
XDG_DATA_DIRS: string;
|
||||
PATH: string;
|
||||
GDMSESSION: string;
|
||||
DBUS_SESSION_BUS_ADDRESS: string;
|
||||
THOR_MERGE: string;
|
||||
npm_node_execpath: string;
|
||||
OLDPWD: string;
|
||||
NODE_ENV: string;
|
||||
[key: `PUBLIC_${string}`]: undefined;
|
||||
[key: `${string}`]: string | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This module provides access to environment variables set _dynamically_ at runtime and that are _publicly_ accessible.
|
||||
*
|
||||
* | | Runtime | Build time |
|
||||
* | ------- | -------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
|
||||
* | Private | [`$env/dynamic/private`](https://svelte.dev/docs/kit/$env-dynamic-private) | [`$env/static/private`](https://svelte.dev/docs/kit/$env-static-private) |
|
||||
* | Public | [`$env/dynamic/public`](https://svelte.dev/docs/kit/$env-dynamic-public) | [`$env/static/public`](https://svelte.dev/docs/kit/$env-static-public) |
|
||||
*
|
||||
* Dynamic environment variables are defined by the platform you're running on. For example if you're using [`adapter-node`](https://github.com/sveltejs/kit/tree/main/packages/adapter-node) (or running [`vite preview`](https://svelte.dev/docs/kit/cli)), this is equivalent to `process.env`.
|
||||
*
|
||||
* **_Public_ access:**
|
||||
*
|
||||
* - This module _can_ be imported into client-side code
|
||||
* - **Only** variables that begin with [`config.kit.env.publicPrefix`](https://svelte.dev/docs/kit/configuration#env) (which defaults to `PUBLIC_`) are included
|
||||
*
|
||||
* > [!NOTE] In `dev`, `$env/dynamic` includes environment variables from `.env`. In `prod`, this behavior will depend on your adapter.
|
||||
*
|
||||
* > [!NOTE] To get correct types, environment variables referenced in your code should be declared (for example in an `.env` file), even if they don't have a value until the app is deployed:
|
||||
* >
|
||||
* > ```env
|
||||
* > MY_FEATURE_FLAG=
|
||||
* > ```
|
||||
* >
|
||||
* > You can override `.env` values from the command line like so:
|
||||
* >
|
||||
* > ```sh
|
||||
* > MY_FEATURE_FLAG="enabled" npm run dev
|
||||
* > ```
|
||||
*
|
||||
* For example, given the following runtime environment:
|
||||
*
|
||||
* ```env
|
||||
* ENVIRONMENT=production
|
||||
* PUBLIC_BASE_URL=http://example.com
|
||||
* ```
|
||||
*
|
||||
* With the default `publicPrefix` and `privatePrefix`:
|
||||
*
|
||||
* ```ts
|
||||
* import { env } from '$env/dynamic/public';
|
||||
* console.log(env.ENVIRONMENT); // => undefined, not public
|
||||
* console.log(env.PUBLIC_BASE_URL); // => "http://example.com"
|
||||
* ```
|
||||
*
|
||||
* ```
|
||||
*
|
||||
* ```
|
||||
*/
|
||||
declare module '$env/dynamic/public' {
|
||||
export const env: {
|
||||
[key: `PUBLIC_${string}`]: string | undefined;
|
||||
}
|
||||
}
|
||||
53
frontend/.svelte-kit/generated/client-optimized/app.js
Normal file
53
frontend/.svelte-kit/generated/client-optimized/app.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
export { matchers } from './matchers.js';
|
||||
|
||||
export const nodes = [
|
||||
() => import('./nodes/0'),
|
||||
() => import('./nodes/1'),
|
||||
() => import('./nodes/2'),
|
||||
() => import('./nodes/3'),
|
||||
() => import('./nodes/4'),
|
||||
() => import('./nodes/5'),
|
||||
() => import('./nodes/6'),
|
||||
() => import('./nodes/7'),
|
||||
() => import('./nodes/8'),
|
||||
() => import('./nodes/9'),
|
||||
() => import('./nodes/10'),
|
||||
() => import('./nodes/11'),
|
||||
() => import('./nodes/12'),
|
||||
() => import('./nodes/13'),
|
||||
() => import('./nodes/14')
|
||||
];
|
||||
|
||||
export const server_loads = [];
|
||||
|
||||
export const dictionary = {
|
||||
"/": [2],
|
||||
"/aliases": [3],
|
||||
"/auth/change-password": [4],
|
||||
"/auth/dashboard": [5],
|
||||
"/auth/forgot": [6],
|
||||
"/auth/login": [7],
|
||||
"/domains": [8],
|
||||
"/domains/[name]": [9],
|
||||
"/domains/[name]/aliases": [10],
|
||||
"/domains/[name]/users": [11],
|
||||
"/logs": [12],
|
||||
"/queue": [13],
|
||||
"/users": [14]
|
||||
};
|
||||
|
||||
export const hooks = {
|
||||
handleError: (({ error }) => { console.error(error) }),
|
||||
|
||||
reroute: (() => {}),
|
||||
transport: {}
|
||||
};
|
||||
|
||||
export const decoders = Object.fromEntries(Object.entries(hooks.transport).map(([k, v]) => [k, v.decode]));
|
||||
export const encoders = Object.fromEntries(Object.entries(hooks.transport).map(([k, v]) => [k, v.encode]));
|
||||
|
||||
export const hash = false;
|
||||
|
||||
export const decode = (type, value) => decoders[type](value);
|
||||
|
||||
export { default as root } from '../root.js';
|
||||
|
|
@ -0,0 +1 @@
|
|||
export const matchers = {};
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default as component } from "../../../../src/routes/+layout.svelte";
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default as component } from "../../../../node_modules/@sveltejs/kit/src/runtime/components/svelte-5/error.svelte";
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default as component } from "../../../../src/routes/domains/[name]/aliases/+page.svelte";
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default as component } from "../../../../src/routes/domains/[name]/users/+page.svelte";
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default as component } from "../../../../src/routes/logs/+page.svelte";
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default as component } from "../../../../src/routes/queue/+page.svelte";
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default as component } from "../../../../src/routes/users/+page.svelte";
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default as component } from "../../../../src/routes/+page.svelte";
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default as component } from "../../../../src/routes/aliases/+page.svelte";
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default as component } from "../../../../src/routes/auth/change-password/+page.svelte";
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default as component } from "../../../../src/routes/auth/dashboard/+page.svelte";
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default as component } from "../../../../src/routes/auth/forgot/+page.svelte";
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default as component } from "../../../../src/routes/auth/login/+page.svelte";
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default as component } from "../../../../src/routes/domains/+page.svelte";
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { default as component } from "../../../../src/routes/domains/[name]/+page.svelte";
|
||||
53
frontend/.svelte-kit/generated/client/app.js
Normal file
53
frontend/.svelte-kit/generated/client/app.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
export { matchers } from './matchers.js';
|
||||
|
||||
export const nodes = [
|
||||
() => import('./nodes/0'),
|
||||
() => import('./nodes/1'),
|
||||
() => import('./nodes/2'),
|
||||
() => import('./nodes/3'),
|
||||
() => import('./nodes/4'),
|
||||
() => import('./nodes/5'),
|
||||
() => import('./nodes/6'),
|
||||
() => import('./nodes/7'),
|
||||
() => import('./nodes/8'),
|
||||
() => import('./nodes/9'),
|
||||
() => import('./nodes/10'),
|
||||
() => import('./nodes/11'),
|
||||
() => import('./nodes/12'),
|
||||
() => import('./nodes/13'),
|
||||
() => import('./nodes/14')
|
||||
];
|
||||
|
||||
export const server_loads = [];
|
||||
|
||||
export const dictionary = {
|
||||
"/": [2],
|
||||
"/aliases": [3],
|
||||
"/auth/change-password": [4],
|
||||
"/auth/dashboard": [5],
|
||||
"/auth/forgot": [6],
|
||||
"/auth/login": [7],
|
||||
"/domains": [8],
|
||||
"/domains/[name]": [9],
|
||||
"/domains/[name]/aliases": [10],
|
||||
"/domains/[name]/users": [11],
|
||||
"/logs": [12],
|
||||
"/queue": [13],
|
||||
"/users": [14]
|
||||
};
|
||||
|
||||
export const hooks = {
|
||||
handleError: (({ error }) => { console.error(error) }),
|
||||
|
||||
reroute: (() => {}),
|
||||
transport: {}
|
||||
};
|
||||
|
||||
export const decoders = Object.fromEntries(Object.entries(hooks.transport).map(([k, v]) => [k, v.decode]));
|
||||
export const encoders = Object.fromEntries(Object.entries(hooks.transport).map(([k, v]) => [k, v.encode]));
|
||||
|
||||
export const hash = false;
|
||||
|
||||
export const decode = (type, value) => decoders[type](value);
|
||||
|
||||
export { default as root } from '../root.js';
|
||||
1
frontend/.svelte-kit/generated/client/matchers.js
Normal file
1
frontend/.svelte-kit/generated/client/matchers.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export const matchers = {};
|
||||
1
frontend/.svelte-kit/generated/client/nodes/0.js
Normal file
1
frontend/.svelte-kit/generated/client/nodes/0.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default as component } from "../../../../src/routes/+layout.svelte";
|
||||
1
frontend/.svelte-kit/generated/client/nodes/1.js
Normal file
1
frontend/.svelte-kit/generated/client/nodes/1.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default as component } from "../../../../node_modules/@sveltejs/kit/src/runtime/components/svelte-5/error.svelte";
|
||||
1
frontend/.svelte-kit/generated/client/nodes/10.js
Normal file
1
frontend/.svelte-kit/generated/client/nodes/10.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default as component } from "../../../../src/routes/domains/[name]/aliases/+page.svelte";
|
||||
1
frontend/.svelte-kit/generated/client/nodes/11.js
Normal file
1
frontend/.svelte-kit/generated/client/nodes/11.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { default as component } from "../../../../src/routes/domains/[name]/users/+page.svelte";
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue