Version 0.1
This commit is contained in:
commit
dcd4925371
|
|
@ -0,0 +1,6 @@
|
|||
# Docker Environment Variables
|
||||
JWT_SECRET=change-this-secret-in-production
|
||||
|
||||
# MariaDB Configuration (if using MariaDB)
|
||||
DB_ROOT_PASSWORD=rootpassword
|
||||
DB_PASSWORD=adminpassword
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
# Environment files
|
||||
.env
|
||||
|
||||
# Backend
|
||||
backend/data/
|
||||
backend/server
|
||||
backend/*.db
|
||||
|
||||
# Frontend
|
||||
frontend/node_modules/
|
||||
frontend/dist/
|
||||
frontend/.vite/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
.PHONY: help dev build docker-build docker-up docker-down clean
|
||||
|
||||
help:
|
||||
@echo "AdminTemplate - Makefile Commands"
|
||||
@echo ""
|
||||
@echo "Development:"
|
||||
@echo " make dev - Start development servers (backend + frontend)"
|
||||
@echo " make backend - Start backend only"
|
||||
@echo " make frontend - Start frontend only"
|
||||
@echo ""
|
||||
@echo "Docker:"
|
||||
@echo " make docker-build - Build Docker images"
|
||||
@echo " make docker-up - Start Docker containers"
|
||||
@echo " make docker-up-db - Start with MariaDB"
|
||||
@echo " make docker-down - Stop Docker containers"
|
||||
@echo " make docker-logs - View Docker logs"
|
||||
@echo ""
|
||||
@echo "Build:"
|
||||
@echo " make build - Build backend and frontend"
|
||||
@echo " make build-backend - Build backend binary"
|
||||
@echo " make build-frontend - Build frontend"
|
||||
@echo ""
|
||||
@echo "Maintenance:"
|
||||
@echo " make clean - Clean build artifacts"
|
||||
@echo " make init - Initialize project (install deps)"
|
||||
|
||||
# Development
|
||||
dev:
|
||||
@echo "Starting development servers..."
|
||||
@make -j2 backend frontend
|
||||
|
||||
backend:
|
||||
@echo "Starting backend..."
|
||||
cd backend && go run cmd/server/main.go
|
||||
|
||||
frontend:
|
||||
@echo "Starting frontend..."
|
||||
cd frontend && npm run dev
|
||||
|
||||
# Build
|
||||
build: build-backend build-frontend
|
||||
|
||||
build-backend:
|
||||
@echo "Building backend..."
|
||||
cd backend && go build -o server cmd/server/main.go
|
||||
|
||||
build-frontend:
|
||||
@echo "Building frontend..."
|
||||
cd frontend && npm run build
|
||||
|
||||
# Docker
|
||||
docker-build:
|
||||
@echo "Building Docker images..."
|
||||
docker-compose build
|
||||
|
||||
docker-up:
|
||||
@echo "Starting Docker containers with SQLite..."
|
||||
docker-compose up -d
|
||||
|
||||
docker-up-db:
|
||||
@echo "Starting Docker containers with MariaDB..."
|
||||
docker-compose -f docker-compose.yml -f docker-compose.mariadb.yml up -d
|
||||
|
||||
docker-down:
|
||||
@echo "Stopping Docker containers..."
|
||||
docker-compose down
|
||||
|
||||
docker-logs:
|
||||
docker-compose logs -f
|
||||
|
||||
# Initialize
|
||||
init:
|
||||
@echo "Installing backend dependencies..."
|
||||
cd backend && go mod download
|
||||
@echo "Installing frontend dependencies..."
|
||||
cd frontend && npm install
|
||||
|
||||
# Clean
|
||||
clean:
|
||||
@echo "Cleaning build artifacts..."
|
||||
rm -f backend/server
|
||||
rm -rf backend/data
|
||||
rm -rf frontend/dist
|
||||
rm -rf frontend/node_modules
|
||||
@echo "Clean complete!"
|
||||
|
|
@ -0,0 +1,241 @@
|
|||
# AdminTemplate
|
||||
|
||||
Eine wiederverwendbare Anwendungsschablone mit Go-Backend, Vue 3 Frontend und Docker-Support.
|
||||
|
||||
## Features
|
||||
|
||||
- **Backend**: Go REST API mit Gin Framework
|
||||
- **Datenbank**: SQLite (default) oder MariaDB
|
||||
- **Frontend**: Vue 3 Admin Dashboard mit Routing und State Management
|
||||
- **Authentifizierung**: JWT-basierte Benutzerauthentifizierung
|
||||
- **Autorisierung**: Rollenbasierte Zugriffskontrolle (Admin/User)
|
||||
- **Docker**: Multi-Plattform Container-Support (Linux/Windows)
|
||||
- **Embedded-Ready**: SQLite für Linux Embedded Systeme
|
||||
|
||||
## Projektstruktur
|
||||
|
||||
```
|
||||
AdminTemplate/
|
||||
├── backend/ # Go Backend
|
||||
│ ├── cmd/server/ # Hauptanwendung
|
||||
│ ├── internal/ # Interne Packages
|
||||
│ │ ├── auth/ # Authentifizierung
|
||||
│ │ ├── database/ # Datenbanklogik
|
||||
│ │ ├── handlers/ # HTTP Handler
|
||||
│ │ ├── middleware/ # Middleware
|
||||
│ │ └── models/ # Datenmodelle
|
||||
│ └── pkg/config/ # Konfiguration
|
||||
├── frontend/ # Vue 3 Frontend
|
||||
│ ├── src/
|
||||
│ │ ├── components/ # Vue Komponenten
|
||||
│ │ ├── views/ # Seiten
|
||||
│ │ ├── router/ # Routing
|
||||
│ │ ├── stores/ # Pinia Stores
|
||||
│ │ ├── services/ # API Services
|
||||
│ │ └── styles/ # CSS
|
||||
├── docker/ # Docker Konfiguration
|
||||
└── docker-compose.yml # Docker Compose Setup
|
||||
```
|
||||
|
||||
## Schnellstart
|
||||
|
||||
### Mit Docker (Empfohlen)
|
||||
|
||||
1. **Repository klonen oder als Schablone nutzen**
|
||||
|
||||
2. **Umgebungsvariablen konfigurieren**
|
||||
```bash
|
||||
cp .env.example .env
|
||||
# .env bearbeiten und JWT_SECRET ändern
|
||||
```
|
||||
|
||||
3. **Mit SQLite starten** (Default)
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
4. **Mit MariaDB starten** (Optional)
|
||||
```bash
|
||||
docker-compose -f docker-compose.yml -f docker-compose.mariadb.yml up -d
|
||||
```
|
||||
|
||||
5. **Anwendung öffnen**
|
||||
- Frontend: http://localhost
|
||||
- Backend API: http://localhost:8080/api
|
||||
|
||||
### Lokale Entwicklung
|
||||
|
||||
#### Backend
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
|
||||
# Abhängigkeiten installieren
|
||||
go mod download
|
||||
|
||||
# Umgebungsvariablen konfigurieren
|
||||
cp .env.example .env
|
||||
|
||||
# Server starten
|
||||
go run cmd/server/main.go
|
||||
```
|
||||
|
||||
#### Frontend
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
|
||||
# Abhängigkeiten installieren
|
||||
npm install
|
||||
|
||||
# Entwicklungsserver starten
|
||||
npm run dev
|
||||
```
|
||||
|
||||
## Standard-Benutzer
|
||||
|
||||
Das System erstellt automatisch zwei Benutzer:
|
||||
|
||||
| Benutzername | Passwort | Rolle |
|
||||
|--------------|----------|--------|
|
||||
| admin | admin123 | admin |
|
||||
| mf | mf123 | user |
|
||||
|
||||
**WICHTIG**: Ändern Sie diese Passwörter in der Produktion!
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Öffentlich
|
||||
- `POST /api/auth/login` - Benutzeranmeldung
|
||||
|
||||
### Authentifiziert
|
||||
- `GET /api/auth/me` - Aktueller Benutzer
|
||||
|
||||
### Admin Only
|
||||
- `GET /api/users` - Alle Benutzer auflisten
|
||||
- `GET /api/users/:id` - Benutzer abrufen
|
||||
- `POST /api/users` - Benutzer erstellen
|
||||
- `PUT /api/users/:id` - Benutzer aktualisieren
|
||||
- `DELETE /api/users/:id` - Benutzer löschen
|
||||
|
||||
## Konfiguration
|
||||
|
||||
### Backend (.env)
|
||||
|
||||
```env
|
||||
# Server
|
||||
SERVER_PORT=8080
|
||||
SERVER_HOST=0.0.0.0
|
||||
ENV=development
|
||||
|
||||
# Datenbank
|
||||
DB_TYPE=sqlite # oder mysql
|
||||
SQLITE_PATH=./data/admintemplate.db
|
||||
|
||||
# MySQL/MariaDB (wenn DB_TYPE=mysql)
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USER=root
|
||||
DB_PASSWORD=
|
||||
DB_NAME=admintemplate
|
||||
|
||||
# Authentifizierung
|
||||
JWT_SECRET=your-secret-key
|
||||
TOKEN_DURATION=24
|
||||
```
|
||||
|
||||
## Docker Deployment
|
||||
|
||||
### Für Linux
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Für Windows
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Multi-Plattform Build
|
||||
|
||||
```bash
|
||||
# Für AMD64 und ARM64
|
||||
docker buildx build --platform linux/amd64,linux/arm64 -f docker/Dockerfile.backend -t admintemplate-backend .
|
||||
docker buildx build --platform linux/amd64,linux/arm64 -f docker/Dockerfile.frontend -t admintemplate-frontend .
|
||||
```
|
||||
|
||||
## Als Schablone verwenden
|
||||
|
||||
1. **Kopieren Sie das gesamte AdminTemplate Verzeichnis**
|
||||
2. **Umbenennen Sie das Projekt**
|
||||
```bash
|
||||
# Go Module umbenennen
|
||||
cd backend
|
||||
# In go.mod: module admintemplate -> module IhrProjektname
|
||||
go mod edit -module IhrProjektname
|
||||
|
||||
# Imports in allen .go Dateien aktualisieren
|
||||
find . -type f -name "*.go" -exec sed -i 's/admintemplate/IhrProjektname/g' {} +
|
||||
```
|
||||
|
||||
3. **Passen Sie die Konfiguration an**
|
||||
4. **Erweitern Sie die Anwendung mit Ihrer Geschäftslogik**
|
||||
|
||||
## Entwicklung
|
||||
|
||||
### Neue API Endpoints hinzufügen
|
||||
|
||||
1. Definieren Sie das Model in `backend/internal/models/`
|
||||
2. Erstellen Sie Handler in `backend/internal/handlers/`
|
||||
3. Registrieren Sie Routen in `backend/cmd/server/main.go`
|
||||
|
||||
### Neue Frontend-Seiten hinzufügen
|
||||
|
||||
1. Erstellen Sie View in `frontend/src/views/`
|
||||
2. Fügen Sie Route in `frontend/src/router/index.js` hinzu
|
||||
3. Erstellen Sie Services in `frontend/src/services/` für API-Calls
|
||||
|
||||
## Sicherheit
|
||||
|
||||
- JWT-Token für Authentifizierung
|
||||
- Passwort-Hashing mit bcrypt
|
||||
- CORS-Support
|
||||
- Rollenbasierte Autorisierung
|
||||
- SQL-Injection-Schutz durch Prepared Statements
|
||||
|
||||
**Produktions-Checkliste**:
|
||||
- [ ] JWT_SECRET in .env ändern
|
||||
- [ ] Standard-Passwörter ändern
|
||||
- [ ] HTTPS aktivieren (Reverse Proxy)
|
||||
- [ ] Datenbank-Backups einrichten
|
||||
- [ ] Logging konfigurieren
|
||||
|
||||
## Technologie-Stack
|
||||
|
||||
**Backend**:
|
||||
- Go 1.21+
|
||||
- Gin Web Framework
|
||||
- JWT Authentication
|
||||
- SQLite/MariaDB
|
||||
|
||||
**Frontend**:
|
||||
- Vue 3
|
||||
- Vue Router
|
||||
- Pinia (State Management)
|
||||
- Axios
|
||||
- Vite
|
||||
|
||||
**Deployment**:
|
||||
- Docker
|
||||
- Docker Compose
|
||||
- Nginx (Frontend Proxy)
|
||||
|
||||
## Lizenz
|
||||
|
||||
MIT License - Frei verwendbar für Ihre Projekte
|
||||
|
||||
## Support
|
||||
|
||||
Bei Fragen oder Problemen öffnen Sie ein Issue oder passen Sie die Schablone an Ihre Bedürfnisse an.
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
# Server Configuration
|
||||
SERVER_PORT=8080
|
||||
SERVER_HOST=0.0.0.0
|
||||
ENV=development
|
||||
|
||||
# Database Configuration
|
||||
# Options: sqlite, mysql
|
||||
DB_TYPE=sqlite
|
||||
SQLITE_PATH=./data/admintemplate.db
|
||||
|
||||
# MySQL Configuration (if DB_TYPE=mysql)
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USER=root
|
||||
DB_PASSWORD=
|
||||
DB_NAME=admintemplate
|
||||
|
||||
# Authentication
|
||||
JWT_SECRET=change-this-secret-in-production
|
||||
TOKEN_DURATION=24
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/joho/godotenv"
|
||||
|
||||
"admintemplate/internal/database"
|
||||
"admintemplate/internal/handlers"
|
||||
"admintemplate/internal/middleware"
|
||||
"admintemplate/pkg/config"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Load .env file if exists
|
||||
_ = godotenv.Load()
|
||||
|
||||
// Load configuration
|
||||
cfg := config.Load()
|
||||
|
||||
// Connect to database
|
||||
db, err := database.Connect(&cfg.Database)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to connect to database: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Run migrations
|
||||
if err := db.Migrate(); err != nil {
|
||||
log.Fatalf("Failed to run migrations: %v", err)
|
||||
}
|
||||
|
||||
// Initialize default users
|
||||
authHandler := handlers.NewAuthHandler(db, cfg)
|
||||
if err := authHandler.InitDefaultUsers(); err != nil {
|
||||
log.Fatalf("Failed to initialize default users: %v", err)
|
||||
}
|
||||
|
||||
// Initialize handlers
|
||||
userHandler := handlers.NewUserHandler(db)
|
||||
|
||||
// Set Gin mode
|
||||
if cfg.Server.Env == "production" {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
|
||||
// Setup router
|
||||
r := gin.Default()
|
||||
|
||||
// CORS middleware
|
||||
r.Use(func(c *gin.Context) {
|
||||
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
|
||||
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE, PATCH")
|
||||
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(204)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
})
|
||||
|
||||
// Health check
|
||||
r.GET("/health", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
// Public routes
|
||||
r.POST("/api/auth/login", authHandler.Login)
|
||||
|
||||
// Protected routes
|
||||
api := r.Group("/api")
|
||||
api.Use(middleware.AuthMiddleware(cfg.Auth.JWTSecret))
|
||||
{
|
||||
// Current user
|
||||
api.GET("/auth/me", authHandler.GetCurrentUser)
|
||||
|
||||
// User management (admin only)
|
||||
users := api.Group("/users")
|
||||
users.Use(middleware.RequireAdmin())
|
||||
{
|
||||
users.GET("", userHandler.ListUsers)
|
||||
users.GET("/:id", userHandler.GetUser)
|
||||
users.POST("", userHandler.CreateUser)
|
||||
users.PUT("/:id", userHandler.UpdateUser)
|
||||
users.DELETE("/:id", userHandler.DeleteUser)
|
||||
}
|
||||
}
|
||||
|
||||
// Start server
|
||||
addr := cfg.Server.Host + ":" + cfg.Server.Port
|
||||
log.Printf("Server starting on %s", addr)
|
||||
if err := r.Run(addr); err != nil {
|
||||
log.Fatalf("Failed to start server: %v", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
module admintemplate
|
||||
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||
github.com/mattn/go-sqlite3 v1.14.22
|
||||
github.com/go-sql-driver/mysql v1.7.1
|
||||
golang.org/x/crypto v0.20.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
)
|
||||
|
|
@ -0,0 +1 @@
|
|||
# This file will be generated by go mod download
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type Claims struct {
|
||||
UserID int64 `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Role string `json:"role"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
func HashPassword(password string) (string, error) {
|
||||
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
return string(bytes), err
|
||||
}
|
||||
|
||||
func CheckPassword(password, hash string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func GenerateToken(userID int64, username, role, secret string, duration int) (string, error) {
|
||||
claims := Claims{
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
Role: role,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * time.Duration(duration))),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString([]byte(secret))
|
||||
}
|
||||
|
||||
func ValidateToken(tokenString, secret string) (*Claims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return []byte(secret), nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("invalid token")
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
|
||||
"admintemplate/pkg/config"
|
||||
)
|
||||
|
||||
type DB struct {
|
||||
*sql.DB
|
||||
Type string
|
||||
}
|
||||
|
||||
func Connect(cfg *config.DatabaseConfig) (*DB, error) {
|
||||
var db *sql.DB
|
||||
var err error
|
||||
|
||||
switch cfg.Type {
|
||||
case "sqlite":
|
||||
// Ensure directory exists for SQLite
|
||||
dir := filepath.Dir(cfg.SQLitePath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create data directory: %w", err)
|
||||
}
|
||||
|
||||
db, err = sql.Open("sqlite3", cfg.SQLitePath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to SQLite: %w", err)
|
||||
}
|
||||
|
||||
case "mysql":
|
||||
dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true",
|
||||
cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.DBName)
|
||||
db, err = sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to MySQL: %w", err)
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported database type: %s", cfg.Type)
|
||||
}
|
||||
|
||||
// Test connection
|
||||
if err := db.Ping(); err != nil {
|
||||
return nil, fmt.Errorf("failed to ping database: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Successfully connected to %s database", cfg.Type)
|
||||
|
||||
return &DB{DB: db, Type: cfg.Type}, nil
|
||||
}
|
||||
|
||||
func (db *DB) Migrate() error {
|
||||
var createUsersTable string
|
||||
|
||||
if db.Type == "sqlite" {
|
||||
createUsersTable = `
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
email TEXT UNIQUE NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'user',
|
||||
active INTEGER NOT NULL DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`
|
||||
} else {
|
||||
createUsersTable = `
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(255) UNIQUE NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
role VARCHAR(50) NOT NULL DEFAULT 'user',
|
||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);
|
||||
`
|
||||
}
|
||||
|
||||
if _, err := db.Exec(createUsersTable); err != nil {
|
||||
return fmt.Errorf("failed to create users table: %w", err)
|
||||
}
|
||||
|
||||
log.Println("Database migration completed successfully")
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"admintemplate/internal/auth"
|
||||
"admintemplate/internal/database"
|
||||
"admintemplate/internal/models"
|
||||
"admintemplate/pkg/config"
|
||||
)
|
||||
|
||||
type AuthHandler struct {
|
||||
db *database.DB
|
||||
config *config.Config
|
||||
}
|
||||
|
||||
func NewAuthHandler(db *database.DB, cfg *config.Config) *AuthHandler {
|
||||
return &AuthHandler{
|
||||
db: db,
|
||||
config: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *AuthHandler) Login(c *gin.Context) {
|
||||
var req models.LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
|
||||
return
|
||||
}
|
||||
|
||||
// Get user from database
|
||||
var user models.User
|
||||
query := "SELECT id, username, password, email, role, active, created_at, updated_at FROM users WHERE username = ?"
|
||||
err := h.db.QueryRow(query, req.Username).Scan(
|
||||
&user.ID, &user.Username, &user.Password, &user.Email,
|
||||
&user.Role, &user.Active, &user.CreatedAt, &user.UpdatedAt,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
|
||||
return
|
||||
}
|
||||
|
||||
// Check if user is active
|
||||
if !user.Active {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "User account is disabled"})
|
||||
return
|
||||
}
|
||||
|
||||
// Verify password
|
||||
if !auth.CheckPassword(req.Password, user.Password) {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
|
||||
return
|
||||
}
|
||||
|
||||
// Generate JWT token
|
||||
token, err := auth.GenerateToken(
|
||||
user.ID,
|
||||
user.Username,
|
||||
user.Role,
|
||||
h.config.Auth.JWTSecret,
|
||||
h.config.Auth.TokenDuration,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.LoginResponse{
|
||||
Token: token,
|
||||
User: user,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AuthHandler) GetCurrentUser(c *gin.Context) {
|
||||
userID, _ := c.Get("user_id")
|
||||
|
||||
var user models.User
|
||||
query := "SELECT id, username, email, role, active, created_at, updated_at FROM users WHERE id = ?"
|
||||
err := h.db.QueryRow(query, userID).Scan(
|
||||
&user.ID, &user.Username, &user.Email,
|
||||
&user.Role, &user.Active, &user.CreatedAt, &user.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch user"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, user)
|
||||
}
|
||||
|
||||
func (h *AuthHandler) InitDefaultUsers() error {
|
||||
// Check if users already exist
|
||||
var count int
|
||||
err := h.db.QueryRow("SELECT COUNT(*) FROM users").Scan(&count)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
return nil // Users already exist
|
||||
}
|
||||
|
||||
// Create default users: admin and mf
|
||||
defaultUsers := []struct {
|
||||
username string
|
||||
password string
|
||||
email string
|
||||
role string
|
||||
}{
|
||||
{"admin", "admin123", "admin@admintemplate.local", "admin"},
|
||||
{"mf", "mf123", "mf@admintemplate.local", "user"},
|
||||
}
|
||||
|
||||
for _, u := range defaultUsers {
|
||||
hashedPassword, err := auth.HashPassword(u.password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
query := `INSERT INTO users (username, password, email, role, active, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
||||
now := time.Now()
|
||||
_, err = h.db.Exec(query, u.username, hashedPassword, u.email, u.role, true, now, now)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"admintemplate/internal/auth"
|
||||
"admintemplate/internal/database"
|
||||
"admintemplate/internal/models"
|
||||
)
|
||||
|
||||
type UserHandler struct {
|
||||
db *database.DB
|
||||
}
|
||||
|
||||
func NewUserHandler(db *database.DB) *UserHandler {
|
||||
return &UserHandler{db: db}
|
||||
}
|
||||
|
||||
func (h *UserHandler) ListUsers(c *gin.Context) {
|
||||
query := "SELECT id, username, email, role, active, created_at, updated_at FROM users ORDER BY id"
|
||||
rows, err := h.db.Query(query)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch users"})
|
||||
return
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var users []models.User
|
||||
for rows.Next() {
|
||||
var user models.User
|
||||
err := rows.Scan(
|
||||
&user.ID, &user.Username, &user.Email,
|
||||
&user.Role, &user.Active, &user.CreatedAt, &user.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to scan user"})
|
||||
return
|
||||
}
|
||||
users = append(users, user)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, users)
|
||||
}
|
||||
|
||||
func (h *UserHandler) GetUser(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
var user models.User
|
||||
query := "SELECT id, username, email, role, active, created_at, updated_at FROM users WHERE id = ?"
|
||||
err := h.db.QueryRow(query, id).Scan(
|
||||
&user.ID, &user.Username, &user.Email,
|
||||
&user.Role, &user.Active, &user.CreatedAt, &user.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "User not found"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, user)
|
||||
}
|
||||
|
||||
type CreateUserRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Role string `json:"role" binding:"required"`
|
||||
}
|
||||
|
||||
func (h *UserHandler) CreateUser(c *gin.Context) {
|
||||
var req CreateUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
hashedPassword, err := auth.HashPassword(req.Password)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to hash password"})
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
query := `INSERT INTO users (username, password, email, role, active, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
||||
result, err := h.db.Exec(query, req.Username, hashedPassword, req.Email, req.Role, true, now, now)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": "Username or email already exists"})
|
||||
return
|
||||
}
|
||||
|
||||
id, _ := result.LastInsertId()
|
||||
c.JSON(http.StatusCreated, gin.H{"id": id, "message": "User created successfully"})
|
||||
}
|
||||
|
||||
type UpdateUserRequest struct {
|
||||
Email string `json:"email"`
|
||||
Role string `json:"role"`
|
||||
Active *bool `json:"active"`
|
||||
}
|
||||
|
||||
func (h *UserHandler) UpdateUser(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
var req UpdateUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
query := "UPDATE users SET email = ?, role = ?, active = ?, updated_at = ? WHERE id = ?"
|
||||
_, err := h.db.Exec(query, req.Email, req.Role, req.Active, time.Now(), id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "User updated successfully"})
|
||||
}
|
||||
|
||||
func (h *UserHandler) DeleteUser(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
|
||||
query := "DELETE FROM users WHERE id = ?"
|
||||
_, err := h.db.Exec(query, id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete user"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "User deleted successfully"})
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"admintemplate/internal/auth"
|
||||
)
|
||||
|
||||
func AuthMiddleware(jwtSecret string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header required"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Bearer token format
|
||||
parts := strings.Split(authHeader, " ")
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid authorization format"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
token := parts[1]
|
||||
claims, err := auth.ValidateToken(token, jwtSecret)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid or expired token"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// Set user info in context
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("username", claims.Username)
|
||||
c.Set("role", claims.Role)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func RequireAdmin() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
role, exists := c.Get("role")
|
||||
if !exists || role != "admin" {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "Admin access required"})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID int64 `json:"id" db:"id"`
|
||||
Username string `json:"username" db:"username"`
|
||||
Password string `json:"-" db:"password"` // Never expose password in JSON
|
||||
Email string `json:"email" db:"email"`
|
||||
Role string `json:"role" db:"role"` // "admin" or "user"
|
||||
Active bool `json:"active" db:"active"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
|
||||
}
|
||||
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
type LoginResponse struct {
|
||||
Token string `json:"token"`
|
||||
User User `json:"user"`
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Server ServerConfig
|
||||
Database DatabaseConfig
|
||||
Auth AuthConfig
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
Port string
|
||||
Host string
|
||||
Env string
|
||||
}
|
||||
|
||||
type DatabaseConfig struct {
|
||||
Type string // "sqlite" or "mysql"
|
||||
Host string
|
||||
Port string
|
||||
User string
|
||||
Password string
|
||||
DBName string
|
||||
SQLitePath string
|
||||
}
|
||||
|
||||
type AuthConfig struct {
|
||||
JWTSecret string
|
||||
TokenDuration int // in hours
|
||||
}
|
||||
|
||||
func Load() *Config {
|
||||
return &Config{
|
||||
Server: ServerConfig{
|
||||
Port: getEnv("SERVER_PORT", "8080"),
|
||||
Host: getEnv("SERVER_HOST", "0.0.0.0"),
|
||||
Env: getEnv("ENV", "development"),
|
||||
},
|
||||
Database: DatabaseConfig{
|
||||
Type: getEnv("DB_TYPE", "sqlite"),
|
||||
Host: getEnv("DB_HOST", "localhost"),
|
||||
Port: getEnv("DB_PORT", "3306"),
|
||||
User: getEnv("DB_USER", "root"),
|
||||
Password: getEnv("DB_PASSWORD", ""),
|
||||
DBName: getEnv("DB_NAME", "admintemplate"),
|
||||
SQLitePath: getEnv("SQLITE_PATH", "./data/admintemplate.db"),
|
||||
},
|
||||
Auth: AuthConfig{
|
||||
JWTSecret: getEnv("JWT_SECRET", "your-secret-key-change-in-production"),
|
||||
TokenDuration: getEnvAsInt("TOKEN_DURATION", 24),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func getEnv(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func getEnvAsInt(key string, defaultValue int) int {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
if intVal, err := strconv.Atoi(value); err == nil {
|
||||
return intVal
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
version: '3.8'
|
||||
|
||||
# Docker Compose configuration with MariaDB
|
||||
# Usage: docker-compose -f docker-compose.yml -f docker-compose.mariadb.yml up
|
||||
|
||||
services:
|
||||
backend:
|
||||
environment:
|
||||
- DB_TYPE=mysql
|
||||
- DB_HOST=mariadb
|
||||
- DB_PORT=3306
|
||||
- DB_USER=adminuser
|
||||
- DB_PASSWORD=${DB_PASSWORD:-adminpassword}
|
||||
- DB_NAME=admintemplate
|
||||
- SQLITE_PATH=
|
||||
depends_on:
|
||||
mariadb:
|
||||
condition: service_healthy
|
||||
|
||||
mariadb:
|
||||
image: mariadb:11
|
||||
container_name: admintemplate-mariadb
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD:-rootpassword}
|
||||
- MYSQL_DATABASE=admintemplate
|
||||
- MYSQL_USER=adminuser
|
||||
- MYSQL_PASSWORD=${DB_PASSWORD:-adminpassword}
|
||||
volumes:
|
||||
- mariadb-data:/var/lib/mysql
|
||||
networks:
|
||||
- admintemplate-network
|
||||
healthcheck:
|
||||
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
volumes:
|
||||
mariadb-data:
|
||||
driver: local
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
version: '3.8'
|
||||
|
||||
services:
|
||||
backend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/Dockerfile.backend
|
||||
container_name: admintemplate-backend
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- SERVER_PORT=8080
|
||||
- SERVER_HOST=0.0.0.0
|
||||
- ENV=production
|
||||
- DB_TYPE=sqlite
|
||||
- SQLITE_PATH=/app/data/admintemplate.db
|
||||
- JWT_SECRET=${JWT_SECRET:-change-this-secret-in-production}
|
||||
- TOKEN_DURATION=24
|
||||
volumes:
|
||||
- backend-data:/app/data
|
||||
networks:
|
||||
- admintemplate-network
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/health"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
frontend:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/Dockerfile.frontend
|
||||
container_name: admintemplate-frontend
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "80:80"
|
||||
depends_on:
|
||||
- backend
|
||||
networks:
|
||||
- admintemplate-network
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:80"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
|
||||
# Optional: MariaDB for production use
|
||||
# Uncomment and set DB_TYPE=mysql in backend environment
|
||||
# mariadb:
|
||||
# image: mariadb:11
|
||||
# container_name: admintemplate-db
|
||||
# restart: unless-stopped
|
||||
# environment:
|
||||
# - MYSQL_ROOT_PASSWORD=${DB_ROOT_PASSWORD:-rootpassword}
|
||||
# - MYSQL_DATABASE=admintemplate
|
||||
# - MYSQL_USER=adminuser
|
||||
# - MYSQL_PASSWORD=${DB_PASSWORD:-adminpassword}
|
||||
# volumes:
|
||||
# - mariadb-data:/var/lib/mysql
|
||||
# networks:
|
||||
# - admintemplate-network
|
||||
# healthcheck:
|
||||
# test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
|
||||
# interval: 30s
|
||||
# timeout: 10s
|
||||
# retries: 3
|
||||
|
||||
volumes:
|
||||
backend-data:
|
||||
driver: local
|
||||
# mariadb-data:
|
||||
# driver: local
|
||||
|
||||
networks:
|
||||
admintemplate-network:
|
||||
driver: bridge
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
# Multi-stage build for Go backend
|
||||
FROM golang:1.21-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install build dependencies
|
||||
RUN apk add --no-cache git gcc musl-dev sqlite-dev
|
||||
|
||||
# Copy go mod files
|
||||
COPY backend/go.mod backend/go.sum* ./
|
||||
RUN go mod download || true
|
||||
|
||||
# Copy source code
|
||||
COPY backend/ .
|
||||
|
||||
# Build the application
|
||||
RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -o server ./cmd/server
|
||||
|
||||
# Final stage
|
||||
FROM alpine:latest
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apk --no-cache add ca-certificates sqlite-libs
|
||||
|
||||
# Copy the binary from builder
|
||||
COPY --from=builder /app/server .
|
||||
|
||||
# Create data directory
|
||||
RUN mkdir -p /app/data
|
||||
|
||||
# Expose port
|
||||
EXPOSE 8080
|
||||
|
||||
# Run the application
|
||||
CMD ["./server"]
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
# Multi-stage build for Vue frontend
|
||||
FROM node:20-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Copy package files
|
||||
COPY frontend/package*.json ./
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install
|
||||
|
||||
# Copy source code
|
||||
COPY frontend/ .
|
||||
|
||||
# Build the application
|
||||
RUN npm run build
|
||||
|
||||
# Production stage with nginx
|
||||
FROM nginx:alpine
|
||||
|
||||
# Copy built files
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
# Copy nginx configuration
|
||||
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
EXPOSE 80
|
||||
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Gzip compression
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_min_length 1024;
|
||||
gzip_types text/plain text/css text/xml text/javascript application/javascript application/json application/xml+rss;
|
||||
|
||||
# Serve static files
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Proxy API requests to backend
|
||||
location /api/ {
|
||||
proxy_pass http://backend:8080;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# Security headers
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
}
|
||||
|
|
@ -0,0 +1,311 @@
|
|||
# Deployment Guide
|
||||
|
||||
## Deployment-Optionen
|
||||
|
||||
### 1. Docker Deployment (Empfohlen)
|
||||
|
||||
#### Standard (SQLite)
|
||||
|
||||
```bash
|
||||
# .env konfigurieren
|
||||
cp .env.example .env
|
||||
nano .env # JWT_SECRET ändern
|
||||
|
||||
# Container starten
|
||||
docker-compose up -d
|
||||
|
||||
# Logs anzeigen
|
||||
docker-compose logs -f
|
||||
|
||||
# Status prüfen
|
||||
docker-compose ps
|
||||
```
|
||||
|
||||
#### Mit MariaDB
|
||||
|
||||
```bash
|
||||
# .env konfigurieren
|
||||
cp .env.example .env
|
||||
nano .env # Passwörter ändern
|
||||
|
||||
# Container mit MariaDB starten
|
||||
docker-compose -f docker-compose.yml -f docker-compose.mariadb.yml up -d
|
||||
```
|
||||
|
||||
### 2. Embedded Linux System (z.B. Raspberry Pi)
|
||||
|
||||
#### Voraussetzungen
|
||||
- Linux ARM/ARM64 System
|
||||
- Docker oder natives Go Runtime
|
||||
|
||||
#### Mit Docker
|
||||
|
||||
```bash
|
||||
# Multi-arch Image bauen
|
||||
docker buildx build --platform linux/arm64 -f docker/Dockerfile.backend -t admintemplate-backend .
|
||||
docker buildx build --platform linux/arm64 -f docker/Dockerfile.frontend -t admintemplate-frontend .
|
||||
|
||||
# Starten
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
#### Native Installation
|
||||
|
||||
```bash
|
||||
# Backend kompilieren (auf Entwicklungsrechner)
|
||||
cd backend
|
||||
GOOS=linux GOARCH=arm64 CGO_ENABLED=1 go build -o server cmd/server/main.go
|
||||
|
||||
# Auf Embedded System kopieren
|
||||
scp server pi@raspberry:/opt/admintemplate/
|
||||
scp -r frontend/dist/* pi@raspberry:/opt/admintemplate/frontend/
|
||||
|
||||
# Auf Embedded System
|
||||
./server
|
||||
```
|
||||
|
||||
### 3. Systemd Service (Linux)
|
||||
|
||||
Erstellen Sie `/etc/systemd/system/admintemplate.service`:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=AdminTemplate Backend
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=admintemplate
|
||||
WorkingDirectory=/opt/admintemplate
|
||||
Environment="SERVER_PORT=8080"
|
||||
Environment="DB_TYPE=sqlite"
|
||||
Environment="SQLITE_PATH=/var/lib/admintemplate/db.sqlite"
|
||||
Environment="JWT_SECRET=your-production-secret"
|
||||
ExecStart=/opt/admintemplate/server
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
```bash
|
||||
# Service aktivieren
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable admintemplate
|
||||
sudo systemctl start admintemplate
|
||||
|
||||
# Status prüfen
|
||||
sudo systemctl status admintemplate
|
||||
```
|
||||
|
||||
### 4. Reverse Proxy (Nginx/Traefik)
|
||||
|
||||
#### Nginx Konfiguration
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name example.com;
|
||||
|
||||
# HTTPS Redirect (optional)
|
||||
# return 301 https://$server_name$request_uri;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:80;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Mit SSL/TLS (Let's Encrypt)
|
||||
|
||||
```bash
|
||||
# Certbot installieren
|
||||
sudo apt install certbot python3-certbot-nginx
|
||||
|
||||
# Zertifikat erstellen
|
||||
sudo certbot --nginx -d example.com
|
||||
```
|
||||
|
||||
## Produktions-Checkliste
|
||||
|
||||
### Sicherheit
|
||||
|
||||
- [ ] JWT_SECRET auf sicheren Zufallswert setzen
|
||||
- [ ] Standard-Benutzerpasswörter ändern
|
||||
- [ ] HTTPS aktivieren
|
||||
- [ ] Firewall konfigurieren (nur Port 80/443 öffnen)
|
||||
- [ ] Regelmäßige Updates planen
|
||||
|
||||
### Datenbank
|
||||
|
||||
- [ ] Backup-Strategie implementieren
|
||||
- [ ] Für SQLite: Regelmäßige Datenbank-Kopien
|
||||
- [ ] Für MariaDB: Automatische Backups einrichten
|
||||
|
||||
```bash
|
||||
# SQLite Backup
|
||||
sqlite3 /app/data/admintemplate.db ".backup /backups/db-$(date +%Y%m%d).sqlite"
|
||||
|
||||
# MariaDB Backup
|
||||
docker exec admintemplate-mariadb mysqldump -u root -p admintemplate > backup-$(date +%Y%m%d).sql
|
||||
```
|
||||
|
||||
### Monitoring
|
||||
|
||||
- [ ] Logs konfigurieren
|
||||
- [ ] Health-Check-Endpunkte überwachen
|
||||
- [ ] Ressourcenverbrauch überwachen
|
||||
|
||||
```bash
|
||||
# Docker Logs
|
||||
docker-compose logs -f --tail=100
|
||||
|
||||
# Health Check
|
||||
curl http://localhost:8080/health
|
||||
```
|
||||
|
||||
### Performance
|
||||
|
||||
- [ ] Frontend-Assets komprimieren (Gzip/Brotli)
|
||||
- [ ] Datenbankindizes optimieren
|
||||
- [ ] Caching aktivieren (Redis/Memory)
|
||||
- [ ] CDN für statische Assets (optional)
|
||||
|
||||
## Umgebungsspezifische Konfigurationen
|
||||
|
||||
### Windows Server
|
||||
|
||||
```powershell
|
||||
# Docker Desktop installieren
|
||||
# WSL2 Backend aktivieren
|
||||
|
||||
# Container starten
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Linux Server
|
||||
|
||||
```bash
|
||||
# Docker installieren
|
||||
curl -fsSL https://get.docker.com -o get-docker.sh
|
||||
sh get-docker.sh
|
||||
|
||||
# Docker Compose installieren
|
||||
sudo apt install docker-compose
|
||||
|
||||
# Container starten
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Embedded Linux (Yocto/BuildRoot)
|
||||
|
||||
1. SQLite in Image integrieren
|
||||
2. Go Runtime oder statisches Binary verwenden
|
||||
3. Minimale Docker Installation oder natives Binary
|
||||
|
||||
```bash
|
||||
# Statisches Binary bauen
|
||||
CGO_ENABLED=1 go build -ldflags="-s -w" -a -installsuffix cgo -o server cmd/server/main.go
|
||||
|
||||
# Binary komprimieren (optional)
|
||||
upx --best --lzma server
|
||||
```
|
||||
|
||||
## Skalierung
|
||||
|
||||
### Horizontal Scaling
|
||||
|
||||
```yaml
|
||||
# docker-compose.scale.yml
|
||||
services:
|
||||
backend:
|
||||
deploy:
|
||||
replicas: 3
|
||||
|
||||
nginx:
|
||||
# Load Balancer Konfiguration
|
||||
```
|
||||
|
||||
### Datenbank-Skalierung
|
||||
|
||||
- SQLite: Für kleine bis mittlere Anwendungen
|
||||
- MariaDB: Master-Slave Replikation für höhere Last
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Container starten nicht
|
||||
|
||||
```bash
|
||||
# Logs prüfen
|
||||
docker-compose logs
|
||||
|
||||
# Container neu bauen
|
||||
docker-compose down
|
||||
docker-compose build --no-cache
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Datenbank-Verbindungsfehler
|
||||
|
||||
```bash
|
||||
# Backend-Container Logs
|
||||
docker-compose logs backend
|
||||
|
||||
# Datenbank-Container prüfen
|
||||
docker-compose exec mariadb mysql -u root -p
|
||||
```
|
||||
|
||||
### Frontend zeigt Fehler
|
||||
|
||||
```bash
|
||||
# Nginx-Konfiguration prüfen
|
||||
docker-compose exec frontend nginx -t
|
||||
|
||||
# Nginx neu laden
|
||||
docker-compose restart frontend
|
||||
```
|
||||
|
||||
## Migration zwischen Datenbanken
|
||||
|
||||
### SQLite zu MariaDB
|
||||
|
||||
```bash
|
||||
# 1. Daten aus SQLite exportieren
|
||||
sqlite3 admintemplate.db .dump > dump.sql
|
||||
|
||||
# 2. MariaDB Container starten
|
||||
docker-compose -f docker-compose.yml -f docker-compose.mariadb.yml up -d mariadb
|
||||
|
||||
# 3. Daten importieren (anpassen für MariaDB Syntax)
|
||||
docker exec -i admintemplate-mariadb mysql -u root -p admintemplate < dump_converted.sql
|
||||
|
||||
# 4. Backend auf MariaDB umstellen
|
||||
# DB_TYPE=mysql in .env setzen
|
||||
```
|
||||
|
||||
## Backup & Restore
|
||||
|
||||
### Backup
|
||||
|
||||
```bash
|
||||
# SQLite
|
||||
docker cp admintemplate-backend:/app/data/admintemplate.db ./backup/
|
||||
|
||||
# MariaDB
|
||||
docker exec admintemplate-mariadb mysqldump -u root -p admintemplate > backup.sql
|
||||
```
|
||||
|
||||
### Restore
|
||||
|
||||
```bash
|
||||
# SQLite
|
||||
docker cp ./backup/admintemplate.db admintemplate-backend:/app/data/
|
||||
|
||||
# MariaDB
|
||||
docker exec -i admintemplate-mariadb mysql -u root -p admintemplate < backup.sql
|
||||
```
|
||||
|
|
@ -0,0 +1,425 @@
|
|||
# Development Guide
|
||||
|
||||
## Entwicklungsumgebung einrichten
|
||||
|
||||
### Voraussetzungen
|
||||
|
||||
- Go 1.21 oder höher
|
||||
- Node.js 18+ und npm
|
||||
- Git
|
||||
- Docker (optional, für Container-Entwicklung)
|
||||
|
||||
### Erste Schritte
|
||||
|
||||
```bash
|
||||
# Repository klonen oder kopieren
|
||||
cd AdminTemplate
|
||||
|
||||
# Backend initialisieren
|
||||
cd backend
|
||||
go mod download
|
||||
cp .env.example .env
|
||||
|
||||
# Frontend initialisieren
|
||||
cd ../frontend
|
||||
npm install
|
||||
|
||||
# Beide starten (mit Makefile)
|
||||
cd ..
|
||||
make dev
|
||||
```
|
||||
|
||||
## Projektstruktur verstehen
|
||||
|
||||
### Backend (Go)
|
||||
|
||||
```
|
||||
backend/
|
||||
├── cmd/server/ # Hauptanwendung (main.go)
|
||||
├── internal/
|
||||
│ ├── auth/ # JWT & Passwort-Hashing
|
||||
│ ├── database/ # DB-Verbindung & Migration
|
||||
│ ├── handlers/ # HTTP Request Handler
|
||||
│ ├── middleware/ # Auth Middleware
|
||||
│ └── models/ # Datenmodelle
|
||||
└── pkg/config/ # Konfiguration
|
||||
```
|
||||
|
||||
### Frontend (Vue 3)
|
||||
|
||||
```
|
||||
frontend/
|
||||
├── src/
|
||||
│ ├── components/ # Wiederverwendbare Komponenten
|
||||
│ ├── views/ # Seiten-Komponenten
|
||||
│ ├── router/ # Vue Router Konfiguration
|
||||
│ ├── stores/ # Pinia State Management
|
||||
│ ├── services/ # API Service Layer
|
||||
│ └── styles/ # CSS Styles
|
||||
```
|
||||
|
||||
## Entwicklungsworkflow
|
||||
|
||||
### Neue Features entwickeln
|
||||
|
||||
#### 1. Backend API Endpoint hinzufügen
|
||||
|
||||
**Schritt 1: Model definieren**
|
||||
|
||||
```go
|
||||
// backend/internal/models/product.go
|
||||
package models
|
||||
|
||||
type Product struct {
|
||||
ID int64 `json:"id" db:"id"`
|
||||
Name string `json:"name" db:"name"`
|
||||
Description string `json:"description" db:"description"`
|
||||
Price float64 `json:"price" db:"price"`
|
||||
CreatedAt time.Time `json:"created_at" db:"created_at"`
|
||||
}
|
||||
```
|
||||
|
||||
**Schritt 2: Handler erstellen**
|
||||
|
||||
```go
|
||||
// backend/internal/handlers/product_handler.go
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"admintemplate/internal/database"
|
||||
"admintemplate/internal/models"
|
||||
)
|
||||
|
||||
type ProductHandler struct {
|
||||
db *database.DB
|
||||
}
|
||||
|
||||
func NewProductHandler(db *database.DB) *ProductHandler {
|
||||
return &ProductHandler{db: db}
|
||||
}
|
||||
|
||||
func (h *ProductHandler) List(c *gin.Context) {
|
||||
// Implementation
|
||||
}
|
||||
|
||||
func (h *ProductHandler) Create(c *gin.Context) {
|
||||
// Implementation
|
||||
}
|
||||
```
|
||||
|
||||
**Schritt 3: Routen registrieren**
|
||||
|
||||
```go
|
||||
// backend/cmd/server/main.go
|
||||
productHandler := handlers.NewProductHandler(db)
|
||||
|
||||
api.GET("/products", productHandler.List)
|
||||
api.POST("/products", productHandler.Create)
|
||||
```
|
||||
|
||||
**Schritt 4: Datenbank-Migration**
|
||||
|
||||
```go
|
||||
// backend/internal/database/database.go
|
||||
func (db *DB) Migrate() error {
|
||||
// Bestehende Migrationen...
|
||||
|
||||
createProductsTable := `
|
||||
CREATE TABLE IF NOT EXISTS products (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
price REAL NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);`
|
||||
|
||||
if _, err := db.Exec(createProductsTable); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. Frontend Features hinzufügen
|
||||
|
||||
**Schritt 1: Service erstellen**
|
||||
|
||||
```javascript
|
||||
// frontend/src/services/products.js
|
||||
import api from './api'
|
||||
|
||||
export const productService = {
|
||||
async getAll() {
|
||||
const response = await api.get('/products')
|
||||
return response.data
|
||||
},
|
||||
|
||||
async create(product) {
|
||||
const response = await api.post('/products', product)
|
||||
return response.data
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Schritt 2: Store erstellen (optional)**
|
||||
|
||||
```javascript
|
||||
// frontend/src/stores/products.js
|
||||
import { defineStore } from 'pinia'
|
||||
import { productService } from '../services/products'
|
||||
|
||||
export const useProductStore = defineStore('products', {
|
||||
state: () => ({
|
||||
products: []
|
||||
}),
|
||||
|
||||
actions: {
|
||||
async fetchProducts() {
|
||||
this.products = await productService.getAll()
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Schritt 3: View/Komponente erstellen**
|
||||
|
||||
```vue
|
||||
<!-- frontend/src/views/Products.vue -->
|
||||
<template>
|
||||
<div>
|
||||
<Navbar />
|
||||
<div class="container">
|
||||
<h1>Produkte</h1>
|
||||
<!-- Liste der Produkte -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { productService } from '../services/products'
|
||||
import Navbar from '../components/Navbar.vue'
|
||||
|
||||
const products = ref([])
|
||||
|
||||
onMounted(async () => {
|
||||
products.value = await productService.getAll()
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
**Schritt 4: Route hinzufügen**
|
||||
|
||||
```javascript
|
||||
// frontend/src/router/index.js
|
||||
{
|
||||
path: '/products',
|
||||
name: 'Products',
|
||||
component: () => import('../views/Products.vue'),
|
||||
meta: { requiresAuth: true }
|
||||
}
|
||||
```
|
||||
|
||||
## Anpassung für Ihr Projekt
|
||||
|
||||
### 1. Projekt umbenennen
|
||||
|
||||
```bash
|
||||
# Backend: go.mod bearbeiten
|
||||
cd backend
|
||||
# module admintemplate -> module IhrProjektname
|
||||
|
||||
# Alle Imports aktualisieren
|
||||
find . -type f -name "*.go" -exec sed -i 's/admintemplate/IhrProjektname/g' {} +
|
||||
|
||||
# Frontend: package.json bearbeiten
|
||||
cd ../frontend
|
||||
# "name": "IhrProjektname-frontend"
|
||||
```
|
||||
|
||||
### 2. Branding anpassen
|
||||
|
||||
```javascript
|
||||
// frontend/src/components/Navbar.vue
|
||||
<div class="navbar-brand">Ihr Projektname</div>
|
||||
|
||||
// frontend/index.html
|
||||
<title>Ihr Projektname</title>
|
||||
```
|
||||
|
||||
### 3. Standard-Benutzer ändern
|
||||
|
||||
```go
|
||||
// backend/internal/handlers/auth_handler.go
|
||||
func (h *AuthHandler) InitDefaultUsers() error {
|
||||
defaultUsers := []struct {
|
||||
username string
|
||||
password string
|
||||
email string
|
||||
role string
|
||||
}{
|
||||
{"admin", "IhrAdminPasswort", "admin@example.com", "admin"},
|
||||
{"user", "IhrUserPasswort", "user@example.com", "user"},
|
||||
}
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Backend Tests
|
||||
|
||||
```go
|
||||
// backend/internal/handlers/auth_handler_test.go
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLogin(t *testing.T) {
|
||||
// Test implementation
|
||||
}
|
||||
```
|
||||
|
||||
```bash
|
||||
# Tests ausführen
|
||||
cd backend
|
||||
go test ./...
|
||||
```
|
||||
|
||||
### Frontend Tests
|
||||
|
||||
```javascript
|
||||
// frontend/src/components/__tests__/Navbar.test.js
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import Navbar from '../Navbar.vue'
|
||||
|
||||
describe('Navbar', () => {
|
||||
it('renders properly', () => {
|
||||
const wrapper = mount(Navbar)
|
||||
expect(wrapper.text()).toContain('Admin Template')
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
### Backend
|
||||
|
||||
```go
|
||||
// Logging hinzufügen
|
||||
import "log"
|
||||
|
||||
log.Printf("Debug: %+v", variable)
|
||||
|
||||
// Mit Debugger (delve)
|
||||
go install github.com/go-delve/delve/cmd/dlv@latest
|
||||
dlv debug cmd/server/main.go
|
||||
```
|
||||
|
||||
### Frontend
|
||||
|
||||
```javascript
|
||||
// Browser DevTools Console
|
||||
console.log('Debug:', data)
|
||||
|
||||
// Vue DevTools Extension installieren
|
||||
// Chrome/Firefox: Vue.js devtools
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Backend
|
||||
|
||||
1. **Fehlerbehandlung**: Immer Fehler zurückgeben und loggen
|
||||
2. **Prepared Statements**: Gegen SQL-Injection
|
||||
3. **Middleware**: Wiederverwendbare Logik auslagern
|
||||
4. **Strukturierung**: Trennung von Handler, Service, Repository
|
||||
|
||||
### Frontend
|
||||
|
||||
1. **Komponenten**: Klein und wiederverwendbar halten
|
||||
2. **State Management**: Komplexen State in Pinia Stores
|
||||
3. **API Calls**: Immer über Service Layer
|
||||
4. **Error Handling**: Try-catch für alle API Calls
|
||||
|
||||
## Häufige Aufgaben
|
||||
|
||||
### Neue Tabelle hinzufügen
|
||||
|
||||
1. Model in `internal/models/` definieren
|
||||
2. Migration in `database.Migrate()` hinzufügen
|
||||
3. Handler erstellen
|
||||
4. Routen registrieren
|
||||
|
||||
### Neue Benutzerrolle hinzufügen
|
||||
|
||||
1. Role in Datenbank-Schema erweitern
|
||||
2. Middleware für neue Rolle erstellen
|
||||
3. Frontend-Router Guards anpassen
|
||||
|
||||
### Custom Middleware hinzufügen
|
||||
|
||||
```go
|
||||
// backend/internal/middleware/custom.go
|
||||
func CustomMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Vor Request
|
||||
c.Next()
|
||||
// Nach Request
|
||||
}
|
||||
}
|
||||
|
||||
// In main.go registrieren
|
||||
r.Use(middleware.CustomMiddleware())
|
||||
```
|
||||
|
||||
## Performance-Optimierung
|
||||
|
||||
### Backend
|
||||
|
||||
- Datenbankindizes hinzufügen
|
||||
- Connection Pooling konfigurieren
|
||||
- Caching implementieren (z.B. Redis)
|
||||
|
||||
### Frontend
|
||||
|
||||
- Lazy Loading für Routen
|
||||
- Code Splitting
|
||||
- Asset Optimization (Vite macht das automatisch)
|
||||
|
||||
## Git Workflow
|
||||
|
||||
```bash
|
||||
# Feature Branch erstellen
|
||||
git checkout -b feature/neue-funktion
|
||||
|
||||
# Änderungen committen
|
||||
git add .
|
||||
git commit -m "feat: Neue Funktion hinzugefügt"
|
||||
|
||||
# In Main mergen
|
||||
git checkout main
|
||||
git merge feature/neue-funktion
|
||||
```
|
||||
|
||||
## Nützliche Befehle
|
||||
|
||||
```bash
|
||||
# Backend Hot Reload (mit air)
|
||||
go install github.com/cosmtrek/air@latest
|
||||
cd backend && air
|
||||
|
||||
# Frontend mit spezifischem Port
|
||||
cd frontend && npm run dev -- --port 3001
|
||||
|
||||
# Alle Container neu bauen
|
||||
docker-compose up -d --build --force-recreate
|
||||
|
||||
# Logs verfolgen
|
||||
docker-compose logs -f backend
|
||||
```
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Admin Template</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "admintemplate-frontend",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.2.5",
|
||||
"pinia": "^2.1.7",
|
||||
"axios": "^1.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
"vite": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<router-view />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
</script>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<template>
|
||||
<nav class="navbar">
|
||||
<div class="navbar-brand">Admin Template</div>
|
||||
<div class="navbar-menu">
|
||||
<router-link to="/">Dashboard</router-link>
|
||||
<router-link v-if="authStore.isAdmin" to="/users">Benutzer</router-link>
|
||||
<router-link to="/profile">Profil</router-link>
|
||||
<span>{{ authStore.user?.username }}</span>
|
||||
<button @click="handleLogout" class="btn btn-secondary">Abmelden</button>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const router = useRouter()
|
||||
|
||||
const handleLogout = () => {
|
||||
authStore.logout()
|
||||
router.push('/login')
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import './styles/main.css'
|
||||
|
||||
const app = createApp(App)
|
||||
const pinia = createPinia()
|
||||
|
||||
app.use(pinia)
|
||||
app.use(router)
|
||||
app.mount('#app')
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { authService } from '../services/auth'
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('../views/Login.vue'),
|
||||
meta: { public: true },
|
||||
},
|
||||
{
|
||||
path: '/',
|
||||
name: 'Dashboard',
|
||||
component: () => import('../views/Dashboard.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/users',
|
||||
name: 'Users',
|
||||
component: () => import('../views/Users.vue'),
|
||||
meta: { requiresAuth: true, requiresAdmin: true },
|
||||
},
|
||||
{
|
||||
path: '/profile',
|
||||
name: 'Profile',
|
||||
component: () => import('../views/Profile.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
]
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes,
|
||||
})
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
const isAuthenticated = authService.isAuthenticated()
|
||||
const user = authService.getUser()
|
||||
|
||||
if (to.meta.requiresAuth && !isAuthenticated) {
|
||||
next('/login')
|
||||
} else if (to.meta.requiresAdmin && user?.role !== 'admin') {
|
||||
next('/')
|
||||
} else if (to.path === '/login' && isAuthenticated) {
|
||||
next('/')
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import axios from 'axios'
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: '/api',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
|
||||
// Add token to requests
|
||||
api.interceptors.request.use((config) => {
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
}
|
||||
return config
|
||||
})
|
||||
|
||||
// Handle 401 errors
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
localStorage.removeItem('token')
|
||||
window.location.href = '/login'
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
export default api
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
import api from './api'
|
||||
|
||||
export const authService = {
|
||||
async login(username, password) {
|
||||
const response = await api.post('/auth/login', { username, password })
|
||||
if (response.data.token) {
|
||||
localStorage.setItem('token', response.data.token)
|
||||
localStorage.setItem('user', JSON.stringify(response.data.user))
|
||||
}
|
||||
return response.data
|
||||
},
|
||||
|
||||
logout() {
|
||||
localStorage.removeItem('token')
|
||||
localStorage.removeItem('user')
|
||||
},
|
||||
|
||||
async getCurrentUser() {
|
||||
const response = await api.get('/auth/me')
|
||||
return response.data
|
||||
},
|
||||
|
||||
isAuthenticated() {
|
||||
return !!localStorage.getItem('token')
|
||||
},
|
||||
|
||||
getUser() {
|
||||
const user = localStorage.getItem('user')
|
||||
return user ? JSON.parse(user) : null
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import api from './api'
|
||||
|
||||
export const userService = {
|
||||
async getAll() {
|
||||
const response = await api.get('/users')
|
||||
return response.data
|
||||
},
|
||||
|
||||
async getById(id) {
|
||||
const response = await api.get(`/users/${id}`)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async create(userData) {
|
||||
const response = await api.post('/users', userData)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async update(id, userData) {
|
||||
const response = await api.put(`/users/${id}`, userData)
|
||||
return response.data
|
||||
},
|
||||
|
||||
async delete(id) {
|
||||
const response = await api.delete(`/users/${id}`)
|
||||
return response.data
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { authService } from '../services/auth'
|
||||
|
||||
export const useAuthStore = defineStore('auth', {
|
||||
state: () => ({
|
||||
user: authService.getUser(),
|
||||
isAuthenticated: authService.isAuthenticated(),
|
||||
}),
|
||||
|
||||
getters: {
|
||||
isAdmin: (state) => state.user?.role === 'admin',
|
||||
},
|
||||
|
||||
actions: {
|
||||
async login(username, password) {
|
||||
const data = await authService.login(username, password)
|
||||
this.user = data.user
|
||||
this.isAuthenticated = true
|
||||
},
|
||||
|
||||
logout() {
|
||||
authService.logout()
|
||||
this.user = null
|
||||
this.isAuthenticated = false
|
||||
},
|
||||
|
||||
async fetchCurrentUser() {
|
||||
const user = await authService.getCurrentUser()
|
||||
this.user = user
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
@ -0,0 +1,293 @@
|
|||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
#app {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Navbar */
|
||||
.navbar {
|
||||
background: #2c3e50;
|
||||
color: white;
|
||||
padding: 1rem 2rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.navbar-menu {
|
||||
display: flex;
|
||||
gap: 2rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.navbar-menu a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.navbar-menu a:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Cards */
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 1rem;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
/* Forms */
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: #3498db;
|
||||
}
|
||||
|
||||
.form-select {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
background: white;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
.btn {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #3498db;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #2980b9;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #27ae60;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #229954;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #e74c3c;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #c0392b;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #95a5a6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #7f8c8d;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
padding: 1rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.table th {
|
||||
background: #f8f9fa;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.table tbody tr:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
/* Alerts */
|
||||
.alert {
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.alert-error {
|
||||
background: #fee;
|
||||
color: #c00;
|
||||
border-left: 4px solid #c00;
|
||||
}
|
||||
|
||||
.alert-success {
|
||||
background: #efe;
|
||||
color: #060;
|
||||
border-left: 4px solid #060;
|
||||
}
|
||||
|
||||
/* Login Page */
|
||||
.login-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
.login-card {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 10px 25px rgba(0,0,0,0.2);
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
color: #2c3e50;
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
/* Dashboard Stats */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
padding: 1.5rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
color: #3498db;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #7f8c8d;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* Utilities */
|
||||
.mt-1 { margin-top: 0.5rem; }
|
||||
.mt-2 { margin-top: 1rem; }
|
||||
.mt-3 { margin-top: 1.5rem; }
|
||||
.mb-1 { margin-bottom: 0.5rem; }
|
||||
.mb-2 { margin-bottom: 1rem; }
|
||||
.mb-3 { margin-bottom: 1.5rem; }
|
||||
|
||||
.text-center { text-align: center; }
|
||||
.text-right { text-align: right; }
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-between {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.gap-1 { gap: 0.5rem; }
|
||||
.gap-2 { gap: 1rem; }
|
||||
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.badge-danger {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.badge-info {
|
||||
background: #d1ecf1;
|
||||
color: #0c5460;
|
||||
}
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
<template>
|
||||
<div>
|
||||
<Navbar />
|
||||
<div class="container">
|
||||
<h1>Dashboard</h1>
|
||||
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ userCount }}</div>
|
||||
<div class="stat-label">Benutzer</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ activeUsers }}</div>
|
||||
<div class="stat-label">Aktive Benutzer</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ authStore.user?.role }}</div>
|
||||
<div class="stat-label">Ihre Rolle</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-title">Willkommen, {{ authStore.user?.username }}!</h2>
|
||||
<p>Sie sind erfolgreich angemeldet.</p>
|
||||
<p v-if="authStore.isAdmin" class="mt-2">
|
||||
Als Administrator haben Sie Zugriff auf die Benutzerverwaltung.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-title">Systeminfo</h2>
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>Benutzername:</strong></td>
|
||||
<td>{{ authStore.user?.username }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>E-Mail:</strong></td>
|
||||
<td>{{ authStore.user?.email }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Rolle:</strong></td>
|
||||
<td>
|
||||
<span :class="authStore.isAdmin ? 'badge badge-info' : 'badge badge-success'">
|
||||
{{ authStore.user?.role }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Status:</strong></td>
|
||||
<td>
|
||||
<span class="badge badge-success">Aktiv</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import { userService } from '../services/users'
|
||||
import Navbar from '../components/Navbar.vue'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const userCount = ref(0)
|
||||
const activeUsers = ref(0)
|
||||
|
||||
onMounted(async () => {
|
||||
if (authStore.isAdmin) {
|
||||
try {
|
||||
const users = await userService.getAll()
|
||||
userCount.value = users.length
|
||||
activeUsers.value = users.filter(u => u.active).length
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch users:', err)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
<template>
|
||||
<div class="login-container">
|
||||
<div class="login-card">
|
||||
<h1 class="login-title">Admin Template</h1>
|
||||
|
||||
<div v-if="error" class="alert alert-error">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="handleLogin">
|
||||
<div class="form-group">
|
||||
<label class="form-label">Benutzername</label>
|
||||
<input
|
||||
v-model="username"
|
||||
type="text"
|
||||
class="form-input"
|
||||
required
|
||||
placeholder="admin oder mf"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Passwort</label>
|
||||
<input
|
||||
v-model="password"
|
||||
type="password"
|
||||
class="form-input"
|
||||
required
|
||||
placeholder="Passwort"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary" style="width: 100%">
|
||||
Anmelden
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div style="margin-top: 1rem; padding: 1rem; background: #f8f9fa; border-radius: 4px; font-size: 0.875rem;">
|
||||
<strong>Standard-Zugänge:</strong><br>
|
||||
admin / admin123<br>
|
||||
mf / mf123
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const username = ref('')
|
||||
const password = ref('')
|
||||
const error = ref('')
|
||||
|
||||
const handleLogin = async () => {
|
||||
try {
|
||||
error.value = ''
|
||||
await authStore.login(username.value, password.value)
|
||||
router.push('/')
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.error || 'Anmeldung fehlgeschlagen'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
<template>
|
||||
<div>
|
||||
<Navbar />
|
||||
<div class="container">
|
||||
<h1>Profil</h1>
|
||||
|
||||
<div class="card">
|
||||
<h2 class="card-title">Benutzerinformationen</h2>
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>ID:</strong></td>
|
||||
<td>{{ authStore.user?.id }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Benutzername:</strong></td>
|
||||
<td>{{ authStore.user?.username }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>E-Mail:</strong></td>
|
||||
<td>{{ authStore.user?.email }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Rolle:</strong></td>
|
||||
<td>
|
||||
<span :class="authStore.isAdmin ? 'badge badge-info' : 'badge badge-success'">
|
||||
{{ authStore.user?.role }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Status:</strong></td>
|
||||
<td>
|
||||
<span class="badge badge-success">Aktiv</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Erstellt am:</strong></td>
|
||||
<td>{{ formatDate(authStore.user?.created_at) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>Aktualisiert am:</strong></td>
|
||||
<td>{{ formatDate(authStore.user?.updated_at) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useAuthStore } from '../stores/auth'
|
||||
import Navbar from '../components/Navbar.vue'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
if (!dateString) return '-'
|
||||
return new Date(dateString).toLocaleDateString('de-DE', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
|
@ -0,0 +1,208 @@
|
|||
<template>
|
||||
<div>
|
||||
<Navbar />
|
||||
<div class="container">
|
||||
<div class="flex-between mb-3">
|
||||
<h1>Benutzerverwaltung</h1>
|
||||
<button @click="showCreateModal = true" class="btn btn-success">
|
||||
Neuer Benutzer
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="alert alert-error">{{ error }}</div>
|
||||
<div v-if="success" class="alert alert-success">{{ success }}</div>
|
||||
|
||||
<div class="card">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Benutzername</th>
|
||||
<th>E-Mail</th>
|
||||
<th>Rolle</th>
|
||||
<th>Status</th>
|
||||
<th>Erstellt</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="user in users" :key="user.id">
|
||||
<td>{{ user.id }}</td>
|
||||
<td>{{ user.username }}</td>
|
||||
<td>{{ user.email }}</td>
|
||||
<td>
|
||||
<span :class="user.role === 'admin' ? 'badge badge-info' : 'badge badge-success'">
|
||||
{{ user.role }}
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<span :class="user.active ? 'badge badge-success' : 'badge badge-danger'">
|
||||
{{ user.active ? 'Aktiv' : 'Inaktiv' }}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{ formatDate(user.created_at) }}</td>
|
||||
<td>
|
||||
<div class="flex gap-1">
|
||||
<button @click="editUser(user)" class="btn btn-primary" style="padding: 0.5rem 1rem;">
|
||||
Bearbeiten
|
||||
</button>
|
||||
<button @click="deleteUser(user.id)" class="btn btn-danger" style="padding: 0.5rem 1rem;">
|
||||
Löschen
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Create/Edit Modal -->
|
||||
<div v-if="showCreateModal || showEditModal"
|
||||
style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000;">
|
||||
<div class="card" style="width: 500px; max-width: 90%;">
|
||||
<h2 class="card-title">{{ showCreateModal ? 'Neuer Benutzer' : 'Benutzer bearbeiten' }}</h2>
|
||||
<form @submit.prevent="showCreateModal ? handleCreate() : handleUpdate()">
|
||||
<div class="form-group" v-if="showCreateModal">
|
||||
<label class="form-label">Benutzername</label>
|
||||
<input v-model="formData.username" type="text" class="form-input" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group" v-if="showCreateModal">
|
||||
<label class="form-label">Passwort</label>
|
||||
<input v-model="formData.password" type="password" class="form-input" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">E-Mail</label>
|
||||
<input v-model="formData.email" type="email" class="form-input" required />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Rolle</label>
|
||||
<select v-model="formData.role" class="form-select" required>
|
||||
<option value="user">User</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group" v-if="showEditModal">
|
||||
<label class="form-label">Status</label>
|
||||
<select v-model="formData.active" class="form-select" required>
|
||||
<option :value="true">Aktiv</option>
|
||||
<option :value="false">Inaktiv</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<button type="submit" class="btn btn-success">Speichern</button>
|
||||
<button type="button" @click="closeModal" class="btn btn-secondary">Abbrechen</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { userService } from '../services/users'
|
||||
import Navbar from '../components/Navbar.vue'
|
||||
|
||||
const users = ref([])
|
||||
const error = ref('')
|
||||
const success = ref('')
|
||||
const showCreateModal = ref(false)
|
||||
const showEditModal = ref(false)
|
||||
const formData = ref({
|
||||
username: '',
|
||||
password: '',
|
||||
email: '',
|
||||
role: 'user',
|
||||
active: true,
|
||||
})
|
||||
const editingUserId = ref(null)
|
||||
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
users.value = await userService.getAll()
|
||||
} catch (err) {
|
||||
error.value = 'Fehler beim Laden der Benutzer'
|
||||
}
|
||||
}
|
||||
|
||||
const handleCreate = async () => {
|
||||
try {
|
||||
error.value = ''
|
||||
success.value = ''
|
||||
await userService.create(formData.value)
|
||||
success.value = 'Benutzer erfolgreich erstellt'
|
||||
closeModal()
|
||||
fetchUsers()
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.error || 'Fehler beim Erstellen des Benutzers'
|
||||
}
|
||||
}
|
||||
|
||||
const editUser = (user) => {
|
||||
editingUserId.value = user.id
|
||||
formData.value = {
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
active: user.active,
|
||||
}
|
||||
showEditModal.value = true
|
||||
}
|
||||
|
||||
const handleUpdate = async () => {
|
||||
try {
|
||||
error.value = ''
|
||||
success.value = ''
|
||||
await userService.update(editingUserId.value, formData.value)
|
||||
success.value = 'Benutzer erfolgreich aktualisiert'
|
||||
closeModal()
|
||||
fetchUsers()
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.error || 'Fehler beim Aktualisieren des Benutzers'
|
||||
}
|
||||
}
|
||||
|
||||
const deleteUser = async (id) => {
|
||||
if (!confirm('Möchten Sie diesen Benutzer wirklich löschen?')) return
|
||||
|
||||
try {
|
||||
error.value = ''
|
||||
success.value = ''
|
||||
await userService.delete(id)
|
||||
success.value = 'Benutzer erfolgreich gelöscht'
|
||||
fetchUsers()
|
||||
} catch (err) {
|
||||
error.value = err.response?.data?.error || 'Fehler beim Löschen des Benutzers'
|
||||
}
|
||||
}
|
||||
|
||||
const closeModal = () => {
|
||||
showCreateModal.value = false
|
||||
showEditModal.value = false
|
||||
formData.value = {
|
||||
username: '',
|
||||
password: '',
|
||||
email: '',
|
||||
role: 'user',
|
||||
active: true,
|
||||
}
|
||||
editingUserId.value = null
|
||||
}
|
||||
|
||||
const formatDate = (dateString) => {
|
||||
return new Date(dateString).toLocaleDateString('de-DE', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(fetchUsers)
|
||||
</script>
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
port: 3000,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
Loading…
Reference in New Issue