finish with user login
parent
e0266d9788
commit
0402892963
12
example.http
12
example.http
|
@ -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="
|
||||||
}
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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")
|
||||||
|
|
|
@ -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,
|
||||||
|
|
40
swagger.yaml
40
swagger.yaml
|
@ -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:
|
||||||
|
|
Loading…
Reference in New Issue