From 8bf90bea0b28e05c4e4abc07562a00ed73cb8f72 Mon Sep 17 00:00:00 2001 From: Gabe Farrell Date: Wed, 17 May 2023 00:04:03 +0000 Subject: [PATCH] Makefile, fix env, bug fixes, tests work --- .env.example | 9 +++ .gitignore | 4 +- Makefile | 30 +++++++ .env.local => dev/db/data/.keep | 0 dev/db/docker-compose.yml | 11 +++ dev/scripts/create-table.sh | 2 + dev/scripts/delete-table.sh | 2 + dev/scripts/dynamo-docker-perms.sh | 5 ++ dev/scripts/schema.json | 35 ++++++++ server/auth/auth_long_test.go | 126 ----------------------------- server/auth/auth_test.go | 122 ++++++++++++++++++++++++++++ server/auth/form_test.go | 3 + server/auth/login.go | 14 +++- server/auth/register.go | 10 ++- server/config/config.go | 2 +- server/db/schema.go | 4 +- server/list/schema.go | 18 +++++ 17 files changed, 262 insertions(+), 135 deletions(-) create mode 100644 .env.example create mode 100644 Makefile rename .env.local => dev/db/data/.keep (100%) create mode 100644 dev/db/docker-compose.yml create mode 100755 dev/scripts/create-table.sh create mode 100755 dev/scripts/delete-table.sh create mode 100755 dev/scripts/dynamo-docker-perms.sh create mode 100644 dev/scripts/schema.json delete mode 100644 server/auth/auth_long_test.go create mode 100644 server/auth/form_test.go diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..041f9a7 --- /dev/null +++ b/.env.example @@ -0,0 +1,9 @@ +ALLOWED_ORIGINS=* +DB_URL=http://localhost:8000 +DB_TABLE=my-db-table +DB_GSI_NAME=gsi1 +DB_GSI_ATTR=gsi1pk +AWS_ACCESS_KEY_ID=SecretAwsKey +AWS_SECRET_ACCESS_KEY=SecretAwsSecret +ENVIRONMENT=local-or-staging-or-production +LISTEN_ADDR=127.0.0.1:3000 \ No newline at end of file diff --git a/.gitignore b/.gitignore index a33eb25..4368ea7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .env* -!.env.local \ No newline at end of file +!.env.example +dev/db/data/* +!dev/db/data/.keep \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8900423 --- /dev/null +++ b/Makefile @@ -0,0 +1,30 @@ +.PHONY: client + +client: + @yarn --cwd ./client vite + +api: + @echo 'Starting API Server...' + @go run ./main.go + +test: test.api + +test.api: + go test -v ./... + +dynamo.start: + @cd ./dev/db && docker compose up -d + +dynamo.stop: + @cd ./dev/db && docker compose down + +dynamo.list: + @aws dynamodb list-tables --endpoint-url http://localhost:8000 + +dynamo.scan: + @aws dynamodb scan --table-name owltier-local --endpoint-url http://localhost:8000 + +dynamo.reset: + @cd ./dev/scripts && ./delete-table.sh + @sleep 1 + @cd ./dev/scripts && ./create-table.sh \ No newline at end of file diff --git a/.env.local b/dev/db/data/.keep similarity index 100% rename from .env.local rename to dev/db/data/.keep diff --git a/dev/db/docker-compose.yml b/dev/db/docker-compose.yml new file mode 100644 index 0000000..78cd274 --- /dev/null +++ b/dev/db/docker-compose.yml @@ -0,0 +1,11 @@ +version: '3.8' +services: + dynamodb-local: + command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ./data" + image: "amazon/dynamodb-local:latest" + container_name: dynamodb-local + ports: + - "8000:8000" + volumes: + - "./data:/home/dynamodblocal/data" + working_dir: /home/dynamodblocal diff --git a/dev/scripts/create-table.sh b/dev/scripts/create-table.sh new file mode 100755 index 0000000..5621e15 --- /dev/null +++ b/dev/scripts/create-table.sh @@ -0,0 +1,2 @@ +#!/bin/bash +aws dynamodb create-table --cli-input-json file://schema.json --endpoint-url http://localhost:8000 --output json \ No newline at end of file diff --git a/dev/scripts/delete-table.sh b/dev/scripts/delete-table.sh new file mode 100755 index 0000000..c032e07 --- /dev/null +++ b/dev/scripts/delete-table.sh @@ -0,0 +1,2 @@ +#!/bin/bash +aws dynamodb delete-table --table-name 'owltier-local' --endpoint-url http://localhost:8000 --output json \ No newline at end of file diff --git a/dev/scripts/dynamo-docker-perms.sh b/dev/scripts/dynamo-docker-perms.sh new file mode 100755 index 0000000..cb5c2fd --- /dev/null +++ b/dev/scripts/dynamo-docker-perms.sh @@ -0,0 +1,5 @@ +#!/bin/bash +# In first time setup, the data folder that dynamo's docker image binds +# to had permissions set incorrectly so dynamo didnt work. This fixes that. +sudo chown $USER ./dev/db/data -R +chmod 775 -R ./dev/db/data \ No newline at end of file diff --git a/dev/scripts/schema.json b/dev/scripts/schema.json new file mode 100644 index 0000000..391625c --- /dev/null +++ b/dev/scripts/schema.json @@ -0,0 +1,35 @@ +{ + "AttributeDefinitions": [ + { + "AttributeName": "pk", + "AttributeType": "S" + }, + { + "AttributeName": "gsi1pk", + "AttributeType": "S" + } + ], + "TableName": "owltier-local", + "KeySchema": [ + { + "AttributeName": "pk", + "KeyType": "HASH" + } + ], + "GlobalSecondaryIndexes": [ + { + "IndexName": "gsi1", + "KeySchema": [ + { + "AttributeName": "gsi1pk", + "KeyType": "HASH" + } + ], + "Projection": { + "ProjectionType": "ALL" + } + } + ], + "BillingMode": "PAY_PER_REQUEST", + "TableClass": "STANDARD" +} \ No newline at end of file diff --git a/server/auth/auth_long_test.go b/server/auth/auth_long_test.go deleted file mode 100644 index 4a6be6f..0000000 --- a/server/auth/auth_long_test.go +++ /dev/null @@ -1,126 +0,0 @@ -package auth_test - -import ( - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - "net/url" - "strings" - "testing" - - "github.com/mnrva-dev/owltier.com/server/auth" - "github.com/mnrva-dev/owltier.com/server/db" -) - -// TODO also write unit tests - -type userdata struct { - Username string `json:"username"` - Password string `json:"password"` -} - -var ( - testuser = &db.UserSchema{ - Username: "test", - Password: "testpassword1234!!", - } - permatestuser = &db.UserSchema{ - Username: "user", - Password: "password1234!!", - } -) - -func runTestServer() *httptest.Server { - return httptest.NewServer(auth.BuildRouter()) -} - -func TestMain(m *testing.M) { - m.Run() -} - -func TestRegister(t *testing.T) { - ts := runTestServer() - defer ts.Close() - data := url.Values{} - data.Set("username", testuser.Username) - data.Set("password", testuser.Password) - resp, err := http.PostForm(fmt.Sprintf("%s/register", ts.URL), data) - if err != nil { - t.Fatal(err) - } - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - t.Fatal("Could not read body") - } - if resp.StatusCode/100 != 2 { - t.Errorf("Expected status in 200-299 range, got %d", resp.StatusCode) - fmt.Println(string(body)) - t.FailNow() - } -} - -func TestLogin(t *testing.T) { - ts := runTestServer() - defer ts.Close() - data := url.Values{} - data.Set("username", testuser.Username) - data.Set("password", testuser.Password) - resp, err := http.PostForm(fmt.Sprintf("%s/login", ts.URL), data) - if err != nil { - t.Fatal(err) - } - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - t.Fatal("Could not read body") - } - if resp.StatusCode/100 != 2 { - t.Errorf("Expected status in 200-299 range, got %d", resp.StatusCode) - fmt.Println(string(body)) - t.FailNow() - } -} - -func TestDeleteAccount(t *testing.T) { - ts := runTestServer() - defer ts.Close() - data := url.Values{} - data.Set("username", testuser.Username) - data.Set("password", testuser.Password) - resp, err := http.PostForm(fmt.Sprintf("%s/login", ts.URL), data) - if err != nil { - t.Fatal(err) - } - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - t.Fatal("Could not read body") - } - if resp.StatusCode/100 != 2 { - t.Error("Failed to login") - t.FailNow() - } - // TODO: Fix this so that it uses session token - AccessToken := strings.Split(string(body), "\n")[0] - data = url.Values{} - data.Set("password", testuser.Password) - req, err := http.NewRequest("POST", fmt.Sprintf("%s/delete", ts.URL), strings.NewReader(data.Encode())) - req.Header.Add("Authorization", "Bearer "+AccessToken) - req.Header.Add("Content-Type", "application/x-www-form-urlencoded") - if err != nil { - t.Fatal(err) - } - resp, err = http.DefaultClient.Do(req) - if err != nil { - t.Fatal(err) - } - body, err = ioutil.ReadAll(resp.Body) - if err != nil { - t.Fatal("Could not read body") - } - // fmt.Println(string(body)) - if resp.StatusCode/100 != 2 { - t.Errorf("Expected status in 200-299 range, got %d", resp.StatusCode) - fmt.Println(string(body)) - t.FailNow() - } -} diff --git a/server/auth/auth_test.go b/server/auth/auth_test.go index 426ea7b..3fe9de1 100644 --- a/server/auth/auth_test.go +++ b/server/auth/auth_test.go @@ -1 +1,123 @@ package auth_test + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "github.com/mnrva-dev/owltier.com/server/auth" + "github.com/mnrva-dev/owltier.com/server/db" +) + +// TODO also write unit tests + +type userdata struct { + Username string `json:"username"` + Password string `json:"password"` +} + +var ( + testuser = &db.UserSchema{ + Username: "test", + Password: "testpassword1234!!", + } +) + +func runTestServer() *httptest.Server { + return httptest.NewServer(auth.BuildRouter()) +} + +func TestMain(m *testing.M) { + m.Run() +} + +func TestRegister(t *testing.T) { + ts := runTestServer() + defer ts.Close() + data := url.Values{} + data.Set("username", testuser.Username) + data.Set("password", testuser.Password) + resp, err := http.PostForm(fmt.Sprintf("%s/register", ts.URL), data) + if err != nil { + t.Fatal(err) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatal("Could not read body") + } + if resp.StatusCode/100 != 2 { + t.Errorf("Expected status in 200-299 range, got %d", resp.StatusCode) + fmt.Println(string(body)) + t.FailNow() + } +} + +func TestLogin(t *testing.T) { + ts := runTestServer() + defer ts.Close() + data := url.Values{} + data.Set("username", testuser.Username) + data.Set("password", testuser.Password) + resp, err := http.PostForm(fmt.Sprintf("%s/login", ts.URL), data) + if err != nil { + t.Fatal(err) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatal("Could not read body") + } + if resp.StatusCode/100 != 2 { + t.Errorf("Expected status in 200-299 range, got %d", resp.StatusCode) + fmt.Println(string(body)) + t.FailNow() + } +} + +func TestDeleteAccount(t *testing.T) { + ts := runTestServer() + defer ts.Close() + data := url.Values{} + data.Set("username", testuser.Username) + data.Set("password", testuser.Password) + resp, err := http.PostForm(fmt.Sprintf("%s/login", ts.URL), data) + if err != nil { + t.Fatal(err) + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatal("Could not read body") + } + if resp.StatusCode/100 != 2 { + t.Error("Failed to login") + t.FailNow() + } + // TODO: Fix this so that it uses session token + sessionC := resp.Cookies()[0] + data = url.Values{} + data.Set("password", testuser.Password) + req, err := http.NewRequest("POST", fmt.Sprintf("%s/delete", ts.URL), strings.NewReader(data.Encode())) + req.Header.Add("Content-Type", "application/x-www-form-urlencoded") + req.AddCookie(sessionC) + fmt.Println("* Got session token ", sessionC.Value) + if err != nil { + t.Fatal(err) + } + resp, err = http.DefaultClient.Do(req) + if err != nil { + t.Fatal(err) + } + body, err = ioutil.ReadAll(resp.Body) + if err != nil { + t.Fatal("Could not read body") + } + // fmt.Println(string(body)) + if resp.StatusCode/100 != 2 { + t.Errorf("Expected status in 200-299 range, got %d", resp.StatusCode) + fmt.Println(string(body)) + t.FailNow() + } +} diff --git a/server/auth/form_test.go b/server/auth/form_test.go new file mode 100644 index 0000000..d37c8e5 --- /dev/null +++ b/server/auth/form_test.go @@ -0,0 +1,3 @@ +package auth_test + +// TODO: test form validation diff --git a/server/auth/login.go b/server/auth/login.go index 96142cc..21fcd2d 100644 --- a/server/auth/login.go +++ b/server/auth/login.go @@ -1,6 +1,7 @@ package auth import ( + "log" "net/http" "time" @@ -37,8 +38,19 @@ func Login(w http.ResponseWriter, r *http.Request) { // prepare login information for the client session := uuid.NewString() - db.Update(user, "session", session) + err = db.Update(user, "session_key", session) + if err != nil { + log.Println(err) + } + // TODO: This is awful. Fix it later + err = db.Update(user, "gsi1pk", "session_key#"+session) + if err != nil { + log.Println(err) + } db.Update(user, "last_login_at", time.Now().Unix()) + if err != nil { + log.Println(err) + } http.SetCookie(w, &http.Cookie{ Name: SESSION_COOKIE, Value: session, diff --git a/server/auth/register.go b/server/auth/register.go index a187661..2c1785c 100644 --- a/server/auth/register.go +++ b/server/auth/register.go @@ -13,7 +13,7 @@ import ( func Register(w http.ResponseWriter, r *http.Request) { var form = &RequestForm{} if err := form.Parse(r); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + jsend.ErrorWithCode(w, 400, "invalid form data") return } @@ -24,8 +24,9 @@ func Register(w http.ResponseWriter, r *http.Request) { }, user) // if we didnt get NotFound error... if err == nil { - w.WriteHeader(http.StatusConflict) - w.Write([]byte("user already exists")) + jsend.Fail(w, http.StatusConflict, map[string]interface{}{ + "username": "user with this username already exists", + }) return } // TODO There is probably a better way to make sure this is just a // "Not Found" error and not an actual error @@ -34,7 +35,7 @@ func Register(w http.ResponseWriter, r *http.Request) { user.LastLoginAt = time.Now().Unix() hashedPassword, err := bcrypt.GenerateFromPassword([]byte(form.Password), bcrypt.DefaultCost) if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) + jsend.Error(w, "internal server error") return } user.Password = string(hashedPassword) @@ -42,6 +43,7 @@ func Register(w http.ResponseWriter, r *http.Request) { session := uuid.NewString() user.Session = session + user.Username = form.Username err = db.Create(user) if err != nil { diff --git a/server/config/config.go b/server/config/config.go index 621be12..c0a53ee 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -18,7 +18,7 @@ func loadEnv() { currentWorkDirectory, _ := os.Getwd() rootPath := projectName.Find([]byte(currentWorkDirectory)) - err := godotenv.Load(string(rootPath) + `/.env.local`) + err := godotenv.Load(string(rootPath) + `/.env`) if err != nil { log.Println("* Error loading .env file") diff --git a/server/db/schema.go b/server/db/schema.go index b0f833c..a6fc13d 100644 --- a/server/db/schema.go +++ b/server/db/schema.go @@ -7,7 +7,7 @@ import ( type UserSchema struct { Pk string `dynamodbav:"pk"` Gsi1pk string `dynamodbav:"gsi1pk"` - Session string `dynamodbav:"session"` + Session string `dynamodbav:"session_key"` Username string `dynamodbav:"username"` Password string `dynamodbav:"password"` CreatedAt int64 `dynamodbav:"created_at"` @@ -16,7 +16,7 @@ type UserSchema struct { func (u *UserSchema) buildKeys() { u.Pk = "user#" + u.Username - u.Gsi1pk = "session#" + u.Session + u.Gsi1pk = "session_key#" + u.Session } func (u *UserSchema) getKey() map[string]types.AttributeValue { diff --git a/server/list/schema.go b/server/list/schema.go index 60069d9..87ff795 100644 --- a/server/list/schema.go +++ b/server/list/schema.go @@ -1,6 +1,9 @@ package list +import "github.com/aws/aws-sdk-go-v2/service/dynamodb/types" + type List struct { + Pk string `dynamodbav:"pk"` Id string `json:"id"` CreatedAt int64 `json:"created_at"` CreatedBy string `json:"created_by"` @@ -10,3 +13,18 @@ type List struct { APAC []string `json:"apac"` Combined []string `json:"combined"` } + +func (u *List) buildKeys() { + u.Pk = "list#" + u.Id +} + +func (u *List) getKey() map[string]types.AttributeValue { + u.buildKeys() + k := make(map[string]types.AttributeValue) + k["pk"] = &types.AttributeValueMemberS{Value: u.Pk} + return k +} + +func (u *List) getGsi() map[string]types.AttributeValue { + return nil +}