Backing up my work
This commit is contained in:
parent
521ce7d8af
commit
ae4044e886
8
.idea/.gitignore
vendored
Normal file
8
.idea/.gitignore
vendored
Normal file
@ -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
|
9
.idea/LuggageLocatorBackend.iml
Normal file
9
.idea/LuggageLocatorBackend.iml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="WEB_MODULE" version="4">
|
||||||
|
<component name="Go" enabled="true" />
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
12
.idea/dataSources.xml
Normal file
12
.idea/dataSources.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
|
||||||
|
<data-source source="LOCAL" name="LuggageDB" uuid="a44d7006-355f-4587-b552-8545e1268664">
|
||||||
|
<driver-ref>mariadb</driver-ref>
|
||||||
|
<synchronize>true</synchronize>
|
||||||
|
<jdbc-driver>org.mariadb.jdbc.Driver</jdbc-driver>
|
||||||
|
<jdbc-url>jdbc:mariadb://mariadb.traceyclan.us:3306/luggage</jdbc-url>
|
||||||
|
<working-dir>$ProjectFileDir$</working-dir>
|
||||||
|
</data-source>
|
||||||
|
</component>
|
||||||
|
</project>
|
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/LuggageLocatorBackend.iml" filepath="$PROJECT_DIR$/.idea/LuggageLocatorBackend.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
7
.idea/sqldialects.xml
Normal file
7
.idea/sqldialects.xml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="SqlDialectMappings">
|
||||||
|
<file url="file://$PROJECT_DIR$/formatDB.sql" dialect="MariaDB" />
|
||||||
|
<file url="PROJECT" dialect="MariaDB" />
|
||||||
|
</component>
|
||||||
|
</project>
|
11
README.md
11
README.md
@ -1,2 +1,13 @@
|
|||||||
# LuggageTracker
|
# 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
|
145
db.go
Normal file
145
db.go
Normal file
@ -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
|
||||||
|
}
|
25
formatDB.sql
Normal file
25
formatDB.sql
Normal file
@ -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)
|
||||||
|
);
|
39
go.mod
Normal file
39
go.mod
Normal file
@ -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
|
||||||
|
)
|
96
go.sum
Normal file
96
go.sum
Normal file
@ -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=
|
227
main.go
Normal file
227
main.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
BIN
static/favicon.ico
Normal file
BIN
static/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 277 KiB |
BIN
static/icon.png
Normal file
BIN
static/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 228 KiB |
29
static/index.html
Normal file
29
static/index.html
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>Luggage Tracker</title>
|
||||||
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<header class="header">
|
||||||
|
<img class="header-icon" src="/static/icon.png" alt="icon"/>
|
||||||
|
<h1 class="title">Luggage Tracker</h1>
|
||||||
|
<div class="spacer"></div>
|
||||||
|
</header>
|
||||||
|
<div class="container">
|
||||||
|
<div class="content">
|
||||||
|
<h3>Registration</h3>
|
||||||
|
<p>Contact Steven Tracey
|
||||||
|
<a class="contact-link" href="mailto:steven@nevets.tech?subject=QR%20Generator%20-%20(Your%20Name)&body=Name%3A%0APhone%20Number%3A%0AEmail%20Address%3A%0AMailing%20Address%3A%0AStreet%3A%0ACity%3A%0AZip%3A%0AState%3A%0ACountry%3A">via email</a>
|
||||||
|
to get your QR code</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
<p class="footer-text">Made hastily by <a class="footer-text" href="https://www.linkedin.com/in/steven-tracey18/">Steven Tracey</a></p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
104
static/style.css
Normal file
104
static/style.css
Normal file
@ -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;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user