Added documentation to functions and basic JWT generation
This commit is contained in:
4
Makefile
4
Makefile
@@ -14,5 +14,9 @@ dev:
|
|||||||
air &\
|
air &\
|
||||||
tailwindcss -i ./static/css/input.css -o ./static/css/output.css --watch
|
tailwindcss -i ./static/css/input.css -o ./static/css/output.css --watch
|
||||||
|
|
||||||
|
test:
|
||||||
|
go mod tidy && \
|
||||||
|
go run . --port 3232 --test
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
go clean
|
go clean
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Check the value of "pagefrom" cookie, delete the cookie, and return the value
|
||||||
func CheckPageFrom(w http.ResponseWriter, r *http.Request) string {
|
func CheckPageFrom(w http.ResponseWriter, r *http.Request) string {
|
||||||
pageFromCookie, err := r.Cookie("pagefrom")
|
pageFromCookie, err := r.Cookie("pagefrom")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -17,12 +18,17 @@ func CheckPageFrom(w http.ResponseWriter, r *http.Request) string {
|
|||||||
return pageFrom
|
return pageFrom
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check the referer of the request, and if it matches the trustedHost, set
|
||||||
|
// the "pagefrom" cookie as the Path of the referer
|
||||||
func SetPageFrom(w http.ResponseWriter, r *http.Request, trustedHost string) {
|
func SetPageFrom(w http.ResponseWriter, r *http.Request, trustedHost string) {
|
||||||
referer := r.Referer()
|
referer := r.Referer()
|
||||||
parsedURL, err := url.Parse(referer)
|
parsedURL, err := url.Parse(referer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// NOTE: its possible this could cause an infinite redirect
|
||||||
|
// if that happens, will need to add a way to 'blacklist' certain paths
|
||||||
|
// from being set here
|
||||||
var pageFrom string
|
var pageFrom string
|
||||||
if parsedURL.Path == "" || parsedURL.Host != trustedHost {
|
if parsedURL.Path == "" || parsedURL.Host != trustedHost {
|
||||||
pageFrom = "/"
|
pageFrom = "/"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
_ "github.com/tursodatabase/libsql-client-go/libsql"
|
_ "github.com/tursodatabase/libsql-client-go/libsql"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Returns a database connection handle for the Turso DB
|
||||||
func ConnectToDatabase(primaryUrl *string, authToken *string) (*sql.DB, error) {
|
func ConnectToDatabase(primaryUrl *string, authToken *string) (*sql.DB, error) {
|
||||||
url := fmt.Sprintf("libsql://%s.turso.io?authToken=%s", *primaryUrl, *authToken)
|
url := fmt.Sprintf("libsql://%s.turso.io?authToken=%s", *primaryUrl, *authToken)
|
||||||
|
|
||||||
|
|||||||
12
db/users.go
12
db/users.go
@@ -9,12 +9,13 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID int
|
ID int // Integer ID (index primary key)
|
||||||
Username string
|
Username string // Username (unique)
|
||||||
Password_hash string
|
Password_hash string // Bcrypt password hash
|
||||||
Created_at int64
|
Created_at int64 // Epoch timestamp when the user was added to the database
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Uses bcrypt to set the users Password_hash from the given password
|
||||||
func (user *User) SetPassword(conn *sql.DB, password string) error {
|
func (user *User) SetPassword(conn *sql.DB, password string) error {
|
||||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -30,6 +31,7 @@ func (user *User) SetPassword(conn *sql.DB, password string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Uses bcrypt to check if the given password matches the users Password_hash
|
||||||
func (user *User) CheckPassword(password string) error {
|
func (user *User) CheckPassword(password string) error {
|
||||||
err := bcrypt.CompareHashAndPassword([]byte(user.Password_hash), []byte(password))
|
err := bcrypt.CompareHashAndPassword([]byte(user.Password_hash), []byte(password))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -38,6 +40,8 @@ func (user *User) CheckPassword(password string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Queries the database for a user matching the given username.
|
||||||
|
// Query is case insensitive
|
||||||
func GetUserFromUsername(conn *sql.DB, username string) (User, error) {
|
func GetUserFromUsername(conn *sql.DB, username string) (User, error) {
|
||||||
query := `SELECT id, username, password_hash, created_at FROM users
|
query := `SELECT id, username, password_hash, created_at FROM users
|
||||||
WHERE username = ? COLLATE NOCASE`
|
WHERE username = ? COLLATE NOCASE`
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -4,6 +4,7 @@ go 1.23.5
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/a-h/templ v0.3.833
|
github.com/a-h/templ v0.3.833
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d
|
github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -4,6 +4,8 @@ github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8
|
|||||||
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
|
github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
|
||||||
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
||||||
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||||
|
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"projectreshoot/view/page"
|
"projectreshoot/view/page"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Handles responses to the / path. Also serves a 404 Page for paths that
|
||||||
|
// don't have explicit handlers
|
||||||
func HandleRoot() http.Handler {
|
func HandleRoot() http.Handler {
|
||||||
return http.HandlerFunc(
|
return http.HandlerFunc(
|
||||||
func(w http.ResponseWriter, r *http.Request) {
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Validates the username matches a user in the database and the password
|
||||||
|
// is correct. Returns the corresponding user
|
||||||
func validateLogin(conn *sql.DB, r *http.Request) (db.User, error) {
|
func validateLogin(conn *sql.DB, r *http.Request) (db.User, error) {
|
||||||
formUsername := r.FormValue("username")
|
formUsername := r.FormValue("username")
|
||||||
formPassword := r.FormValue("password")
|
formPassword := r.FormValue("password")
|
||||||
@@ -29,6 +31,7 @@ func validateLogin(conn *sql.DB, r *http.Request) (db.User, error) {
|
|||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns result of the "Remember me?" checkbox as a boolean
|
||||||
func checkRememberMe(r *http.Request) bool {
|
func checkRememberMe(r *http.Request) bool {
|
||||||
rememberMe := r.FormValue("remember-me")
|
rememberMe := r.FormValue("remember-me")
|
||||||
if rememberMe == "on" {
|
if rememberMe == "on" {
|
||||||
@@ -38,7 +41,10 @@ func checkRememberMe(r *http.Request) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func HandleLoginRequest(conn *sql.DB) http.Handler {
|
// Handles an attempted login request. On success will return a HTMX redirect
|
||||||
|
// and on fail will return the login form again, passing the error to the
|
||||||
|
// template for user feedback
|
||||||
|
func HandleLoginRequest(conn *sql.DB, secretKey string) http.Handler {
|
||||||
return http.HandlerFunc(
|
return http.HandlerFunc(
|
||||||
func(w http.ResponseWriter, r *http.Request) {
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
r.ParseForm()
|
r.ParseForm()
|
||||||
@@ -62,6 +68,8 @@ func HandleLoginRequest(conn *sql.DB) http.Handler {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handles a request to view the login page. Will attempt to set "pagefrom"
|
||||||
|
// cookie so a successful login can redirect the user to the page they came
|
||||||
func HandleLoginPage(trustedHost string) http.Handler {
|
func HandleLoginPage(trustedHost string) http.Handler {
|
||||||
return http.HandlerFunc(
|
return http.HandlerFunc(
|
||||||
func(w http.ResponseWriter, r *http.Request) {
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/a-h/templ"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/a-h/templ"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Handler for static pages. Will render the given templ.Component to the
|
||||||
|
// http.ResponseWriter
|
||||||
func HandlePage(Page templ.Component) http.Handler {
|
func HandlePage(Page templ.Component) http.Handler {
|
||||||
return http.HandlerFunc(
|
return http.HandlerFunc(
|
||||||
func(w http.ResponseWriter, r *http.Request) {
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -5,26 +5,43 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Wrapper for default FileSystem
|
||||||
type justFilesFilesystem struct {
|
type justFilesFilesystem struct {
|
||||||
fs http.FileSystem
|
fs http.FileSystem
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wrapper for default File
|
||||||
type neuteredReaddirFile struct {
|
type neuteredReaddirFile struct {
|
||||||
http.File
|
http.File
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Modifies the behavior of FileSystem.Open to return the neutered version of File
|
||||||
func (fs justFilesFilesystem) Open(name string) (http.File, error) {
|
func (fs justFilesFilesystem) Open(name string) (http.File, error) {
|
||||||
f, err := fs.fs.Open(name)
|
f, err := fs.fs.Open(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the requested path is a directory
|
||||||
|
// and explicitly return an error to trigger a 404
|
||||||
|
fileInfo, err := f.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if fileInfo.IsDir() {
|
||||||
|
return nil, os.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
return neuteredReaddirFile{f}, nil
|
return neuteredReaddirFile{f}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Overrides the Readdir method of File to always return nil
|
||||||
func (f neuteredReaddirFile) Readdir(count int) ([]os.FileInfo, error) {
|
func (f neuteredReaddirFile) Readdir(count int) ([]os.FileInfo, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handles requests for static files, without allowing access to the
|
||||||
|
// directory viewer and returning 404 if an exact file is not found
|
||||||
func HandleStatic() http.Handler {
|
func HandleStatic() http.Handler {
|
||||||
return http.HandlerFunc(
|
return http.HandlerFunc(
|
||||||
func(w http.ResponseWriter, r *http.Request) {
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
43
jwt/createtoken.go
Normal file
43
jwt/createtoken.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package jwt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"projectreshoot/db"
|
||||||
|
"projectreshoot/server"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Generates an access token for the provided user, using the variables set
|
||||||
|
// in the config object
|
||||||
|
func GenerateAccessToken(
|
||||||
|
config *server.Config,
|
||||||
|
user *db.User,
|
||||||
|
fresh bool,
|
||||||
|
) (string, error) {
|
||||||
|
issuedAt := time.Now().Unix()
|
||||||
|
expiresAt := issuedAt + (config.AccessTokenExpiry * 60)
|
||||||
|
var freshExpiresAt int64
|
||||||
|
if fresh {
|
||||||
|
freshExpiresAt = issuedAt + (config.TokenFreshTime * 60)
|
||||||
|
} else {
|
||||||
|
freshExpiresAt = issuedAt
|
||||||
|
}
|
||||||
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256,
|
||||||
|
jwt.MapClaims{
|
||||||
|
"iss": config.TrustedHost,
|
||||||
|
"sub": user.ID,
|
||||||
|
"aud": config.TrustedHost,
|
||||||
|
"iat": issuedAt,
|
||||||
|
"exp": expiresAt,
|
||||||
|
"fresh": freshExpiresAt,
|
||||||
|
})
|
||||||
|
|
||||||
|
signedToken, err := token.SignedString([]byte(config.SecretKey))
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.Wrap(err, "token.SignedString")
|
||||||
|
}
|
||||||
|
return signedToken, nil
|
||||||
|
}
|
||||||
23
main.go
23
main.go
@@ -3,12 +3,14 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"embed"
|
"embed"
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -18,16 +20,17 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
func run(ctx context.Context, w io.Writer) error {
|
// Initializes and runs the server
|
||||||
|
func run(ctx context.Context, w io.Writer, args []string) error {
|
||||||
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
|
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
config, err := server.GetConfig()
|
config, err := server.GetConfig(args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "server.GetConfig")
|
return errors.Wrap(err, "server.GetConfig")
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, err := db.ConnectToDatabase(&config.TursoURL, &config.TursoToken)
|
conn, err := db.ConnectToDatabase(&config.TursoDBName, &config.TursoToken)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrap(err, "db.ConnectToDatabase")
|
return errors.Wrap(err, "db.ConnectToDatabase")
|
||||||
}
|
}
|
||||||
@@ -38,6 +41,12 @@ func run(ctx context.Context, w io.Writer) error {
|
|||||||
Handler: srv,
|
Handler: srv,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TEST: runs function for testing in dev if --test flag true
|
||||||
|
if args[1] == "true" {
|
||||||
|
test(config, conn, httpServer)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
fmt.Fprintf(w, "Listening on %s\n", httpServer.Addr)
|
fmt.Fprintf(w, "Listening on %s\n", httpServer.Addr)
|
||||||
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
@@ -65,9 +74,15 @@ func run(ctx context.Context, w io.Writer) error {
|
|||||||
//go:embed static/*
|
//go:embed static/*
|
||||||
var static embed.FS
|
var static embed.FS
|
||||||
|
|
||||||
|
// Start of runtime. Parse commandline arguments & flags, Initializes context
|
||||||
|
// and starts the server
|
||||||
func main() {
|
func main() {
|
||||||
|
port := flag.String("port", "", "Override port")
|
||||||
|
test := flag.Bool("test", false, "Run test function")
|
||||||
|
flag.Parse()
|
||||||
|
args := []string{*port, strconv.FormatBool(*test)}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
if err := run(ctx, os.Stdout); err != nil {
|
if err := run(ctx, os.Stdout, args); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,16 +6,19 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Wraps the http.ResponseWriter, adding a statusCode field
|
||||||
type wrappedWriter struct {
|
type wrappedWriter struct {
|
||||||
http.ResponseWriter
|
http.ResponseWriter
|
||||||
statusCode int
|
statusCode int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extends WriteHeader to the ResponseWriter to add the status code
|
||||||
func (w *wrappedWriter) WriteHeader(statusCode int) {
|
func (w *wrappedWriter) WriteHeader(statusCode int) {
|
||||||
w.ResponseWriter.WriteHeader(statusCode)
|
w.ResponseWriter.WriteHeader(statusCode)
|
||||||
w.statusCode = statusCode
|
w.statusCode = statusCode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Middleware to add logs to console with details of the request
|
||||||
func Logging(next http.Handler) http.Handler {
|
func Logging(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|||||||
63
server/config.go
Normal file
63
server/config.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Host string // Host to listen on
|
||||||
|
Port string // Port to listen on
|
||||||
|
TrustedHost string // Domain/Hostname to accept as trusted
|
||||||
|
TursoDBName string // DB Name for Turso DB/Branch
|
||||||
|
TursoToken string // Bearer token for Turso DB/Branch
|
||||||
|
SecretKey string // Secret key for signing tokens
|
||||||
|
AccessTokenExpiry int64 // Access token expiry in minutes
|
||||||
|
RefreshTokenExpiry int64 // Refresh token expiry in minutes
|
||||||
|
TokenFreshTime int64 // Time for tokens to stay fresh in minutes
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the application configuration and get a pointer to the Config object
|
||||||
|
func GetConfig(args []string) (*Config, error) {
|
||||||
|
err := godotenv.Load(".env")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(".env file not found.")
|
||||||
|
}
|
||||||
|
var port string
|
||||||
|
|
||||||
|
if args[0] != "" {
|
||||||
|
port = args[0]
|
||||||
|
} else {
|
||||||
|
port = GetEnvDefault("PORT", "3333")
|
||||||
|
}
|
||||||
|
|
||||||
|
config := &Config{
|
||||||
|
Host: GetEnvDefault("HOST", "127.0.0.1"),
|
||||||
|
Port: port,
|
||||||
|
TrustedHost: os.Getenv("TRUSTED_HOST"),
|
||||||
|
TursoDBName: os.Getenv("TURSO_DB_NAME"),
|
||||||
|
TursoToken: os.Getenv("TURSO_AUTH_TOKEN"),
|
||||||
|
SecretKey: os.Getenv("SECRET_KEY"),
|
||||||
|
AccessTokenExpiry: GetEnvInt64("ACCESS_TOKEN_EXPIRY", 5),
|
||||||
|
RefreshTokenExpiry: GetEnvInt64("REFRESH_TOKEN_EXPIRY", 1440), // defaults to 1 day
|
||||||
|
TokenFreshTime: GetEnvInt64("TOKEN_FRESH_TIME", 5),
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.TrustedHost == "" {
|
||||||
|
return nil, errors.New("Envar not set: TRUSTED_HOST")
|
||||||
|
}
|
||||||
|
if config.TursoDBName == "" {
|
||||||
|
return nil, errors.New("Envar not set: TURSO_DB_NAME")
|
||||||
|
}
|
||||||
|
if config.TursoToken == "" {
|
||||||
|
return nil, errors.New("Envar not set: TURSO_AUTH_TOKEN")
|
||||||
|
}
|
||||||
|
if config.SecretKey == "" {
|
||||||
|
return nil, errors.New("Envar not set: SECRET_KEY")
|
||||||
|
}
|
||||||
|
|
||||||
|
return config, nil
|
||||||
|
}
|
||||||
31
server/environment.go
Normal file
31
server/environment.go
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get an environment variable, specifying a default value if its not set
|
||||||
|
func GetEnvDefault(key string, defaultValue string) string {
|
||||||
|
val, exists := os.LookupEnv(key)
|
||||||
|
if !exists {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get an environment variable as an int64, specifying a default value if its
|
||||||
|
// not set or can't be parsed properly into an int64
|
||||||
|
func GetEnvInt64(key string, defaultValue int64) int64 {
|
||||||
|
val, exists := os.LookupEnv(key)
|
||||||
|
if !exists {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
intVal, err := strconv.ParseInt(val, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
return intVal
|
||||||
|
|
||||||
|
}
|
||||||
@@ -3,10 +3,12 @@ package server
|
|||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"projectreshoot/handlers"
|
"projectreshoot/handlers"
|
||||||
"projectreshoot/view/page"
|
"projectreshoot/view/page"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Add all the handled routes to the mux
|
||||||
func addRoutes(
|
func addRoutes(
|
||||||
mux *http.ServeMux,
|
mux *http.ServeMux,
|
||||||
config *Config,
|
config *Config,
|
||||||
@@ -23,5 +25,5 @@ func addRoutes(
|
|||||||
|
|
||||||
// Login page and handlers
|
// Login page and handlers
|
||||||
mux.Handle("GET /login", handlers.HandleLoginPage(config.TrustedHost))
|
mux.Handle("GET /login", handlers.HandleLoginPage(config.TrustedHost))
|
||||||
mux.Handle("POST /login", handlers.HandleLoginRequest(conn))
|
mux.Handle("POST /login", handlers.HandleLoginRequest(conn, config.SecretKey))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,56 +2,12 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
|
|
||||||
"projectreshoot/middleware"
|
"projectreshoot/middleware"
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
// Returns a new http.Handler with all the routes and middleware added
|
||||||
Host string
|
|
||||||
Port string
|
|
||||||
TrustedHost string
|
|
||||||
TursoURL string
|
|
||||||
TursoToken string
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetConfig() (*Config, error) {
|
|
||||||
err := godotenv.Load(".env")
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(".env file not found.")
|
|
||||||
}
|
|
||||||
|
|
||||||
config := &Config{
|
|
||||||
Host: os.Getenv("HOST"),
|
|
||||||
Port: os.Getenv("PORT"),
|
|
||||||
TrustedHost: os.Getenv("TRUSTED_HOST"),
|
|
||||||
TursoURL: os.Getenv("TURSO_DATABASE_URL"),
|
|
||||||
TursoToken: os.Getenv("TURSO_AUTH_TOKEN"),
|
|
||||||
}
|
|
||||||
if config.Host == "" {
|
|
||||||
return nil, errors.New("Envar not set: HOST")
|
|
||||||
}
|
|
||||||
if config.Port == "" {
|
|
||||||
return nil, errors.New("Envar not set: PORT")
|
|
||||||
}
|
|
||||||
if config.TrustedHost == "" {
|
|
||||||
return nil, errors.New("Envar not set: TRUSTED_HOST")
|
|
||||||
}
|
|
||||||
if config.TursoURL == "" {
|
|
||||||
return nil, errors.New("Envar not set: TURSO_DATABASE_URL")
|
|
||||||
}
|
|
||||||
if config.TursoToken == "" {
|
|
||||||
return nil, errors.New("Envar not set: TURSO_AUTH_TOKEN")
|
|
||||||
}
|
|
||||||
|
|
||||||
return config, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewServer(config *Config, conn *sql.DB) http.Handler {
|
func NewServer(config *Config, conn *sql.DB) http.Handler {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
addRoutes(
|
addRoutes(
|
||||||
|
|||||||
15
tester.go
Normal file
15
tester.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"projectreshoot/server"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This function will only be called if the --test commandline flag is set.
|
||||||
|
// After the function finishes the application will close.
|
||||||
|
// Running command `make test` will run the test using port 3232 to avoid
|
||||||
|
// conflicts on the default 3333. Useful for testing things out during dev
|
||||||
|
func test(config *server.Config, conn *sql.DB, srv *http.Server) {
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ type FooterItem struct {
|
|||||||
href string
|
href string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Specify the links to show in the footer
|
||||||
func getFooterItems() []FooterItem {
|
func getFooterItems() []FooterItem {
|
||||||
return []FooterItem{
|
return []FooterItem{
|
||||||
{
|
{
|
||||||
@@ -18,6 +19,7 @@ func getFooterItems() []FooterItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns the template fragment for the Footer
|
||||||
templ Footer() {
|
templ Footer() {
|
||||||
<footer class="bg-mantle mt-10">
|
<footer class="bg-mantle mt-10">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ package form
|
|||||||
|
|
||||||
import "fmt"
|
import "fmt"
|
||||||
|
|
||||||
|
// Login Form. If loginError is not an empty string, it will display the
|
||||||
|
// contents of loginError to the user.
|
||||||
|
// If loginError is "Username or password incorrect" it will also show
|
||||||
|
// error icons on the username and password field
|
||||||
templ LoginForm(loginError string) {
|
templ LoginForm(loginError string) {
|
||||||
{{
|
{{
|
||||||
var errCreds string
|
var errCreds string
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
package nav
|
package nav
|
||||||
|
|
||||||
type NavItem struct {
|
type NavItem struct {
|
||||||
name string
|
name string // Label to display
|
||||||
href string
|
href string // Link reference
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Return the list of navbar links
|
||||||
func getNavItems() []NavItem {
|
func getNavItems() []NavItem {
|
||||||
return []NavItem{
|
return []NavItem{
|
||||||
{
|
{
|
||||||
@@ -14,6 +15,7 @@ func getNavItems() []NavItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns the navbar template fragment
|
||||||
templ Navbar() {
|
templ Navbar() {
|
||||||
{{ navItems := getNavItems() }}
|
{{ navItems := getNavItems() }}
|
||||||
<div x-data="{ open: false }">
|
<div x-data="{ open: false }">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package nav
|
package nav
|
||||||
|
|
||||||
|
// Returns the left portion of the navbar
|
||||||
templ navLeft(navItems []NavItem) {
|
templ navLeft(navItems []NavItem) {
|
||||||
<nav aria-label="Global" class="hidden sm:block">
|
<nav aria-label="Global" class="hidden sm:block">
|
||||||
<ul class="flex items-center gap-6 text-xl">
|
<ul class="flex items-center gap-6 text-xl">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package nav
|
package nav
|
||||||
|
|
||||||
|
// Returns the right portion of the navbar
|
||||||
templ navRight() {
|
templ navRight() {
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div class="sm:flex sm:gap-2">
|
<div class="sm:flex sm:gap-2">
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package nav
|
package nav
|
||||||
|
|
||||||
|
// Returns the mobile version of the navbar thats only visible when activated
|
||||||
templ sideNav(navItems []NavItem) {
|
templ sideNav(navItems []NavItem) {
|
||||||
<div
|
<div
|
||||||
x-show="open"
|
x-show="open"
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package layout
|
|||||||
import "projectreshoot/view/component/nav"
|
import "projectreshoot/view/component/nav"
|
||||||
import "projectreshoot/view/component/footer"
|
import "projectreshoot/view/component/footer"
|
||||||
|
|
||||||
|
// Global page layout. Includes HTML document settings, header tags
|
||||||
|
// navbar and footer
|
||||||
templ Global() {
|
templ Global() {
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html
|
<html
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package page
|
|||||||
|
|
||||||
import "projectreshoot/view/layout"
|
import "projectreshoot/view/layout"
|
||||||
|
|
||||||
|
// Returns the about page content
|
||||||
templ About() {
|
templ About() {
|
||||||
@layout.Global() {
|
@layout.Global() {
|
||||||
<div class="text-center max-w-150 m-auto">
|
<div class="text-center max-w-150 m-auto">
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ package page
|
|||||||
|
|
||||||
import "projectreshoot/view/layout"
|
import "projectreshoot/view/layout"
|
||||||
|
|
||||||
|
// Page template for Error pages. Error code should be a HTTP status code as
|
||||||
|
// a string, and err should be the corresponding response title.
|
||||||
|
// Message is a custom error message displayed below the code and error.
|
||||||
templ Error(code string, err string, message string) {
|
templ Error(code string, err string, message string) {
|
||||||
@layout.Global() {
|
@layout.Global() {
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package page
|
|||||||
|
|
||||||
import "projectreshoot/view/layout"
|
import "projectreshoot/view/layout"
|
||||||
|
|
||||||
|
// Page content for the index page
|
||||||
templ Index() {
|
templ Index() {
|
||||||
@layout.Global() {
|
@layout.Global() {
|
||||||
<div class="text-center mt-24">
|
<div class="text-center mt-24">
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package page
|
|||||||
import "projectreshoot/view/layout"
|
import "projectreshoot/view/layout"
|
||||||
import "projectreshoot/view/component/form"
|
import "projectreshoot/view/component/form"
|
||||||
|
|
||||||
|
// Returns the login page
|
||||||
templ Login() {
|
templ Login() {
|
||||||
@layout.Global() {
|
@layout.Global() {
|
||||||
<div class="max-w-100 mx-auto px-2">
|
<div class="max-w-100 mx-auto px-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user