diff --git a/backend/cmd/cli/user/get.go b/backend/cmd/cli/user/get.go index 3fde5b7..a7aa0d8 100644 --- a/backend/cmd/cli/user/get.go +++ b/backend/cmd/cli/user/get.go @@ -33,7 +33,7 @@ var getCmd = &cobra.Command{ } fmt.Printf("ID: %d\nEmail: %s\nName: %s\n", - user.ID, user.Email, user.Name) + user.ID, user.Email, user.Username) return nil }, } diff --git a/backend/db/schema.sql b/backend/db/schema.sql index a32b3cf..93fdf80 100644 --- a/backend/db/schema.sql +++ b/backend/db/schema.sql @@ -7,10 +7,10 @@ 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, + username 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, + updated_at timestamp with time zone , CONSTRAINT users_pkey PRIMARY KEY (id), CONSTRAINT users_email_key UNIQUE (email) ) diff --git a/backend/internal/auth/handler.go b/backend/internal/auth/handler.go index 02ca9a6..e9d1b76 100644 --- a/backend/internal/auth/handler.go +++ b/backend/internal/auth/handler.go @@ -2,7 +2,9 @@ package auth import ( "database/sql" + "errors" "net/http" + "studia/internal/logger" "studia/internal/user" "time" @@ -11,70 +13,77 @@ import ( ) type LoginRequest struct { - Email string `json:"email"` - Password string `json:"password"` + Email string + Password string } type RegisterRequest struct { - Email string `json:"email"` - Password string `json:"password"` - Username string `json:"username"` + Email string + Password string + Username string } const defaultRole = "user" var secret = []byte("secret") -func Login(c *gin.Context, db *sql.DB) { +func Login(c *gin.Context, db *sql.DB) error { var req LoginRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) - return + return err } if req.Email == "" || req.Password == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Email and password are required"}) - return + return errors.New("Email and password are required") } User, err := user.GetUserByEmail(db, req.Email) if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid email or password"}) - return + c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid email "}) + return err } + logger.Log.Info().Msgf("User: %+v", User) - if !user.CheckPasswordHash(db, User.Email, req.Password) { - c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid email or password"}) - return + err = user.CheckPasswordHash(db, User.Email, req.Password) + if err != nil { + return err } token, err := GenerateJWT(User.ID, User.Email, User.Role) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Could not generate token"}) - return + return err } c.JSON(http.StatusOK, gin.H{"token": token}) - + return nil } -func Register(c *gin.Context, db *sql.DB) { +func Register(c *gin.Context, db *sql.DB) error { var req RegisterRequest if err := c.ShouldBindJSON(&req); err != nil { + // Log the error for debugging purposes + logger.Log.Error().Err(err).Msg("Failed to bind JSON for registration") + // Respond with a bad request status and an error message c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"}) - return + return errors.New("Invalid request") } + logger.Log.Info().Msgf("Register Request: %+v", req) if req.Email == "" || req.Password == "" || req.Username == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Email and password are required"}) - return + return errors.New("Email and password are required") } - error := user.CreateUser(db, req.Email, req.Username, req.Password, []string{defaultRole}) - if error != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": error.Error()}) - return + err := user.CreateUser(db, req.Email, req.Username, req.Password, []string{defaultRole}) + if err != nil { + logger.Log.Error().Err(err).Msg("Failed to create user") + c.JSON(http.StatusBadRequest, gin.H{"error": err}) + return err } c.JSON(http.StatusOK, gin.H{"message": "User created successfully"}) + return nil } func GenerateJWT(uuid string, email string, roles []string) (any, error) { diff --git a/backend/internal/server/server.go b/backend/internal/server/server.go index 4a9e9df..b9d6454 100644 --- a/backend/internal/server/server.go +++ b/backend/internal/server/server.go @@ -24,6 +24,7 @@ func StartServer(cfg *config.Config) { // Lokaler Fallback (wichtig für die Entwicklung) allowedOrigins := []string{ "http://localhost:5173", // Gängiger Vite-Dev-Port + "http://127.0.0.1:5173", } if cfg.FrontendURL != "" { @@ -50,11 +51,18 @@ func StartServer(cfg *config.Config) { router.Use(cors.New(config)) router.POST("/login", func(c *gin.Context) { - auth.Login(c, db) // Pass the actual DB connection instead of nil + err := auth.Login(c, db) + if err != nil { + logger.Log.Error().Msg(err.Error()) + } }) router.POST("/register", func(c *gin.Context) { - auth.Register(c, db) + er := auth.Register(c, db) + if er != nil { + logger.Log.Error().Msg("register error") + } + }) router.Run(":" + cfg.Port) diff --git a/backend/internal/user/model.go b/backend/internal/user/model.go index da733ef..b8cf2fc 100644 --- a/backend/internal/user/model.go +++ b/backend/internal/user/model.go @@ -2,6 +2,7 @@ package user import ( "database/sql" + "studia/internal/logger" "time" "golang.org/x/crypto/bcrypt" @@ -10,7 +11,7 @@ import ( type User struct { ID string `json:"id"` Email string `json:"email"` - Name string `json:"name"` + Username string `json:"username"` PasswordHash string `json:"-"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` @@ -28,26 +29,33 @@ type User struct { // } func GetUserByEmail(db *sql.DB, email string) (*User, error) { - row := db.QueryRow("SELECT id, email, password_hash, role FROM users WHERE email=$1", email) + row := db.QueryRow("SELECT id, email, username FROM users WHERE email=$1", email) var user User - err := row.Scan(&user.ID, &user.Email, &user.PasswordHash, &user.Role) + err := row.Scan(&user.ID, &user.Email, &user.Username) if err != nil { return nil, err } return &user, nil } -func CheckPasswordHash(db *sql.DB, email string, password string) bool { +func CheckPasswordHash(db *sql.DB, email string, password string) error { row := db.QueryRow("SELECT password_hash FROM users WHERE email=$1", email) - var hash string + var hash []byte if err := row.Scan(&hash); err != nil { - return false + return err } - UserPasswordHash, error := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - if error != nil { - return false + UserPasswordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return err } - return bcrypt.CompareHashAndPassword([]byte(hash), []byte(UserPasswordHash)) == nil + logger.Log.Info().Msgf("UserPasswordHash: %s", UserPasswordHash) + logger.Log.Info().Msgf("hash: %s", hash) + logger.Log.Info().Msgf("password: %s", []byte(password)) + logger.Log.Info().Msgf("email: %s", []byte(email)) + + err = bcrypt.CompareHashAndPassword(hash, []byte(password)) + + return err } func CreateUser(db *sql.DB, email string, name string, password string, role []string) error { @@ -56,7 +64,7 @@ func CreateUser(db *sql.DB, email string, name string, password string, role []s return err } - _, err = db.Exec("INSERT INTO users (email, name, password_hash, role, created_at, updated_at) VALUES ($1, $2, $3, $4, $5, $6)", - email, name, string(passwordHash), role, time.Now(), time.Now()) + _, err = db.Exec("INSERT INTO users (email, username, password_hash) VALUES ($1, $2, $3)", + email, name, string(passwordHash)) return err } diff --git a/frontend/studia/package-lock.json b/frontend/studia/package-lock.json index b76f59c..52e107f 100644 --- a/frontend/studia/package-lock.json +++ b/frontend/studia/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@tailwindcss/vite": "^4.1.18", + "framer-motion": "^12.23.26", "jwt-decode": "^4.0.0", "react": "^19.2.0", "react-dom": "^19.2.0", @@ -2553,6 +2554,33 @@ "dev": true, "license": "ISC" }, + "node_modules/framer-motion": { + "version": "12.23.26", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.26.tgz", + "integrity": "sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.23", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -3116,6 +3144,21 @@ "node": "*" } }, + "node_modules/motion-dom": { + "version": "12.23.23", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", + "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3568,6 +3611,12 @@ "typescript": ">=4.8.4" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/frontend/studia/package.json b/frontend/studia/package.json index d6808a5..66ba1ad 100644 --- a/frontend/studia/package.json +++ b/frontend/studia/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@tailwindcss/vite": "^4.1.18", + "framer-motion": "^12.23.26", "jwt-decode": "^4.0.0", "react": "^19.2.0", "react-dom": "^19.2.0", diff --git a/frontend/studia/src/App.tsx b/frontend/studia/src/App.tsx index 7e52b00..7c20a58 100644 --- a/frontend/studia/src/App.tsx +++ b/frontend/studia/src/App.tsx @@ -1,29 +1,39 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { BrowserRouter, Routes, Route } from "react-router-dom"; -import { AuthProvider } from "./components/AuthContext"; +import { AuthProvider, useAuth } from "./components/AuthContext"; import ProtectedRoute from "./components/ProtectedRoute"; import Landing from "./pages/Landing"; import Dashboard from "./pages/Dashboard" import LoginModal from "./components/LoginModal"; +import Navigation from "./components/Navigation"; export default function App() { - const [token] = useState(localStorage.getItem("token")); + // const [token] = useState(localStorage.getItem("token")); // const [showLogin, setShowLogin] = useState(false); const [modalOpen, setModalOpen] = useState(false); + // const {token, logout } = useAuth(); - if (!token) + // useEffect(() => { + // if (token) { + // // setShowLogin(false); + // setModalOpen(false); + // } + // }, [token]); + + + // if (!token) return ( + setModalOpen(true)} /> setModalOpen(false)} /> + setModalOpen(true)} />} /> {/* } /> */} - } - - /> + } /> @@ -36,5 +46,5 @@ export default function App() { // ); - return ; + // return ; } diff --git a/frontend/studia/src/api/user.tsx b/frontend/studia/src/api/user.tsx index 99d36fe..e5c51b3 100644 --- a/frontend/studia/src/api/user.tsx +++ b/frontend/studia/src/api/user.tsx @@ -1,24 +1,24 @@ const API_URL = 'http://localhost:8080'; -export async function loginUser(email:string, password: string) { +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 }), + body: JSON.stringify({ Email, Password }), }); - if (!res.ok) throw new Error('Login fehlgeschlagen'); - return res.json(); // { token: string } + return res; } -export async function registerUser(email:string, username:string, password: string){ +export async function registerUser(request: { Email: string; Username: string; Password: string; }){ + console.log(request); const res = await fetch(`${API_URL}/register`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ email,username, password }), + body: JSON.stringify(request), }); - if (!res.ok) throw new Error('Registrierung fehlgeschlagen'); - return res.json(); // { token: string } + + return res; } export async function fetchUserProfile(token: string) { diff --git a/frontend/studia/src/components/AuthContext.tsx b/frontend/studia/src/components/AuthContext.tsx index 8bc60f7..6c4187b 100644 --- a/frontend/studia/src/components/AuthContext.tsx +++ b/frontend/studia/src/components/AuthContext.tsx @@ -13,38 +13,74 @@ type AuthContextType = { const AuthContext = createContext(null); -export const AuthProvider = ({ children }: { children: JSX.Element }) => { +export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { +// 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'); + // console.log(storedToken); + // const storedUserId = localStorage.getItem('userId'); + // if (!storedToken) { + // return; + // } + // // if (storedToken && storedUserId) { + // // setToken(storedToken); + // // setUserId(storedUserId); + // // } + // if (storedToken!==null) { + // setToken(storedToken); + // } + // if (storedUserId) { + // setUserId(storedUserId); + // } + // console.log(token); + + + // 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); + // }, [token]); + useEffect(() => { const storedToken = localStorage.getItem('token'); const storedUserId = localStorage.getItem('userId'); if (!storedToken) { - return; + return } if (storedToken && storedUserId) { setToken(storedToken); setUserId(storedUserId); } - const user = getUserFromToken(storedToken); - if (!user) { - logout(); // z. B. localStorage.clear() + navigate("/login") - return; - } + 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()); + const timeout = setTimeout(() => { + logout(); + }, user.exp * 1000 - Date.now()); + + return () => clearTimeout(timeout); - 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 @@ -55,6 +91,7 @@ export const AuthProvider = ({ children }: { children: JSX.Element }) => { setUserId(null); localStorage.removeItem('token'); localStorage.removeItem('userId'); + localStorage.clear(); }; return ( @@ -64,4 +101,9 @@ export const AuthProvider = ({ children }: { children: JSX.Element }) => { ); }; -export const useAuth = () => useContext(AuthContext); + export function useAuth() { + const context = useContext(AuthContext); + if (!context) throw new Error('useAuth must be used within AuthProvider'); + return context; + } +// export const useAuth = () => useContext(AuthContext); diff --git a/frontend/studia/src/components/Card.tsx b/frontend/studia/src/components/Card.tsx new file mode 100644 index 0000000..76aff75 --- /dev/null +++ b/frontend/studia/src/components/Card.tsx @@ -0,0 +1,71 @@ +import { useState } from "react"; +import { motion } from "framer-motion"; + +export default function FlipCard({ front, back }: { front: string; back: string }) { + const [flipped, setFlipped] = useState(false); + + return ( +
setFlipped(!flipped)} + onKeyDown={(e) => e.key === " " && setFlipped(!flipped)} + tabIndex={0} + role="button" + aria-label="Flip card" + > + + {/* Front */} +
+

+ {front} +

+
+ + {/* Back */} +
+

+ {back} +

+
+
+ + {/* Tailwind can't handle these directly */} + +
+ ); +} + +/* +USAGE (Vite + Tailwind): + + + +Stack: +- Vite +- React +- Tailwind CSS +- Framer Motion + +Behavior: +- Quizlet-style flip +- Click or spacebar to flip +- Smooth 3D animation +*/ diff --git a/frontend/studia/src/components/LoginModal.tsx b/frontend/studia/src/components/LoginModal.tsx index 477de01..584c54e 100644 --- a/frontend/studia/src/components/LoginModal.tsx +++ b/frontend/studia/src/components/LoginModal.tsx @@ -1,7 +1,11 @@ import {loginUser, registerUser} from "../api/user"; import { useState } from "react"; +import { useNavigate } from 'react-router-dom'; +import { useAuth } from './AuthContext'; export default function LoginModal({ isOpen, onClose, onSuccess }: any) { + const navigate = useNavigate(); // ← Navigation-Hook + const { login } = useAuth(); const [isRegistering, setIsRegistering] = useState(false); if (!isOpen) return null; // 👈 THIS is the key @@ -35,8 +39,10 @@ export default function LoginModal({ isOpen, onClose, onSuccess }: any) { return; } - // TODO: Call registerUser API - const res = await registerUser(email as string, username as string, password as string); + const res = await registerUser( + { Email: email as string, Username: username as string, Password: password as string } + ); + console.log(res); if (!res.ok) { alert("Registration failed!"); return; @@ -98,12 +104,18 @@ export default function LoginModal({ isOpen, onClose, onSuccess }: any) { 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); - + if (!res.ok) { + alert("Login failed!"); + return; + } const data = await res.json(); - localStorage.setItem("token", data.token); - onSuccess(data.token); + console.log(data); + login(data.token, data.userId,[]); + // localStorage.setItem("token", data.token); + + onClose(); + navigate("/"); }} className="space-y-4" > diff --git a/frontend/studia/src/components/Navigation.tsx b/frontend/studia/src/components/Navigation.tsx new file mode 100644 index 0000000..f40a443 --- /dev/null +++ b/frontend/studia/src/components/Navigation.tsx @@ -0,0 +1,39 @@ +import { Link } from 'react-router-dom'; +import { useAuth } from './AuthContext'; + +export default function Navigation({ onLogin }: { onLogin: () => void }) { + const { token, logout } = useAuth(); + + return ( + + ); +} diff --git a/frontend/studia/src/components/ProtectedRoute.tsx b/frontend/studia/src/components/ProtectedRoute.tsx index a6a8d92..a44364f 100644 --- a/frontend/studia/src/components/ProtectedRoute.tsx +++ b/frontend/studia/src/components/ProtectedRoute.tsx @@ -3,8 +3,15 @@ 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 : ; + // const { token } = useAuth(); +const token = localStorage.getItem('token'); + // console.log(token); + if(token!=null) + return children; + else + console.log(token) + return ; + // return token ? children : ; }; export default ProtectedRoute; diff --git a/frontend/studia/src/pages/Dashboard.tsx b/frontend/studia/src/pages/Dashboard.tsx index 9be8773..253c101 100644 --- a/frontend/studia/src/pages/Dashboard.tsx +++ b/frontend/studia/src/pages/Dashboard.tsx @@ -1,14 +1,17 @@ import Builder from "./Builder"; import Learn from "./Learn"; import Admin from "./Admin"; - +// import { useAuth } from "../components/AuthContext"; export default function Dashboard() { + return ( -
- - - +
+
+ + + +
); } diff --git a/frontend/studia/src/pages/Landing.tsx b/frontend/studia/src/pages/Landing.tsx index 66371d7..f80337c 100644 --- a/frontend/studia/src/pages/Landing.tsx +++ b/frontend/studia/src/pages/Landing.tsx @@ -1,27 +1,42 @@ +import { useEffect } from "react"; +import { useAuth } from "../components/AuthContext"; +import Card from "../components/Card" + export default function Landing({ onLogin }: { onLogin: () => void }) { + const {token} = useAuth(); + + useEffect(() => { + console.log(token) + }, []); + return (
-

Cardify

+

Studia

Learn smarter with flashcards & spaced repetition

{["HTTP", "JWT", "REST"].map(t => ( -
-

{t}

-

Sample definition

-
+ //
+ //

{t}

+ //

Sample definition

+ //
+ ))}
- -
+ ) : + ( + + ) }
);