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")
+}