2016-04-05 20:22:13 +00:00
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 :
2016-04-08 17:05:54 +00:00
md . Err ( err )
2016-04-05 20:22:13 +00:00
r = Err500
return
}
2016-04-10 09:51:34 +00:00
if nFailedAttempts ( ret . ID ) > 20 {
r . Code = 429
r . Message = "You've made too many login attempts. Try again later."
return
}
2016-04-05 20:22:13 +00:00
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 {
2016-04-10 09:51:34 +00:00
go addFailedAttempt ( ret . ID )
2016-04-05 20:22:13 +00:00
r . Code = 403
r . Message = "That password doesn't match!"
return
}
2016-04-08 17:05:54 +00:00
md . Err ( err )
2016-04-05 20:22:13 +00:00
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
2016-04-07 10:43:39 +00:00
err := md . DB . QueryRow ( "SELECT id FROM tokens WHERE token=? LIMIT 1" , tokenMD5 ) . Scan ( & id )
2016-04-05 20:22:13 +00:00
if err == sql . ErrNoRows {
break
}
if err != nil {
2016-04-08 17:05:54 +00:00
md . Err ( err )
2016-04-05 20:22:13 +00:00
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 {
2016-04-08 17:05:54 +00:00
md . Err ( err )
2016-04-05 20:22:13 +00:00
r = Err500
return
}
r . Code = 200
r . Data = ret
return
}