ADD: added basic cli tool and updated the login modal

This commit is contained in:
hwinkel
2025-12-15 00:41:30 +01:00
parent a047d57824
commit 7ec17e1e8b
15 changed files with 199 additions and 103 deletions

BIN
backend/cli Executable file

Binary file not shown.

View File

@@ -9,8 +9,8 @@ import (
)
var RootCmd = &cobra.Command{
Use: "myapp",
Short: "MyApp admin CLI",
Use: "studia",
Short: "studia admin CLI",
}
func Execute() {

View File

@@ -3,25 +3,31 @@ package user
import (
"fmt"
// "studia/internal/db"
usersvc "studia/internal/user"
"studia/internal/config"
"studia/internal/database"
"studia/internal/user"
"github.com/spf13/cobra"
)
var id int64
var email string
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
}
cfg := config.New()
service := usersvc.NewService(database)
user, err := service.GetByID(id)
cfg.DatabaseHost = "192.168.178.171"
cfg.DatabasePort = "5432"
cfg.DatabaseUser = "admin"
cfg.DatabasePassword = "12345678"
cfg.DatabaseName = "studia"
database := database.New(cfg)
user, err := user.GetUserByEmail(database, email)
if err != nil {
return err
}
@@ -33,6 +39,6 @@ var getCmd = &cobra.Command{
}
func init() {
getCmd.Flags().Int64Var(&id, "id", 0, "user ID")
getCmd.MarkFlagRequired("id")
getCmd.Flags().StringVar(&email, "email", "", "user email")
getCmd.MarkFlagRequired("email")
}

View File

@@ -1,32 +0,0 @@
// 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
},
}

View File

@@ -8,6 +8,6 @@ var UserCmd = &cobra.Command{
}
func init() {
UserCmd.AddCommand(listCmd)
// UserCmd.AddCommand(listCmd)
UserCmd.AddCommand(getCmd)
}

View File

@@ -8,6 +8,7 @@ require (
)
require (
github.com/gin-gonic/contrib v0.0.0-20250521004450-2b1292699c15 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/pflag v1.0.9 // indirect
)

View File

@@ -16,6 +16,8 @@ github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQ
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/contrib v0.0.0-20250521004450-2b1292699c15 h1:AoSudS8CW8Mc9rRf5sO1vBtNxr2Ok6TaAICjgg5oKUY=
github.com/gin-gonic/contrib v0.0.0-20250521004450-2b1292699c15/go.mod h1:iqneQ2Df3omzIVTkIfn7c1acsVnMGiSLn4XF5Blh3Yg=
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=

View File

@@ -15,6 +15,14 @@ type LoginRequest struct {
Password string `json:"password"`
}
type RegisterRequest struct {
Email string `json:"email"`
Password string `json:"password"`
Username string `json:"username"`
}
const defaultRole = "user"
var secret = []byte("secret")
func Login(c *gin.Context, db *sql.DB) {
@@ -49,6 +57,26 @@ func Login(c *gin.Context, db *sql.DB) {
}
func Register(c *gin.Context, db *sql.DB) {
var req RegisterRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
return
}
if req.Email == "" || req.Password == "" || req.Username == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "Email and password are required"})
return
}
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
}
c.JSON(http.StatusOK, gin.H{"message": "User created successfully"})
}
func GenerateJWT(uuid string, email string, roles []string) (any, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"user": uuid,

View File

@@ -55,6 +55,10 @@ func StartServer(cfg *config.Config) {
auth.Login(c, db) // Pass the actual DB connection instead of nil
})
router.POST("/register", func(c *gin.Context) {
auth.Register(c, db)
})
router.Run(":" + cfg.Port)
}

View File

@@ -62,7 +62,6 @@
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
@@ -1607,7 +1606,6 @@
"integrity": "sha512-gqkrWUsS8hcm0r44yn7/xZeV1ERva/nLgrLxFRUGb7aoNMIJfZJ3AC261zDQuOAKC7MiXai1WCpYc48jAHoShQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -1618,7 +1616,6 @@
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -1678,7 +1675,6 @@
"integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.49.0",
"@typescript-eslint/types": "8.49.0",
@@ -1930,7 +1926,6 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2036,7 +2031,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -2292,7 +2286,6 @@
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -3256,7 +3249,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -3317,7 +3309,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -3327,7 +3318,6 @@
"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"
},
@@ -3597,7 +3587,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -3683,7 +3672,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.7.tgz",
"integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -3805,7 +3793,6 @@
"integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==",
"dev": true,
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}

View File

@@ -8,7 +8,7 @@ import Dashboard from "./pages/Dashboard"
import LoginModal from "./components/LoginModal";
export default function App() {
const [token, setToken] = useState(localStorage.getItem("token"));
const [token] = useState(localStorage.getItem("token"));
// const [showLogin, setShowLogin] = useState(false);
const [modalOpen, setModalOpen] = useState(false);
@@ -19,6 +19,8 @@ export default function App() {
<LoginModal isOpen={modalOpen} onClose={() => setModalOpen(false)} />
<Routes>
<Route path="/" element={<Landing onLogin={() => setModalOpen(true)} />} />
{/* <Route path="/signup" element={<SignUp/>} /> */}
<Route path="/dashboard" element={ <ProtectedRoute><Dashboard /></ProtectedRoute> }
/>

View File

@@ -11,6 +11,16 @@ export async function loginUser(email:string, password: string) {
return res.json(); // { token: string }
}
export async function registerUser(email:string, password: string){
const res = await fetch(`${API_URL}/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
if (!res.ok) throw new Error('Registrierung fehlgeschlagen');
return res.json(); // { token: string }
}
export async function fetchUserProfile(token: string) {
const res = await fetch(`${API_URL}/profile`, {
headers: { 'Authorization': `Bearer ${token}` },

View File

@@ -1,9 +1,9 @@
import { createContext, useState, useContext, useEffect } from "react";
import type { JSX } from 'react';
import { getUserFromToken } from '../utils/jwt';
import { loginUser } from "../api/user";
// import { loginUser } from "../api/user";
type AuthUser = { token: string } | null;
// type AuthUser = { token: string } | null;
type AuthContextType = {
token: string | null;
userId: string | null;
@@ -16,7 +16,7 @@ const AuthContext = createContext<AuthContextType | null>(null);
export const AuthProvider = ({ children }: { children: JSX.Element }) => {
const [token, setToken] = useState<string | null>(null);
const [userId, setUserId] = useState<string | null>(null);
const [userEmail, setuserEmail] = useState<string | null>(null);
// const [userEmail, setuserEmail] = useState<string | null>(null);
useEffect(() => {
const storedToken = localStorage.getItem('token');
@@ -47,7 +47,7 @@ export const AuthProvider = ({ children }: { children: JSX.Element }) => {
setUserId(userId);
localStorage.setItem('token', token);
localStorage.setItem('userId', userId);
// localStorage.setItem('role', JSON.stringify(role)); // Store array as string
localStorage.setItem('role', JSON.stringify(role)); // Store array as string
};
const logout = () => {

View File

@@ -1,50 +1,138 @@
import {loginUser} from "../api/user";
import { useState } from "react";
export default function LoginModal({ isOpen, onSuccess }: any) {
export default function LoginModal({ isOpen, onClose, onSuccess }: any) {
const [isRegistering, setIsRegistering] = useState(false);
if (!isOpen) return null; // 👈 THIS is the key
return(
<div className="fixed inset-0 z-50 bg-black/40 flex items-center justify-center">
<div className="bg-white rounded-2xl p-8 w-96 shadow-xl">
<h2 className="text-2xl font-bold mb-6">Login</h2>
<form
onSubmit={async (e: any) => {
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"
>
<input
name="email"
type="email"
placeholder="Email"
required
className="w-full border rounded-xl px-3 py-2"
/>
<input
name="password"
type="password"
placeholder="Password"
required
className="w-full border rounded-xl px-3 py-2"
/>
<button
type="submit"
className="w-full bg-indigo-600 text-white py-3 rounded-xl hover:bg-indigo-700"
>
Login
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold">Login</h2>
<button onClick={onClose} className="text-gray-400 hover:text-gray-600 p-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6">
<path strokeLinecap="round" strokeLinejoin="round" d="m9.75 9.75 4.5 4.5m0-4.5-4.5 4.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />
</svg>
</button>
</form>
</div>
{isRegistering ? (
<form
onSubmit={async (e: any) => {
e.preventDefault();
const fd = new FormData(e.currentTarget);
const email = fd.get("email");
const username = fd.get("username");
const password = fd.get("password");
const confirmPassword = fd.get("confirmPassword");
if (password !== confirmPassword) {
alert("Passwords do not match!");
return;
}
if (!username) {
alert("Please enter a username!");
return;
}
// TODO: Call registerUser API
// const res = await registerUser(email as string, username as string, password as string);
// TODO: Implement actual registration logic here
console.log("Registering with:", email, password);
// For now, let's just switch back to login after a "successful" registration
setIsRegistering(false);
}}
className="space-y-4"
>
<input
name="email"
type="email"
placeholder="Email"
required
className="w-full border rounded-xl px-3 py-2"
/>
<input
name="username"
type="text"
placeholder="Username"
required
className="w-full border rounded-xl px-3 py-2"
/>
<input
name="password"
type="password"
placeholder="Password"
required
className="w-full border rounded-xl px-3 py-2"
/>
<input
name="confirmPassword"
type="password"
placeholder="Confirm Password"
required
className="w-full border rounded-xl px-3 py-2"
/>
<button
type="submit"
className="w-full bg-indigo-600 text-white py-3 rounded-xl hover:bg-indigo-700"
>
Register
</button>
<button
type="button"
onClick={() => setIsRegistering(false)}
className="w-full bg-gray-200 text-gray-800 py-3 rounded-xl hover:bg-gray-300 mt-2"
>
Back to Login
</button>
</form>
) : (
<form
onSubmit={async (e: any) => {
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"
>
<input
name="email"
type="email"
placeholder="Email"
required
className="w-full border rounded-xl px-3 py-2"
/>
<input
name="password"
type="password"
placeholder="Password"
required
className="w-full border rounded-xl px-3 py-2"
/>
<button
type="submit"
className="w-full bg-indigo-600 text-white py-3 rounded-xl hover:bg-indigo-700"
>
Login
</button>
<button
type="button"
onClick={() => setIsRegistering(true)}
className="w-full bg-gray-200 text-gray-800 py-3 rounded-xl hover:bg-gray-300 mt-2"
>
Register
</button>
</form>
)}
</div>
</div>
);

View File