hlfw.ca

webbing

Download patch

ref: 7c4c558a2874f01c3931c4a74f645cc6fceca81c
parent: 9e5033241d6218200205b676117acc0d702b9654
author: halfwit <michaelmisch1985@gmail.com>
date: Thu Aug 6 05:20:06 PDT 2020

Add database code in

--- a/.github/workflows/test.yml
+++ /dev/null
@@ -1,38 +1,0 @@
-name: Malformed Pages test
-
-on:
-  push:
-    branches: [ master ]
-  pull_request:
-    branches: [ master ]
-
-jobs:
-
-  build:
-    name: Build
-    runs-on: ubuntu-latest
-    steps:
-    - name: Configure git for private modules
-      env:
-        TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
-      run: git config --global url."https://halfwit:${TOKEN}@github.com".insteadOf "https://github.com"
-    
-    - name: Set up Go 1.14
-      uses: actions/setup-go@v1
-      with:
-        go-version: 1.14
-      id: go
-
-    - name: Check out code into the Go module directory
-      uses: actions/checkout@v2
-
-    - name: Get dependencies
-      run: |
-        go get -v -t -d ./...
-        if [ -f Gopkg.toml ]; then
-            curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh
-            dep ensure
-        fi
-
-    - name: Test
-      run: go test -v ./...
--- /dev/null
+++ b/database/README.md
@@ -1,0 +1,19 @@
+# Database
+
+## Testing
+
+Current testing database:
+
+- holds ~1000 randomly generated users in the users/ table
+- holds ~200 randomly generated doctors in doctors/, with a few assigned countries + specialties
+
+## Caveats
+
+Username is unique, email is unique for every entry
+
+- if a user tries to register as an existing email, error out
+- if a user tries to register an existing username, error out
+
+ID only set for users, which is used internally in the session mananger only
+
+- Doctors cannot update from their profile page. They can do so only via correspondence
--- /dev/null
+++ b/database/appointments.go
@@ -1,0 +1,184 @@
+package database
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"log"
+	"time"
+)
+
+type ApptRequest struct {
+	Block  time.Time
+	Doctor *Doctor
+	User   *User
+	Booked bool
+	Token  string
+	ID     uint
+}
+
+type ApptResponse struct {
+	ID     int
+	Block  time.Time
+	Doctor *Doctor
+}
+
+// We use the nickname to guard against id errors
+const bookAppt = `UPDATE appointments
+SET client_id = $1
+WHERE id = $2 AND username = $3
+`
+
+func (d *Database) BookAppointment(ctx context.Context, apt *ApptRequest) error {
+	defer delete(d.valid, apt.Token)
+
+	t, ok := d.valid[apt.Token]
+	if !ok || t.Before(time.Now()) {
+		return errors.New("request token invalid")
+	}
+
+	_, err := d.db.ExecContext(ctx, bookAppt, apt.User.id, apt.ID, apt.Doctor.Username)
+	return err
+}
+
+const apptAdd = `INSERT into appointments
+(username, date, time) values ($1, $2, $3)
+`
+
+// Add an appointment a client can purchase
+func (d *Database) AddAppointment(ctx context.Context, apt *ApptRequest) error {
+	defer delete(d.valid, apt.Token)
+
+	t, ok := d.valid[apt.Token]
+	if !ok || t.Before(time.Now()) {
+		return errors.New("request token invalid")
+	}
+
+	year, month, day := apt.Block.Date()
+	hour, min, _ := apt.Block.Clock()
+
+	if min%15 > 0 {
+		return errors.New("blocks must be 15 minute sessions, starting at either 0, 15, 30, or 45 past the hour")
+	}
+
+	date := fmt.Sprintf("%d-%d-%d", year, month, day)
+	when := fmt.Sprintf("%d:%d:00", hour, min)
+
+	_, err := d.db.ExecContext(ctx, apptAdd, apt.Doctor.Username, date, when)
+	return err
+}
+
+const findAvail = `SELECT date, time, id FROM appointments
+WHERE username = $1
+`
+
+// From a search for doctors, this will be shown on the purchase a block page
+func (d *Database) ListAvailable(ctx context.Context, apt *ApptRequest) []*ApptResponse {
+	var appts []*ApptResponse
+
+	defer delete(d.valid, apt.Token)
+
+	t, ok := d.valid[apt.Token]
+	if !ok || t.Before(time.Now()) {
+		return nil
+	}
+
+	rows, err := d.db.QueryContext(ctx, findAvail, apt.Doctor.Username)
+	if err != nil {
+		return nil
+	}
+
+	for rows.Next() {
+		var tie, date time.Time
+
+		appt := ApptResponse{}
+
+		if e := rows.Scan(&date, &tie, &appt.ID); e != nil {
+			break
+		}
+
+		date.Add(time.Duration(tie.Nanosecond()))
+		appt.Block = date
+
+		appts = append(appts, &appt)
+	}
+
+	return appts
+}
+
+const findClientBookings = `SELECT a.date, a.time, a.id, a.username FROM appointments a
+JOIN users on a.client_id = users.id 
+WHERE users.name = $1
+`
+
+// Show any upcoming bookings on a user profile
+func (d *Database) ListClientBookings(ctx context.Context, user *User) []*ApptResponse {
+	var appts []*ApptResponse
+
+	defer delete(d.valid, user.Token)
+
+	t, ok := d.valid[user.Token]
+	if !ok || t.Before(time.Now()) {
+		return nil
+	}
+
+	rows, err := d.db.QueryContext(ctx, findClientBookings, user.Username)
+	if err != nil {
+		log.Print(err)
+		return nil
+	}
+
+	for rows.Next() {
+		var date, tie time.Time
+
+		appt := ApptResponse{}
+		if e := rows.Scan(&date, &tie, &appt.ID); e != nil {
+			log.Print(e)
+			break
+		}
+
+		date.Add(time.Duration(tie.Nanosecond()))
+		appt.Block = date
+
+		appts = append(appts, &appt)
+	}
+
+	return appts
+}
+
+const findDoctorBookings = `SELECT a.date, a.time, a.id, a.username FROM appointments a
+JOIN doctors on a.doctor_id = doctors.id 
+WHERE doctors.nickname = $1
+`
+
+func (d *Database) ListDoctorBookings(ctx context.Context, doctor *Doctor) []*ApptResponse {
+	var appts []*ApptResponse
+
+	defer delete(d.valid, doctor.Token)
+
+	t, ok := d.valid[doctor.Token]
+	if !ok || t.Before(time.Now()) {
+		return nil
+	}
+
+	rows, err := d.db.QueryContext(ctx, findDoctorBookings, doctor.Username)
+	if err != nil {
+		return nil
+	}
+
+	for rows.Next() {
+		var date, tie time.Time
+
+		appt := ApptResponse{}
+		if e := rows.Scan(&date, &tie, &appt.ID); e != nil {
+			break
+		}
+
+		date.Add(time.Duration(tie.Nanosecond()))
+		appt.Block = date
+
+		appts = append(appts, &appt)
+	}
+
+	return appts
+}
--- /dev/null
+++ b/database/appointments_test.go
@@ -1,0 +1,107 @@
+package database
+
+import (
+	"context"
+	"testing"
+	"time"
+)
+
+func TestAddAppointment(t *testing.T) {
+	ctx, cancel := context.WithCancel(context.Background())
+	db, err := Connect("user=postgres dbname=testing password=cinnamon sslmode=disable")
+	if err != nil {
+		panic(err)
+	}
+
+	doctor := &Doctor{
+		Username: "johndoe",
+	}
+
+	req := &ApptRequest{
+		Block:  time.Now().Round(time.Minute * 15),
+		Doctor: doctor,
+		Token:  <-db.Tokens,
+		ID:     42,
+	}
+
+	if e := db.AddAppointment(ctx, req); e != nil {
+		t.Error(e)
+	}
+
+	cancel()
+}
+
+// John Doe works out of Antarctica
+func TestBookAppointment(t *testing.T) {
+	ctx, cancel := context.WithCancel(context.Background())
+	db, err := Connect("user=postgres dbname=testing password=cinnamon sslmode=disable")
+	if err != nil {
+		panic(err)
+	}
+
+	doctor := &Doctor{
+		Username: "johndoe",
+	}
+
+	user := &User{
+		Username: "ebreukelman1",
+	}
+
+	req := &ApptRequest{
+		Doctor: doctor,
+		User:   user,
+		Token:  <-db.Tokens,
+		ID:     42,
+	}
+
+	if e := db.BookAppointment(ctx, req); e != nil {
+		t.Error(e)
+	}
+
+	cancel()
+}
+
+func TestListAvailable(t *testing.T) {
+	ctx, cancel := context.WithCancel(context.Background())
+	db, err := Connect("user=postgres dbname=testing password=cinnamon sslmode=disable")
+	if err != nil {
+		panic(err)
+	}
+
+	doctor := &Doctor{
+		Username: "spassman1",
+	}
+
+	req := &ApptRequest{
+		Doctor: doctor,
+		Token:  <-db.Tokens,
+		ID:     42,
+	}
+
+	appts := db.ListAvailable(ctx, req)
+	if len(appts) < 1 {
+		t.Error("Unable to find test appointments")
+	}
+
+	cancel()
+}
+
+func TestListClientBookings(t *testing.T) {
+	ctx, cancel := context.WithCancel(context.Background())
+	db, err := Connect("user=postgres dbname=testing password=cinnamon sslmode=disable")
+	if err != nil {
+		panic(err)
+	}
+
+	user := &User{
+		Username: "ebreukelman1",
+		Token:    <-db.Tokens,
+	}
+
+	appts := db.ListClientBookings(ctx, user)
+	if len(appts) < 1 {
+		t.Error("Unable to find test bookings")
+	}
+
+	cancel()
+}
--- /dev/null
+++ b/database/database.go
@@ -1,0 +1,82 @@
+package database
+
+import (
+	"database/sql"
+	"time"
+
+	"github.com/google/uuid"
+	_ "github.com/lib/pq"
+)
+
+type Database struct {
+	Tokens chan string
+	Temps  chan string
+
+	// internal
+	db    *sql.DB
+	valid map[string]time.Time
+}
+
+// Open the socket
+func Connect(credentials string) (*Database, error) {
+	db, err := sql.Open("postgres", credentials)
+	if err != nil {
+		return nil, err
+	}
+
+	// Make sure we can actually ping the thing
+	if e := db.Ping(); e != nil {
+		return nil, e
+	}
+
+	d := &Database{
+		valid: make(map[string]time.Time),
+		db:    db,
+	}
+
+	d.Tokens = createTokens(d)
+	d.Temps = createTemps(d)
+	return d, nil
+}
+
+func (d *Database) Close() {
+	d.db.Close()
+}
+
+func createTokens(d *Database) chan string {
+	tokens := make(chan string)
+	go func(tokens chan string) {
+		for {
+			u, _ := uuid.NewRandom()
+			t := u.String()
+
+			tokens <- t
+			// After token unblocks, add a timeout to it
+			// Plugins are destroyed on-use, the only case
+			// where errant tokens would hang around is if
+			// a plugin is programmed incorrectly
+			d.valid[t] = time.Now().Add(time.Second * 5)
+		}
+	}(tokens)
+
+	return tokens
+}
+
+func createTemps(d *Database) chan string {
+	tokens := make(chan string)
+	go func(tokens chan string) {
+		for {
+			u, _ := uuid.NewRandom()
+			t := u.String()
+
+			tokens <- t
+
+			// Temp tokens are internally destruct-on-use
+			// But in general a signup, or otherwise may take
+			// quite a while. We give a forgiving timeout
+			d.valid[t] = time.Now().Add(time.Hour)
+		}
+	}(tokens)
+
+	return tokens
+}
--- /dev/null
+++ b/database/database_test.go
@@ -1,0 +1,15 @@
+package database
+
+import (
+	"testing"
+)
+
+func TestTokenTimeout(t *testing.T) {
+	db, err := Connect("user=postgres dbname=testing password=cinnamon sslmode=disable")
+	if err != nil {
+		t.Error(err)
+	}
+
+	// Grab a token and make sure it's valid
+	db.Close()
+}
--- /dev/null
+++ b/database/go.mod
@@ -1,0 +1,8 @@
+module github.com/olmaxmedical/database
+
+go 1.14
+
+require (
+	github.com/google/uuid v1.1.1
+	github.com/lib/pq v1.3.0
+)
--- /dev/null
+++ b/database/go.sum
@@ -1,0 +1,4 @@
+github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
+github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/lib/pq v1.3.0 h1:/qkRGz8zljWiDcFvgpwUpwIAPu3r07TDvs3Rws+o/pU=
+github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
--- /dev/null
+++ b/database/messages.go
@@ -1,0 +1,138 @@
+package database
+
+import (
+	"context"
+	"log"
+	"time"
+)
+
+type Thread struct {
+	ID     int
+	Doctor *Doctor
+}
+
+type Message struct {
+	Content string
+	Sent    time.Time
+}
+
+const clientThreadList = `
+SELECT d.first_name, d.last_name, d.nickname, mt.id FROM doctors d
+JOIN message_thread mt on d.id = mt.doctor_id
+JOIN messages m on mt.msg_id = m.id
+WHERE mt.client_id = $1
+ORDER BY datetime
+`
+
+// There is no inter-client messages, only client to doctor/doctor to client.
+func (d *Database) ListClientThreads(ctx context.Context, user *User) []*Thread {
+	var threadlist []*Thread
+
+	defer delete(d.valid, user.Token)
+
+	// validate token
+	t, ok := d.valid[user.Token]
+	if !ok || t.Before(time.Now()) {
+		return threadlist
+	}
+
+	rows, err := d.db.QueryContext(ctx, clientThreadList, user.id)
+	if err != nil {
+		log.Print(err)
+		return nil
+	}
+
+	for rows.Next() {
+		var id int
+
+		doc := &Doctor{}
+		if e := rows.Scan(&doc.First, &doc.Last, &doc.Username, &id); e != nil {
+			break
+		}
+
+		thread := &Thread{
+			Doctor: doc,
+			ID:     id,
+		}
+
+		threadlist = append(threadlist, thread)
+	}
+
+	return threadlist
+}
+
+const listMessages = `
+SELECT mt.datetime, m.content 
+FROM doctors d
+JOIN message_thread mt on d.id = mt.doctor_id
+JOIN users u on mt.client_id = u.id
+JOIN messages m on mt.msg_id = m.id
+WHERE d.nickname = $1
+AND mt.client_id = $2
+ORDER BY datetime
+`
+
+func (d *Database) ListThread(ctx context.Context, user *User, doctor *Doctor) []*Message {
+	var msglist []*Message
+
+	defer delete(d.valid, user.Token)
+
+	// validate token
+	t, ok := d.valid[user.Token]
+	if !ok || t.Before(time.Now()) {
+		return msglist
+	}
+
+	rows, err := d.db.QueryContext(ctx, listMessages, doctor.Username, user.id)
+	if err != nil {
+		log.Print(err)
+		return nil
+	}
+
+	for rows.Next() {
+		var content string
+		var datetime time.Time
+		if e := rows.Scan(&datetime, &content); e != nil {
+			break
+		}
+
+		msg := &Message{
+			Content: content,
+			Sent:    datetime,
+		}
+
+		msglist = append(msglist, msg)
+	}
+
+	return msglist
+}
+
+const messageSend = `
+INSERT into messages(content)
+VALUES($1)
+`
+
+const messageLink = `INSERT into message_thread (doctor_id, client_id, msg_id, datetime) values ($1, $2, $3, $4)`
+
+func (d *Database) SendMessage(ctx context.Context, user *User, doctor *Doctor, msg *Message) error {
+	defer delete(d.valid, user.Token)
+
+	// validate token
+	t, ok := d.valid[user.Token]
+	if !ok || t.Before(time.Now()) {
+		return nil
+	}
+
+	res, err := d.db.ExecContext(ctx, messageSend, msg.Content)
+	if err != nil {
+		return err
+	}
+
+	id, err := res.RowsAffected()
+	if err != nil {
+		return err
+	}
+
+	_, err = d.db.ExecContext(ctx, messageLink, doctor.ID, user.id, id, time.Now())
+	return err
+}
--- /dev/null
+++ b/database/messages_test.go
@@ -1,0 +1,78 @@
+package database
+
+import (
+	"context"
+	"testing"
+)
+
+func TestListClientMessages(t *testing.T) {
+	ctx, cancel := context.WithCancel(context.Background())
+	db, err := Connect("user=postgres dbname=testing password=cinnamon sslmode=disable")
+	if err != nil {
+		panic(err)
+	}
+
+	user := &User{
+		Username: "kshirlandm",
+		id:       23,
+		Token:    <-db.Tokens,
+	}
+
+	if len(db.ListClientThreads(ctx, user)) < 1 {
+		t.Error("unable to retrieve message threads for client")
+	}
+
+	cancel()
+}
+
+func TestListThread(t *testing.T) {
+	// doctorid 7, clientid 410
+	ctx, cancel := context.WithCancel(context.Background())
+	db, err := Connect("user=postgres dbname=testing password=cinnamon sslmode=disable")
+	if err != nil {
+		panic(err)
+	}
+
+	user := &User{
+		id:    23,
+		Token: <-db.Tokens,
+	}
+
+	doctor := &Doctor{
+		Username: "ppattrickl",
+	}
+
+	if len(db.ListThread(ctx, user, doctor)) < 1 {
+		t.Error("unable to retrieve message threads for client")
+	}
+
+	cancel()
+}
+
+func TestSendMessage(t *testing.T) {
+	ctx, cancel := context.WithCancel(context.Background())
+	db, err := Connect("user=postgres dbname=testing password=cinnamon sslmode=disable")
+	if err != nil {
+		panic(err)
+	}
+
+	user := &User{
+		id:    23,
+		Token: <-db.Tokens,
+	}
+
+	doctor := &Doctor{
+		ID:       22,
+		Username: "ppattrickl",
+	}
+
+	message := &Message{
+		Content: "Lorum ipsum dolor sit amet",
+	}
+
+	if e := db.SendMessage(ctx, user, doctor, message); e != nil {
+		t.Error(e)
+	}
+
+	cancel()
+}
--- /dev/null
+++ b/database/query.go
@@ -1,0 +1,199 @@
+package database
+
+import (
+	"context"
+	"fmt"
+	"strings"
+	"time"
+)
+
+type Aux int
+
+const (
+	AuxNone Aux = iota
+	AuxEmail
+	AuxWage
+)
+
+var aux = map[Aux]string{
+	AuxNone:  "",
+	AuxEmail: "d.email",
+	AuxWage:  "d.wage",
+}
+
+// Query token must be a valid query token
+// These are issued by the plugin manager
+type Query struct {
+	Service string
+	Country string
+	Query   Aux
+	Sort    string
+	Token   string
+}
+
+// Doctor is a single result from a query
+// This likely will be populated with more fields soon
+type Doctor struct {
+	ID       int
+	First    string
+	Last     string
+	Username string
+	// TODO(halfwit) Add Rate field
+	//Rate     float32
+	Aux interface{}
+}
+
+// This can likely be cleaned up now
+const queryAux = `
+SELECT d.first_name, d.last_name, d.id, `
+
+const queryDoctorStart = `
+SELECT d.first_name, d.last_name, d.id`
+
+const queryDoctorAll = `
+FROM doctors d`
+
+const queryDoctorNiche = `
+FROM doctors d
+JOIN doctor_specialties ds ON ds.doctor_id = d.id
+JOIN doctor_countries dc ON dc.doctor_id = d.id
+JOIN specialties s on s.id = ds.specialty_id
+JOIN countries c on c.id = dc.country_id
+WHERE s.specialty_name = ?
+AND c.official_name = ?`
+
+const queryDoctorCountry = `
+FROM doctors d
+JOIN doctor_countries dc ON dc.doctor_id = d.id
+JOIN countries c ON dc.country_id = c.id
+WHERE c.official_name = ?`
+
+const queryDoctorSpecialty = `
+FROM doctors d
+JOIN doctor_specialties ds ON ds.doctor_id = d.id
+JOIN specialties s on s.id = ds.specialty_id
+WHERE s.specialty_name = ?`
+
+const queryDoctorEnd = `
+ORDER BY d.last_name;`
+
+func (d *Database) RunQuery(ctx context.Context, query *Query) chan *Doctor {
+	// Unabashedly blow up this token regardless, if it exists
+	defer delete(d.valid, query.Token)
+
+	doctor := make(chan *Doctor)
+
+	// Ensure we have a good token
+	t, ok := d.valid[query.Token]
+	if !ok || t.Before(time.Now()) {
+		defer close(doctor)
+		return doctor
+	}
+
+	// Make sure we sanitize inputs
+	if _, ok := aux[query.Query]; !ok {
+		query.Query = AuxNone
+	}
+
+	if query.Sort == "" {
+		query.Sort = "doctors.last_name"
+	}
+
+	go func(ctx context.Context, query *Query, doctor chan *Doctor) {
+		hasaux, format, args := buildArgs(ctx, query)
+
+		for doc := range d.listDoctors(ctx, hasaux, format, args...) {
+			doctor <- doc
+		}
+
+		close(doctor)
+	}(ctx, query, doctor)
+
+	return doctor
+}
+
+func buildArgs(ctx context.Context, query *Query) (bool, string, []string) {
+	var hasaux bool
+	var format string
+	var args []string
+
+	switch {
+	// Specific query
+	case query.Query != AuxNone:
+		hasaux = true
+
+		switch {
+		case query.Service != "" && query.Country != "":
+			format = queryAux + aux[query.Query] + queryDoctorNiche + queryDoctorEnd
+			args = []string{query.Service, query.Country}
+		case query.Service != "":
+			format = queryAux + aux[query.Query] + queryDoctorSpecialty + queryDoctorEnd
+			args = []string{query.Service}
+		case query.Country != "":
+			format = queryAux + aux[query.Query] + queryDoctorCountry + queryDoctorEnd
+			args = []string{query.Country}
+		default:
+			format = queryAux + aux[query.Query] + queryDoctorAll + queryDoctorEnd
+		}
+	case query.Service != "" && query.Country != "":
+		format = queryDoctorStart + queryDoctorNiche + queryDoctorEnd
+		args = []string{query.Service, query.Country}
+	case query.Service != "":
+		format = queryDoctorStart + queryDoctorSpecialty + queryDoctorEnd
+		args = []string{query.Service}
+	case query.Country != "":
+		format = queryDoctorStart + queryDoctorCountry + queryDoctorEnd
+		args = []string{query.Country}
+	default:
+		format = queryDoctorStart + queryDoctorAll + queryDoctorEnd
+	}
+
+	return hasaux, format, args
+}
+
+func (d *Database) listDoctors(ctx context.Context, hasaux bool, query string, args ...string) chan *Doctor {
+	docchan := make(chan *Doctor)
+	go func(docchan chan *Doctor) {
+		query = sanitizeQuery(query, len(args))
+
+		rows, err := d.db.QueryContext(ctx, query, strings.Join(args, " "))
+		if err != nil {
+			close(docchan)
+			return
+		}
+
+		for rows.Next() {
+			d := Doctor{}
+			var err error
+
+			if hasaux {
+				err = rows.Scan(&d.First, &d.Last, &d.ID, &d.Aux)
+			} else {
+				err = rows.Scan(&d.First, &d.Last, &d.ID)
+			}
+
+			if err != nil {
+				close(docchan)
+				return
+			}
+
+			docchan <- &d
+		}
+
+		close(docchan)
+	}(docchan)
+
+	return docchan
+}
+
+func sanitizeQuery(query string, n int) string {
+	for i := 1; i <= n; i++ {
+		n := strings.IndexByte(query, '?')
+		if n < 0 {
+			break
+		}
+		query = fmt.Sprintf("%s$%d%s", query[:n], i, query[n+1:])
+	}
+
+	return query
+}
--- /dev/null
+++ b/database/query_test.go
@@ -1,0 +1,58 @@
+package database
+
+import (
+	"context"
+	"testing"
+	"time"
+)
+
+func TestAuxQuery(t *testing.T) {
+	// Ensure token creation and running works as expected
+	db, err := Connect("user=postgres dbname=testing password=cinnamon sslmode=disable")
+	if err != nil {
+		t.Error(err)
+	}
+
+	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
+
+	q := &Query{
+		Country: "Afghanistan",
+		Query:   AuxEmail,
+		Token:   <-db.Tokens,
+	}
+
+	for d := range db.RunQuery(ctx, q) {
+		if d != nil {
+			cancel()
+			return
+		}
+	}
+
+	cancel()
+	t.Error("found no results")
+}
+
+func TestNormalQuery(t *testing.T) {
+	// Ensure token creation and running works as expected
+	db, err := Connect("user=postgres dbname=testing password=cinnamon sslmode=disable")
+	if err != nil {
+		t.Error(err)
+	}
+
+	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
+	defer cancel()
+
+	q := &Query{
+		Country: "Afghanistan",
+		Query:   AuxNone,
+		Token:   <-db.Tokens,
+	}
+
+	for d := range db.RunQuery(ctx, q) {
+		if d != nil {
+			return
+		}
+	}
+
+	t.Error("found no results")
+}
--- /dev/null
+++ b/database/reset.go
@@ -1,0 +1,34 @@
+package database
+
+import (
+	"context"
+	"time"
+)
+
+const updateEmail = `
+UPDATE users
+SET password = $1
+WHERE users.email = $2;`
+
+//SET hash = $1
+//WHERE users.email = $2;``
+
+type Reset struct {
+	Email string
+	//Hash string
+	Password string
+	Token    string
+}
+
+func (d *Database) ResetPassword(ctx context.Context, r *Reset) {
+	defer delete(d.valid, r.Token)
+
+	// validate token
+	t, ok := d.valid[r.Token]
+	if !ok || t.Before(time.Now()) {
+		return
+	}
+
+	d.db.ExecContext(ctx, updateEmail, r.Password, r.Email)
+	//d.db.ExecContext(ctx, updateEmail, r.Hash, r.Email)
+}
--- /dev/null
+++ b/database/temporary.go
@@ -1,0 +1,104 @@
+package database
+
+import (
+	"context"
+	"errors"
+	"time"
+)
+
+const tempEntry = `
+INSERT INTO tmpusers(username, country, email, password, token)
+VALUES($1, $2, $3, $4, $5)
+`
+
+func (d *Database) CreateTempEntry(ctx context.Context, user *User, email, password string) string {
+	defer delete(d.valid, user.Token)
+
+	t, ok := d.valid[user.Token]
+	if !ok || t.Before(time.Now()) {
+		return "BADTOKEN"
+	}
+
+	token := <-d.Temps
+
+	//res, err := d.db.ExecContext(ctx, tempEntry, user.Username, email, password, token)
+	//if err != nil { return "BADTOKEN" }
+	//if row, e := res.RowsAffected(); e != nil || row < 1 { return "BADTOKEN" }
+	if _, e := d.db.ExecContext(ctx, tempEntry, user.Username, user.Country, email, password, token); e != nil {
+		return "BADTOKEN"
+	}
+
+	return token
+}
+
+// If this doesn't work, we'll have to first fetch the user ID
+// Then update based on that.  Not a big deal, but still annoying
+const updateToken = `
+UPDATE tmpusers
+SET token = $1
+WHERE token = $2
+`
+
+// ForwardToken will try to update a user based on an old token
+// If the user doesn't exist in the temp database, or the token is
+// invalid, BADTOKEN will be returned
+func (d *Database) ForwardToken(ctx context.Context, old string) string {
+	defer delete(d.valid, old)
+
+	t, ok := d.valid[old]
+	if !ok || t.Before(time.Now()) {
+		return "BADTOKEN"
+	}
+
+	token := <-d.Temps
+
+	//res, err := d.db.ExecContext(ctx, updateToken, token, old)
+	//if err != nil { return "BADTOKEN" }
+	//if row, e := res.RowsAffected(); e != nil || row < 1 { return "BADTOKEN" }
+	if _, e := d.db.ExecContext(ctx, updateToken, token, old); e != nil {
+		return "BADTOKEN"
+	}
+
+	return token
+}
+
+const findUserTemp = `
+SELECT username, email, country, password
+FROM tmpusers
+WHERE token = $1
+`
+
+const deleteUserTemp = `
+DELETE FROM tmpusers
+WHERE token = $1
+`
+
+// Take in temp entry token
+func (d *Database) UserFromTemp(ctx context.Context, token string) error {
+	var email, password string
+	user := User{
+		Token: <-d.Tokens,
+	}
+
+	defer delete(d.valid, token)
+	defer d.db.ExecContext(ctx, deleteUserTemp, token)
+
+	t, ok := d.valid[token]
+	if !ok || t.Before(time.Now()) {
+		return errors.New("invalid/expired token")
+	}
+
+	rows, err := d.db.QueryContext(ctx, findUserTemp, token)
+	if err != nil {
+		return err
+	}
+
+	for rows.Next() {
+		rows.Scan(&user.Username, &email, &user.Country, &password)
+
+		// tokens are unique, so take the first and run away laughing
+		return d.CreateUser(ctx, &user, email, password)
+	}
+
+	return errors.New("invalid/expired signup session")
+}
--- /dev/null
+++ b/database/temporary_test.go
@@ -1,0 +1,43 @@
+package database
+
+import (
+	"context"
+	"testing"
+	"time"
+)
+
+func TestCreateTempEntry(t *testing.T) {
+	// Ensure token creation and running works as expected
+	db, err := Connect("user=postgres dbname=testing password=cinnamon sslmode=disable")
+	if err != nil {
+		t.Error(err)
+	}
+
+	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
+	defer cancel()
+
+	user := &User{
+		Token:    <-db.Tokens,
+		Username: "johndoe123_test",
+		Country:  "canada",
+	}
+
+	if token := db.CreateTempEntry(ctx, user, "foo@bar.com", "hunter2"); token == "BADTOKEN" {
+		t.Error("was unable to create a temp entry")
+	}
+
+}
+
+func TestForwardToken(t *testing.T) {
+	db, err := Connect("user=postgres dbname=testing password=cinnamon sslmode=disable")
+	if err != nil {
+		t.Error(err)
+	}
+
+	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
+	defer cancel()
+
+	if token := db.ForwardToken(ctx, <-db.Temps); token == "BADTOKEN" {
+		t.Error("was unable to create new token")
+	}
+}
--- /dev/null
+++ b/database/user.go
@@ -1,0 +1,148 @@
+package database
+
+import (
+	"context"
+	"errors"
+	"time"
+)
+
+// Access - Who can access the data
+type Access uint8
+
+const (
+	GuestAuth Access = 1 << iota
+	PatientAuth
+	DoctorAuth
+)
+
+// User - Any registered user on the site
+type User struct {
+	Username string
+	Country  string
+	Token    string
+	access   Access
+	id       int
+}
+
+// UserInfo - request for a user
+type UserInfo struct {
+	// Must be valid
+	Token string
+	//Hash string
+	Password string
+	Username string
+}
+
+const createUser = `
+INSERT INTO users(name, email, country, password)
+VALUES($1, $2, $3, $4)
+`
+
+// CreateUser is called from email/signup only
+func (d *Database) CreateUser(ctx context.Context, u *User, email, password string) error {
+	defer delete(d.valid, u.Token)
+
+	// validate token
+	t, ok := d.valid[u.Token]
+	if !ok || t.Before(time.Now()) || u.access != PatientAuth {
+		return errors.New("invalid/expired access token")
+	}
+
+	_, err := d.db.ExecContext(ctx, createUser, u.Username, email, u.Country, password)
+	return err
+}
+
+const updateUser = `
+UPDATE users
+SET Country = $2, Username = $3
+WHERE users.id = $1`
+
+func (d *Database) UserUpdate(ctx context.Context, u *User) chan struct{} {
+	defer delete(d.valid, u.Token)
+
+	// validate token
+	t, ok := d.valid[u.Token]
+	if !ok || t.Before(time.Now()) || u.access != PatientAuth {
+		return make(chan struct{})
+	}
+
+	d.db.ExecContext(ctx, updateUser, u.id, u.Country, u.Username)
+	return make(chan struct{})
+}
+
+const queryUser = `
+SELECT users.id, users.name, users.country
+FROM users WHERE users.name = $1 AND users.password = $2`
+
+//FROM users WHERE users.name = $1 AND users.hash = $2`
+
+// Attempt to find a user (Most common case by an order of magnitude)
+// if we fail, attempt to find a doctor; then fail outright
+func (d *Database) FindUser(ctx context.Context, u *UserInfo) *User {
+	user := User{
+		access:   GuestAuth,
+		Username: "none",
+		Country:  "none",
+	}
+
+	defer delete(d.valid, u.Token)
+
+	// validate token
+	t, ok := d.valid[u.Token]
+	if !ok || t.Before(time.Now()) {
+		return &user
+	}
+
+	rows, err := d.db.QueryContext(ctx, queryUser, u.Username, u.Password)
+	if err != nil {
+		return d.FindDoctor(ctx, u)
+	}
+
+	for rows.Next() {
+		if e := rows.Scan(&user.id, &user.Username, &user.Country); e != nil {
+			break
+		}
+
+		// Only set Access on a successful lookup
+		user.access = PatientAuth
+		break
+	}
+
+	return &user
+}
+
+const queryDoctor = `
+SELECT doctors.nickname, doctors.country
+FROM doctors WHERE doctors.nickname = $1 AND doctors.password = $2`
+
+//FROM doctors WHERE doctors.hash = $1`
+
+func (d *Database) FindDoctor(ctx context.Context, u *UserInfo) *User {
+	user := User{
+		access:   GuestAuth,
+		Username: "none",
+		Country:  "none",
+	}
+
+	rows, err := d.db.QueryContext(ctx, queryUser, u.Username, u.Password)
+	if err != nil {
+		return &user
+	}
+
+	for rows.Next() {
+		if e := rows.Scan(&user.Username, &user.Country); e != nil {
+			break
+		}
+
+		// Only set Access on a successful lookup
+		user.access = DoctorAuth
+		break
+	}
+
+	return &user
+}
+
+// Retrieve the access for the given client
+func (u *User) Access() Access {
+	return u.access
+}
--- /dev/null
+++ b/database/user_test.go
@@ -1,0 +1,35 @@
+package database
+
+import (
+	"context"
+	"testing"
+	"time"
+)
+
+func TestUpdateUser(t *testing.T) {
+
+}
+
+func TestFindUser(t *testing.T) {
+	// Ensure token creation and running works as expected
+	db, err := Connect("user=postgres dbname=testing password=cinnamon sslmode=disable")
+	if err != nil {
+		t.Error(err)
+	}
+
+	ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
+	defer cancel()
+
+	u := &UserInfo{
+		// Random pull from mock database
+		Username: "jcoyishix",
+		Password: "cUco9wEq",
+		Token:    <-db.Tokens,
+	}
+
+	if user := db.FindUser(ctx, u); user != nil {
+		return
+	}
+
+	t.Error("found no results")
+}