diff --git a/backend/cmd/cli/main.go b/backend/cmd/cli/main.go new file mode 100644 index 0000000..9d76a9d --- /dev/null +++ b/backend/cmd/cli/main.go @@ -0,0 +1,8 @@ +// cmd/cli/main.go +package main + +import "studia/cmd/cli/root" + +func main() { + root.Execute() +} diff --git a/backend/cmd/cli/root/root.go b/backend/cmd/cli/root/root.go new file mode 100644 index 0000000..4e7f0db --- /dev/null +++ b/backend/cmd/cli/root/root.go @@ -0,0 +1,25 @@ +package root + +import ( + "fmt" + "os" + "studia/cmd/cli/user" + + "github.com/spf13/cobra" +) + +var RootCmd = &cobra.Command{ + Use: "myapp", + Short: "MyApp admin CLI", +} + +func Execute() { + if err := RootCmd.Execute(); err != nil { + fmt.Println(err) + os.Exit(1) + } +} + +func init() { + RootCmd.AddCommand(user.UserCmd) +} diff --git a/backend/cmd/cli/user/get.go b/backend/cmd/cli/user/get.go new file mode 100644 index 0000000..e00b1cc --- /dev/null +++ b/backend/cmd/cli/user/get.go @@ -0,0 +1,38 @@ +// cmd/cli/user/get.go +package user + +import ( + "fmt" + // "studia/internal/db" + usersvc "studia/internal/user" + + "github.com/spf13/cobra" +) + +var id int64 + +var getCmd = &cobra.Command{ + Use: "get", + Short: "Get user by ID", + RunE: func(cmd *cobra.Command, args []string) error { + database, err := db.New(getDSN()) + if err != nil { + return err + } + + service := usersvc.NewService(database) + user, err := service.GetByID(id) + if err != nil { + return err + } + + fmt.Printf("ID: %d\nEmail: %s\nName: %s\n", + user.ID, user.Email, user.Name) + return nil + }, +} + +func init() { + getCmd.Flags().Int64Var(&id, "id", 0, "user ID") + getCmd.MarkFlagRequired("id") +} diff --git a/backend/cmd/cli/user/list.go b/backend/cmd/cli/user/list.go new file mode 100644 index 0000000..f131b91 --- /dev/null +++ b/backend/cmd/cli/user/list.go @@ -0,0 +1,32 @@ +// cmd/cli/user/list.go +package user + +import ( + "fmt" + "myapp/internal/db" + usersvc "myapp/internal/user" + + "github.com/spf13/cobra" +) + +var listCmd = &cobra.Command{ + Use: "list", + Short: "List users", + RunE: func(cmd *cobra.Command, args []string) error { + database, err := db.New(getDSN()) + if err != nil { + return err + } + + service := usersvc.NewService(database) + users, err := service.List() + if err != nil { + return err + } + + for _, u := range users { + fmt.Printf("%d | %s | %s\n", u.ID, u.Email, u.Name) + } + return nil + }, +} diff --git a/backend/cmd/cli/user/user.go b/backend/cmd/cli/user/user.go new file mode 100644 index 0000000..a49921a --- /dev/null +++ b/backend/cmd/cli/user/user.go @@ -0,0 +1,13 @@ +package user + +import "github.com/spf13/cobra" + +var UserCmd = &cobra.Command{ + Use: "user", + Short: "User management", +} + +func init() { + UserCmd.AddCommand(listCmd) + UserCmd.AddCommand(getCmd) +} diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 76f9477..b6a94ae 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -1,12 +1,23 @@ package main import ( - "studia/internal/server" - "log" + "studia/internal/config" + "studia/internal/server" ) func main() { + + cfg := config.New() + + cfg.DatabaseHost = "192.168.178.171" + cfg.DatabasePort = "5432" + cfg.DatabaseUser = "admin" + cfg.DatabasePassword = "12345678" + cfg.DatabaseName = "studia" + + log.Println("Configuration loaded:", cfg) + log.Println("Starting server...") - server.StartServer() + server.StartServer(cfg) } diff --git a/backend/db/schema.sql b/backend/db/schema.sql index 4a80598..a32b3cf 100644 --- a/backend/db/schema.sql +++ b/backend/db/schema.sql @@ -1,9 +1,21 @@ -CREATE TABLE users ( - id UUID PRIMARY KEY, - email VARCHAR(255) UNIQUE NOT NULL, - name VARCHAR(255) NOT NULL, - password_hash VARCHAR(255) NOT NULL, - created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP - updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP -); \ No newline at end of file +-- Table: public.users + +-- DROP TABLE IF EXISTS public.users; + +CREATE TABLE IF NOT EXISTS public.users +( + id character varying(255) COLLATE pg_catalog."default" NOT NULL DEFAULT uuid_generate_v4(), + email character varying(255) COLLATE pg_catalog."default" NOT NULL, + name character varying(255) COLLATE pg_catalog."default" NOT NULL, + password_hash character varying(255) COLLATE pg_catalog."default" NOT NULL, + created_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP, + updated_at timestamp with time zone DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT users_pkey PRIMARY KEY (id), + CONSTRAINT users_email_key UNIQUE (email) +) + +TABLESPACE pg_default; + +ALTER TABLE IF EXISTS public.users + OWNER to admin; \ No newline at end of file diff --git a/backend/go.mod b/backend/go.mod index 0d72753..d1dad2a 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -7,27 +7,34 @@ require ( github.com/golang-jwt/jwt/v5 v5.3.0 ) +require ( + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/spf13/pflag v1.0.9 // indirect +) + require ( github.com/bytedance/sonic v1.14.0 // indirect github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect - github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/gabriel-vasile/mimetype v1.4.9 // indirect + github.com/gin-contrib/cors v1.7.6 github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.27.0 // indirect - github.com/goccy/go-json v0.10.2 // indirect + github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.18.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/lib/pq v1.10.9 github.com/mattn/go-isatty v0.0.20 // indirect - github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/quic-go v0.54.0 // indirect + github.com/spf13/cobra v1.10.2 github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.0 // indirect go.uber.org/mock v0.5.0 // indirect diff --git a/backend/go.sum b/backend/go.sum index bb00ac9..b3bce3f 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -4,11 +4,16 @@ github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZw github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= +github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= +github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= +github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= +github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= @@ -23,6 +28,8 @@ github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHO github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= @@ -30,6 +37,8 @@ github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArs github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= @@ -42,6 +51,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= @@ -52,6 +63,11 @@ github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -67,6 +83,7 @@ github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM= diff --git a/backend/internal/auth/handler.go b/backend/internal/auth/handler.go index f42fe00..f4e7180 100644 --- a/backend/internal/auth/handler.go +++ b/backend/internal/auth/handler.go @@ -17,7 +17,7 @@ type LoginRequest struct { var secret = []byte("secret") -func LoginHandler(c *gin.Context, db *sql.DB) { +func Login(c *gin.Context, db *sql.DB) { var req LoginRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go new file mode 100644 index 0000000..f00e8b2 --- /dev/null +++ b/backend/internal/config/config.go @@ -0,0 +1,48 @@ +package config + +import ( + "os" +) + +type Config struct { + Port string + Env string + DatabaseDriver string + DatabaseHost string + DatabasePort string + DatabaseName string + DatabaseUser string + DatabasePassword string + FrontendURL string +} + +func New() *Config { + + return generateConfig() +} + +func generateConfig() *Config { + + cfg := Config{ + Port: getEnv("PORT", "8080"), + DatabaseDriver: getEnv("DB_DRIVER", "postgres"), + DatabaseHost: getEnv("database", "localhost"), + DatabasePort: getEnv("DB_PORT", "5432"), + DatabaseName: getEnv("DB_NAME", "studia"), + DatabaseUser: getEnv("DB_USER", "user"), + DatabasePassword: getEnv("DB_PASSWORD", "password"), + Env: getEnv("ENV", "development"), + FrontendURL: getEnv("FRONTEND_URL", "http://localhost:5173"), + } + + return &cfg +} + +// helper function to get env var or default +func getEnv(key, defaultVal string) string { + value, exists := os.LookupEnv(key) + if !exists || value == "" { + return defaultVal + } + return value +} diff --git a/backend/internal/database/database.go b/backend/internal/database/database.go new file mode 100644 index 0000000..18998d9 --- /dev/null +++ b/backend/internal/database/database.go @@ -0,0 +1,97 @@ +package database + +import ( + "database/sql" + "fmt" + "log" + "studia/internal/config" + + _ "github.com/lib/pq" // Import the PostgreSQL driver +) + +var expectedTables = []string{ + "users", +} + +func New(cfg *config.Config) *sql.DB { + db := setupDatabase(cfg) + + existing, err := getExistingTables(db, ` + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public' + `) + if err != nil { + log.Println("failed to query existing tables:", err) + } + + missing := checkTables(expectedTables, existing) + + if len(missing) > 0 { + log.Println("Missing tables detected:", missing) + // Here you would normally run migrations to create the missing tables + // For simplicity, we just log the missing tables + } else { + log.Println("All expected tables are present.") + } + + return db +} + +func setupDatabase(cfg *config.Config) *sql.DB { + // Database connection setup logic here + + log.Println(cfg) + switch cfg.DatabaseDriver { + case "postgres": + // Setup Postgres connection + log.Println("Setting up Postgres connection") + psqlInfo := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", + cfg.DatabaseHost, cfg.DatabasePort, cfg.DatabaseUser, cfg.DatabasePassword, cfg.DatabaseName) + db, err := sql.Open("postgres", psqlInfo) + if err != nil { + log.Fatal("Failed to connect to database:", err) + } + return db + case "mysql": + // Setup MySQL connection + dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", + cfg.DatabaseUser, cfg.DatabasePassword, cfg.DatabaseHost, cfg.DatabasePort, cfg.DatabaseName) + db, err := sql.Open("mysql", dsn) + if err != nil { + // Handle error + } + return db + default: + // Handle unsupported database driver + } + return nil +} + +func getExistingTables(db *sql.DB, query string) (map[string]bool, error) { + rows, err := db.Query(query) + if err != nil { + return nil, err + } + defer rows.Close() + + tables := make(map[string]bool) + for rows.Next() { + var name string + if err := rows.Scan(&name); err != nil { + return nil, err + } + tables[name] = true + } + return tables, nil +} + +func checkTables(expected []string, existing map[string]bool) []string { + var missing []string + for _, table := range expected { + if !existing[table] { + missing = append(missing, table) + } + } + return missing +} diff --git a/backend/internal/server/server.go b/backend/internal/server/server.go index dc6f6fa..987646b 100644 --- a/backend/internal/server/server.go +++ b/backend/internal/server/server.go @@ -1,26 +1,60 @@ package server import ( + "fmt" + "log" + "os" + "studia/internal/auth" + "studia/internal/config" + "studia/internal/database" "time" + "github.com/gin-contrib/cors" "github.com/gin-gonic/gin" - "github.com/golang-jwt/jwt/v5" ) -var secret = []byte("secret") +func StartServer(cfg *config.Config) { -func StartServer() { + router := gin.Default() - r := gin.Default() + db := database.New(cfg) - r.POST("/login", func(c *gin.Context) { - token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ - "user": "demo", - "role": "admin", - "exp": time.Now().Add(24 * time.Hour).Unix(), - }) - signed, _ := token.SignedString(secret) - c.JSON(200, gin.H{"token": signed}) + // 2. CORS-Konfiguration + // Lese die Frontend-URL aus den Umgebungsvariablen + frontendURL := os.Getenv("FRONTEND_URL") + + // Lokaler Fallback (wichtig für die Entwicklung) + allowedOrigins := []string{ + "http://localhost:5173", // Gängiger Vite-Dev-Port + } + + if frontendURL != "" { + allowedOrigins = append(allowedOrigins, frontendURL) + fmt.Printf("CORS: Erlaubte Produktiv-URL hinzugefügt: %s\n", frontendURL) + } else { + log.Println("ACHTUNG: FRONTEND_URL fehlt in den Umgebungsvariablen. Nur lokale URLs erlaubt.") + } + + // CORS + // Konfiguriere die CORS-Middleware + config := cors.Config{ + // Setze die erlaubten Ursprünge (deine React-URLs) + AllowOrigins: allowedOrigins, + // Erlaube die notwendigen HTTP-Methoden + AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"}, + // Erlaube Header (z.B. für JSON und Authentifizierung) + AllowHeaders: []string{"Origin", "Content-Type", "Accept", "Authorization"}, + // Erlaube Cookies und Credentials (falls du Tokens oder Sessions nutzt) + AllowCredentials: true, + // Wie lange die Preflight-Anfrage (OPTIONS) gecacht werden darf + MaxAge: 12 * time.Hour, + } + router.Use(cors.New(config)) + + router.POST("/login", func(c *gin.Context) { + auth.Login(c, db) // Pass the actual DB connection instead of nil }) + router.Run(":" + cfg.Port) + } diff --git a/frontend/studia/package-lock.json b/frontend/studia/package-lock.json index 9763656..2e32943 100644 --- a/frontend/studia/package-lock.json +++ b/frontend/studia/package-lock.json @@ -9,8 +9,11 @@ "version": "0.0.0", "dependencies": { "@tailwindcss/vite": "^4.1.18", + "jwt-decode": "^4.0.0", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-router": "^7.10.1", + "react-router-dom": "^6.30.2", "tailwindcss": "^4.1.18" }, "devDependencies": { @@ -981,6 +984,15 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@remix-run/router": { + "version": "1.23.1", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.1.tgz", + "integrity": "sha512-vDbaOzF7yT2Qs4vO6XV1MHcJv+3dgR1sT+l3B8xxOVhUC336prMvqrvsLL/9Dnw2xr6Qhz4J0dmS0llNAbnUmQ==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.53", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", @@ -2121,6 +2133,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2761,6 +2786,15 @@ "node": ">=6" } }, + "node_modules/jwt-decode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", + "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -3293,6 +3327,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -3310,6 +3345,60 @@ "node": ">=0.10.0" } }, + "node_modules/react-router": { + "version": "7.10.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.10.1.tgz", + "integrity": "sha512-gHL89dRa3kwlUYtRQ+m8NmxGI6CgqN+k4XyGjwcFoQwwCWF6xXpOCUlDovkXClS0d0XJN/5q7kc5W3kiFEd0Yw==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "6.30.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.2.tgz", + "integrity": "sha512-l2OwHn3UUnEVUqc6/1VMmR1cvZryZ3j3NzapC2eUXO1dB0sYp5mvwdjiXhpUbRb21eFow3qSxpP8Yv6oAU824Q==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.1", + "react-router": "6.30.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-router-dom/node_modules/react-router": { + "version": "6.30.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.2.tgz", + "integrity": "sha512-H2Bm38Zu1bm8KUE5NVWRMzuIyAV8p/JrOaBJAwVmp37AXG72+CZJlEBw6pdn9i5TBgLMhNDgijS4ZlblpHyWTA==", + "license": "MIT", + "dependencies": { + "@remix-run/router": "1.23.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -3377,6 +3466,12 @@ "semver": "bin/semver.js" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", diff --git a/frontend/studia/package.json b/frontend/studia/package.json index eab8487..d6808a5 100644 --- a/frontend/studia/package.json +++ b/frontend/studia/package.json @@ -11,8 +11,11 @@ }, "dependencies": { "@tailwindcss/vite": "^4.1.18", + "jwt-decode": "^4.0.0", "react": "^19.2.0", "react-dom": "^19.2.0", + "react-router": "^7.10.1", + "react-router-dom": "^6.30.2", "tailwindcss": "^4.1.18" }, "devDependencies": { diff --git a/frontend/studia/src/App.tsx b/frontend/studia/src/App.tsx index f09f072..eb852a1 100644 --- a/frontend/studia/src/App.tsx +++ b/frontend/studia/src/App.tsx @@ -1,19 +1,37 @@ import { useState } from "react"; +import { BrowserRouter, Routes, Route } from "react-router-dom"; +import { AuthProvider } from "./components/AuthContext"; +import ProtectedRoute from "./components/ProtectedRoute"; + import Landing from "./pages/Landing"; -// import Dashboard from "./pages/Dashboard"; import Dashboard from "./pages/Dashboard" import LoginModal from "./components/LoginModal"; export default function App() { const [token, setToken] = useState(localStorage.getItem("token")); - const [showLogin, setShowLogin] = useState(false); + // const [showLogin, setShowLogin] = useState(false); + const [modalOpen, setModalOpen] = useState(false); if (!token) return ( - <> - setShowLogin(true)} /> - {showLogin && } - + + + setModalOpen(false)} /> + + setModalOpen(true)} />} /> + } + + /> + + + + + + + // <> + // setShowLogin(true)} /> + // {showLogin && } + // ); return ; diff --git a/frontend/studia/src/api/user.tsx b/frontend/studia/src/api/user.tsx new file mode 100644 index 0000000..19fa954 --- /dev/null +++ b/frontend/studia/src/api/user.tsx @@ -0,0 +1,20 @@ +const API_URL = 'http://localhost:8080'; + + +export async function loginUser(email:string, password: string) { + const res = await fetch(`${API_URL}/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, password }), + }); + if (!res.ok) throw new Error('Login fehlgeschlagen'); + return res.json(); // { token: string } +} + +export async function fetchUserProfile(token: string) { + const res = await fetch(`${API_URL}/profile`, { + headers: { 'Authorization': `Bearer ${token}` }, + }); + if (!res.ok) throw new Error('Profil konnte nicht geladen werden'); + return res.json(); // { id: number, email: string, name: string } +} \ No newline at end of file diff --git a/frontend/studia/src/components/AuthContext.tsx b/frontend/studia/src/components/AuthContext.tsx new file mode 100644 index 0000000..f45e47a --- /dev/null +++ b/frontend/studia/src/components/AuthContext.tsx @@ -0,0 +1,67 @@ +import { createContext, useState, useContext, useEffect } from "react"; +import type { JSX } from 'react'; +import { getUserFromToken } from '../utils/jwt'; +import { loginUser } from "../api/user"; + +type AuthUser = { token: string } | null; +type AuthContextType = { + token: string | null; + userId: string | null; + login: (token: string, userId: string, role?: string[]) => void; + logout: () => void; +}; + +const AuthContext = createContext(null); + +export const AuthProvider = ({ children }: { children: JSX.Element }) => { + const [token, setToken] = useState(null); + const [userId, setUserId] = useState(null); + const [userEmail, setuserEmail] = useState(null); + + useEffect(() => { + const storedToken = localStorage.getItem('token'); + const storedUserId = localStorage.getItem('userId'); + if (!storedToken) { + return; + } + if (storedToken && storedUserId) { + setToken(storedToken); + setUserId(storedUserId); + } + + const user = getUserFromToken(storedToken); + if (!user) { + logout(); // z. B. localStorage.clear() + navigate("/login") + return; + } + // ⏳ Logout bei Ablauf + const timeout = setTimeout(() => { + logout(); + }, user.exp * 1000 - Date.now()); + + return () => clearTimeout(timeout); + }, []); + + const login = (token: string, userId: string, role: string[] = []) => { + setToken(token); + setUserId(userId); + localStorage.setItem('token', token); + localStorage.setItem('userId', userId); + // localStorage.setItem('role', JSON.stringify(role)); // Store array as string + }; + + const logout = () => { + setToken(null); + setUserId(null); + localStorage.removeItem('token'); + localStorage.removeItem('userId'); + }; + + return ( + + {children} + + ); +}; + +export const useAuth = () => useContext(AuthContext); diff --git a/frontend/studia/src/components/LoginModal.tsx b/frontend/studia/src/components/LoginModal.tsx index 30a3f8e..7eeee08 100644 --- a/frontend/studia/src/components/LoginModal.tsx +++ b/frontend/studia/src/components/LoginModal.tsx @@ -1,23 +1,51 @@ +import {loginUser} from "../api/user"; -export default function LoginModal({ onSuccess }: any) { - const login = async () => { - const res = await fetch("http://localhost:8080/login", { method: "POST" }); - const data = await res.json(); - localStorage.setItem("token", data.token); - onSuccess(data.token); - }; +export default function LoginModal({ isOpen, onSuccess }: any) { + if (!isOpen) return null; // 👈 THIS is the key - return ( -
-
-

Login

- -
+ return( +
+
+

Login

+ +
{ + e.preventDefault(); + const fd = new FormData(e.currentTarget); + const email = fd.get("email"); + const password = fd.get("password"); + + const res = await loginUser(email as string, password as string); + + const data = await res.json(); + localStorage.setItem("token", data.token); + onSuccess(data.token); + }} + className="space-y-4" + > + + + + +
+
); } diff --git a/frontend/studia/src/components/ProtectedRoute.tsx b/frontend/studia/src/components/ProtectedRoute.tsx new file mode 100644 index 0000000..a6a8d92 --- /dev/null +++ b/frontend/studia/src/components/ProtectedRoute.tsx @@ -0,0 +1,10 @@ +import { Navigate } from "react-router-dom"; +import { useAuth } from "../components/AuthContext"; +import type { JSX } from 'react'; + +const ProtectedRoute = ({ children }: { children: JSX.Element }) => { + const { token } = useAuth() as { token?: string | null }; + return token ? children : ; +}; + +export default ProtectedRoute; diff --git a/frontend/studia/src/utils/jwt.tsx b/frontend/studia/src/utils/jwt.tsx new file mode 100644 index 0000000..d8cccbf --- /dev/null +++ b/frontend/studia/src/utils/jwt.tsx @@ -0,0 +1,23 @@ +// utils/jwt.ts +import { jwtDecode } from 'jwt-decode'; + +export interface TokenPayload { + userId: string; + email: string; + role: string[]; + exp: number; +} + +export function getUserFromToken(token: string): TokenPayload | null { + try { + const decoded = jwtDecode(token); + if (decoded.exp && decoded.exp < Date.now() / 1000) { + console.warn("Token ist abgelaufen"); + return null; + } + return decoded; + } catch (error) { + console.error("Fehler beim Decodieren des Tokens:", error); + return null; + } +}