Add token creation (login)
This commit is contained in:
parent
b3b4dde8f2
commit
d02f3f9951
|
@ -18,6 +18,8 @@ func Start(conf common.Conf, db *sql.DB) {
|
|||
{
|
||||
gv1 := api.Group("/v1")
|
||||
{
|
||||
gv1.POST("/token/new", Method(v1.TokenNewPOST, db))
|
||||
|
||||
// Auth-free API endpoints
|
||||
gv1.GET("/ping", Method(v1.PingGET, db))
|
||||
gv1.GET("/surprise_me", Method(v1.SurpriseMeGET, db))
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
|
||||
"github.com/osuripple/api/common"
|
||||
)
|
||||
|
@ -10,7 +12,7 @@ import (
|
|||
func GetTokenFull(token string, db *sql.DB) (common.Token, bool) {
|
||||
var uid int
|
||||
var privs int
|
||||
err := db.QueryRow("SELECT user, privileges FROM tokens WHERE token = ? LIMIT 1", token).Scan(&uid, &privs)
|
||||
err := db.QueryRow("SELECT user, privileges FROM tokens WHERE token = ? LIMIT 1", fmt.Sprintf("%x", md5.Sum([]byte(token)))).Scan(&uid, &privs)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
return common.Token{}, false
|
||||
|
|
|
@ -1,13 +1,27 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/osuripple/api/common"
|
||||
)
|
||||
|
||||
// Boilerplate errors
|
||||
var (
|
||||
Err500 = common.Response{
|
||||
Code: 0,
|
||||
Code: 500,
|
||||
Message: "An error occurred. Try again, perhaps?",
|
||||
}
|
||||
ErrBadJSON = common.Response{
|
||||
Code: 400,
|
||||
Message: "There was an error processing your JSON data.",
|
||||
}
|
||||
)
|
||||
|
||||
// ErrMissingField generates a response to a request when some fields in the JSON are missing.
|
||||
func ErrMissingField(missingFields ...string) common.Response {
|
||||
return common.Response{
|
||||
Code: 422, // http://stackoverflow.com/a/10323055/5328069
|
||||
Message: "Missing fields: " + strings.Join(missingFields, ", ") + ".",
|
||||
}
|
||||
}
|
||||
|
|
134
app/v1/token.go
Normal file
134
app/v1/token.go
Normal file
|
@ -0,0 +1,134 @@
|
|||
package v1
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/osuripple/api/common"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
type tokenNewInData struct {
|
||||
// either username or userid must be given in the request.
|
||||
// if none is given, the request is trashed.
|
||||
Username string `json:"username"`
|
||||
UserID int `json:"id"`
|
||||
Password string `json:"password"`
|
||||
Privileges int `json:"privileges"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type tokenNewOutData struct {
|
||||
Username string `json:"username"`
|
||||
ID int `json:"id"`
|
||||
Privileges int `json:"privileges"`
|
||||
Token string `json:"token,omitempty"`
|
||||
Banned bool `json:"banned"`
|
||||
}
|
||||
|
||||
// TokenNewPOST is the handler for POST /token/new.
|
||||
func TokenNewPOST(md common.MethodData) (r common.Response) {
|
||||
data := tokenNewInData{}
|
||||
err := json.Unmarshal(md.RequestData, &data)
|
||||
if err != nil {
|
||||
r = ErrBadJSON
|
||||
return
|
||||
}
|
||||
|
||||
var miss []string
|
||||
if data.Username == "" && data.UserID == 0 {
|
||||
miss = append(miss, "username|id")
|
||||
}
|
||||
if data.Password == "" {
|
||||
miss = append(miss, "password")
|
||||
}
|
||||
if len(miss) != 0 {
|
||||
r = ErrMissingField(miss...)
|
||||
return
|
||||
}
|
||||
|
||||
var q *sql.Row
|
||||
const base = "SELECT id, username, rank, password_md5, password_version, allowed FROM users "
|
||||
if data.UserID != 0 {
|
||||
q = md.DB.QueryRow(base+"WHERE id = ? LIMIT 1", data.UserID)
|
||||
} else {
|
||||
q = md.DB.QueryRow(base+"WHERE username = ? LIMIT 1", data.Username)
|
||||
}
|
||||
|
||||
ret := tokenNewOutData{}
|
||||
var (
|
||||
rank int
|
||||
pw string
|
||||
pwVersion int
|
||||
allowed int
|
||||
)
|
||||
|
||||
err = q.Scan(&ret.ID, &ret.Username, &rank, &pw, &pwVersion, &allowed)
|
||||
switch {
|
||||
case err == sql.ErrNoRows:
|
||||
r.Code = 404
|
||||
r.Message = "No user with that username/id was found."
|
||||
return
|
||||
case err != nil:
|
||||
md.C.Error(err)
|
||||
r = Err500
|
||||
return
|
||||
}
|
||||
|
||||
if pwVersion == 1 {
|
||||
r.Code = 418 // Teapots!
|
||||
r.Message = "That user still has a password in version 1. Unfortunately, in order for the API to check for the password to be OK, the user has to first log in through the website."
|
||||
return
|
||||
}
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(pw), []byte(fmt.Sprintf("%x", md5.Sum([]byte(data.Password))))); err != nil {
|
||||
if err == bcrypt.ErrMismatchedHashAndPassword {
|
||||
r.Code = 403
|
||||
r.Message = "That password doesn't match!"
|
||||
return
|
||||
}
|
||||
md.C.Error(err)
|
||||
r = Err500
|
||||
return
|
||||
}
|
||||
if allowed == 0 {
|
||||
r.Code = 200
|
||||
r.Message = "That user is banned."
|
||||
ret.Banned = true
|
||||
r.Data = ret
|
||||
return
|
||||
}
|
||||
ret.Privileges = int(common.Privileges(data.Privileges).CanOnly(rank))
|
||||
|
||||
var (
|
||||
tokenStr string
|
||||
tokenMD5 string
|
||||
)
|
||||
for {
|
||||
tokenStr = common.RandomString(32)
|
||||
tokenMD5 = fmt.Sprintf("%x", md5.Sum([]byte(tokenStr)))
|
||||
ret.Token = tokenStr
|
||||
id := 0
|
||||
|
||||
err := md.DB.QueryRow("SELECT id FROM tokens WHERE token=?", tokenMD5).Scan(&id)
|
||||
if err == sql.ErrNoRows {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
md.C.Error(err)
|
||||
r = Err500
|
||||
return
|
||||
}
|
||||
}
|
||||
_, err = md.DB.Exec("INSERT INTO tokens(user, privileges, description, token) VALUES (?, ?, ?, ?)", ret.ID, ret.Privileges, data.Description, tokenMD5)
|
||||
if err != nil {
|
||||
md.C.Error(err)
|
||||
r = Err500
|
||||
return
|
||||
}
|
||||
|
||||
r.Code = 200
|
||||
r.Data = ret
|
||||
return
|
||||
}
|
|
@ -98,3 +98,31 @@ func (p Privileges) String() string {
|
|||
}
|
||||
return strings.Join(pvs, ", ")
|
||||
}
|
||||
|
||||
var privilegeMustBe = [...]int{
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
3,
|
||||
3,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
4,
|
||||
3,
|
||||
}
|
||||
|
||||
// CanOnly removes any privilege that the user has requested to have, but cannot have due to their rank.
|
||||
func (p Privileges) CanOnly(rank int) Privileges {
|
||||
newPrivilege := 0
|
||||
for i, v := range privilegeMustBe {
|
||||
wants := p&1 == 1
|
||||
can := rank >= v
|
||||
if wants && can {
|
||||
newPrivilege |= 1 << uint(i)
|
||||
}
|
||||
p >>= 1
|
||||
}
|
||||
return Privileges(newPrivilege)
|
||||
}
|
||||
|
|
33
common/random_string.go
Normal file
33
common/random_string.go
Normal file
|
@ -0,0 +1,33 @@
|
|||
package common
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
const letterBytes = "0123456789abcdef"
|
||||
const (
|
||||
letterIdxBits = 6 // 6 bits to represent a letter index
|
||||
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
|
||||
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
|
||||
)
|
||||
|
||||
var randSrc = rand.NewSource(time.Now().UnixNano())
|
||||
|
||||
// RandomString generates a random string.
|
||||
func RandomString(n int) string {
|
||||
b := make([]byte, n)
|
||||
// A randSrc.Int63() generates 63 random bits, enough for letterIdxMax characters!
|
||||
for i, cache, remain := n-1, randSrc.Int63(), letterIdxMax; i >= 0; {
|
||||
if remain == 0 {
|
||||
cache, remain = randSrc.Int63(), letterIdxMax
|
||||
}
|
||||
if idx := int(cache & letterIdxMask); idx < len(letterBytes) {
|
||||
b[i] = letterBytes[idx]
|
||||
i--
|
||||
}
|
||||
cache >>= letterIdxBits
|
||||
remain--
|
||||
}
|
||||
return string(b)
|
||||
}
|
Loading…
Reference in New Issue
Block a user