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