mirror of
https://github.com/mentos1386/zdravko.git
synced 2024-11-21 15:26:29 +00:00
feat: folder structure change and initial oauth flow
This commit is contained in:
parent
4984ed2250
commit
92453200ce
25 changed files with 225 additions and 40 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -1,4 +1,7 @@
|
|||
dist/
|
||||
|
||||
# Database
|
||||
gorm.db
|
||||
zdravko.db
|
||||
|
||||
# Config
|
||||
.env
|
||||
|
|
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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
18
example.env
Normal 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
|
7
fly.toml
7
fly.toml
|
@ -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
4
go.mod
|
@ -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
6
go.sum
|
@ -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
53
internal/config.go
Normal 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"),
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
17
internal/handlers/handlers.go
Normal file
17
internal/handlers/handlers.go
Normal 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}
|
||||
}
|
|
@ -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",
|
||||
)
|
63
internal/handlers/oauth2.go
Normal file
63
internal/handlers/oauth2.go
Normal 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
|
||||
}
|
||||
|
||||
}
|
|
@ -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",
|
||||
)
|
|
@ -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}
|
||||
}
|
4
justfile
4
justfile
|
@ -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
|
||||
|
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
|
@ -1,4 +1,4 @@
|
|||
package ui
|
||||
package templates
|
||||
|
||||
import (
|
||||
"embed"
|
Loading…
Reference in a new issue