2016-04-11 20:43:18 -07:00
package vain
import (
2016-06-03 16:44:50 -07:00
"database/sql"
2016-04-11 20:43:18 -07:00
"fmt"
"log"
"net/http"
"time"
"github.com/jmoiron/sqlx"
verrors "mcquay.me/vain/errors"
vsql "mcquay.me/vain/sql"
)
2016-05-23 23:54:35 -07:00
// DB wraps a sqlx.DB connection and provides methods for interating with
// a vain database.
2016-04-11 20:43:18 -07:00
type DB struct {
conn * sqlx . DB
}
2016-05-23 23:54:35 -07:00
// NewDB opens a sqlite3 file, sets options, and reports errors.
2016-04-11 20:43:18 -07:00
func NewDB ( path string ) ( * DB , error ) {
conn , err := sqlx . Open ( "sqlite3" , fmt . Sprintf ( "file:%s?cache=shared&mode=rwc" , path ) )
if _ , err := conn . Exec ( "PRAGMA foreign_keys = ON" ) ; err != nil {
return nil , err
}
return & DB { conn } , err
}
2016-06-22 22:41:34 -07:00
// NewPGDB returns a populated DB and verifies the connection is usable.
func NewPGDB ( dbhost string , dbname string , maxConn int ) ( * DB , error ) {
dsn := fmt . Sprintf ( "host=%s dbname=%s sslmode=disable" , dbhost , dbname )
conn , err := sqlx . Open ( "postgres" , dsn )
if err != nil {
return nil , err
}
if err := conn . Ping ( ) ; err != nil {
return nil , err
}
conn . SetMaxOpenConns ( maxConn )
return & DB { conn : conn } , nil
}
2016-05-23 23:54:35 -07:00
// Init runs the embedded sql to initialize tables.
2016-04-11 20:43:18 -07:00
func ( db * DB ) Init ( ) error {
content , err := vsql . Asset ( "sql/init.sql" )
if err != nil {
return err
}
_ , err = db . conn . Exec ( string ( content ) )
return err
}
2016-05-23 23:54:35 -07:00
// Close the underlying connection.
2016-04-11 20:43:18 -07:00
func ( db * DB ) Close ( ) error {
return db . conn . Close ( )
}
2016-05-23 23:54:35 -07:00
// AddPackage adds p into packages table.
2016-04-11 20:43:18 -07:00
func ( db * DB ) AddPackage ( p Package ) error {
_ , err := db . conn . NamedExec (
"INSERT INTO packages(vcs, repo, path, ns) VALUES (:vcs, :repo, :path, :ns)" ,
& p ,
)
return err
}
2016-05-23 23:54:35 -07:00
// RemovePackage removes package with given path
2016-04-11 20:43:18 -07:00
func ( db * DB ) RemovePackage ( path string ) error {
2016-06-22 22:41:34 -07:00
_ , err := db . conn . Exec ( "DELETE FROM packages WHERE path = $1" , path )
2016-04-11 20:43:18 -07:00
return err
}
2016-05-23 23:54:35 -07:00
// Pkgs returns all packages from the database
2016-04-11 20:43:18 -07:00
func ( db * DB ) Pkgs ( ) [ ] Package {
r := [ ] Package { }
rows , err := db . conn . Queryx ( "SELECT * FROM packages" )
if err != nil {
log . Printf ( "%+v" , err )
return nil
}
for rows . Next ( ) {
var p Package
err = rows . StructScan ( & p )
if err != nil {
log . Printf ( "%+v" , err )
return nil
}
r = append ( r , p )
}
return r
}
2016-05-23 23:54:35 -07:00
// PackageExists tells if a package with path is in the database.
2016-04-11 20:43:18 -07:00
func ( db * DB ) PackageExists ( path string ) bool {
var count int
2016-06-22 22:41:34 -07:00
if err := db . conn . Get ( & count , "SELECT COUNT(*) FROM packages WHERE path = $1" , path ) ; err != nil {
2016-04-11 20:43:18 -07:00
log . Printf ( "%+v" , err )
}
r := false
switch count {
case 1 :
r = true
default :
log . Printf ( "unexpected count of packages matching %q: %d" , path , count )
}
return r
}
2016-05-23 23:54:35 -07:00
// Package fetches the package associated with path.
2016-04-11 20:43:18 -07:00
func ( db * DB ) Package ( path string ) ( Package , error ) {
r := Package { }
2016-06-22 22:41:34 -07:00
err := db . conn . Get ( & r , "SELECT * FROM packages WHERE path = $1" , path )
2016-06-03 16:44:50 -07:00
if err == sql . ErrNoRows {
return r , verrors . HTTP {
Message : fmt . Sprintf ( "couldn't find package %q" , path ) ,
Code : http . StatusNotFound ,
}
}
2016-04-11 20:43:18 -07:00
return r , err
}
2016-05-23 23:54:35 -07:00
// NSForToken creates an entry namespaces with a relation to the token.
2016-04-11 20:43:18 -07:00
func ( db * DB ) NSForToken ( ns string , tok string ) error {
var err error
txn , err := db . conn . Beginx ( )
if err != nil {
return verrors . HTTP {
Message : fmt . Sprintf ( "problem creating transaction: %v" , err ) ,
Code : http . StatusInternalServerError ,
}
}
defer func ( ) {
if err != nil {
txn . Rollback ( )
} else {
txn . Commit ( )
}
} ( )
var count int
2016-06-22 22:41:34 -07:00
if err = txn . Get ( & count , "SELECT COUNT(*) FROM namespaces WHERE namespaces.ns = $1" , ns ) ; err != nil {
2016-04-11 20:43:18 -07:00
return verrors . HTTP {
Message : fmt . Sprintf ( "problem matching fetching namespaces matching %q" , ns ) ,
Code : http . StatusInternalServerError ,
}
}
if count == 0 {
2016-06-21 22:02:06 -07:00
var email string
if err = txn . Get ( & email , "SELECT email FROM users WHERE token = $1" , tok ) ; err != nil {
return verrors . HTTP {
Message : fmt . Sprintf ( "could not find user for token %q" , tok ) ,
Code : http . StatusInternalServerError ,
}
}
2016-04-11 20:43:18 -07:00
if _ , err = txn . Exec (
2016-06-21 22:02:06 -07:00
"INSERT INTO namespaces(ns, email) VALUES ($1, $2)" ,
2016-04-11 20:43:18 -07:00
ns ,
2016-06-21 22:02:06 -07:00
email ,
2016-04-11 20:43:18 -07:00
) ; err != nil {
return verrors . HTTP {
Message : fmt . Sprintf ( "problem inserting %q into namespaces for token %q: %v" , ns , tok , err ) ,
Code : http . StatusInternalServerError ,
}
}
return err
}
2016-06-22 22:41:34 -07:00
if err = txn . Get ( & count , "SELECT COUNT(*) FROM namespaces JOIN users ON namespaces.email = users.email WHERE users.token = $1 AND namespaces.ns = $2" , tok , ns ) ; err != nil {
2016-04-11 20:43:18 -07:00
return verrors . HTTP {
Message : fmt . Sprintf ( "ns: %q, tok: %q; %v" , ns , tok , err ) ,
Code : http . StatusInternalServerError ,
}
}
switch count {
case 1 :
err = nil
case 0 :
err = verrors . HTTP {
Message : fmt . Sprintf ( "not authorized against namespace %q" , ns ) ,
Code : http . StatusUnauthorized ,
}
default :
err = verrors . HTTP {
2016-05-23 23:54:35 -07:00
Message : fmt . Sprintf ( "inconsistent db; found %d results with ns (%s) with token (%s)" , count , ns , tok ) ,
2016-04-11 20:43:18 -07:00
Code : http . StatusInternalServerError ,
}
}
return err
}
2016-05-23 23:54:35 -07:00
// Register adds email to the database, returning an error if there was one.
2016-04-11 20:43:18 -07:00
func ( db * DB ) Register ( email string ) ( string , error ) {
var err error
txn , err := db . conn . Beginx ( )
if err != nil {
return "" , verrors . HTTP {
Message : fmt . Sprintf ( "problem creating transaction: %v" , err ) ,
Code : http . StatusInternalServerError ,
}
}
defer func ( ) {
if err != nil {
txn . Rollback ( )
} else {
txn . Commit ( )
}
} ( )
var count int
2016-06-22 22:41:34 -07:00
if err = txn . Get ( & count , "SELECT COUNT(*) FROM users WHERE email = $1" , email ) ; err != nil {
2016-04-11 20:43:18 -07:00
return "" , verrors . HTTP {
Message : fmt . Sprintf ( "could not search for email %q in db: %v" , email , err ) ,
Code : http . StatusInternalServerError ,
}
}
if count != 0 {
return "" , verrors . HTTP {
Message : fmt . Sprintf ( "duplicate email %q" , email ) ,
Code : http . StatusConflict ,
}
}
tok := FreshToken ( )
_ , err = txn . Exec (
2016-06-22 22:41:34 -07:00
"INSERT INTO users(email, token, requested) VALUES ($1, $2, $3)" ,
2016-04-11 20:43:18 -07:00
email ,
tok ,
time . Now ( ) ,
)
return tok , err
}
2016-05-23 23:54:35 -07:00
// Confirm modifies the user with the given token. Used on register confirmation.
2016-04-11 20:43:18 -07:00
func ( db * DB ) Confirm ( token string ) ( string , error ) {
var err error
txn , err := db . conn . Beginx ( )
if err != nil {
return "" , verrors . HTTP {
Message : fmt . Sprintf ( "problem creating transaction: %v" , err ) ,
Code : http . StatusInternalServerError ,
}
}
defer func ( ) {
if err != nil {
txn . Rollback ( )
} else {
txn . Commit ( )
}
} ( )
var count int
2016-06-22 22:41:34 -07:00
if err = txn . Get ( & count , "SELECT COUNT(*) FROM users WHERE token = $1" , token ) ; err != nil {
2016-04-11 20:43:18 -07:00
return "" , verrors . HTTP {
Message : fmt . Sprintf ( "could not perform search for user with token %q in db: %v" , token , err ) ,
Code : http . StatusInternalServerError ,
}
}
if count != 1 {
return "" , verrors . HTTP {
Message : fmt . Sprintf ( "bad token: %s" , token ) ,
Code : http . StatusNotFound ,
}
}
newToken := FreshToken ( )
_ , err = txn . Exec (
2016-06-22 22:41:34 -07:00
"UPDATE users SET token = $1, registered = true WHERE token = $2" ,
2016-04-11 20:43:18 -07:00
newToken ,
token ,
)
if err != nil {
return "" , verrors . HTTP {
2016-06-21 07:54:00 -07:00
Message : fmt . Sprintf ( "couldn't update user with token %q: %v" , token , err ) ,
2016-04-11 20:43:18 -07:00
Code : http . StatusInternalServerError ,
}
}
return newToken , nil
}
2016-05-14 21:30:58 -07:00
func ( db * DB ) forgot ( email string , window time . Duration ) ( string , error ) {
txn , err := db . conn . Beginx ( )
if err != nil {
2016-04-11 20:43:18 -07:00
return "" , verrors . HTTP {
2016-05-14 21:30:58 -07:00
Message : fmt . Sprintf ( "problem creating transaction: %v" , err ) ,
Code : http . StatusInternalServerError ,
}
}
defer func ( ) {
if err != nil {
txn . Rollback ( )
} else {
txn . Commit ( )
}
} ( )
out := struct {
Token string
Requested time . Time
} { }
2016-06-22 22:41:34 -07:00
if err = txn . Get ( & out , "SELECT token, requested FROM users WHERE email = $1" , email ) ; err != nil {
2016-05-14 21:30:58 -07:00
return "" , verrors . HTTP {
Message : fmt . Sprintf ( "could not find email %q in db" , email ) ,
Code : http . StatusNotFound ,
}
}
if out . Requested . After ( time . Now ( ) ) {
return "" , verrors . HTTP {
Message : fmt . Sprintf ( "rate limit hit for %q; try again in %0.2f mins" , email , out . Requested . Sub ( time . Now ( ) ) . Minutes ( ) ) ,
Code : http . StatusTooManyRequests ,
}
}
2016-06-22 22:41:34 -07:00
_ , err = txn . Exec ( "UPDATE users SET requested = $1 WHERE email = $2" , time . Now ( ) . Add ( window ) , email )
2016-05-14 21:30:58 -07:00
if err != nil {
return "" , verrors . HTTP {
Message : fmt . Sprintf ( "could not update last requested time for %q: %v" , email , err ) ,
2016-04-11 20:43:18 -07:00
Code : http . StatusInternalServerError ,
}
}
2016-05-14 21:30:58 -07:00
return out . Token , nil
2016-04-11 20:43:18 -07:00
}
func ( db * DB ) addUser ( email string ) ( string , error ) {
tok := FreshToken ( )
_ , err := db . conn . Exec (
2016-06-22 22:41:34 -07:00
"INSERT INTO users(email, token, requested) VALUES ($1, $2, $3)" ,
2016-04-11 20:43:18 -07:00
email ,
tok ,
time . Now ( ) ,
)
return tok , err
}
2016-06-03 13:42:05 -07:00
func ( db * DB ) user ( email string ) ( User , error ) {
u := User { }
err := db . conn . Get (
& u ,
2016-06-22 22:41:34 -07:00
"SELECT email, token, registered, requested FROM users WHERE email = $1" ,
2016-06-03 13:42:05 -07:00
email ,
)
if err == sql . ErrNoRows {
return User { } , verrors . HTTP {
Message : fmt . Sprintf ( "could not find requested user's email: %q: %v" , email , err ) ,
Code : http . StatusNotFound ,
}
}
return u , err
}