diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/LuggageLocatorBackend.iml b/.idea/LuggageLocatorBackend.iml
new file mode 100644
index 0000000..5e764c4
--- /dev/null
+++ b/.idea/LuggageLocatorBackend.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml
new file mode 100644
index 0000000..b9c388e
--- /dev/null
+++ b/.idea/dataSources.xml
@@ -0,0 +1,12 @@
+
+
+
+
+ mariadb
+ true
+ org.mariadb.jdbc.Driver
+ jdbc:mariadb://mariadb.traceyclan.us:3306/luggage
+ $ProjectFileDir$
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..748877f
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml
new file mode 100644
index 0000000..b963e5f
--- /dev/null
+++ b/.idea/sqldialects.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index ee33d1c..3415aed 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,13 @@
# LuggageTracker
+## Envs
+- PORT | 8080
+- DB_USER | user
+- DB_PASS | pass
+- DB_HOST | 10.0.0.1
+- DB_PORT | 3306
+- DB_SCHEMA | luggage
+- DB_CA_CERT_PATH | /etc/ssl/ca-cert.pem
+- DB_CERT_PATH | /etc/ssl/client-cert.pem
+- DB_KEY_PATH | /etc/ssl/private/client-key.pem
+- DB_SKIP_VERIFY | true
\ No newline at end of file
diff --git a/db.go b/db.go
new file mode 100644
index 0000000..f885862
--- /dev/null
+++ b/db.go
@@ -0,0 +1,145 @@
+package main
+
+import (
+ "crypto/tls"
+ "crypto/x509"
+ "database/sql"
+ "errors"
+ "fmt"
+ "github.com/go-sql-driver/mysql"
+ "log"
+ "os"
+ "strconv"
+ "time"
+)
+
+type LDB struct {
+ db *sql.DB
+}
+
+var NoEntriesFoundError error = errors.New("no entries found")
+
+func createTLSConf(skipVerify bool) tls.Config {
+ rootCertPool := x509.NewCertPool()
+ pem, err := os.ReadFile(os.Getenv("DB_CA_CERT_PATH"))
+ if err != nil {
+ log.Fatal(err)
+ }
+ if ok := rootCertPool.AppendCertsFromPEM(pem); !ok {
+ log.Fatal("Failed to append PEM.")
+ }
+ clientCert := make([]tls.Certificate, 0, 1)
+
+ keyPair, err := tls.LoadX509KeyPair(os.Getenv("DB_CERT_PATH"), os.Getenv("DB_KEY_PATH"))
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ clientCert = append(clientCert, keyPair)
+
+ return tls.Config{
+ RootCAs: rootCertPool,
+ Certificates: clientCert,
+ InsecureSkipVerify: skipVerify,
+ }
+}
+
+func connect() *LDB {
+ var db *sql.DB
+ caCert := os.Getenv("DB_CA_CERT_PATH")
+
+ if len(caCert) > 0 {
+ skipVerify, err := strconv.ParseBool(os.Getenv("DB_SKIP_VERIFY"))
+ if err != nil {
+ log.Fatal(err)
+ }
+ tlsConf := createTLSConf(skipVerify)
+ err = mysql.RegisterTLSConfig("custom", &tlsConf)
+ if err != nil {
+ log.Fatalf("Error %s on RegisterTLSConfig\n", err)
+ return nil
+ }
+ connStr := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?tls=custom&parseTime=true&loc=Local",
+ os.Getenv("DB_USER"),
+ os.Getenv("DB_PASS"),
+ os.Getenv("DB_HOST"),
+ os.Getenv("DB_PORT"),
+ os.Getenv("DB_SCHEMA"),
+ )
+
+ db, err = sql.Open("mysql", connStr)
+ if err != nil {
+ log.Fatalf("Error connecting to db: %s", err)
+ return nil
+ }
+ } else {
+ connStr := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?parseTime=true&loc=Local",
+ os.Getenv("DB_USER"),
+ os.Getenv("DB_PASS"),
+ os.Getenv("DB_HOST"),
+ os.Getenv("DB_PORT"),
+ os.Getenv("DB_SCHEMA"),
+ )
+ var err error
+ db, err = sql.Open("mysql", connStr)
+ if err != nil {
+ log.Fatal(err)
+ }
+ }
+
+ return &LDB{db}
+}
+
+func (ldb *LDB) queryUser(userName string) (u *User, newError error) {
+ db := ldb.db
+
+ queryStr := `
+ SELECT users.user_name, users.secret_codes, users.current_token, users.contact_number, users.email_address,
+ addresses.street1, addresses.street2, addresses.city, addresses.state, addresses.postal_code,
+ addresses.country, addresses.created_at, addresses.updated_at
+ FROM users
+ INNER JOIN addresses ON users.mailing_address=addresses.id
+ WHERE users.user_name=?;
+`
+ stmt, err := db.Prepare(queryStr)
+ if err != nil {
+ return nil, err
+ }
+ res, err := stmt.Query(userName)
+ if err != nil {
+ return nil, err
+ }
+ defer res.Close()
+ if !res.Next() {
+ return nil, NoEntriesFoundError
+ }
+ user := new(User)
+ err = res.Scan(&user.UserName, &user.SecretCodes, &user.CurrentToken, &user.ContactNumber, &user.EmailAddress,
+ &user.MailingAddress.Street1, &user.MailingAddress.Street2, &user.MailingAddress.City,
+ &user.MailingAddress.State, &user.MailingAddress.PostalCode, &user.MailingAddress.Country,
+ &user.MailingAddress.CreatedAt, &user.MailingAddress.UpdatedAt,
+ )
+ if err != nil {
+ return nil, err
+ }
+ return user, nil
+}
+
+func (ldb *LDB) updateToken(userName string, token string) error {
+ db := ldb.db
+
+ updateStr := `
+ UPDATE users
+ SET current_token=?,token_creation=?
+ WHERE user_name=?;
+`
+ stmt, err := db.Prepare(updateStr)
+ if err != nil {
+ return err
+ }
+ _, err = stmt.Exec(token, time.Now(), userName)
+ if err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/formatDB.sql b/formatDB.sql
new file mode 100644
index 0000000..d6c86db
--- /dev/null
+++ b/formatDB.sql
@@ -0,0 +1,25 @@
+CREATE TABLE addresses (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ street1 TEXT NOT NULL ,
+ street2 TEXT,
+ city TEXT NOT NULL ,
+ state TEXT,
+ postal_code TEXT NOT NULL,
+ country TEXT NOT NULL,
+ created_at TIMESTAMP NOT NULL,
+ updated_at TIMESTAMP NOT NULL,
+ PRIMARY KEY (id)
+);
+
+CREATE TABLE users (
+ id BIGINT NOT NULL AUTO_INCREMENT,
+ user_name TEXT NOT NULL,
+ secret_codes TEXT NOT NULL,
+ current_token TEXT,
+ token_creation TIMESTAMP,
+ contact_number TEXT NOT NULL,
+ email_address TEXT NOT NULL,
+ mailing_address BIGINT NOT NULL,
+ PRIMARY KEY (id),
+ FOREIGN KEY (mailing_address) REFERENCES addresses(id)
+);
\ No newline at end of file
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..c400387
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,39 @@
+module LuggageLocatorBackend
+
+go 1.23.2
+
+require (
+ github.com/gin-contrib/cors v1.7.6
+ github.com/gin-gonic/gin v1.10.1
+ github.com/go-sql-driver/mysql v1.9.3
+)
+
+require (
+ filippo.io/edwards25519 v1.1.0 // indirect
+ github.com/bytedance/sonic v1.13.3 // indirect
+ github.com/bytedance/sonic/loader v0.2.4 // indirect
+ github.com/cloudwego/base64x v0.1.5 // indirect
+ github.com/gabriel-vasile/mimetype v1.4.9 // indirect
+ github.com/gin-contrib/sse v1.1.0 // indirect
+ github.com/go-playground/locales v0.14.1 // indirect
+ github.com/go-playground/universal-translator v0.18.1 // indirect
+ github.com/go-playground/validator/v10 v10.26.0 // indirect
+ github.com/goccy/go-json v0.10.5 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/klauspost/cpuid/v2 v2.2.10 // indirect
+ github.com/kr/text v0.2.0 // indirect
+ github.com/leodido/go-urn v1.4.0 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/pelletier/go-toml/v2 v2.2.4 // indirect
+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+ github.com/ugorji/go/codec v1.3.0 // indirect
+ golang.org/x/arch v0.18.0 // indirect
+ golang.org/x/crypto v0.39.0 // indirect
+ golang.org/x/net v0.41.0 // indirect
+ golang.org/x/sys v0.33.0 // indirect
+ golang.org/x/text v0.26.0 // indirect
+ google.golang.org/protobuf v1.36.6 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..e129fe2
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,96 @@
+filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
+filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
+github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0=
+github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
+github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
+github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
+github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
+github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
+github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
+github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
+github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
+github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY=
+github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk=
+github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
+github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
+github.com/gin-gonic/gin v1.10.1 h1:T0ujvqyCSqRopADpgPgiTT63DUQVSfojyME59Ei63pQ=
+github.com/gin-gonic/gin v1.10.1/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
+github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
+github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
+github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
+github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
+github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
+github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
+github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
+github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
+github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
+github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
+github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
+github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
+github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
+golang.org/x/arch v0.18.0 h1:WN9poc33zL4AzGxqf8VtpKUnGvMi8O9lhNyBMF/85qc=
+golang.org/x/arch v0.18.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
+golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
+golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
+golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
+golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
+golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
+golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
+golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
+google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
+google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..29f9308
--- /dev/null
+++ b/main.go
@@ -0,0 +1,227 @@
+package main
+
+import (
+ "context"
+ "crypto/rand"
+ "database/sql"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "github.com/gin-contrib/cors"
+ "github.com/gin-gonic/gin"
+ "log"
+ "net/http"
+ "strings"
+ "time"
+)
+
+type Address struct {
+ Id int64 `json:"address_id"`
+ Street1 string `json:"street1"`
+ Street2 *string `json:"street2,omitempty"`
+ City string `json:"city"`
+ State *string `json:"state,omitempty"`
+ PostalCode string `json:"postal_code"`
+ Country string `json:"country"`
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
+}
+
+type User struct {
+ Status int32 `json:"status"`
+ Id int64 `json:"user_id"`
+ UserName string `json:"user_name"`
+ SecretCodes string `json:"secret_codes"`
+ CurrentToken *string
+ ContactNumber string `json:"contact_number"`
+ EmailAddress string `json:"email_address"`
+ MailingAddress Address `json:"mailing_address"`
+}
+
+func GenerateToken(n int) (string, error) {
+ b := make([]byte, n)
+ if _, err := rand.Read(b); err != nil {
+ return "", fmt.Errorf("reading random bytes: %w\n", err)
+ }
+ return base64.RawURLEncoding.EncodeToString(b), nil
+}
+
+func startCleanupLoop(ctx context.Context, db *sql.DB, interval, ttl time.Duration) {
+ ticker := time.NewTicker(interval)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-ctx.Done():
+ return
+
+ case <-ticker.C:
+ if err := cleanupExpired(db, ttl); err != nil {
+ log.Printf("token cleanup error: %v\n", err)
+ }
+ }
+ }
+}
+
+func cleanupExpired(db *sql.DB, ttl time.Duration) error {
+ sqlStmt := `
+ DELETE FROM users
+ WHERE token_creation < DATE_SUB(NOW(), INTERVAL ? SECOND);
+`
+ res, err := db.Exec(sqlStmt, int(ttl.Seconds()))
+ if err != nil {
+ return err
+ }
+
+ n, _ := res.RowsAffected()
+ log.Printf("token cleanup: deleted %d rows\n", n)
+ return nil
+}
+
+func main() {
+ db := connect()
+ defer db.db.Close()
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ go startCleanupLoop(ctx, db.db, time.Hour, 24*time.Hour)
+
+ r := gin.Default()
+
+ r.Use(cors.New(cors.Config{
+ AllowOrigins: []string{"http://localhost:8080", "http://localhost:8000"},
+ AllowMethods: []string{"GET", "POST"},
+ AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
+ ExposeHeaders: []string{"Content-Length", "Authorization"},
+ AllowCredentials: true,
+ }))
+
+ r.Static("/static", "./static")
+ r.StaticFile("/favicon.ico", "./static/favicon.ico")
+
+ r.GET("/", func(c *gin.Context) {
+ c.File("./static/index.html")
+ })
+
+ api := r.Group("/api")
+ api.GET("/luggage/:user", func(c *gin.Context) {
+ user, err := db.queryUser(c.Param("user"))
+ if err != nil {
+ if errors.Is(err, NoEntriesFoundError) {
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
+ "error": "User not found",
+ })
+ } else {
+ fmt.Printf("Error: %s\n", err)
+ c.AbortWithStatus(http.StatusBadRequest)
+ }
+ } else {
+ c.JSON(http.StatusOK, user)
+ }
+ })
+ api.GET("/verify/:user", func(c *gin.Context) {
+ user, err := db.queryUser(c.Param("user"))
+ if err != nil {
+ if errors.Is(err, NoEntriesFoundError) {
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
+ "status": 404,
+ "user": "",
+ "error": "User not found",
+ })
+ } else {
+ fmt.Printf("Error: %s\n", err)
+ c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
+ "status": 500,
+ "user": "",
+ "error": err.Error(),
+ })
+ }
+ } else {
+ codes := strings.Split(user.SecretCodes, "'")
+ responded := false
+ for _, code := range codes {
+ codeHeader := c.GetHeader("Authorization")
+ reqCode := strings.Split(codeHeader, " ")
+ if strings.Compare(code, reqCode[len(reqCode)-1]) == 0 {
+ token, err := GenerateToken(16)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "status": 500,
+ "user": "",
+ "error": err.Error(),
+ })
+ responded = true
+ break
+ } else {
+ err = db.updateToken(user.UserName, token)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{
+ "status": 500,
+ "user": "",
+ "error": err.Error(),
+ })
+ responded = true
+ break
+ } else {
+ user.Status = 200
+ c.JSON(http.StatusOK, gin.H{
+ "status": 200,
+ "user": user.UserName,
+ "error": "",
+ "token": token,
+ })
+ responded = true
+ break
+ }
+ }
+ }
+ }
+ if !responded {
+ c.JSON(http.StatusNotFound, gin.H{
+ "status": "404",
+ "user": "",
+ "error": "User not found",
+ })
+ }
+ }
+ })
+ api.GET("/ping", func(c *gin.Context) {
+ c.JSON(http.StatusOK, gin.H{
+ "message": "pong",
+ })
+ })
+
+ r.GET("/u/:user/info", func(c *gin.Context) {
+ user, err := db.queryUser(c.Param("user"))
+ if user.CurrentToken == nil {
+ c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
+ "status": 403,
+ "error": "Unauthorized",
+ })
+ } else if strings.Compare(c.Query("token"), *user.CurrentToken) == 0 {
+ if err != nil {
+ if errors.Is(err, NoEntriesFoundError) {
+ c.AbortWithStatusJSON(http.StatusNotFound, gin.H{
+ "error": "User not found",
+ })
+ } else {
+ fmt.Printf("Error: %s\n", err)
+ c.AbortWithStatus(http.StatusBadRequest)
+ }
+ } else {
+ c.JSON(http.StatusOK, user)
+ }
+ } else {
+ c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
+ "status": 403,
+ "user": "",
+ "error": "Unauthorized",
+ })
+ }
+ })
+
+ err := r.Run()
+ if err != nil {
+ log.Fatal(err)
+ }
+}
diff --git a/static/favicon.ico b/static/favicon.ico
new file mode 100644
index 0000000..6e68533
Binary files /dev/null and b/static/favicon.ico differ
diff --git a/static/icon.png b/static/icon.png
new file mode 100644
index 0000000..62bc996
Binary files /dev/null and b/static/icon.png differ
diff --git a/static/index.html b/static/index.html
new file mode 100644
index 0000000..9164718
--- /dev/null
+++ b/static/index.html
@@ -0,0 +1,29 @@
+
+
+
+
+ Luggage Tracker
+
+
+
+
+
+
+
+
Registration
+
Contact Steven Tracey
+ via email
+ to get your QR code
+
+
+
+
+
+
+
diff --git a/static/style.css b/static/style.css
new file mode 100644
index 0000000..bc0b40c
--- /dev/null
+++ b/static/style.css
@@ -0,0 +1,104 @@
+:root {
+ --dark: #1f363dff;
+ --mid-dark: #40798cff;
+ --mid: #70a9a1ff;
+ --mid-light: #9ec1a3ff;
+ --light: #cfe0c3ff;
+ font-family: Verdana,sans-serif;
+}
+
+body {
+ margin: 0;
+}
+
+.header {
+ display: flex;
+ align-items: center; /* vertical centering */
+ justify-content: space-between; /* icon left, spacer right */
+ background-color: var(--mid-dark);
+ padding: 0 1rem;
+ height: 80px;
+}
+
+.spacer {
+ flex: 0 0 64px; /* or your icon’s width */
+}
+
+.header-icon {
+ display: block; /* remove inline whitespace */
+ flex: 0 0 64px; /* or your icon’s width */
+ width: 15vw; /* fill the 64px container */
+ height: auto; /* keep aspect ratio */
+ max-height: 80px; /* never exceed 80px bar height */
+}
+
+.title {
+ flex: 1; /* take up all the space between */
+ text-align: center; /* center text within that space */
+ color: var(--light);
+}
+
+.footer {
+ background-color: var(--dark);
+ display: flex;
+ height: 80px;
+}
+
+.footer-text {
+ display: inherit;
+ align-content: center;
+ justify-content: center;
+ margin: auto;
+ background-color: inherit;
+ border: none;
+ outline: none;
+ text-align: center;
+ color: white;
+}
+
+.footer-text a {
+ cursor: pointer;
+ text-decoration: none;
+ color: var(--mid-light);
+ padding-left: 5px;
+}
+
+.footer-text a:hover {
+ color: var(--mid);
+}
+
+.contact-link {
+ cursor: pointer;
+ color: var(--mid-dark);
+ text-decoration: none;
+}
+
+.contact-link a:hover {
+ color: var(--mid);
+}
+
+.container {
+ padding: 20vh 0 calc(80vh - 303px) 0;
+ background-image: linear-gradient(
+ to bottom right,
+ var(--mid-light),
+ var(--mid-dark)
+ );
+}
+
+.content {
+ margin: 0 auto;
+ width: 90vw; /* takes 90% of viewport (or container) width */
+ max-width: 800px; /* but never wider than 800px */
+ padding: 1.5rem; /* give it some breathing room inside */
+ background: white;
+ border-radius: 5px;
+}
+
+.content h3 {
+ text-align: center;
+}
+
+.content p {
+ text-align: center;
+}
\ No newline at end of file