appidea-restapi/routers/api/v1/user/user.go

439 lines
10 KiB
Go

package user
import (
"crypto/rand"
"database/sql"
"encoding/base64"
"math/big"
"regexp"
"strings"
"time"
"unicode"
"git.umbach.dev/app-idea/rest-api/modules/database"
"git.umbach.dev/app-idea/rest-api/modules/serversettings"
"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"
)
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 !isValid(input.Username, 3, 30) || !isEmailValid(input.Email) || !isValid(input.Password, 6, 250) {
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.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)
}
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 isValid(s string, min int, max int) bool {
if len(s) < min || len(s) > max {
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) < 3 || len(e) > 255 {
return false
}
return emailRegex.MatchString(e)
}
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(serversettings.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) || input.Email != "" && !isEmailValid(input.Email) || input.Username == "" && input.Email == "" || input.Password == "" {
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 input.Username != "" {
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 {
return c.SendStatus(fiber.StatusUnauthorized)
} */
db := database.DB
user := structs.User{}
if input.Username != "" {
db.Select("id, hashtag, password").Where("name = ?", input.Username).Find(&user)
log.Infoln("a", user)
} else {
db.Select("id, hashtag, name, password").Where("email = ?", input.Email).Find(&user)
log.Infoln("a", user)
}
log.Infoln("pass", input.Password, user.Password)
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)
}