1
0
Эх сурвалжийг харах

/user/me endpoint and page

Aaro Saila 2 долоо хоног өмнө
parent
commit
9f87e33940

+ 27 - 0
src/auth-server/args/args.go

@@ -0,0 +1,27 @@
+package args
+
+import (
+	"flag"
+
+	"github.com/golang-jwt/jwt/v5"
+)
+
+type Args struct {
+	ResPath   string
+	JwtSecret []byte
+}
+
+var Values Args = Args{}
+
+func Parse() {
+	resPath := flag.String("res-path", "./res", "Path to web resources")
+	jwtSecret := flag.String("jwt-secret", "defaultSecret", "Secret for generating and validating JWTs.")
+	flag.Parse()
+
+	Values.ResPath = *resPath
+	Values.JwtSecret = []byte(*jwtSecret)
+}
+
+func JwtKeyFunc(token *jwt.Token) (any, error) {
+	return Values.JwtSecret, nil
+}

+ 63 - 18
src/auth-server/controllers/user.go

@@ -2,43 +2,53 @@ package controllers
 
 import (
 	"context"
+	"errors"
 	"net/http"
+	"pong-auth/args"
 	"pong-auth/db"
+	"pong-auth/middleware"
 	"pong-auth/models"
 	"strings"
 
 	"github.com/gin-gonic/gin"
 	"github.com/golang-jwt/jwt/v5"
+	"github.com/jackc/pgx/v5"
 	"golang.org/x/crypto/bcrypt"
 )
 
-var jwtSecret []byte = []byte("secret")
+func resWithError(statusCode int, err error, c *gin.Context) {
+	c.JSON(statusCode, gin.H{
+		"err": err.Error(),
+	})
+}
 
 func UserSignup(c *gin.Context) {
 	dto := models.UserSignupDto{}
 	err := c.ShouldBindJSON(&dto)
 	if err != nil {
-		c.JSON(http.StatusInternalServerError, err.Error())
+		resWithError(http.StatusInternalServerError, err, c)
 		return
 	}
 
 	dto.Username = strings.TrimSpace(dto.Username)
 	if dto.Username == "" {
-		c.JSON(http.StatusBadRequest, gin.H{
-			"err": "Username was empty",
-		})
+		resWithError(
+			http.StatusBadRequest,
+			errors.New("Username was empty"),
+			c,
+		)
 		return
 	}
 
 	hashed, err := bcrypt.GenerateFromPassword([]byte(dto.Password), bcrypt.MinCost)
 	if err != nil {
-		c.JSON(http.StatusInternalServerError, err.Error())
+		resWithError(http.StatusInternalServerError, err, c)
 		return
 	}
 
 	conn, err := db.Connect()
 	if err != nil {
-		c.JSON(http.StatusInternalServerError, err.Error())
+		resWithError(http.StatusInternalServerError, err, c)
 		return
 	}
 	defer conn.Close(context.Background())
@@ -52,9 +62,7 @@ func UserSignup(c *gin.Context) {
 	user := models.User{}
 	err = rows.Scan(&user.Id, &user.Username, &user.Password)
 	if err != nil {
-		c.JSON(http.StatusInternalServerError, gin.H{
-			"err": err.Error(),
-		})
+		resWithError(http.StatusInternalServerError, err, c)
 		return
 	}
 
@@ -65,13 +73,13 @@ func UserLogin(c *gin.Context) {
 	dto := models.UserSignupDto{}
 	err := c.ShouldBindJSON(&dto)
 	if err != nil {
-		c.JSON(http.StatusInternalServerError, err.Error())
+		resWithError(http.StatusInternalServerError, err, c)
 		return
 	}
 
 	conn, err := db.Connect()
 	if err != nil {
-		c.JSON(http.StatusInternalServerError, err.Error())
+		resWithError(http.StatusInternalServerError, err, c)
 		return
 	}
 	defer conn.Close(context.Background())
@@ -85,7 +93,7 @@ func UserLogin(c *gin.Context) {
 	user := models.User{}
 	err = row.Scan(&user.Id, &user.Username, &user.Password)
 	if err != nil {
-		c.JSON(http.StatusInternalServerError, err.Error())
+		resWithError(http.StatusInternalServerError, err, c)
 		return
 	}
 
@@ -94,9 +102,11 @@ func UserLogin(c *gin.Context) {
 		[]byte(dto.Password),
 	)
 	if err != nil {
-		c.JSON(http.StatusUnauthorized, gin.H{
-			"err": "Wrong password",
-		})
+		resWithError(
+			http.StatusUnauthorized,
+			errors.New("Wrong password"),
+			c,
+		)
 		return
 	}
 
@@ -105,9 +115,9 @@ func UserLogin(c *gin.Context) {
 		Subject: user.Id,
 	}
 	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
-	tokenStr, err := token.SignedString(jwtSecret)
+	tokenStr, err := token.SignedString(args.Values.JwtSecret)
 	if err != nil {
-		c.JSON(http.StatusInternalServerError, err.Error())
+		resWithError(http.StatusInternalServerError, err, c)
 		return
 	}
 
@@ -121,5 +131,40 @@ func UserLogin(c *gin.Context) {
 }
 
 func UserMe(c *gin.Context) {
+	userId, ok := middleware.UserIdGet(c)
+	if !ok {
+		return
+	}
+
+	conn, err := db.Connect()
+	if err != nil {
+		resWithError(http.StatusInternalServerError, err, c)
+		return
+	}
+	defer conn.Close(context.Background())
+
+	row := conn.QueryRow(
+		context.Background(), 
+		"SELECT * FROM users WHERE id = $1",
+		userId,
+	)
+
+	user := models.User{}
+	err = row.Scan(&user.Id, &user.Username, &user.Password)
+	if err != nil {
+		if errors.Is(err, pgx.ErrNoRows) {
+			resWithError(http.StatusNotFound, errors.New("User not found"), c)
+			return
+		} else {
+			resWithError(http.StatusInternalServerError, err, c)
+			return
+		}
+	}
+
+	dto := models.UserMeDto{
+		Id:       user.Id,
+		Username: user.Username,
+	}
 
+	c.JSON(http.StatusOK, dto)
 }

+ 3 - 8
src/auth-server/main.go

@@ -1,23 +1,18 @@
 package main
 
 import (
-	"flag"
+	"pong-auth/args"
 
 	"github.com/gin-gonic/gin"
 	cors "github.com/rs/cors/wrapper/gin"
 )
 
-var resPath string = ""
-
 func main() {
-	_resPath := flag.String("res-path", "./res", "Path to web resources")
-	flag.Parse()
-
-	resPath = *_resPath
+	args.Parse()
 
 	r := gin.Default()
 
-	r.Use(cors.Default())
+	r.Use(cors.AllowAll())
 
 	RoutesSet(r)
 

+ 44 - 2
src/auth-server/middleware/auth.go

@@ -2,16 +2,19 @@ package middleware
 
 import (
 	"net/http"
+	"pong-auth/args"
 	"strings"
 
 	"github.com/gin-gonic/gin"
 	"github.com/golang-jwt/jwt/v5"
 )
 
+const userIdKey string = "user-id"
+
 func Auth() gin.HandlerFunc {
 	return func(c *gin.Context) {
 		authHeader := strings.TrimSpace(c.GetHeader("Authorization"))
-		authHeaderSplit := strings.Split(authHeader, "Bearer")
+		authHeaderSplit := strings.Split(authHeader, "Bearer ")
 		if len(authHeaderSplit) != 2 {
 			c.JSON(http.StatusBadRequest, gin.H{
 				"err": "Invalid Authorization header",
@@ -20,6 +23,45 @@ func Auth() gin.HandlerFunc {
 			return
 		}
 
-		token := authHeaderSplit[1]
+		tokenStr := authHeaderSplit[1]
+
+		token, err := jwt.Parse(tokenStr, args.JwtKeyFunc)
+		if err != nil {
+			c.JSON(http.StatusUnauthorized, gin.H{
+				"err": err.Error(),
+			})
+			c.Abort()
+			return
+		}
+
+		userId, err := token.Claims.GetSubject()
+		if err != nil {
+			c.JSON(http.StatusInternalServerError, gin.H{
+				"err": err.Error(),
+			})
+			c.Abort()
+			return
+		}
+		c.Set(userIdKey, userId)
+
+		c.Next()
+	}
+}
+
+func UserIdGet(c *gin.Context) (id string, ok bool) {
+	idAny, ok := c.Get(userIdKey)
+	if !ok {
+		c.JSON(http.StatusUnauthorized, gin.H{
+			"err": "JWT required",
+		})
+		return "", false
+	}
+	id, ok = idAny.(string)
+	if !ok {
+		c.JSON(http.StatusInternalServerError, gin.H{
+			"err": "Failed to get userId from JWT",
+		})
+		return "", false
 	}
+	return id, true
 }

+ 2 - 2
src/auth-server/res/index.html

@@ -5,12 +5,12 @@
         <title>Pong</title>
     </head>
     <body>
-        <h1>Signup</h1>
         <label>Username</label>
         <input id="input-signup-username" type=text></input>
         <label>Password</label>
         <input id="input-signup-password" type=password></input>
-        <button id="button-signup" type=button>Send</button>
+        <button id="button-signup" type=button>Signup</button>
+        <button id="button-login" type=button>Login</button>
 
         <p id="result-text"></p>
     </body>

+ 29 - 0
src/auth-server/res/index.js

@@ -1,6 +1,7 @@
 const inputUsername = document.getElementById("input-signup-username");
 const inputPassword = document.getElementById("input-signup-password");
 const buttonSignup = document.getElementById("button-signup");
+const buttonLogin = document.getElementById("button-login");
 const resultTxt = document.getElementById("result-text");
 
 buttonSignup.onclick = async () => {
@@ -24,3 +25,31 @@ buttonSignup.onclick = async () => {
 
     resultTxt.textContent = `${res.status} ${resJson}`;
 };
+
+buttonLogin.onclick = async () => {
+    console.log("click");
+    const username = inputUsername.value;
+    const password = inputPassword.value;
+
+    const res = await fetch("http://127.0.0.1:8080/api/user/login", {
+        method: "POST",
+        headers: {
+            "Content-Type": "application/json"
+        },
+        body: JSON.stringify({
+            "username": username,
+            "password": password
+        })
+    });
+
+    if (!res.ok) {
+        const resTxt = await res.text();
+        resultTxt.textContent = `${res.status} ${resTxt}`;
+        return;
+    }
+
+    const resJson = await res.json();
+
+    localStorage.setItem("token", resJson.jwt);
+    navigation.navigate("/web/me/me.html");
+};

+ 3 - 0
src/auth-server/res/me/me.css

@@ -0,0 +1,3 @@
+.me-field {
+    display: block;
+}

+ 13 - 0
src/auth-server/res/me/me.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+    <head>
+        <script src="me.js" defer></script>
+        <link rel="stylesheet" href="me.css"></link>
+        <title>Pong</title>
+    </head>
+    <body>
+        <h1>Me</h1>
+        <span class="me-field">Id: <span id="id-text"></span></span>
+        <span class="me-field">Username: <span id="username-text"></span></span>
+    </body>
+</html>

+ 25 - 0
src/auth-server/res/me/me.js

@@ -0,0 +1,25 @@
+const idText = document.getElementById("id-text");
+const usernameText = document.getElementById("username-text");
+
+window.onload = async () => {
+    const token = localStorage.getItem("token");
+
+    const res = await fetch("http://127.0.0.1:8080/api/user/me", {
+        headers: {
+            Authorization: `Bearer ${token}`
+        }
+    });
+
+    if (!res.ok) {
+        idText.textContent = "error";
+        usernameText.textContent = "error";
+        const resJson = await res.json();
+        console.error(resJson);
+        return;
+    }
+
+    const userMe = await res.json();
+
+    idText.textContent = userMe.id;
+    usernameText.textContent = userMe.username;
+};

+ 10 - 3
src/auth-server/routes.go

@@ -1,8 +1,10 @@
 package main
 
 import (
-	"fmt"
+	"net/http"
+	"pong-auth/args"
 	"pong-auth/controllers"
+	"pong-auth/middleware"
 
 	"github.com/gin-gonic/gin"
 )
@@ -12,11 +14,16 @@ func UserRoutes(r *gin.RouterGroup) {
 
 	g.POST("/signup", controllers.UserSignup)
 	g.POST("/login", controllers.UserLogin)
+
+	g.Use(middleware.Auth())
+	g.GET("/me", controllers.UserMe)
 }
 
 func RoutesSet(r *gin.Engine) {
-	fmt.Println(resPath)
-	r.Static("/", resPath)
+	r.Static("/web", args.Values.ResPath)
+	r.GET("/", func(c *gin.Context) {
+		c.Redirect(http.StatusPermanentRedirect, "/web/index.html")
+	})
 
 	g := r.Group("api")
 	UserRoutes(g)