diff --git a/.gitignore b/.gitignore index 8b04583..dd18b48 100644 --- a/.gitignore +++ b/.gitignore @@ -126,3 +126,6 @@ fabric.properties # Android studio 3.1+ serialized cache file .idea/caches/build_file_checksums.ser +luggagelocator-all +luggagelocator.tar.gz +LuggageLocator \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 3415aed..1fb3b56 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,26 @@ # 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 +- 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 +- CORS_ALLOWED_ORIGINS.| http://localhost:8080,https://example.com + +## Endpoints +### GET +- / +- /ping +- /api/u/:user + - requires `token` query param +- /api/verify/:user + - requires `Authorization` header to be set to `Basic (code)` +- /u/:user +- /u/:user/info + - requires `token` query param diff --git a/build.bat b/build.bat new file mode 100644 index 0000000..1d41c4a --- /dev/null +++ b/build.bat @@ -0,0 +1,21 @@ +@echo off +title Building LuggageLocator + +set GOARCH=amd64 +set GOOS=linux +set GIN_MODE=release +go build -ldflags="-s -w" -o LuggageLocator . + +mkdir luggagelocator-all +rmdir /s /q .\luggagelocator-all +mkdir luggagelocator-all + +xcopy /S .\html .\luggagelocator-all\html\ +xcopy /S .\static .\luggagelocator-all\static\ +xcopy /S .\templates .\luggagelocator-all\templates\ +xcopy .\formatDB.sql .\luggagelocator-all\ +xcopy .\LuggageLocator .\luggagelocator-all\ +xcopy .\README.md .\luggagelocator-all\ + +del /s /q .\luggagelocator.tar.gz +tar -czvf luggagelocator.tar.gz luggagelocator-all \ No newline at end of file diff --git a/db.go b/db.go index f885862..617f50a 100644 --- a/db.go +++ b/db.go @@ -94,7 +94,7 @@ 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, + SELECT users.user_name, users.contact_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 @@ -114,7 +114,7 @@ func (ldb *LDB) queryUser(userName string) (u *User, newError error) { return nil, NoEntriesFoundError } user := new(User) - err = res.Scan(&user.UserName, &user.SecretCodes, &user.CurrentToken, &user.ContactNumber, &user.EmailAddress, + err = res.Scan(&user.UserName, &user.ContactName, &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, diff --git a/static/index.html b/html/home.html similarity index 74% rename from static/index.html rename to html/home.html index 9164718..2701d6a 100644 --- a/static/index.html +++ b/html/home.html @@ -2,18 +2,19 @@ - Luggage Tracker - + + My Luggage Info +
icon -

Luggage Tracker

+

My Luggage Info

-
+

Registration

Contact Steven Tracey via email @@ -26,4 +27,3 @@ - diff --git a/html/unauthorized.html b/html/unauthorized.html new file mode 100644 index 0000000..10fc688 --- /dev/null +++ b/html/unauthorized.html @@ -0,0 +1,29 @@ + + + + + + My Luggage Info + + + + +

+ icon +

My Luggage Info

+
+
+
+
+
+

Unauthorized

+

Maybe you had the wrong code?

+
+
+
+ + + + diff --git a/html/verify.html b/html/verify.html new file mode 100644 index 0000000..c01bdc4 --- /dev/null +++ b/html/verify.html @@ -0,0 +1,34 @@ + + + + + + My Luggage Info + + + + +
+ icon +

My Luggage Info

+
+
+
+
+
+ + + +
+
+ +
+
+
+ + + + + diff --git a/main.go b/main.go index 29f9308..58053a7 100644 --- a/main.go +++ b/main.go @@ -11,28 +11,30 @@ import ( "github.com/gin-gonic/gin" "log" "net/http" + "os" "strings" "time" ) type Address struct { - Id int64 `json:"address_id"` + Id int64 `json:"-"` 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"` + CreatedAt time.Time `json:"-"` + UpdatedAt time.Time `json:"-"` } type User struct { - Status int32 `json:"status"` - Id int64 `json:"user_id"` - UserName string `json:"user_name"` - SecretCodes string `json:"secret_codes"` - CurrentToken *string + Status int32 `json:"status"` + Id int64 `json:"-"` + UserName string `json:"user_name"` + ContactName string `json:"contact_name"` + SecretCodes string `json:"-"` + CurrentToken *string `json:"-"` ContactNumber string `json:"contact_number"` EmailAddress string `json:"email_address"` MailingAddress Address `json:"mailing_address"` @@ -87,9 +89,11 @@ func main() { go startCleanupLoop(ctx, db.db, time.Hour, 24*time.Hour) r := gin.Default() + gin.SetMode(gin.ReleaseMode) + allowedOrigins := strings.Split(os.Getenv("CORS_ALLOWED_ORIGINS"), ",") r.Use(cors.New(cors.Config{ - AllowOrigins: []string{"http://localhost:8080", "http://localhost:8000"}, + AllowOrigins: allowedOrigins, AllowMethods: []string{"GET", "POST"}, AllowHeaders: []string{"Origin", "Content-Type", "Authorization"}, ExposeHeaders: []string{"Content-Length", "Authorization"}, @@ -98,25 +102,47 @@ func main() { r.Static("/static", "./static") r.StaticFile("/favicon.ico", "./static/favicon.ico") + r.LoadHTMLGlob("./templates/*") r.GET("/", func(c *gin.Context) { - c.File("./static/index.html") + c.File("./html/home.html") + }) + r.GET("/ping", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{ + "message": "pong", + }) }) api := r.Group("/api") - api.GET("/luggage/:user", func(c *gin.Context) { + api.GET("/u/: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", - }) + if user.CurrentToken == nil { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ + "status": 401, + "error": "Unauthorized", + }) + } else if strings.Compare(c.Query("token"), *user.CurrentToken) == 0 { + if err != nil { + if errors.Is(err, NoEntriesFoundError) { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ + "status": 401, + "error": "Unauthorized", + }) + } else { + fmt.Printf("Error: %s\n", err) + c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{ + "status": 500, + "error": err.Error(), + }) + } } else { - fmt.Printf("Error: %s\n", err) - c.AbortWithStatus(http.StatusBadRequest) + c.JSON(http.StatusOK, user) } } else { - c.JSON(http.StatusOK, user) + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ + "status": 401, + "error": "Unauthorized", + }) } }) api.GET("/verify/:user", func(c *gin.Context) { @@ -185,38 +211,49 @@ func main() { } } }) - 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 := r.Group("/u") + user.GET("/:user", func(c *gin.Context) { + c.File("./html/verify.html") + }) + user.GET("/: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", - }) + c.Status(http.StatusUnauthorized) + c.File("./html/unauthorized.html") } 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", - }) + c.Status(http.StatusUnauthorized) + c.File("./html/unauthorized.html") } else { fmt.Printf("Error: %s\n", err) - c.AbortWithStatus(http.StatusBadRequest) + c.HTML(http.StatusInternalServerError, "error.html.tmpl", gin.H{ + "error": err.Error(), + }) } } else { - c.JSON(http.StatusOK, user) + addressStr := fmt.Sprintf("%s", user.MailingAddress.Street1) + if user.MailingAddress.Street2 == nil { + addressStr = fmt.Sprintf("%s, %s", addressStr, user.MailingAddress.City) + } else { + addressStr = fmt.Sprintf("%s, %s, %s", addressStr, *user.MailingAddress.Street2, user.MailingAddress.City) + } + if user.MailingAddress.State == nil { + addressStr = fmt.Sprintf("%s, %s, %s", addressStr, user.MailingAddress.PostalCode, user.MailingAddress.Country) + } else { + addressStr = fmt.Sprintf("%s, %s, %s, %s", addressStr, *user.MailingAddress.State, user.MailingAddress.PostalCode, user.MailingAddress.Country) + } + c.HTML(http.StatusOK, "info.html.tmpl", gin.H{ + "contact_name": user.ContactName, + "phone_number": user.ContactNumber, + "email_address": user.EmailAddress, + "address": addressStr, + }) } } else { - c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ - "status": 403, - "user": "", - "error": "Unauthorized", - }) + c.Status(http.StatusUnauthorized) + c.File("./html/unauthorized.html") } }) diff --git a/renderHtml.bat b/renderHtml.bat new file mode 100644 index 0000000..98df8c8 --- /dev/null +++ b/renderHtml.bat @@ -0,0 +1,17 @@ +@echo off +title Render HTML from PHP + +del .\html\* +del .\static\style.css +del .\static\verify.js +del .\templates\* + +curl -o ./html/home.html http://localhost:8080/home.php +curl -o ./html/unauthorized.html http://localhost:8080/unauthorized.php +curl -o ./html/verify.html http://localhost:8080/verify.php + +curl -o ./templates/info.html.tmpl http://localhost:8080/info.php +curl -o ./templates/error.html.tmpl http://localhost:8080/error.php + +curl -o ./static/verify.js http://localhost:8080/static/verify.js +curl -o ./static/style.css http://localhost:8080/static/style.css diff --git a/static/style.css b/static/style.css index bc0b40c..fd60afd 100644 --- a/static/style.css +++ b/static/style.css @@ -1,5 +1,6 @@ :root { --dark: #1f363dff; + --dark-click: #152429; --mid-dark: #40798cff; --mid: #70a9a1ff; --mid-light: #9ec1a3ff; @@ -7,43 +8,152 @@ font-family: Verdana,sans-serif; } -body { - margin: 0; +@media only screen and (min-width: 300px) { + body { + margin: 0; + height: 92vh; + display: flex; + flex-direction: column; + } + + .header { + display: flex; + flex: 0 0 auto; + align-items: center; + justify-content: space-between; + background-color: var(--mid-dark); + padding: 0 1rem; + height: 120px; + } + + .spacer { + flex: 0 0 72px; + } + + .header-icon { + display: block; + flex: 0 0 72px; + width: 20vw; + height: auto; + max-height: 85px; + } + + .footer { + background-color: var(--dark); + display: flex; + flex: 0 0 auto; + height: 120px; + } + + .content { + display: flex; + width: 85vw; + max-width: 800px; + padding: 1.5rem; + background: white; + border-radius: 5px; + text-align: center; + } + + .content input { + font-size: 16px; + } + + .content button { + color: var(--dark); + background-color: var(--light); + border-radius: 8px; + border: 2px solid; + border-color: var(--dark); + padding: 8px 12px; + text-align: center; + text-decoration: none; + font-size: 16px; + cursor: pointer; + } + + .align-left { + align-items: flex-start; + margin-left: 5vw; + } } -.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; -} +@media only screen and (min-width: 700px) { + body { + margin: 0; + height: 100vh; + display: flex; + flex-direction: column; + } -.spacer { - flex: 0 0 64px; /* or your icon’s width */ -} + .header { + display: flex; + flex: 0 0 auto; + align-items: center; + justify-content: space-between; + background-color: var(--mid-dark); + padding: 0 1rem; + height: 80px; + } -.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 */ + .spacer { + flex: 0 0 64px; + } + + .header-icon { + display: block; + flex: 0 0 64px; + width: 15vw; + height: auto; + max-height: 80px; + } + + .footer { + background-color: var(--dark); + display: flex; + flex: 0 0 auto; + height: 80px; + } + + .content { + display: flex; + width: 90vw; + max-width: 800px; + padding: 1.5rem; + background: white; + border-radius: 5px; + text-align: center; + } + + .content input { + font-size: 16px; + } + + .content button { + color: var(--dark); + background-color: var(--light); + border-radius: 8px; + border: 2px solid; + border-color: var(--dark); + padding: 8px 12px; + text-align: center; + text-decoration: none; + font-size: 16px; + cursor: pointer; + } + + .align-left { + align-items: flex-start; + margin-left: 5vw; + } } .title { - flex: 1; /* take up all the space between */ - text-align: center; /* center text within that space */ + flex: 1; + text-align: center; color: var(--light); } -.footer { - background-color: var(--dark); - display: flex; - height: 80px; -} - .footer-text { display: inherit; align-content: center; @@ -78,7 +188,11 @@ body { } .container { - padding: 20vh 0 calc(80vh - 303px) 0; + flex: 1 0 auto; + display: flex; + justify-content: center; + align-items: center; + padding: 0; background-image: linear-gradient( to bottom right, var(--mid-light), @@ -86,13 +200,8 @@ body { ); } -.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 h1 { + text-align: center; } .content h3 { @@ -101,4 +210,38 @@ body { .content p { text-align: center; +} + +.content button:hover { + color: var(--light); + background-color: var(--dark); +} + +.content button:active { + background-color: var(--dark-click); +} + +.flex-vertical { + flex-direction: column; +} + +.flex-horizontal { + flex-direction: row; +} + +.hidden { + display: none; +} + +.section-one { + flex: 0 0 auto; +} + +.section-two { + display: flex; + flex: 1 0 auto; +} + +#status { + margin-bottom: 0; } \ No newline at end of file diff --git a/static/verify.js b/static/verify.js new file mode 100644 index 0000000..40924ad --- /dev/null +++ b/static/verify.js @@ -0,0 +1,32 @@ +document.getElementById("submitBtn").addEventListener('click', function(e) { + let code = document.getElementById("code").value; + console.log("Clicked: " + code); + + const path = window.location.pathname; + const parts = path.split("/"); + const user = parts.pop(); + + fetch("/api/verify/" + user, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': "Basic " + code, + } + }).then(response => response.json()) + .then(data => { + let statusText = document.getElementById("status"); + console.log(data); + statusText.classList.remove("hidden"); + if (data.status === 404) { + // Not found + statusText.innerText = "User with that code not found"; + } else if (data.status === 200) { + // Display found and redirect to baseUrl/u/user/info with auth token + statusText.innerText = "User found, redirecting..."; + window.location.replace("/u/" + user + "/info?token=" + data.token); + } else { + // Error + statusText.innerText = "Error, please send this to Steven to be fixed. Error: " + data.error; + } + }) +}) \ No newline at end of file diff --git a/templates/error.html.tmpl b/templates/error.html.tmpl new file mode 100644 index 0000000..2b971d9 --- /dev/null +++ b/templates/error.html.tmpl @@ -0,0 +1,30 @@ + + + + + + My Luggage Info + + + + +
+ icon +

My Luggage Info

+
+
+
+
+
+

Error

+

Please report the following to Steven

+

{{ .error }}

+
+
+
+ + + + diff --git a/templates/info.html.tmpl b/templates/info.html.tmpl new file mode 100644 index 0000000..18965df --- /dev/null +++ b/templates/info.html.tmpl @@ -0,0 +1,34 @@ + + + + + + My Luggage Info + + + + +
+ icon +

My Luggage Info

+
+
+
+
+
+

Owner Information

+
+
+

Contact Name: {{ .contact_name }}

+

Phone Number: {{ .phone_number }}

+

Email Address: {{ .email_address }}

+

Address: {{ .address }}

+
+
+
+ + + + \ No newline at end of file