finish with user login

master
Alex 2021-05-16 21:23:06 +02:00
parent e0266d9788
commit 0402892963
5 changed files with 157 additions and 51 deletions

View File

@ -4,13 +4,13 @@ POST http://localhost:3000/api/v1/user/login
Content-Type: application/json Content-Type: application/json
{ {
"username": "107", "email": "107@roese.dev",
"password": "my-passworda" "password": "bXktcGFzc3dvcmQ="
} }
### get users ### get users
POST http://localhost:3000/api/v1/users GET http://localhost:3000/api/v1/users
Content-Type: application/xml Content-Type: application/xml
Cookie: session_id=5CLPfNbit0SCNoyRy2AWslJSWTascm3q 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 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", "username": "125",
"email": "107@roese.dev", "email": "125@roese.dev",
"password": "my-password" "password": "cGFzc3dvcmQ="
} }

View File

@ -3,7 +3,7 @@ package user
import ( import (
"crypto/rand" "crypto/rand"
"database/sql" "database/sql"
"errors" "encoding/base64"
"fmt" "fmt"
"math/big" "math/big"
"regexp" "regexp"
@ -27,7 +27,7 @@ type LoginInput struct {
} }
func NewUser(c *fiber.Ctx) error { func NewUser(c *fiber.Ctx) error {
// swagger:operation POST /users user usersNewUser // swagger:operation POST /users usersNewUser
// --- // ---
// summary: Create new user // summary: Create new user
// produces: // produces:
@ -40,12 +40,12 @@ func NewUser(c *fiber.Ctx) error {
// required: true // required: true
// - name: email // - name: email
// in: query // in: query
// description: email of the user (length 3-254) // description: email of the user (length 3-255)
// type: string // type: string
// required: true // required: true
// - name: password // - name: password
// in: query // in: query
// description: password of the user (length 6-250) // description: password (base64) of the user (length 6-250)
// type: string // type: string
// required: true // required: true
// - name: hashtag // - name: hashtag
@ -58,7 +58,7 @@ func NewUser(c *fiber.Ctx) error {
// type: string // type: string
// - name: location // - name: location
// in: query // 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 // type: string
// responses: // responses:
// '201': // '201':
@ -71,9 +71,19 @@ func NewUser(c *fiber.Ctx) error {
var input LoginInput var input LoginInput
if err := c.BodyParser(&input); err != nil { if err := c.BodyParser(&input); err != nil {
log.Debugln("bodyParser failed:", err)
return c.SendStatus(fiber.StatusBadRequest) 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) { if !isValid(input.Username, 3, 30) || !isEmailValid(input.Email) || !isValid(input.Password, 6, 250) {
return c.SendStatus(fiber.StatusForbidden) return c.SendStatus(fiber.StatusForbidden)
} }
@ -94,15 +104,13 @@ func NewUser(c *fiber.Ctx) error {
input.Hashtag, err = generateRandomHashtag(db, 6) input.Hashtag, err = generateRandomHashtag(db, 6)
if err != nil { if err != nil {
log.Warn(err)
return c.SendStatus(fiber.StatusInternalServerError) return c.SendStatus(fiber.StatusInternalServerError)
} }
} else if !isHashtagValid(db, input.Hashtag) { } else if !isHashtagValid(db, input.Hashtag) {
return c.SendStatus(fiber.StatusUnprocessableEntity) return c.SendStatus(fiber.StatusUnprocessableEntity)
} }
password := []byte(input.Password) hashedPassword, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost)
hashedPassword, err := bcrypt.GenerateFromPassword(password, bcrypt.DefaultCost)
if err != nil { if err != nil {
log.Warnln("Failed to bcrypt password", err) log.Warnln("Failed to bcrypt password", err)
@ -118,14 +126,13 @@ func NewUser(c *fiber.Ctx) error {
stmt.Close() stmt.Close()
if err != nil { 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) return c.SendStatus(fiber.StatusInternalServerError)
} }
sessionId, err := createUserSession(db, userId, c.IP(), string(c.Context().UserAgent())) sessionId, err := createUserSession(db, userId, c.IP(), string(c.Context().UserAgent()))
if err != nil { if err != nil {
log.Warnln(err)
return c.SendStatus(fiber.StatusInternalServerError) return c.SendStatus(fiber.StatusInternalServerError)
} }
@ -164,7 +171,8 @@ func generateRandomHashtag(db *sql.DB, n int) (string, error) {
s, err = generateRandomString(6) s, err = generateRandomString(6)
if err != nil { if err != nil {
return "", errors.New("Error generating Hashtag " + err.Error()) log.Warnln("Error generating Hashtag:", err)
return "", err
} }
go func() { 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])?)*$") 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 { func isEmailValid(e string) bool {
if len(e) < 3 || len(e) > 254 { if len(e) < 3 || len(e) > 255 {
return false return false
} }
return emailRegex.MatchString(e) return emailRegex.MatchString(e)
@ -233,10 +241,15 @@ func isEmailAvailable(db *sql.DB, e string) bool {
} }
func SessionIdCheck(c *fiber.Ctx) error { 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 { if err != nil {
log.Warn(err)
return fiber.ErrInternalServerError return fiber.ErrInternalServerError
} }
@ -251,33 +264,64 @@ func isSessionIdValid(sessionId string) (bool, error) {
db, err := database.GetDatabase() db, err := database.GetDatabase()
if db == nil || err != nil { if db == nil || err != nil {
return false, errors.New("DB error " + err.Error()) log.Warn("DB error:", err)
return false, err
} }
defer db.Close() 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 { if err == sql.ErrNoRows {
return false, nil 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 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) { func createUserSession(db *sql.DB, userId string, ip string, userAgent string) (string, error) {
sessionId, err := generateRandomString(32) sessionId, err := generateRandomString(32)
if err != nil { 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 (?, ?, ?, ?, ?, ?);") stmt, err := db.Prepare("INSERT INTO sessions (user_id, session_id, ip, user_agent, last_login, expires) VALUES (?, ?, ?, ?, ?, ?);")
if err != nil { 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) 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.Exec(userId, sessionId, ip, ua.OS+" "+ua.Name, time.Now(), getExpiresTime())
stmt.Close() stmt.Close()
return "", nil return sessionId, nil
} }
func getExpiresTime() time.Time { func getExpiresTime() time.Time {
@ -294,6 +338,28 @@ func getExpiresTime() time.Time {
} }
func Login(c *fiber.Ctx) error { 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 var input LoginInput
if err := c.BodyParser(&input); err != nil { if err := c.BodyParser(&input); err != nil {
@ -302,34 +368,39 @@ func Login(c *fiber.Ctx) error {
log.Println(input) log.Println(input)
if input.Username != "" && !isValid(input.Username, 3, 30) { if input.Username != "" && !isValid(input.Username, 3, 30) || input.Email != "" && !isEmailValid(input.Email) || input.Username == "" && input.Email == "" || input.Password == "" {
log.Info("for1")
return c.SendStatus(fiber.StatusBadRequest) return c.SendStatus(fiber.StatusBadRequest)
} }
if input.Email != "" && !isEmailValid(input.Email) { decodedPassword, err := base64.StdEncoding.DecodeString(input.Password)
log.Info("for2")
return c.SendStatus(fiber.StatusForbidden) if err != nil {
log.Debugln("base64 decoding failed:", err)
return c.SendStatus(fiber.StatusBadRequest)
} }
input.Password = string(decodedPassword)
db, err := database.GetDatabase() db, err := database.GetDatabase()
if db == nil || err != nil { if db == nil || err != nil {
return c.SendStatus(fiber.StatusInternalServerError) return c.SendStatus(fiber.StatusInternalServerError)
} }
defer db.Close()
var userId string
var username string
var userHashtag string
var hashedPassword string var hashedPassword string
if input.Username != "" { if input.Username != "" {
err = db.QueryRow("SELECT password FROM users WHERE username = ?", input.Username).Scan(&hashedPassword) err = db.QueryRow("SELECT user_id, user_hashtag, password FROM users WHERE username = ?", input.Username).Scan(&userId, &userHashtag, &hashedPassword)
log.Info(hashedPassword)
} else { } 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 { if err != nil {
log.Infoln("una", err)
return c.SendStatus(fiber.StatusUnauthorized) return c.SendStatus(fiber.StatusUnauthorized)
} }
@ -340,14 +411,21 @@ func Login(c *fiber.Ctx) error {
return c.SendStatus(fiber.StatusUnauthorized) 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")) if err != nil {
//fmt.Println(err) 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 { func GetUser(c *fiber.Ctx) error {

View File

@ -10,7 +10,7 @@ func SetupRoutes(app *fiber.App) {
api := app.Group("/api/v1") api := app.Group("/api/v1")
u := api.Group("/user") u := api.Group("/user")
u.Get("/user", user.GetUser) u.Get("/", user.GetUser)
u.Post("/login", user.Login) u.Post("/login", user.Login)
users := api.Group("/users") users := api.Group("/users")

View File

@ -12,7 +12,7 @@ CREATE TABLE `users` (
`user_id` varchar(32) NOT NULL, `user_id` varchar(32) NOT NULL,
`user_hashtag` varchar(6) NOT NULL, `user_hashtag` varchar(6) NOT NULL,
`username` varchar(30) NOT NULL, `username` varchar(30) NOT NULL,
`email` varchar(200) NOT NULL, `email` varchar(255) NOT NULL,
`password` char(60) NOT NULL, `password` char(60) NOT NULL,
`rank` tinyint(4) DEFAULT 0, `rank` tinyint(4) DEFAULT 0,
`avatar_url` varchar(255) DEFAULT NULL, `avatar_url` varchar(255) DEFAULT NULL,

View File

@ -12,6 +12,28 @@ info:
title: App-Idea Rest-API Documentation title: App-Idea Rest-API Documentation
version: 0.0.1 version: 0.0.1
paths: 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: /users:
post: post:
operationId: usersNewUser operationId: usersNewUser
@ -21,20 +43,28 @@ paths:
name: username name: username
required: true required: true
type: string type: string
- description: email of the user (length max 200) - description: email of the user (length 3-255)
in: query in: query
name: email name: email
required: true required: true
type: string type: string
- description: password of the user (length 6-250) - description: password (base64) of the user (length 6-250)
in: query in: query
name: password name: password
required: true required: true
type: string type: string
- description: hashtag of the client (length 1-6, UPPERCASE) - description: hashtag of the client (length 2-6, UPPERCASE)
in: query in: query
name: hashtag name: hashtag
type: string 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: produces:
- application/json - application/json
responses: responses:
@ -43,10 +73,8 @@ paths:
"400": "400":
description: format is not correct description: format is not correct
"422": "422":
description: username or/and email already already assigned description: username, email or/and hashtag already assigned
summary: Create new user summary: Create new user
tags:
- user
produces: produces:
- application/json - application/json
securityDefinitions: securityDefinitions: