From 7220ffdcd28531ec778083d28f1db027ad0b350f Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 27 Jun 2021 21:29:37 +0200 Subject: [PATCH] Changed file structure --- routers/api/v1/user/activation.go | 65 ++++++ routers/api/v1/user/login.go | 99 +++++++++ routers/api/v1/user/register.go | 193 ++++++++++++++++++ routers/api/v1/user/session.go | 83 ++++++++ routers/api/v1/user/user.go | 326 ++---------------------------- 5 files changed, 458 insertions(+), 308 deletions(-) create mode 100644 routers/api/v1/user/activation.go create mode 100644 routers/api/v1/user/login.go create mode 100644 routers/api/v1/user/register.go create mode 100644 routers/api/v1/user/session.go diff --git a/routers/api/v1/user/activation.go b/routers/api/v1/user/activation.go new file mode 100644 index 0000000..82f1757 --- /dev/null +++ b/routers/api/v1/user/activation.go @@ -0,0 +1,65 @@ +package user + +import ( + "time" + + "git.umbach.dev/app-idea/rest-api/modules/database" + "git.umbach.dev/app-idea/rest-api/modules/structs" + "github.com/gofiber/fiber/v2" + log "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +func createUserActivation(userId string) (string, error) { + userActivationId, err := generateRandomString(16, 1) + + if err != nil { + log.Warnln("Failed to generate random string") + return "", err + } + + db := database.DB + + db.Create(structs.UserActivation{Id: userActivationId, UserId: userId, Expires: getUserActivationExpiresTime()}) + + return userActivationId, nil +} + +func deleteExpiredUserActivations(db *gorm.DB) { + var res string + + db.Raw("DELETE FROM user_activations WHERE expires < ?", time.Now()).Scan(&res) +} + +func ActivateUser(c *fiber.Ctx) error { + // swagger:operation POST /user/activate/:id User activation + // --- + // summary: Activate user + // responses: + // '200': + // description: User was activated + // '401': + // description: Activation Id is incorrect or expired + + db := database.DB + + deleteExpiredUserActivations(db) + + userActivation := structs.UserActivation{Id: c.Params("id")} + + db.Select("user_id").Where("id = ?", c.Params("id")).Find(&userActivation) + + if userActivation.UserId == "" { + return c.SendStatus(fiber.StatusUnauthorized) + } + + db.Delete(userActivation) + + db.Model(structs.User{Id: userActivation.UserId}).Update("state", 0) + + return c.SendStatus(fiber.StatusOK) +} + +func getUserActivationExpiresTime() time.Time { + return time.Now().Add(time.Minute * time.Duration(cfg.Settings.Expires.UserActivation)) +} diff --git a/routers/api/v1/user/login.go b/routers/api/v1/user/login.go new file mode 100644 index 0000000..dd084ee --- /dev/null +++ b/routers/api/v1/user/login.go @@ -0,0 +1,99 @@ +package user + +import ( + "encoding/base64" + + "git.umbach.dev/app-idea/rest-api/modules/database" + "git.umbach.dev/app-idea/rest-api/modules/structs" + "github.com/gofiber/fiber/v2" + log "github.com/sirupsen/logrus" + "golang.org/x/crypto/bcrypt" +) + +type LoginInput struct { + Username string `json:"username"` + Email string `json:"email"` + Password string `json:"password"` +} + +func Login(c *fiber.Ctx) error { + // swagger:operation POST /user/login User 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 { + return c.SendStatus(fiber.StatusBadRequest) + } + + log.Println(input) + + if input.Username != "" && /*!isValid(input.Username, 3, 30) */ !isUsernameValid(input.Username) || input.Email != "" && !isEmailValid(input.Email) || input.Username == "" && input.Email == "" || input.Password == "" { + log.Info("bad") + 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) + + db := database.DB + user := structs.User{} + + if input.Username != "" { + db.Select("id, hashtag, password").Where("name = ?", input.Username).Find(&user) + } else { + db.Select("id, hashtag, name, password").Where("email = ?", input.Email).Find(&user) + } + + if user.Name == "" && user.Email == "" { + return c.SendStatus(fiber.StatusUnauthorized) + } + + err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(input.Password)) + + if err != nil { + log.Warnln("Failed to comapare bcrypt password", err) + return c.SendStatus(fiber.StatusUnauthorized) + } + + sessionId, err := createUserSession(database.DB, user.Id, c.IP(), string(c.Context().UserAgent())) + + if err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + + expires := getUserSessionExpiresTime() + + c.Cookie(&fiber.Cookie{Name: "session_id", Value: sessionId, Secure: true, HTTPOnly: true, Expires: expires}) + if user.Name != "" { + c.Cookie(&fiber.Cookie{Name: "name", Value: user.Name, Secure: true, Expires: expires}) + } + c.Cookie(&fiber.Cookie{Name: "hashtag", Value: user.Hashtag, Secure: true, Expires: expires}) + + return c.SendStatus(fiber.StatusCreated) +} diff --git a/routers/api/v1/user/register.go b/routers/api/v1/user/register.go new file mode 100644 index 0000000..cba7577 --- /dev/null +++ b/routers/api/v1/user/register.go @@ -0,0 +1,193 @@ +package user + +import ( + "encoding/base64" + "encoding/json" + "strings" + "time" + + "git.umbach.dev/app-idea/rest-api/modules/database" + "git.umbach.dev/app-idea/rest-api/modules/rabbitmq" + "git.umbach.dev/app-idea/rest-api/modules/structs" + "github.com/gofiber/fiber/v2" + "github.com/google/uuid" + log "github.com/sirupsen/logrus" + "golang.org/x/crypto/bcrypt" +) + +type RegisterInput struct { + Username string `json:"username"` + Email string `json:"email"` + Password string `json:"password"` + Hashtag string `json:"hashtag"` + LanguageId int `json:"languageId"` +} + +func NewUser(c *fiber.Ctx) error { + // swagger:operation POST /users User usersNewUser + // --- + // summary: Create new user + // produces: + // - application/json + // parameters: + // - name: username + // in: query + // description: username of the user (length 3-30) + // type: string + // required: true + // - name: email + // in: query + // description: email of the user (length 3-255) + // type: string + // required: true + // - name: password + // in: query + // description: password (base64) of the user (length 6-250) + // type: string + // required: true + // - name: hashtag + // in: query + // description: hashtag of the client (length 2-6, UPPERCASE (Letters, Numbers)) + // type: string + // - name: avatar_url + // in: query + // description: avatar url of the client + // type: string + // - name: location + // in: query + // description: location of the client (length 1-20) (for example Frankfurt) + // type: string + // responses: + // '201': + // description: user created + // "$ref": "#/definitions/User" + // '400': + // description: format is not correct + // '422': + // description: username, email or/and hashtag already assigned + + var input RegisterInput + + 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 !isUsernameValid(input.Username) || !isEmailValid(input.Email) || !isPasswordValid(input.Password) { + return c.SendStatus(fiber.StatusForbidden) + } + + user := structs.User{Email: input.Email} + db := database.DB + + if !isEmailAvailable(db, user.Email) { + return c.SendStatus(fiber.StatusUnprocessableEntity) + } + + if input.Hashtag == "" { + input.Hashtag, err = generateRandomHashtag(db, 6) + + if err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + } else if !isHashtagValid(db, 1, input.Hashtag) { + return c.SendStatus(fiber.StatusUnprocessableEntity) + } + + hashedPassword, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost) + + if err != nil { + log.Warnln("Failed to bcrypt password", err) + return c.SendStatus(fiber.StatusInternalServerError) + } + + now := time.Now() + + user.Id = strings.Replace(uuid.New().String(), "-", "", -1) + user.Hashtag = input.Hashtag + user.Name = input.Username + user.Password = string(hashedPassword) + user.LanguageId = input.LanguageId + user.State = 1 + user.LastLogin = now + user.CreatedAt = now + + res := db.Create(&user) + + if res.Error != nil { + log.Warnln("Failed to insert user to db:", res.Error) + return c.SendStatus(fiber.StatusInternalServerError) + } + + // account activation via email + + userActivationId, err := createUserActivation(user.Id) + + if err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + + type JsonMessage struct { + Key string `json:"k"` + Mail string `json:"m"` + TemplateId int `json:"t"` + LanguageId int `json:"l"` + BodyData *json.RawMessage `json:"d"` + } + + bodyData := json.RawMessage(`{"name": "` + user.Name + `", + "email": "` + user.Email + `", + "url": "http://localhost:3000/api/v1/user/activate/` + userActivationId + `"}`) + + js := JsonMessage{Key: "mykey", Mail: "app@roese.dev", TemplateId: 0, LanguageId: user.LanguageId, BodyData: &bodyData} + + reqBody, err := json.MarshalIndent(&js, "", "\t") + + if err != nil { + log.Infoln("error reqBody", err) + } + + rabbitmq.Publish(string(reqBody)) + + /*resp, err := http.Post(cfg.Mail.Host+"/send", "application/json", bytes.NewBuffer(reqBody)) + + if err != nil { + log.Infoln("err http post", err) + } + + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + + if err != nil { + log.Infoln("err reading body", err) + } + + log.Infoln("body", body) + log.Infoln("StatusCode", resp.StatusCode)*/ + + sessionId, err := createUserSession(db, user.Id, c.IP(), string(c.Context().UserAgent())) + + if err != nil { + return c.SendStatus(fiber.StatusInternalServerError) + } + + expires := getUserSessionExpiresTime() + + c.Cookie(&fiber.Cookie{Name: "session_id", Value: sessionId, Secure: true, HTTPOnly: true, Expires: expires}) + c.Cookie(&fiber.Cookie{Name: "username", Value: input.Username, Secure: true, Expires: expires}) + c.Cookie(&fiber.Cookie{Name: "user_hashtag", Value: input.Hashtag, Secure: true, Expires: expires}) + + log.Debugln("user created", user) + + return c.SendStatus(fiber.StatusCreated) +} diff --git a/routers/api/v1/user/session.go b/routers/api/v1/user/session.go new file mode 100644 index 0000000..d651e3b --- /dev/null +++ b/routers/api/v1/user/session.go @@ -0,0 +1,83 @@ +package user + +import ( + "database/sql" + "time" + + "git.umbach.dev/app-idea/rest-api/modules/database" + "git.umbach.dev/app-idea/rest-api/modules/structs" + "github.com/gofiber/fiber/v2" + ua "github.com/mileusna/useragent" + log "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +func isSessionIdValid(sessionId string) bool { + deleteExpiredSessions(database.DB) + + var res string + var db = database.DB + + db.Raw("SELECT session_id FROM sessions WHERE session_id = ?", sessionId).Scan(&res) + + if res == "" { + return false + } else { + return true + } +} + +func deleteSession(db *sql.DB, sessionId string) { + _, err := db.Exec("DELETE FROM sessions WHERE session_id = ?", sessionId) + + if err != nil { + log.Warnln("err deleting session:", err) + } +} + +func deleteExpiredSessions(db *gorm.DB) { + var res string + + db.Raw("DELETE FROM sessions WHERE expires < ?", time.Now()).Scan(&res) +} + +func createUserSession(db *gorm.DB, userId string, ip string, userAgent string) (string, error) { + sessionId, err := generateRandomString(32, 1) + + if err != nil { + log.Warnln("Failed to generate user session:", err) + return "", err + } + + ua := ua.Parse(userAgent) + session := structs.Session{UserId: userId, SessionId: sessionId, IP: ip, UserAgent: ua.OS + " " + ua.Name, LastLogin: time.Now(), Expires: getUserSessionExpiresTime()} + + res := db.Create(&session) + + if res.Error != nil { + log.Warnln("failed to create session:", res.Error) + return "", err + } + + return sessionId, nil +} + +func getUserSessionExpiresTime() time.Time { + return time.Now().Add(time.Hour * time.Duration(cfg.Settings.Expires.UserSession)) +} + +func SessionIdCheck(c *fiber.Ctx) error { + sessionId := c.Cookies("session_id") + + if sessionId == "" { + return fiber.ErrUnauthorized + } + + valid := isSessionIdValid(sessionId) + + if valid { + return c.Next() + } + + return fiber.ErrUnauthorized +} diff --git a/routers/api/v1/user/user.go b/routers/api/v1/user/user.go index c68dd4d..1b77faf 100644 --- a/routers/api/v1/user/user.go +++ b/routers/api/v1/user/user.go @@ -2,202 +2,21 @@ package user import ( "crypto/rand" - "database/sql" - "encoding/base64" - "encoding/json" "math/big" "regexp" "strings" - "time" "unicode" "git.umbach.dev/app-idea/rest-api/modules/config" "git.umbach.dev/app-idea/rest-api/modules/database" - "git.umbach.dev/app-idea/rest-api/modules/rabbitmq" "git.umbach.dev/app-idea/rest-api/modules/structs" "github.com/gofiber/fiber/v2" - "github.com/google/uuid" - ua "github.com/mileusna/useragent" log "github.com/sirupsen/logrus" - "golang.org/x/crypto/bcrypt" "gorm.io/gorm" ) var cfg = &config.Cfg -type LoginInput struct { - Username string `json:"username"` - Email string `json:"email"` - Password string `json:"password"` - Hashtag string `json:"hashtag"` - LanguageId int `json:"languageId"` -} - -func NewUser(c *fiber.Ctx) error { - // swagger:operation POST /users User usersNewUser - // --- - // summary: Create new user - // produces: - // - application/json - // parameters: - // - name: username - // in: query - // description: username of the user (length 3-30) - // type: string - // required: true - // - name: email - // in: query - // description: email of the user (length 3-255) - // type: string - // required: true - // - name: password - // in: query - // description: password (base64) of the user (length 6-250) - // type: string - // required: true - // - name: hashtag - // in: query - // description: hashtag of the client (length 2-6, UPPERCASE (Letters, Numbers)) - // type: string - // - name: avatar_url - // in: query - // description: avatar url of the client - // type: string - // - name: location - // in: query - // description: location of the client (length 1-20) (for example Frankfurt) - // type: string - // responses: - // '201': - // description: user created - // "$ref": "#/definitions/User" - // '400': - // description: format is not correct - // '422': - // description: username, email or/and hashtag already assigned - - 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 !isUsernameValid(input.Username) || !isEmailValid(input.Email) || !isPasswordValid(input.Password) { - return c.SendStatus(fiber.StatusForbidden) - } - - user := structs.User{Email: input.Email} - db := database.DB - - if !isEmailAvailable(db, user.Email) { - return c.SendStatus(fiber.StatusUnprocessableEntity) - } - - if input.Hashtag == "" { - input.Hashtag, err = generateRandomHashtag(db, 6) - - if err != nil { - return c.SendStatus(fiber.StatusInternalServerError) - } - } else if !isHashtagValid(db, 1, input.Hashtag) { - return c.SendStatus(fiber.StatusUnprocessableEntity) - } - - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(input.Password), bcrypt.DefaultCost) - - if err != nil { - log.Warnln("Failed to bcrypt password", err) - return c.SendStatus(fiber.StatusInternalServerError) - } - - now := time.Now() - - user.Id = strings.Replace(uuid.New().String(), "-", "", -1) - user.Hashtag = input.Hashtag - user.Name = input.Username - user.Password = string(hashedPassword) - - log.Debugln("lgId", input.LanguageId) - - user.LanguageId = input.LanguageId - user.LastLogin = now - user.CreatedAt = now - - res := db.Create(&user) - - if res.Error != nil { - log.Warnln("Failed to insert user to db:", res.Error) - return c.SendStatus(fiber.StatusInternalServerError) - } - - // account activation via email - - type JsonMessage struct { - Key string `json:"k"` - Mail string `json:"m"` - TemplateId int `json:"t"` - LanguageId int `json:"l"` - BodyData *json.RawMessage `json:"d"` - } - - bodyData := json.RawMessage(`{"name": "` + user.Name + `", - "email": "` + user.Email + `", - "url": "https://roese.dev/activate/1234567890"}`) - - js := JsonMessage{Key: "mykey", Mail: "app@roese.dev", TemplateId: 0, LanguageId: user.LanguageId, BodyData: &bodyData} - - reqBody, err := json.MarshalIndent(&js, "", "\t") - - if err != nil { - log.Infoln("error reqBody", err) - } - - rabbitmq.Publish(string(reqBody)) - - /*resp, err := http.Post(cfg.Mail.Host+"/send", "application/json", bytes.NewBuffer(reqBody)) - - if err != nil { - log.Infoln("err http post", err) - } - - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - - if err != nil { - log.Infoln("err reading body", err) - } - - log.Infoln("body", body) - log.Infoln("StatusCode", resp.StatusCode)*/ - - sessionId, err := createUserSession(db, user.Id, c.IP(), string(c.Context().UserAgent())) - - if err != nil { - return c.SendStatus(fiber.StatusInternalServerError) - } - - expires := getExpiresTime() - - c.Cookie(&fiber.Cookie{Name: "session_id", Value: sessionId, Secure: true, HTTPOnly: true, Expires: expires}) - c.Cookie(&fiber.Cookie{Name: "username", Value: input.Username, Secure: true, Expires: expires}) - c.Cookie(&fiber.Cookie{Name: "user_hashtag", Value: input.Hashtag, Secure: true, Expires: expires}) - - log.Debugln("user created", user) - - return c.SendStatus(fiber.StatusCreated) -} - func generateRandomString(n int, t int) (string, error) { var letters string @@ -305,156 +124,47 @@ func isEmailAvailable(db *gorm.DB, email string) bool { } } -func SessionIdCheck(c *fiber.Ctx) error { - sessionId := c.Cookies("session_id") +func getUserIdBySessionId(sessionId string) (string, error) { + db := database.DB + session := structs.Session{} - if sessionId == "" { - return fiber.ErrUnauthorized - } + db.Select("user_id").Where("session_id = ?", sessionId).Find(&session) - valid := isSessionIdValid(sessionId) - - if valid { - return c.Next() - } - - return fiber.ErrUnauthorized + return session.UserId, nil } -func isSessionIdValid(sessionId string) bool { - deleteExpiredSessions(database.DB) +func GetUser(c *fiber.Ctx) error { + log.Infoln("body", c.Body()) - var res string - var db = database.DB - - db.Raw("SELECT session_id FROM sessions WHERE session_id = ?", sessionId).Scan(&res) - - if res == "" { - return false - } else { - return true - } -} - -func deleteSession(db *sql.DB, sessionId string) { - _, err := db.Exec("DELETE FROM sessions WHERE session_id = ?", sessionId) - - if err != nil { - log.Warnln("err deleting session:", err) - } -} - -func deleteExpiredSessions(db *gorm.DB) { - var res string - - db.Raw("DELETE FROM sessions WHERE expires < ?", time.Now()).Scan(&res) -} - -func createUserSession(db *gorm.DB, userId string, ip string, userAgent string) (string, error) { - sessionId, err := generateRandomString(32, 1) - - if err != nil { - log.Warnln("Failed to generate user session:", err) - return "", err + type Input struct { + Val []string `json:"val"` } - ua := ua.Parse(userAgent) - session := structs.Session{UserId: userId, SessionId: sessionId, IP: ip, UserAgent: ua.OS + " " + ua.Name, LastLogin: time.Now(), Expires: getExpiresTime()} + log.Infoln("arr", string(c.Body())) - res := db.Create(&session) - - if res.Error != nil { - log.Warnln("failed to create session:", res.Error) - return "", err - } - - return sessionId, nil -} - -func getExpiresTime() time.Time { - return time.Now().Add(time.Hour * time.Duration(cfg.Settings.ExpiredTime)) -} - -func Login(c *fiber.Ctx) error { - // swagger:operation POST /user/login User 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 Input if err := c.BodyParser(&input); err != nil { + log.Infoln("bad", input) return c.SendStatus(fiber.StatusBadRequest) } - log.Println(input) + log.Infoln("test", strings.Join(input.Val, ",")) - if input.Username != "" && /*!isValid(input.Username, 3, 30) */ !isUsernameValid(input.Username) || input.Email != "" && !isEmailValid(input.Email) || input.Username == "" && input.Email == "" || input.Password == "" { - log.Info("bad") - 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) + log.Println("input", input) db := database.DB - user := structs.User{} - - if input.Username != "" { - db.Select("id, hashtag, password").Where("name = ?", input.Username).Find(&user) - } else { - db.Select("id, hashtag, name, password").Where("email = ?", input.Email).Find(&user) - } - - err = bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(input.Password)) - - if err != nil { - log.Warnln("Failed to comapare bcrypt password", err) - return c.SendStatus(fiber.StatusUnauthorized) - } - - sessionId, err := createUserSession(database.DB, user.Id, c.IP(), string(c.Context().UserAgent())) + userId, err := getUserIdBySessionId(c.Cookies("session_id")) if err != nil { return c.SendStatus(fiber.StatusInternalServerError) } - expires := getExpiresTime() + user := structs.User{} - c.Cookie(&fiber.Cookie{Name: "session_id", Value: sessionId, Secure: true, HTTPOnly: true, Expires: expires}) - if user.Name != "" { - c.Cookie(&fiber.Cookie{Name: "name", Value: user.Name, Secure: true, Expires: expires}) - } - c.Cookie(&fiber.Cookie{Name: "hashtag", Value: user.Hashtag, Secure: true, Expires: expires}) + db.Table("users").Select(strings.Join(input.Val, ",")).First(&user, "id = ?", userId) - return c.SendStatus(fiber.StatusCreated) -} - -func GetUser(c *fiber.Ctx) error { - return c.SendString("user") + return c.JSON(&user) } func GetUsers(c *fiber.Ctx) error {