From 04028929631dc509ae17c789aa1a232a77563d2d Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 16 May 2021 21:23:06 +0200 Subject: [PATCH] finish with user login --- example.http | 12 +-- routers/api/v1/user/user.go | 152 +++++++++++++++++++++++++++--------- routers/router/router.go | 2 +- scheme.sql | 2 +- swagger.yaml | 40 ++++++++-- 5 files changed, 157 insertions(+), 51 deletions(-) diff --git a/example.http b/example.http index e27ea04..5d407a9 100644 --- a/example.http +++ b/example.http @@ -4,13 +4,13 @@ POST http://localhost:3000/api/v1/user/login Content-Type: application/json { - "username": "107", - "password": "my-passworda" + "email": "107@roese.dev", + "password": "bXktcGFzc3dvcmQ=" } ### get users -POST http://localhost:3000/api/v1/users +GET http://localhost:3000/api/v1/users Content-Type: application/xml Cookie: session_id=5CLPfNbit0SCNoyRy2AWslJSWTascm3q @@ -21,7 +21,7 @@ Content-Type: application/json User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/44.0.2403.157 Safari/537.36 { - "username": "107", - "email": "107@roese.dev", - "password": "my-password" + "username": "125", + "email": "125@roese.dev", + "password": "cGFzc3dvcmQ=" } \ No newline at end of file diff --git a/routers/api/v1/user/user.go b/routers/api/v1/user/user.go index d40a379..3ed7c76 100644 --- a/routers/api/v1/user/user.go +++ b/routers/api/v1/user/user.go @@ -3,7 +3,7 @@ package user import ( "crypto/rand" "database/sql" - "errors" + "encoding/base64" "fmt" "math/big" "regexp" @@ -27,7 +27,7 @@ type LoginInput struct { } func NewUser(c *fiber.Ctx) error { - // swagger:operation POST /users user usersNewUser + // swagger:operation POST /users usersNewUser // --- // summary: Create new user // produces: @@ -40,12 +40,12 @@ func NewUser(c *fiber.Ctx) error { // required: true // - name: email // in: query - // description: email of the user (length 3-254) + // description: email of the user (length 3-255) // type: string // required: true // - name: password // in: query - // description: password of the user (length 6-250) + // description: password (base64) of the user (length 6-250) // type: string // required: true // - name: hashtag @@ -58,7 +58,7 @@ func NewUser(c *fiber.Ctx) error { // type: string // - name: location // in: query - // description: location of the client (length: 1-20) (for example: Frankfurt) + // description: location of the client (length 1-20) (for example Frankfurt) // type: string // responses: // '201': @@ -71,9 +71,19 @@ func NewUser(c *fiber.Ctx) error { var input LoginInput if err := c.BodyParser(&input); err != nil { + log.Debugln("bodyParser failed:", err) return c.SendStatus(fiber.StatusBadRequest) } + decodedPassword, err := base64.StdEncoding.DecodeString(input.Password) + + if err != nil { + log.Debugln("base64 decoding failed:", err) + return c.SendStatus(fiber.StatusBadRequest) + } + + input.Password = string(decodedPassword) + if !isValid(input.Username, 3, 30) || !isEmailValid(input.Email) || !isValid(input.Password, 6, 250) { return c.SendStatus(fiber.StatusForbidden) } @@ -94,15 +104,13 @@ func NewUser(c *fiber.Ctx) error { input.Hashtag, err = generateRandomHashtag(db, 6) if err != nil { - log.Warn(err) return c.SendStatus(fiber.StatusInternalServerError) } } else if !isHashtagValid(db, input.Hashtag) { return c.SendStatus(fiber.StatusUnprocessableEntity) } - password := []byte(input.Password) - hashedPassword, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost) + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost) if err != nil { log.Warnln("Failed to bcrypt password", err) @@ -118,14 +126,13 @@ func NewUser(c *fiber.Ctx) error { stmt.Close() if err != nil { - log.Warnln("Failed to insert user to db", err.Error()) + log.Warnln("Failed to insert user to db:", err.Error()) return c.SendStatus(fiber.StatusInternalServerError) } sessionId, err := createUserSession(db, userId, c.IP(), string(c.Context().UserAgent())) if err != nil { - log.Warnln(err) return c.SendStatus(fiber.StatusInternalServerError) } @@ -164,7 +171,8 @@ func generateRandomHashtag(db *sql.DB, n int) (string, error) { s, err = generateRandomString(6) if err != nil { - return "", errors.New("Error generating Hashtag " + err.Error()) + log.Warnln("Error generating Hashtag:", err) + return "", err } go func() { @@ -217,7 +225,7 @@ func isValid(s string, min int, max int) bool { var emailRegex = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") func isEmailValid(e string) bool { - if len(e) < 3 || len(e) > 254 { + if len(e) < 3 || len(e) > 255 { return false } return emailRegex.MatchString(e) @@ -233,10 +241,15 @@ func isEmailAvailable(db *sql.DB, e string) bool { } func SessionIdCheck(c *fiber.Ctx) error { - valid, err := isSessionIdValid(c.Cookies("session_id")) + sessionId := c.Cookies("session_id") + + if sessionId == "" { + return fiber.ErrUnauthorized + } + + valid, err := isSessionIdValid(sessionId) if err != nil { - log.Warn(err) return fiber.ErrInternalServerError } @@ -251,33 +264,64 @@ func isSessionIdValid(sessionId string) (bool, error) { db, err := database.GetDatabase() if db == nil || err != nil { - return false, errors.New("DB error " + err.Error()) + log.Warn("DB error:", err) + return false, err } defer db.Close() - err = db.QueryRow("SELECT session_id FROM sessions WHERE session_id = ?", sessionId).Scan(&sessionId) + var expires string + + err = db.QueryRow("SELECT session_id, expires FROM sessions WHERE session_id = ?", sessionId).Scan(&sessionId, &expires) if err == sql.ErrNoRows { return false, nil } + + t, err := time.Parse("2006-01-02 15:04:05", expires) + + log.Infoln("expires", expires, time.Now().Add(time.Hour*73).Unix(), t.Unix()) + + if err != nil { + log.Warn("Failed to parse session datetime", err) + return false, err + } + + // session has expired + if time.Now().Add(time.Hour*73).Unix() > t.Unix() { + log.Info("bigger") + deleteSession(db, sessionId) + + return false, err + } + + log.Info("not bigger") return true, nil } +func deleteSession(db *sql.DB, sessionId string) { + res, err := db.Exec("DELETE FROM sessions WHERE session_id = ?", sessionId) + + if err != nil { + log.Infoln("err delete session", err) + } + + log.Infoln("delete session res", res) +} + func createUserSession(db *sql.DB, userId string, ip string, userAgent string) (string, error) { sessionId, err := generateRandomString(32) if err != nil { - return "", errors.New("Failed to generate user session " + err.Error()) + log.Warnln("Failed to generate user session:", err) + return "", err } - log.Info(time.Now().String()) - log.Info(getExpiresTime()) - stmt, err := db.Prepare("INSERT INTO sessions (user_id, session_id, ip, user_agent, last_login, expires) VALUES (?, ?, ?, ?, ?, ?);") if err != nil { - return "", errors.New("Failed to insert user into db " + err.Error()) + log.Warnln("Failed to insert user into db:", err) + return "", err } ua := ua.Parse(userAgent) @@ -285,7 +329,7 @@ func createUserSession(db *sql.DB, userId string, ip string, userAgent string) ( stmt.Exec(userId, sessionId, ip, ua.OS+" "+ua.Name, time.Now(), getExpiresTime()) stmt.Close() - return "", nil + return sessionId, nil } func getExpiresTime() time.Time { @@ -294,6 +338,28 @@ func getExpiresTime() time.Time { } func Login(c *fiber.Ctx) error { + // swagger:operation POST /user/login userLogin + // --- + // summary: Login a user + // produces: + // - application/json + // parameters: + // - name: username or email + // in: query + // description: username or email + // type: string + // required: true + // - name: password + // in: query + // description: password (base64) of the user + // type: string + // required: true + // responses: + // '200': + // description: login success + // '401': + // description: login credentials not correct + var input LoginInput if err := c.BodyParser(&input); err != nil { @@ -302,34 +368,39 @@ func Login(c *fiber.Ctx) error { log.Println(input) - if input.Username != "" && !isValid(input.Username, 3, 30) { - log.Info("for1") + if input.Username != "" && !isValid(input.Username, 3, 30) || input.Email != "" && !isEmailValid(input.Email) || input.Username == "" && input.Email == "" || input.Password == "" { return c.SendStatus(fiber.StatusBadRequest) } - if input.Email != "" && !isEmailValid(input.Email) { - log.Info("for2") - return c.SendStatus(fiber.StatusForbidden) + decodedPassword, err := base64.StdEncoding.DecodeString(input.Password) + + if err != nil { + log.Debugln("base64 decoding failed:", err) + return c.SendStatus(fiber.StatusBadRequest) } + input.Password = string(decodedPassword) + db, err := database.GetDatabase() if db == nil || err != nil { return c.SendStatus(fiber.StatusInternalServerError) } + defer db.Close() + + var userId string + var username string + var userHashtag string var hashedPassword string if input.Username != "" { - err = db.QueryRow("SELECT password FROM users WHERE username = ?", input.Username).Scan(&hashedPassword) - - log.Info(hashedPassword) + err = db.QueryRow("SELECT user_id, user_hashtag, password FROM users WHERE username = ?", input.Username).Scan(&userId, &userHashtag, &hashedPassword) } else { - + err = db.QueryRow("SELECT user_id, user_hashtag, username, password FROM users WHERE email = ?", input.Email).Scan(&userId, &userHashtag, &username, &hashedPassword) } if err != nil { - log.Infoln("una", err) return c.SendStatus(fiber.StatusUnauthorized) } @@ -340,14 +411,21 @@ func Login(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusUnauthorized) } - log.Info("password correct") + sessionId, err := createUserSession(db, userId, c.IP(), string(c.Context().UserAgent())) - //err = bcrypt.CompareHashAndPassword(hashedPassword, []byte("hello wolrd")) - //fmt.Println(err) + if err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } - defer db.Close() + expires := getExpiresTime() - return c.JSON(fiber.Map{"test": hashedPassword}) + c.Cookie(&fiber.Cookie{Name: "session_id", Value: sessionId, Secure: true, HTTPOnly: true, Expires: expires}) + if username != "" { + c.Cookie(&fiber.Cookie{Name: "username", Value: username, Secure: true, Expires: expires}) + } + c.Cookie(&fiber.Cookie{Name: "user_hashtag", Value: userHashtag, Secure: true, Expires: expires}) + + return c.SendStatus(fiber.StatusCreated) } func GetUser(c *fiber.Ctx) error { diff --git a/routers/router/router.go b/routers/router/router.go index be1dad1..bf43c82 100644 --- a/routers/router/router.go +++ b/routers/router/router.go @@ -10,7 +10,7 @@ func SetupRoutes(app *fiber.App) { api := app.Group("/api/v1") u := api.Group("/user") - u.Get("/user", user.GetUser) + u.Get("/", user.GetUser) u.Post("/login", user.Login) users := api.Group("/users") diff --git a/scheme.sql b/scheme.sql index 8361f35..8b5d37b 100644 --- a/scheme.sql +++ b/scheme.sql @@ -12,7 +12,7 @@ CREATE TABLE `users` ( `user_id` varchar(32) NOT NULL, `user_hashtag` varchar(6) NOT NULL, `username` varchar(30) NOT NULL, - `email` varchar(200) NOT NULL, + `email` varchar(255) NOT NULL, `password` char(60) NOT NULL, `rank` tinyint(4) DEFAULT 0, `avatar_url` varchar(255) DEFAULT NULL, diff --git a/swagger.yaml b/swagger.yaml index 14e0f25..e5933f5 100644 --- a/swagger.yaml +++ b/swagger.yaml @@ -12,6 +12,28 @@ info: title: App-Idea Rest-API Documentation version: 0.0.1 paths: + /user/login: + post: + operationId: userLogin + parameters: + - description: username or email + in: query + name: username or email + required: true + type: string + - description: password of the user + in: query + name: password + required: true + type: string + produces: + - application/json + responses: + "200": + description: login success + "401": + description: login credentials not correct + summary: Login a user /users: post: operationId: usersNewUser @@ -21,20 +43,28 @@ paths: name: username required: true type: string - - description: email of the user (length max 200) + - description: email of the user (length 3-255) in: query name: email required: true type: string - - description: password of the user (length 6-250) + - description: password (base64) of the user (length 6-250) in: query name: password required: true type: string - - description: hashtag of the client (length 1-6, UPPERCASE) + - description: hashtag of the client (length 2-6, UPPERCASE) in: query name: hashtag type: string + - description: avatar url of the client + in: query + name: avatar_url + type: string + - description: location of the client (length 1-20) (for example Frankfurt) + in: query + name: location + type: string produces: - application/json responses: @@ -43,10 +73,8 @@ paths: "400": description: format is not correct "422": - description: username or/and email already already assigned + description: username, email or/and hashtag already assigned summary: Create new user - tags: - - user produces: - application/json securityDefinitions: