feat: folder structure change and initial oauth flow

This commit is contained in:
Tine 2024-02-11 20:28:00 +01:00
parent 4984ed2250
commit 92453200ce
Signed by: mentos1386
SSH key fingerprint: SHA256:MNtTsLbihYaWF8j1fkOHfkKNlnN1JQfxEU/rBU8nCGw
25 changed files with 225 additions and 40 deletions

5
.gitignore vendored
View file

@ -1,4 +1,7 @@
dist/
# Database
gorm.db
zdravko.db
# Config
.env

View file

@ -20,6 +20,9 @@ Demo is available at https://zdravko.fly.dev.
* [justfile](https://github.com/casey/just)
```sh
# Configure
cp example.env .env
# Start development environment
just run
```

View file

@ -3,37 +3,53 @@ package main
import (
"log"
"net/http"
"os"
"github.com/gorilla/mux"
"code.tjo.space/mentos1386/zdravko/internal"
"code.tjo.space/mentos1386/zdravko/internal/pages"
"code.tjo.space/mentos1386/zdravko/internal/static"
"code.tjo.space/mentos1386/zdravko/internal/handlers"
"code.tjo.space/mentos1386/zdravko/web/static"
)
func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8000"
}
config := internal.NewConfig()
r := mux.NewRouter()
db, err := internal.ConnectToDatabase()
db, query, err := internal.ConnectToDatabase(config.SQLITE_DB_PATH)
if err != nil {
log.Fatal(err)
}
log.Println("Connected to database")
page := pages.NewPageHandler(db)
h := handlers.NewBaseHandler(db, query, config)
// Health
r.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
d, err := db.DB()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
err = d.Ping()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
_, err = w.Write([]byte("OK"))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
})
// Server static files
r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.FS(static.Static))))
r.HandleFunc("/", page.Index).Methods("GET")
r.HandleFunc("/settings", page.Settings).Methods("GET")
r.HandleFunc("/", h.Index).Methods("GET")
r.HandleFunc("/settings", h.Settings).Methods("GET")
log.Println("Server started on", port)
log.Fatal(http.ListenAndServe(":"+port, r))
// OAuth2
r.HandleFunc("/oauth2/login", h.OAuth2LoginGET).Methods("GET")
r.HandleFunc("/oauth2/callback", h.OAuth2CallbackGET).Methods("GET")
log.Println("Server started on", config.PORT)
log.Fatal(http.ListenAndServe(":"+config.PORT, r))
}

18
example.env Normal file
View file

@ -0,0 +1,18 @@
# PORT
PORT=8000
ROOT_URL=http://localhost:8000
# SQLite
SQLITE_DB_PATH=zdravko.db
# Session
SESSION_SECRET=your_secret
# OAUTH2
# The redirect/callback url is ${ROOT_URL}/auth/callback
OAUTH2_CLIENT_ID=your_client_id
OAUTH2_CLIENT_SECRET=your_client_secret
OAUTH2_SCOPES=openid,profile,email
OAUTH2_ENDPOINT_TOKEN_URL=https://your_oauth2_provider/token
OAUTH2_ENDPOINT_AUTH_URL=https://your_oauth2_provider/auth
OAUTH2_ENDPOINT_USER_INFO_URL=https://your_oauth2_provider/userinfo

View file

@ -9,6 +9,13 @@ primary_region = 'waw'
[env]
PORT = '8080'
ROOT_URL = 'https://zdravko.fly.dev'
SQLITE_DB_PATH = 'zdravko.db'
# Other are defined in secrets
OAUTH2_SCOPES = 'openid,profile,email'
OAUTH2_ENDPOINT_TOKEN_URL = 'https://id.tjo.space/application/o/token/'
OAUTH2_ENDPOINT_AUTH_URL = 'https://id.tjo.space/application/o/authorize/'
OAUTH2_ENDPOINT_USER_INFO_URL = 'https://id.tjo.space/application/o/userinfo/'
[processes]
server = "server"

4
go.mod
View file

@ -38,11 +38,13 @@ require (
go.temporal.io/api v1.24.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
golang.org/x/mod v0.15.0 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/net v0.21.0 // indirect
golang.org/x/oauth2 v0.17.0 // indirect
golang.org/x/sys v0.17.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.17.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20230815205213-6bfd019c3878 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230815205213-6bfd019c3878 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230815205213-6bfd019c3878 // indirect

6
go.sum
View file

@ -1130,6 +1130,7 @@ golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -1253,6 +1254,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -1283,6 +1286,8 @@ golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I
golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw=
golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4=
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
golang.org/x/oauth2 v0.17.0 h1:6m3ZPmLEFdVxKKWnKq4VqZ60gutO35zm+zrAHVmHyDQ=
golang.org/x/oauth2 v0.17.0/go.mod h1:OzPDGQiuQMguemayvdylqddI7qcD9lnSDb+1FiwQ5HA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -1572,6 +1577,7 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=

53
internal/config.go Normal file
View file

@ -0,0 +1,53 @@
package internal
import (
"os"
"strings"
)
type Config struct {
PORT string
ROOT_URL string // Needed for oauth2 redirect
SQLITE_DB_PATH string
SESSION_SECRET string
OAUTH2_CLIENT_ID string
OAUTH2_CLIENT_SECRET string
OAUTH2_SCOPES []string
OAUTH2_ENDPOINT_TOKEN_URL string
OAUTH2_ENDPOINT_AUTH_URL string
OAUTH2_ENDPOINT_USER_INFO_URL string
}
func getEnv(key, fallback string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
return fallback
}
func getEnvRequired(key string) string {
if value, ok := os.LookupEnv(key); ok {
return value
}
panic("Environment variable " + key + " is required")
}
func NewConfig() *Config {
return &Config{
PORT: getEnv("PORT", "8000"),
ROOT_URL: getEnvRequired("ROOT_URL"),
SQLITE_DB_PATH: getEnv("SQLITE_DB_PATH", "zdravko.db"),
SESSION_SECRET: getEnvRequired("SESSION_SECRET"),
OAUTH2_CLIENT_ID: getEnvRequired("OAUTH2_CLIENT_ID"),
OAUTH2_CLIENT_SECRET: getEnvRequired("OAUTH2_CLIENT_SECRET"),
OAUTH2_SCOPES: strings.Split(getEnvRequired("OAUTH2_SCOPES"), ","),
OAUTH2_ENDPOINT_TOKEN_URL: getEnvRequired("OAUTH2_ENDPOINT_TOKEN_URL"),
OAUTH2_ENDPOINT_AUTH_URL: getEnvRequired("OAUTH2_ENDPOINT_AUTH_URL"),
OAUTH2_ENDPOINT_USER_INFO_URL: getEnvRequired("OAUTH2_ENDPOINT_USER_INFO_URL"),
}
}

View file

@ -1,16 +1,22 @@
package internal
import (
"code.tjo.space/mentos1386/zdravko/internal/models"
"code.tjo.space/mentos1386/zdravko/internal/models/query"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
)
//go:generate just _generate-gorm
func ConnectToDatabase() (*gorm.DB, error) {
db, err := gorm.Open(sqlite.Open("gorm.db"), &gorm.Config{})
func ConnectToDatabase(path string) (*gorm.DB, *query.Query, error) {
db, err := gorm.Open(sqlite.Open(path), &gorm.Config{})
if err != nil {
return nil, err
return nil, nil, err
}
return db, nil
db.AutoMigrate(&models.Healthcheck{})
q := query.Use(db)
return db, q, nil
}

View file

@ -0,0 +1,17 @@
package handlers
import (
"code.tjo.space/mentos1386/zdravko/internal"
"code.tjo.space/mentos1386/zdravko/internal/models/query"
"gorm.io/gorm"
)
type BaseHandler struct {
db *gorm.DB
query *query.Query
config *internal.Config
}
func NewBaseHandler(db *gorm.DB, q *query.Query, config *internal.Config) *BaseHandler {
return &BaseHandler{db, q, config}
}

View file

@ -1,14 +1,14 @@
package pages
package handlers
import (
"net/http"
"text/template"
"code.tjo.space/mentos1386/zdravko/internal/ui"
"code.tjo.space/mentos1386/zdravko/web/templates"
)
func (p *PageHandler) Index(w http.ResponseWriter, r *http.Request) {
ts, err := template.ParseFS(ui.Templates,
func (h *BaseHandler) Index(w http.ResponseWriter, r *http.Request) {
ts, err := template.ParseFS(templates.Templates,
"components/base.tmpl",
"pages/index.tmpl",
)

View file

@ -0,0 +1,63 @@
package handlers
import (
"context"
"io"
"net/http"
"code.tjo.space/mentos1386/zdravko/internal"
"golang.org/x/oauth2"
)
func newOAuth2(config *internal.Config) *oauth2.Config {
return &oauth2.Config{
ClientID: config.OAUTH2_CLIENT_ID,
ClientSecret: config.OAUTH2_CLIENT_SECRET,
Scopes: config.OAUTH2_SCOPES,
RedirectURL: config.ROOT_URL + "/oauth2/callback",
Endpoint: oauth2.Endpoint{
TokenURL: config.OAUTH2_ENDPOINT_TOKEN_URL,
AuthURL: config.OAUTH2_ENDPOINT_AUTH_URL,
},
}
}
func (h *BaseHandler) OAuth2LoginGET(w http.ResponseWriter, r *http.Request) {
conf := newOAuth2(h.config)
url := conf.AuthCodeURL("state", oauth2.AccessTypeOffline)
http.Redirect(w, r, url, http.StatusTemporaryRedirect)
}
func (h *BaseHandler) OAuth2CallbackGET(w http.ResponseWriter, r *http.Request) {
ctx := context.Background()
conf := newOAuth2(h.config)
// Exchange the code for a new token.
tok, err := conf.Exchange(r.Context(), r.URL.Query().Get("code"))
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
// Ge the user information.
client := oauth2.NewClient(ctx, oauth2.StaticTokenSource(tok))
resp, err := client.Get(h.config.OAUTH2_ENDPOINT_USER_INFO_URL)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
_, err = w.Write(body)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}

View file

@ -1,14 +1,14 @@
package pages
package handlers
import (
"net/http"
"text/template"
"code.tjo.space/mentos1386/zdravko/internal/ui"
"code.tjo.space/mentos1386/zdravko/web/templates"
)
func (p *PageHandler) Settings(w http.ResponseWriter, r *http.Request) {
ts, err := template.ParseFS(ui.Templates,
func (h *BaseHandler) Settings(w http.ResponseWriter, r *http.Request) {
ts, err := template.ParseFS(templates.Templates,
"components/base.tmpl",
"pages/settings.tmpl",
)

View file

@ -1,11 +0,0 @@
package pages
import "gorm.io/gorm"
type PageHandler struct {
db *gorm.DB
}
func NewPageHandler(db *gorm.DB) *PageHandler {
return &PageHandler{db}
}

View file

@ -1,5 +1,7 @@
# Always use devbox environment to run commands.
set shell := ["devbox", "run"]
# Load dotenv
set dotenv-load
STATIC_DIR := "./internal/static"
@ -45,4 +47,4 @@ _feather-icons-download:
curl -sLo {{STATIC_DIR}}/icons/feather-sprite.svg https://unpkg.com/feather-icons/dist/feather-sprite.svg
_generate-gorm:
go run cmd/generate/main.go
go run tools/generate/main.go

View file

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

View file

@ -1,4 +1,4 @@
package ui
package templates
import (
"embed"