package user import ( "bytes" "crypto/rand" "database/sql" "encoding/base64" "encoding/json" "io/ioutil" "math/big" "net/http" "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/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"` } 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) user.Language = 0 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: 0, BodyData: &bodyData} reqBody, err := json.MarshalIndent(&js, "", "\t") if err != nil { log.Infoln("error reqBody", err) } 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 if t == 1 { letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" } else { letters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" } r := make([]byte, n) for i := 0; i < n; i++ { num, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters)))) if err != nil { return "", err } r[i] = letters[num.Int64()] } return string(r), nil } func generateRandomHashtag(db *gorm.DB, n int) (string, error) { c := make(chan bool) var s string var err error for { s, err = generateRandomString(6, 0) if err != nil { log.Warnln("error generating hashtag:", err) return "", err } go func() { c <- isHashtagValid(db, 0, s) }() if msg := <-c; msg { break } } return s, nil } func isHashtagValid(db *gorm.DB, t int, h string) bool { if t == 1 && !isUpper(h) || len(h) < 2 || len(h) > 6 { return false } var res string db.Raw("SELECT hashtag FROM users WHERE hashtag = ?", h).Scan(&res) if res == "" { return true } else { return false } } func isUpper(s string) bool { for _, r := range s { if !unicode.IsUpper(r) && unicode.IsLetter(r) { return false } } return true } func isUsernameValid(u string) bool { if len(u) < int(cfg.Settings.Lengths.UsernameMinLen) || len(u) > int(cfg.Settings.Lengths.UsernameMaxLen) { return false } return true } 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) < int(cfg.Settings.Lengths.EmailMinLen) || len(e) > int(cfg.Settings.Lengths.EmailMaxLen) { return false } return emailRegex.MatchString(e) } func isPasswordValid(p string) bool { if len(p) < int(cfg.Settings.Lengths.PasswordMinLen) || len(p) > int(cfg.Settings.Lengths.PasswordMaxLen) { return false } return true } func isEmailAvailable(db *gorm.DB, email string) bool { var res string db.Raw("SELECT email FROM users WHERE email = ?", email).Scan(&res) if res == "" { return true } else { return false } } 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 } 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: getExpiresTime()} 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 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) } 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 := getExpiresTime() 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) } func GetUser(c *fiber.Ctx) error { return c.SendString("user") } func GetUsers(c *fiber.Ctx) error { list := []string{} /* var ( name string )*/ /* rows, err := db.Query("SELECT username FROM users;") fmt.Println("err", err) defer rows.Close() fmt.Println("reading data:") for rows.Next() { err := rows.Scan(&name) fmt.Printf("Data row = (%s, %s)\n", name, err) list = append(list, name) } err = rows.Err()*/ return c.JSON(list) }