Version 0.1

This commit is contained in:
Michael Franz 2025-11-26 17:29:09 +01:00
commit dcd4925371
38 changed files with 3012 additions and 0 deletions

6
.env.example Normal file
View File

@ -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

25
.gitignore vendored Normal file
View File

@ -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

85
Makefile Normal file
View File

@ -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!"

241
README.md Normal file
View File

@ -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.

20
backend/.env.example Normal file
View File

@ -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

100
backend/cmd/server/main.go Normal file
View File

@ -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)
}
}

12
backend/go.mod Normal file
View File

@ -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
)

1
backend/go.sum Normal file
View File

@ -0,0 +1 @@
# This file will be generated by go mod download

View File

@ -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")
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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"})
}

View File

@ -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()
}
}

View File

@ -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"`
}

View File

@ -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
}

View File

@ -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

76
docker-compose.yml Normal file
View File

@ -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

37
docker/Dockerfile.backend Normal file
View File

@ -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"]

View File

@ -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;"]

35
docker/nginx.conf Normal file
View File

@ -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;
}

311
docs/DEPLOYMENT.md Normal file
View File

@ -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
```

425
docs/DEVELOPMENT.md Normal file
View File

@ -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
```

12
frontend/index.html Normal file
View File

@ -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>

20
frontend/package.json Normal file
View File

@ -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"
}
}

6
frontend/src/App.vue Normal file
View File

@ -0,0 +1,6 @@
<template>
<router-view />
</template>
<script setup>
</script>

View File

@ -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>

12
frontend/src/main.js Normal file
View File

@ -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')

View File

@ -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

View File

@ -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

View File

@ -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
},
}

View File

@ -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
},
}

View File

@ -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
},
},
})

View File

@ -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;
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

15
frontend/vite.config.js Normal file
View File

@ -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,
}
}
}
})