Compare commits
6 Commits
578da90b55
...
v1-pre
| Author | SHA1 | Date | |
|---|---|---|---|
| f1dcc7acf4 | |||
| 000b16e487 | |||
| db57ae4447 | |||
| df4dbfdb1b | |||
| 635f2650e9 | |||
| 8b2d298488 |
25
.drone.yml
Normal file
25
.drone.yml
Normal file
@@ -0,0 +1,25 @@
|
||||
kind: pipeline
|
||||
name: test-docker-build
|
||||
|
||||
steps:
|
||||
- name: golangci-lint
|
||||
image: git.eeqj.de/sneak/drone-golangci-lint:2024-06-01
|
||||
- name: test-docker-build
|
||||
image: plugins/docker
|
||||
network_mode: bridge
|
||||
settings:
|
||||
repo: sneak/directory
|
||||
dry_run: true
|
||||
tags:
|
||||
- ${DRONE_COMMIT_SHA}
|
||||
- ${DRONE_BRANCH}
|
||||
- name: notify
|
||||
image: plugins/slack
|
||||
secrets: [SLACK_WEBHOOK]
|
||||
settings:
|
||||
webhook: $SLACK_WEBHOOK
|
||||
link_names: true
|
||||
when:
|
||||
status:
|
||||
- success
|
||||
- failure
|
||||
20
.gitea/workflows/demo.yaml
Normal file
20
.gitea/workflows/demo.yaml
Normal file
@@ -0,0 +1,20 @@
|
||||
name: Gitea Actions Demo
|
||||
run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
Explore-Gitea-Actions:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "🎉 The job was automatically triggered by a ${{ gitea.event_name }} event."
|
||||
- run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by Gitea!"
|
||||
- run: echo "🔎 The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
- run: echo "💡 The ${{ gitea.repository }} repository has been cloned to the runner."
|
||||
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
|
||||
- name: List files in the repository
|
||||
run: |
|
||||
ls ${{ gitea.workspace }}
|
||||
- run: echo "🍏 This job's status is ${{ job.status }}."
|
||||
- run: curl ipinfo.io
|
||||
16
Makefile
16
Makefile
@@ -13,28 +13,30 @@ commit: fmt lint
|
||||
git commit -a
|
||||
|
||||
fmt:
|
||||
gofumpt -l -w .
|
||||
golangci-lint run --fix
|
||||
|
||||
lint:
|
||||
golangci-lint run
|
||||
sh -c 'test -z "$$(gofmt -l .)"'
|
||||
|
||||
debug: ./$(FN)d
|
||||
debug: ./$(FN)
|
||||
DEBUG=1 GOTRACEBACK=all ./$(FN)d
|
||||
|
||||
debugger:
|
||||
cd cmd/$(FN)d && dlv debug main.go
|
||||
cd cmd/$(FN) && dlv debug main.go
|
||||
|
||||
run: ./$(FN)d
|
||||
./$(FN)d
|
||||
|
||||
clean:
|
||||
-rm -f ./$(FN)d debug.log
|
||||
-rm -f ./$(FN) debug.log
|
||||
|
||||
docker:
|
||||
docker build --progress plain .
|
||||
|
||||
./$(FN)d: cmd/$(FN)d/main.go internal/*/*.go assets/*/*
|
||||
cd ./cmd/$(FN)d && \
|
||||
go build -o ../../$(FN)d $(GOFLAGS) .
|
||||
./$(FN): cmd/$(FN)/main.go internal/*/*.go assets/*/*
|
||||
cd ./cmd/$(FN) && \
|
||||
go build -o ../../$(FN) $(GOFLAGS) .
|
||||
|
||||
./importer: cmd/importer/main.go internal/*/*.go
|
||||
go build -o ./importer $(GOFLAGS) ./cmd/importer
|
||||
|
||||
45
assets/embed.go
Normal file
45
assets/embed.go
Normal file
@@ -0,0 +1,45 @@
|
||||
package assets
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"net/http"
|
||||
|
||||
"github.com/CloudyKit/jet/v6"
|
||||
)
|
||||
|
||||
//go:embed templates/*.jet
|
||||
var templatesFS embed.FS
|
||||
|
||||
//go:embed static/*
|
||||
var staticFS embed.FS
|
||||
|
||||
var Views *jet.Set
|
||||
|
||||
func init() {
|
||||
loader := jet.NewInMemLoader()
|
||||
|
||||
// Load templates from embed.FS
|
||||
templateFiles, err := templatesFS.ReadDir("templates")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
for _, tmpl := range templateFiles {
|
||||
if tmpl.IsDir() {
|
||||
continue
|
||||
}
|
||||
tmplPath := "templates/" + tmpl.Name()
|
||||
tmplContent, err := templatesFS.ReadFile(tmplPath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
name := tmpl.Name()
|
||||
loader.Set(name, string(tmplContent))
|
||||
}
|
||||
|
||||
Views = jet.NewSet(loader)
|
||||
}
|
||||
|
||||
func StaticHandler() http.Handler {
|
||||
return http.FileServer(http.FS(staticFS))
|
||||
}
|
||||
1
assets/static/tailwind.v2.2.19.min.css
vendored
Normal file
1
assets/static/tailwind.v2.2.19.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
12
assets/templates/flashes.jet
Normal file
12
assets/templates/flashes.jet
Normal file
@@ -0,0 +1,12 @@
|
||||
{{ block flashes() }}
|
||||
{{ if len(Flashes) > 0 }}
|
||||
<div class="space-y-4 my-8 mx-auto max-w-2xl">
|
||||
{{ range Flashes }}
|
||||
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded shadow-lg relative" role="alert">
|
||||
<span class="block sm:inline">{{ . }}</span>
|
||||
</div>
|
||||
{{ end }}
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
||||
8
assets/templates/footer.jet
Normal file
8
assets/templates/footer.jet
Normal file
@@ -0,0 +1,8 @@
|
||||
{{ block footer() }}
|
||||
<footer>
|
||||
<p>Directory <a
|
||||
href="https://git.eeqj.de/sneak/directory/commit/{{version}}"><code>{{
|
||||
version }}</code></a></p>
|
||||
</footer>
|
||||
{{ end }}
|
||||
|
||||
12
assets/templates/html.jet
Normal file
12
assets/templates/html.jet
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ block title() }}Default Title{{ end }}</title>
|
||||
<link rel="stylesheet" href="/static/tailwind.v2.2.19.min.css">
|
||||
</head>
|
||||
<body>
|
||||
{{ block body() . }}{{ end }}
|
||||
</body>
|
||||
</html>
|
||||
13
assets/templates/index.jet
Normal file
13
assets/templates/index.jet
Normal file
@@ -0,0 +1,13 @@
|
||||
{{ extends "pagelayout.jet" }}
|
||||
|
||||
{{ block title() }}The Directory{{ end }}
|
||||
|
||||
{{ block pageContent() }}
|
||||
<h2>Directory</h2>
|
||||
|
||||
<h3>Tags</h3>
|
||||
<ul>
|
||||
<li><a href="/tags/tag1">Tag1</a></li>
|
||||
</ul>
|
||||
|
||||
{{ end }}
|
||||
29
assets/templates/login.jet
Normal file
29
assets/templates/login.jet
Normal file
@@ -0,0 +1,29 @@
|
||||
{{ extends "pagelayout.jet" }}
|
||||
|
||||
{{ block pageContent() }}
|
||||
<div class="flex items-center justify-center min-h-screen bg-gray-100">
|
||||
<div class="bg-white p-8 rounded-lg shadow-lg w-full max-w-md mx-4">
|
||||
<h2 class="text-2xl font-bold mb-6 text-center">Login</h2>
|
||||
<form action="/login" method="post">
|
||||
<div class="mb-4">
|
||||
<label for="username" class="block text-gray-700">Username:</label>
|
||||
<input type="text" id="username" name="username" required class="mt-1 w-full p-2 border border-gray-300 rounded">
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<label for="password" class="block text-gray-700">Password:</label>
|
||||
<input type="password" id="password" name="password" required class="mt-1 w-full p-2 border border-gray-300 rounded">
|
||||
</div>
|
||||
<div class="flex items-center justify-center">
|
||||
<input type="submit" value="Login" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded cursor-pointer">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.min-h-screen {
|
||||
min-height: calc(100vh - 4rem); /* Adjust based on navbar height */
|
||||
}
|
||||
</style>
|
||||
{{ end }}
|
||||
|
||||
39
assets/templates/navbar.jet
Normal file
39
assets/templates/navbar.jet
Normal file
@@ -0,0 +1,39 @@
|
||||
{{ block navbar() }}
|
||||
<nav class="bg-gray-100 border-b border-gray-300 fixed top-0 left-0 right-0 w-full z-50">
|
||||
<div class="container mx-auto px-4 flex justify-between items-center h-16">
|
||||
<ul class="flex space-x-4">
|
||||
<li><a href="/" class="text-black hover:text-blue-500">Home</a></li>
|
||||
<li><a href="/status" class="text-black hover:text-blue-500">Status</a></li>
|
||||
</ul>
|
||||
<div class="relative">
|
||||
<input type="checkbox" id="dropdown-toggle" class="hidden">
|
||||
<label for="dropdown-toggle" class="cursor-pointer p-2 border border-gray-300 rounded bg-white">☰</label>
|
||||
<ul class="absolute right-0 mt-2 w-48 bg-white border border-gray-300 rounded shadow-lg hidden">
|
||||
{{ if isset("LoggedInUser") && LoggedInUser != nil }}
|
||||
<li><a href="/users/{{LoggedInUser.ID}}" class="block px-4 py-2 text-black hover:bg-gray-100">Profile</a></li>
|
||||
<li><a href="/logout" class="block px-4 py-2 text-black hover:bg-gray-100">Logout</a></li>
|
||||
{{ if LoggedInUser.IsSuperAdmin() }}
|
||||
<li><a href="/users/new" class="block px-4 py-2 text-black hover:bg-gray-100">Create User</a></li>
|
||||
{{ end }}
|
||||
<li class="block px-4 py-2 bg-gray-100 border-t border-gray-300">Welcome,
|
||||
<a href="/users/{{LoggedInUser.ID}}"><code>{{ LoggedInUser.Username }}</code></a>
|
||||
</li>
|
||||
{{ else }}
|
||||
<li><a href="/login" class="block px-4 py-2 text-black hover:bg-gray-100">Login</a></li>
|
||||
{{ end }}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
body, html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
#dropdown-toggle:checked + label + ul {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
{{ end }}
|
||||
|
||||
25
assets/templates/pagelayout.jet
Normal file
25
assets/templates/pagelayout.jet
Normal file
@@ -0,0 +1,25 @@
|
||||
{{ extends "html.jet" }}
|
||||
|
||||
{{ block body() . }}
|
||||
<!-- pagelayout.jet -->
|
||||
{{ include "navbar.jet" . }}
|
||||
<div class="content">
|
||||
{{ include "flashes.jet" . }}
|
||||
{{ block pageContent() . }}default page content{{ end }}
|
||||
</div>
|
||||
{{ include "footer.jet" . }}
|
||||
<style>
|
||||
body, html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.content {
|
||||
margin-top: 4rem; /* Adjust this value based on your navbar height */
|
||||
padding: 1em 0; /* Only top and bottom padding */
|
||||
background-color: #f8f9fa; /* Ensure background color spans full width */
|
||||
width: 100%; /* Ensure the content spans full width */
|
||||
box-sizing: border-box; /* Include padding in the element's total width and height */
|
||||
}
|
||||
</style>
|
||||
{{ end }}
|
||||
|
||||
52
cmd/importer/main.go
Normal file
52
cmd/importer/main.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
|
||||
"go.uber.org/fx"
|
||||
"sneak.berlin/go/directory/internal/config"
|
||||
"sneak.berlin/go/directory/internal/database"
|
||||
"sneak.berlin/go/directory/internal/globals"
|
||||
"sneak.berlin/go/directory/internal/importer"
|
||||
"sneak.berlin/go/directory/internal/logger"
|
||||
"sneak.berlin/go/directory/internal/store"
|
||||
)
|
||||
|
||||
var (
|
||||
Appname string = "importer"
|
||||
Version string
|
||||
Buildarch string
|
||||
)
|
||||
|
||||
var Commit = func() string {
|
||||
if info, ok := debug.ReadBuildInfo(); ok {
|
||||
for _, setting := range info.Settings {
|
||||
if setting.Key == "vcs.revision" {
|
||||
return setting.Value
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}()
|
||||
|
||||
func main() {
|
||||
|
||||
fmt.Println("Commit: ", Commit)
|
||||
|
||||
globals.Appname = Appname
|
||||
globals.Version = Version
|
||||
globals.Buildarch = Buildarch
|
||||
|
||||
fx.New(
|
||||
fx.Provide(
|
||||
config.New,
|
||||
database.New,
|
||||
store.New,
|
||||
globals.New,
|
||||
logger.New,
|
||||
importer.New,
|
||||
),
|
||||
fx.Invoke(func(*importer.Importer) {}),
|
||||
).Run()
|
||||
}
|
||||
42
go.mod
42
go.mod
@@ -4,38 +4,61 @@ go 1.22.2
|
||||
|
||||
require (
|
||||
github.com/99designs/basicauth-go v0.0.0-20230316000542-bf6f9cbbf0f8
|
||||
github.com/CloudyKit/jet/v6 v6.2.0
|
||||
github.com/getsentry/sentry-go v0.28.0
|
||||
github.com/go-chi/chi v1.5.5
|
||||
github.com/go-chi/cors v1.2.1
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mmcdole/gofeed v1.3.0
|
||||
github.com/oklog/ulid/v2 v2.1.0
|
||||
github.com/prometheus/client_golang v1.19.1
|
||||
github.com/rs/zerolog v1.33.0
|
||||
github.com/schollz/progressbar/v3 v3.14.3
|
||||
github.com/slok/go-http-metrics v0.12.0
|
||||
github.com/spf13/cobra v1.8.0
|
||||
github.com/spf13/viper v1.18.2
|
||||
go.uber.org/fx v1.22.0
|
||||
golang.org/x/crypto v0.23.0
|
||||
gorm.io/driver/postgres v1.5.7
|
||||
gorm.io/driver/sqlite v1.5.5
|
||||
gorm.io/gorm v1.25.10
|
||||
sneak.berlin/go/util v0.0.0-20240601223751-9302c14a6cdc
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect
|
||||
github.com/PuerkitoBio/goquery v1.9.2 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/go-chi/chi/v5 v5.0.12 // indirect
|
||||
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 // indirect
|
||||
github.com/jackc/pgx/v5 v5.6.0 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/lib/pq v1.10.9 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.17 // indirect
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.1 // indirect
|
||||
github.com/mmcdole/goxpp v1.1.1 // 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.2 // indirect
|
||||
github.com/prometheus/client_model v0.6.1 // indirect
|
||||
github.com/prometheus/common v0.53.0 // indirect
|
||||
github.com/prometheus/procfs v0.13.0 // indirect
|
||||
github.com/prometheus/procfs v0.15.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
@@ -44,12 +67,15 @@ require (
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.uber.org/dig v1.17.1 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
go.uber.org/zap v1.26.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect
|
||||
golang.org/x/sys v0.19.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
google.golang.org/protobuf v1.33.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc // indirect
|
||||
golang.org/x/net v0.25.0 // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/term v0.20.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
google.golang.org/protobuf v1.34.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
128
go.sum
128
go.sum
@@ -1,5 +1,17 @@
|
||||
github.com/99designs/basicauth-go v0.0.0-20230316000542-bf6f9cbbf0f8 h1:nMpu1t4amK3vJWBibQ5X/Nv0aXL+b69TQf2uK5PH7Go=
|
||||
github.com/99designs/basicauth-go v0.0.0-20230316000542-bf6f9cbbf0f8/go.mod h1:3cARGAK9CfW3HoxCy1a0G4TKrdiKke8ftOMEOHyySYs=
|
||||
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c=
|
||||
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno=
|
||||
github.com/CloudyKit/jet/v6 v6.2.0 h1:EpcZ6SR9n28BUGtNJSvlBqf90IpjeFr36Tizxhn/oME=
|
||||
github.com/CloudyKit/jet/v6 v6.2.0/go.mod h1:d3ypHeIRNo2+XyqnGA8s+aphtcVpjP5hPwP/Lzo7Ro4=
|
||||
github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U=
|
||||
github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI=
|
||||
github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE=
|
||||
github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
|
||||
github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c=
|
||||
github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA=
|
||||
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
|
||||
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
@@ -7,6 +19,7 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
@@ -19,6 +32,8 @@ github.com/getsentry/sentry-go v0.28.0 h1:7Rqx9M3ythTKy2J6uZLHmc8Sz9OGgIlseuO1iB
|
||||
github.com/getsentry/sentry-go v0.28.0/go.mod h1:1fQZ+7l7eeJ3wYi82q5Hg8GqAPgefRq+FP/QhafYVgg=
|
||||
github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE=
|
||||
github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw=
|
||||
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
|
||||
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4=
|
||||
github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
@@ -26,22 +41,42 @@ github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3Bop
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/hako/durafmt v0.0.0-20191009132224-3f39dc1ed9f4 h1:60gBOooTSmNtrqNaRvrDbi8VAne0REaek2agjnITKSw=
|
||||
github.com/hako/durafmt v0.0.0-20191009132224-3f39dc1ed9f4/go.mod h1:5Scbynm8dF1XAPwIwkGPqzkM/shndPm79Jd1003hTjE=
|
||||
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b h1:wDUNC2eKiL35DbLvsDhiblTUXHxcOPwQSCzi7xpQUN4=
|
||||
github.com/hako/durafmt v0.0.0-20210608085754-5c1018a4e16b/go.mod h1:VzxiSdG6j1pi7rwGm/xYI5RbtpBgM8sARDXlvEvxlu0=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9 h1:L0QtFUgDarD7Fpv9jeVMgy/+Ec0mtnmYuImjTz6dtDA=
|
||||
github.com/jackc/pgservicefile v0.0.0-20231201235250-de7065d80cb9/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY=
|
||||
github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA=
|
||||
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
||||
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
||||
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
|
||||
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
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/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
@@ -52,10 +87,28 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM=
|
||||
github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mmcdole/gofeed v1.3.0 h1:5yn+HeqlcvjMeAI4gu6T+crm7d0anY85+M+v6fIFNG4=
|
||||
github.com/mmcdole/gofeed v1.3.0/go.mod h1:9TGv2LcJhdXePDzxiuMnukhV2/zb6VtnZt1mS+SjkLE=
|
||||
github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23 h1:Zr92CAlFhy2gL+V1F+EyIuzbQNbSgP4xhTODZtrXUtk=
|
||||
github.com/mmcdole/goxpp v1.1.1-0.20240225020742-a0c311522b23/go.mod h1:v+25+lT2ViuQ7mVxcncQ8ch1URund48oH+jhjiwEgS8=
|
||||
github.com/mmcdole/goxpp v1.1.1 h1:RGIX+D6iQRIunGHrKqnA2+700XMCnNv0bAOOv5MUhx8=
|
||||
github.com/mmcdole/goxpp v1.1.1/go.mod h1:v+25+lT2ViuQ7mVxcncQ8ch1URund48oH+jhjiwEgS8=
|
||||
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/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
|
||||
github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ=
|
||||
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
|
||||
github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg=
|
||||
github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
|
||||
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
@@ -71,15 +124,22 @@ github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+a
|
||||
github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U=
|
||||
github.com/prometheus/procfs v0.13.0 h1:GqzLlQyfsPbaEHaQkO7tbDlriv/4o5Hudv6OXHGKX7o=
|
||||
github.com/prometheus/procfs v0.13.0/go.mod h1:cd4PFCR54QLnGKPaKGA6l+cfuNXtht43ZKY6tow0Y1g=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||
github.com/rs/zerolog v1.33.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/schollz/progressbar/v3 v3.14.3 h1:oOuWW19ka12wxYU1XblR4n16wF/2Y1dBLMarMo6p4xU=
|
||||
github.com/schollz/progressbar/v3 v3.14.3/go.mod h1:aT3UQ7yGm+2ZjeXPqsjTenwL3ddUiuZ0kfQ/2tHlyNI=
|
||||
github.com/slok/go-http-metrics v0.12.0 h1:mAb7hrX4gB4ItU6NkFoKYdBslafg3o60/HbGBRsKaG8=
|
||||
github.com/slok/go-http-metrics v0.12.0/go.mod h1:Ee/mdT9BYvGrlGzlClkK05pP2hRHmVbRF9dtUVS8LNA=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
@@ -88,6 +148,8 @@ github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
|
||||
@@ -97,6 +159,8 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
@@ -104,27 +168,89 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.uber.org/dig v1.17.1 h1:Tga8Lz8PcYNsWsyHMZ1Vm0OQOUaJNDyvPImgbAu9YSc=
|
||||
go.uber.org/dig v1.17.1/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
|
||||
go.uber.org/fx v1.22.0 h1:pApUK7yL0OUHMd8vkunWSlLxZVFFk70jR2nKde8X2NM=
|
||||
go.uber.org/fx v1.22.0/go.mod h1:HT2M7d7RHo+ebKGh9NRcrsrHHfpZ60nW3QRubMRfv48=
|
||||
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
|
||||
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
|
||||
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY=
|
||||
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI=
|
||||
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc h1:O9NuF4s+E/PvMIy+9IUZB9znFwUIXEWSstNjek6VpVg=
|
||||
golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
|
||||
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg=
|
||||
google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||
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=
|
||||
@@ -133,6 +259,8 @@ gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
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=
|
||||
gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM=
|
||||
gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA=
|
||||
gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E=
|
||||
gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE=
|
||||
gorm.io/gorm v1.25.10 h1:dQpO+33KalOA+aFYGlK+EfxcI5MbO7EP2yYygwh9h+s=
|
||||
|
||||
@@ -26,7 +26,7 @@ type ConfigParams struct {
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
DBURL string
|
||||
DBDSN string
|
||||
Debug bool
|
||||
MaintenanceMode bool
|
||||
DevelopmentMode bool
|
||||
@@ -59,7 +59,7 @@ func New(lc fx.Lifecycle, params ConfigParams) (*Config, error) {
|
||||
viper.SetDefault("DEV_ADMIN_USERNAME", "")
|
||||
viper.SetDefault("DEV_ADMIN_PASSWORD", "")
|
||||
viper.SetDefault("PORT", "8080")
|
||||
viper.SetDefault("DBURL", "")
|
||||
viper.SetDefault("DB_DSN", "")
|
||||
viper.SetDefault("SENTRY_DSN", "")
|
||||
viper.SetDefault("METRICS_USERNAME", "")
|
||||
viper.SetDefault("METRICS_PASSWORD", "")
|
||||
@@ -76,7 +76,7 @@ func New(lc fx.Lifecycle, params ConfigParams) (*Config, error) {
|
||||
}
|
||||
|
||||
s := &Config{
|
||||
DBURL: viper.GetString("DBURL"),
|
||||
DBDSN: viper.GetString("DB_DSN"),
|
||||
Debug: viper.GetBool("debug"),
|
||||
Port: viper.GetInt("PORT"),
|
||||
SentryDSN: viper.GetString("SENTRY_DSN"),
|
||||
|
||||
@@ -2,28 +2,19 @@ package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"go.uber.org/fx"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/driver/postgres"
|
||||
"gorm.io/gorm"
|
||||
"sneak.berlin/go/directory/internal/config"
|
||||
"sneak.berlin/go/directory/internal/globals"
|
||||
"sneak.berlin/go/directory/internal/logger"
|
||||
"sneak.berlin/go/util"
|
||||
|
||||
// spooky action at a distance!
|
||||
// this populates the environment
|
||||
// from a ./.env file automatically
|
||||
// for development configuration.
|
||||
// .env contents should be things like
|
||||
// `DBURL=postgres://user:pass@.../`
|
||||
// (without the backticks, of course)
|
||||
_ "github.com/joho/godotenv/autoload"
|
||||
_ "github.com/lib/pq" // Import the PostgreSQL driver
|
||||
)
|
||||
|
||||
type DatabaseParams struct {
|
||||
@@ -39,8 +30,6 @@ type Database struct {
|
||||
params *DatabaseParams
|
||||
DB *gorm.DB
|
||||
SQLDB *sql.DB
|
||||
dbdir string
|
||||
dbfn string
|
||||
}
|
||||
|
||||
func New(lc fx.Lifecycle, params DatabaseParams) (*Database, error) {
|
||||
@@ -53,11 +42,21 @@ func New(lc fx.Lifecycle, params DatabaseParams) (*Database, error) {
|
||||
lc.Append(fx.Hook{
|
||||
OnStart: func(ctx context.Context) error {
|
||||
s.log.Info().Msg("Database OnStart Hook")
|
||||
// FIXME connect to db
|
||||
err := s.Connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
OnStop: func(ctx context.Context) error {
|
||||
// FIXME disconnect from db
|
||||
if s.SQLDB != nil {
|
||||
err := s.SQLDB.Close()
|
||||
if err != nil {
|
||||
s.log.Error().Err(err).Msg("failed to close database connection")
|
||||
return err
|
||||
}
|
||||
}
|
||||
s.log.Info().Msg("Database connection closed")
|
||||
return nil
|
||||
},
|
||||
})
|
||||
@@ -80,36 +79,48 @@ func (d *Database) Get() (*gorm.DB, error) {
|
||||
}
|
||||
|
||||
func (d *Database) Connect() error {
|
||||
// FIXME make this get the data dir path from config
|
||||
d.dbdir = os.Getenv("HOME")
|
||||
err := util.Mkdirp(d.dbdir)
|
||||
if err != nil {
|
||||
d.log.Error().
|
||||
Err(err).
|
||||
Str("dbdir", d.dbdir).
|
||||
Msg("unable to create directory")
|
||||
dsn := d.params.Config.DBDSN
|
||||
if dsn == "" {
|
||||
err := errors.New("database DSN is empty")
|
||||
d.log.Error().Err(err).Msg("failed to get database DSN from config")
|
||||
return err
|
||||
}
|
||||
d.dbfn = d.dbdir + "/" + d.params.Globals.Appname + ".db"
|
||||
d.log.Info().
|
||||
Str("file", d.dbfn).
|
||||
Msg("opening store db")
|
||||
|
||||
// Open the database using the pure Go SQLite driver
|
||||
sqlDB, err := sql.Open("sqlite", d.dbfn+"?_pragma=foreign_keys(1)&_pragma=journal_mode(WAL)")
|
||||
d.log.Info().
|
||||
Str("dsn", dsn).
|
||||
Msg("connecting to database")
|
||||
|
||||
sqlDB, err := sql.Open("postgres", dsn)
|
||||
if err != nil {
|
||||
d.log.Error().Err(err).Msg("failed to open database")
|
||||
return err
|
||||
}
|
||||
d.SQLDB = sqlDB
|
||||
|
||||
// Use the generic Gorm SQL driver to integrate it
|
||||
db, err := gorm.Open(sqlite.Dialector{Conn: d.SQLDB}, &gorm.Config{})
|
||||
// Ping the database to ensure the connection is valid
|
||||
err = d.Ping()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to connect database")
|
||||
d.log.Error().Err(err).Msg("failed to ping database")
|
||||
return err
|
||||
}
|
||||
|
||||
db, err := gorm.Open(postgres.New(postgres.Config{
|
||||
Conn: d.SQLDB,
|
||||
}), &gorm.Config{})
|
||||
if err != nil {
|
||||
d.log.Error().Err(err).Msg("failed to connect database")
|
||||
return err
|
||||
}
|
||||
d.DB = db
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Database) Ping() error {
|
||||
err := d.SQLDB.Ping()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.log.Info().Msg("successfully pinged the database")
|
||||
return nil
|
||||
}
|
||||
|
||||
40
internal/importer/cmd.go
Normal file
40
internal/importer/cmd.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package importer
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func setupCommands(i *Importer) *cobra.Command {
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "importer",
|
||||
Short: "Importer is a CLI for importing data into the directory",
|
||||
}
|
||||
|
||||
importCmd := &cobra.Command{
|
||||
Use: "import",
|
||||
Short: "Import data into the directory",
|
||||
}
|
||||
|
||||
importJSONCmd := &cobra.Command{
|
||||
Use: "json [file]",
|
||||
Short: "Import data from a JSON file",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
i.importFromJSON(args[0])
|
||||
},
|
||||
}
|
||||
|
||||
importOPMLCmd := &cobra.Command{
|
||||
Use: "opml [file]",
|
||||
Short: "Import data from an OPML file",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
i.importFromOPML(args[0])
|
||||
},
|
||||
}
|
||||
|
||||
importCmd.AddCommand(importJSONCmd, importOPMLCmd)
|
||||
rootCmd.AddCommand(importCmd)
|
||||
|
||||
return rootCmd
|
||||
}
|
||||
84
internal/importer/importer.go
Normal file
84
internal/importer/importer.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package importer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"go.uber.org/fx"
|
||||
"sneak.berlin/go/directory/internal/config"
|
||||
"sneak.berlin/go/directory/internal/globals"
|
||||
"sneak.berlin/go/directory/internal/logger"
|
||||
"sneak.berlin/go/directory/internal/store"
|
||||
)
|
||||
|
||||
type ImporterParams struct {
|
||||
fx.In
|
||||
Logger *logger.Logger
|
||||
Globals *globals.Globals
|
||||
Config *config.Config
|
||||
Store *store.Store
|
||||
}
|
||||
|
||||
type Importer struct {
|
||||
startupTime time.Time
|
||||
exitCode int
|
||||
log *zerolog.Logger
|
||||
params ImporterParams
|
||||
ctx context.Context
|
||||
cancelFunc context.CancelFunc
|
||||
}
|
||||
|
||||
func New(lc fx.Lifecycle, params ImporterParams) (*Importer, error) {
|
||||
i := new(Importer)
|
||||
i.params = params
|
||||
i.log = params.Logger.Get()
|
||||
|
||||
lc.Append(fx.Hook{
|
||||
OnStart: func(ctx context.Context) error {
|
||||
i.startupTime = time.Now()
|
||||
go i.Run(ctx)
|
||||
return nil
|
||||
},
|
||||
OnStop: func(ctx context.Context) error {
|
||||
i.cleanShutdown(ctx)
|
||||
return nil
|
||||
},
|
||||
})
|
||||
return i, nil
|
||||
}
|
||||
|
||||
func (i *Importer) Run(ctx context.Context) {
|
||||
i.ctx, i.cancelFunc = context.WithCancel(ctx)
|
||||
|
||||
rootCmd := setupCommands(i)
|
||||
|
||||
go func() {
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Ignore(syscall.SIGPIPE)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
sig := <-c
|
||||
i.log.Info().Msgf("signal received: %+v", sig)
|
||||
if i.cancelFunc != nil {
|
||||
i.cancelFunc()
|
||||
}
|
||||
}()
|
||||
|
||||
if err := rootCmd.ExecuteContext(i.ctx); err != nil {
|
||||
i.log.Error().Err(err).Msg("command execution failed")
|
||||
i.exitCode = 1
|
||||
}
|
||||
|
||||
<-i.ctx.Done()
|
||||
i.exitCode = 0
|
||||
i.cleanShutdown(ctx)
|
||||
}
|
||||
|
||||
func (i *Importer) cleanShutdown(ctx context.Context) {
|
||||
i.log.Info().Msgf("shutting down")
|
||||
os.Exit(i.exitCode)
|
||||
}
|
||||
|
||||
42
internal/importer/json.go
Normal file
42
internal/importer/json.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package importer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
func (i *Importer) importFromJSON(file string) {
|
||||
i.log.Info().Msgf("importing from JSON file: %s", file)
|
||||
data, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
i.log.Error().Err(err).Msg("failed to read JSON file")
|
||||
return
|
||||
}
|
||||
|
||||
var records []map[string]interface{}
|
||||
if err := json.Unmarshal(data, &records); err != nil {
|
||||
i.log.Error().Err(err).Msg("failed to unmarshal JSON")
|
||||
return
|
||||
}
|
||||
|
||||
//totalRecords := len(records)
|
||||
|
||||
/*
|
||||
bar := progressbar.NewOptions(totalRecords,
|
||||
progressbar.OptionSetDescription("Importing records"),
|
||||
progressbar.OptionShowCount(),
|
||||
progressbar.OptionShowIts(),
|
||||
progressbar.OptionSetPredictTime(true),
|
||||
)
|
||||
*/
|
||||
|
||||
/*
|
||||
for _, record := range records {
|
||||
// Insert record into the database
|
||||
// db.InsertRecord(record) // Replace with actual database insertion logic
|
||||
bar.Add(1)
|
||||
}
|
||||
*/
|
||||
|
||||
i.log.Info().Msg("JSON import completed")
|
||||
}
|
||||
35
internal/importer/opml.go
Normal file
35
internal/importer/opml.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package importer
|
||||
|
||||
func (i *Importer) importFromOPML(file string) {
|
||||
/*
|
||||
i.log.Info().Msgf("importing from OPML file: %s", file)
|
||||
data, err := ioutil.ReadFile(file)
|
||||
if err != nil {
|
||||
i.log.Error().Err(err).Msg("failed to read OPML file")
|
||||
return
|
||||
}
|
||||
|
||||
fp := gofeed.NewParser()
|
||||
feed, err := fp.ParseString(string(data))
|
||||
if err != nil {
|
||||
i.log.Error().Err(err).Msg("failed to parse OPML")
|
||||
return
|
||||
}
|
||||
|
||||
totalOutlines := len(feed.Items)
|
||||
bar := progressbar.NewOptions(totalOutlines,
|
||||
progressbar.OptionSetDescription("Importing outlines"),
|
||||
progressbar.OptionShowCount(),
|
||||
progressbar.OptionShowIts(),
|
||||
progressbar.OptionSetPredictTime(true),
|
||||
)
|
||||
|
||||
for _, outline := range feed.Items {
|
||||
// Insert outline into the database
|
||||
// db.InsertOutline(outline) // Replace with actual database insertion logic
|
||||
bar.Add(1)
|
||||
}
|
||||
|
||||
*/
|
||||
i.log.Info().Msg("OPML import completed")
|
||||
}
|
||||
88
internal/store/site_tag.go
Normal file
88
internal/store/site_tag.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/oklog/ulid/v2"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type SiteTag struct {
|
||||
ID ulid.ULID `gorm:"primaryKey"`
|
||||
SiteID ulid.ULID `gorm:"not null"`
|
||||
Value string `gorm:"not null"`
|
||||
db *gorm.DB `gorm:"-"`
|
||||
}
|
||||
|
||||
func (SiteTag) TableName() string {
|
||||
return "site_tags"
|
||||
}
|
||||
|
||||
func (s *Site) AddTag(value string) error {
|
||||
value = strings.ToLower(value)
|
||||
if s.HasTag(value) {
|
||||
log.Debug().Str("tag", value).Msg("tag already exists")
|
||||
return nil
|
||||
}
|
||||
|
||||
tag := &SiteTag{
|
||||
SiteID: s.ID,
|
||||
Value: value,
|
||||
db: s.db,
|
||||
}
|
||||
|
||||
result := s.db.Create(tag)
|
||||
if result.Error != nil {
|
||||
log.Error().Err(result.Error).Msg("unable to add site tag")
|
||||
return result.Error
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
Str("siteid", s.ID.String()).
|
||||
Str("tag", value).
|
||||
Msg("site tag added")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Site) DeleteAttribute(value string) error {
|
||||
value = strings.ToLower(value)
|
||||
result := s.db.Where("user_id = ? AND value = ?", s.ID, value).Delete(&SiteTag{})
|
||||
if result.Error != nil {
|
||||
log.Error().Err(result.Error).Msg("unable to delete site tag")
|
||||
return result.Error
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
Str("siteid", s.ID.String()).
|
||||
Str("tag", value).
|
||||
Msg("site tag deleted")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Site) HasTag(value string) bool {
|
||||
value = strings.ToLower(value)
|
||||
var count int64
|
||||
result := s.db.Model(&SiteTag{}).Where("site_id = ? AND value = ?", s.ID, value).Count(&count)
|
||||
if result.Error != nil {
|
||||
log.Error().Err(result.Error).Msg("unable to check site tag existence")
|
||||
return false
|
||||
}
|
||||
return count > 0
|
||||
}
|
||||
|
||||
func (s *Site) GetTags() ([]string, error) {
|
||||
var tags []SiteTag
|
||||
result := s.db.Where("site_id = ?", s.ID).Find(&tags)
|
||||
if result.Error != nil {
|
||||
log.Error().Err(result.Error).Msg("unable to get site tags")
|
||||
return nil, result.Error
|
||||
}
|
||||
|
||||
var tagValues []string
|
||||
for _, attr := range tags {
|
||||
tagValues = append(tagValues, attr.Value)
|
||||
}
|
||||
|
||||
return tagValues, nil
|
||||
}
|
||||
63
internal/store/store.go
Normal file
63
internal/store/store.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"go.uber.org/fx"
|
||||
"gorm.io/gorm"
|
||||
"sneak.berlin/go/directory/internal/config"
|
||||
"sneak.berlin/go/directory/internal/database"
|
||||
"sneak.berlin/go/directory/internal/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
UsersTableName string = "users"
|
||||
UserAttributesTableName string = "userattributes"
|
||||
)
|
||||
|
||||
type StoreParams struct {
|
||||
fx.In
|
||||
Logger *logger.Logger
|
||||
Config *config.Config
|
||||
Database *database.Database
|
||||
}
|
||||
|
||||
type Store struct {
|
||||
db *gorm.DB
|
||||
logger *logger.Logger
|
||||
log *zerolog.Logger
|
||||
params *StoreParams
|
||||
}
|
||||
|
||||
func New(lc fx.Lifecycle, params StoreParams) (*Store, error) {
|
||||
s := new(Store)
|
||||
|
||||
s.logger = params.Logger
|
||||
s.log = params.Logger.Get()
|
||||
|
||||
var err error
|
||||
s.db, err = params.Database.Get()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to get database object")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Auto migrate the schema
|
||||
err = s.db.AutoMigrate(&User{}, &UserAttribute{})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to migrate database schema")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *Store) Close() {
|
||||
log.Info().Msg("closing store db")
|
||||
db, err := s.db.DB()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to get generic database object from gorm")
|
||||
return
|
||||
}
|
||||
db.Close()
|
||||
}
|
||||
206
internal/store/user.go
Normal file
206
internal/store/user.go
Normal file
@@ -0,0 +1,206 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/oklog/ulid/v2"
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID ulid.ULID `gorm:"primaryKey"`
|
||||
Username string `gorm:"unique"`
|
||||
Email *string
|
||||
PasswordHash string
|
||||
CreatedAt *time.Time
|
||||
LastLogin *time.Time
|
||||
LastSeen *time.Time
|
||||
LastFailedAuth *time.Time
|
||||
RecentFailedAuthCount int
|
||||
LastSeenIP *string
|
||||
Disabled bool
|
||||
Attributes []UserAttribute `gorm:"foreignKey:UserID"`
|
||||
db *gorm.DB `gorm:"-"`
|
||||
}
|
||||
|
||||
func (User) TableName() string {
|
||||
return UsersTableName
|
||||
}
|
||||
|
||||
func (u *User) IsDisabled() bool {
|
||||
return u.Disabled
|
||||
}
|
||||
|
||||
func (s *Store) AddUser(username, password string) (*User, error) {
|
||||
var hashedPassword string
|
||||
if password != "" {
|
||||
hashedPasswordBytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("unable to hash password")
|
||||
return nil, err
|
||||
}
|
||||
hashedPassword = string(hashedPasswordBytes)
|
||||
}
|
||||
|
||||
userID := ulid.MustNew(ulid.Timestamp(time.Now()), rand.Reader)
|
||||
createdAt := time.Now()
|
||||
user := &User{
|
||||
ID: userID,
|
||||
Username: username,
|
||||
PasswordHash: hashedPassword,
|
||||
CreatedAt: &createdAt,
|
||||
db: s.db,
|
||||
}
|
||||
|
||||
result := s.db.Create(user)
|
||||
if result.Error != nil {
|
||||
log.Error().
|
||||
Str("username", username).
|
||||
Err(result.Error).
|
||||
Msg("unable to insert user into db")
|
||||
return nil, result.Error
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
Str("username", username).
|
||||
Msg("user added to db")
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *Store) FindUserByUsername(username string) (*User, error) {
|
||||
if len(username) < 3 {
|
||||
return nil, errors.New("usernames are at least 3 characters")
|
||||
}
|
||||
|
||||
var user User
|
||||
result := s.db.Where("username = ? AND disabled = ?", username, false).First(&user)
|
||||
if result.Error != nil {
|
||||
log.Error().Err(result.Error).Msg("unable to find user")
|
||||
return nil, result.Error
|
||||
}
|
||||
user.db = s.db
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (s *Store) FindUserByID(userID ulid.ULID) (*User, error) {
|
||||
var user User
|
||||
result := s.db.Where("id = ? and disabled = ?", userID, false).First(&user)
|
||||
if result.Error != nil {
|
||||
log.Error().Err(result.Error).Msg("unable to find user")
|
||||
return nil, result.Error
|
||||
}
|
||||
user.db = s.db
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (s *Store) LogFailedAuth(UserID ulid.ULID, r *http.Request) {
|
||||
// FIXME implement
|
||||
}
|
||||
|
||||
func (s *Store) LogSuccessfulAuth(UserID ulid.ULID, r *http.Request) {
|
||||
now := time.Now()
|
||||
u, err := s.FindUserByID(UserID)
|
||||
if err != nil {
|
||||
panic("unable to find user")
|
||||
}
|
||||
s.db.Model(u).Update("LastSeen", now)
|
||||
//s.db.Model(u).Update("LastSeenIP", FIXME)
|
||||
}
|
||||
|
||||
func (s *Store) AuthenticateUser(username, password, rootPasswordHash string, r *http.Request) (bool, *User, error) {
|
||||
|
||||
if username == "root" {
|
||||
user, err := s.FindUserByUsername(username)
|
||||
if err != nil {
|
||||
// we probably need to create the root user in the db
|
||||
// and set 'user' because it's returned
|
||||
user, err = s.AddUser("root", "*")
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("unable to create root user")
|
||||
return false, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// we also ignore if the root user in the database is disabled,
|
||||
// because root can always log in using the env var password
|
||||
|
||||
// even if the root user exists in the db, we still use the root
|
||||
// password from the environment var, and ignore the root password
|
||||
// hash in the database
|
||||
err = bcrypt.CompareHashAndPassword([]byte(rootPasswordHash), []byte(password))
|
||||
if err != nil {
|
||||
s.LogFailedAuth(user.ID, r)
|
||||
return false, nil, nil
|
||||
}
|
||||
s.LogSuccessfulAuth(user.ID, r)
|
||||
return true, user, nil
|
||||
}
|
||||
|
||||
// FIXME update user record when auth fails
|
||||
user, err := s.FindUserByUsername(username)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
if len(password) < 8 {
|
||||
s.LogFailedAuth(user.ID, r)
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
// how long are bcrypt hashes anyway?
|
||||
if len(user.PasswordHash) < 10 {
|
||||
s.LogFailedAuth(user.ID, r)
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
if user.Disabled {
|
||||
s.LogFailedAuth(user.ID, r)
|
||||
return false, nil, nil
|
||||
}
|
||||
|
||||
err = bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password))
|
||||
if err != nil {
|
||||
if err == bcrypt.ErrMismatchedHashAndPassword {
|
||||
s.LogFailedAuth(user.ID, r)
|
||||
return false, nil, nil
|
||||
}
|
||||
log.Error().Err(err).Msg("password comparison error")
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
s.LogSuccessfulAuth(user.ID, r)
|
||||
return true, user, nil
|
||||
}
|
||||
|
||||
func (u *User) Disable() error {
|
||||
u.Disabled = true
|
||||
result := u.db.Save(u)
|
||||
if result.Error != nil {
|
||||
log.Error().
|
||||
Str("email", *u.Email).
|
||||
Err(result.Error).
|
||||
Msg("unable to disable user")
|
||||
return result.Error
|
||||
}
|
||||
log.Debug().
|
||||
Str("email", *u.Email).
|
||||
Msg("user disabled")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) IsAdmin() bool {
|
||||
return u.HasAttribute("admin")
|
||||
}
|
||||
|
||||
func (u *User) IsSuperAdmin() bool {
|
||||
return u.HasAttribute("superadmin")
|
||||
}
|
||||
88
internal/store/user_attribute.go
Normal file
88
internal/store/user_attribute.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/oklog/ulid/v2"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserAttribute struct {
|
||||
ID ulid.ULID `gorm:"primaryKey"`
|
||||
UserID ulid.ULID `gorm:"not null"`
|
||||
Value string `gorm:"not null"`
|
||||
db *gorm.DB `gorm:"-"`
|
||||
}
|
||||
|
||||
func (UserAttribute) TableName() string {
|
||||
return UserAttributesTableName
|
||||
}
|
||||
|
||||
func (u *User) AddAttribute(value string) error {
|
||||
value = strings.ToLower(value)
|
||||
if u.HasAttribute(value) {
|
||||
log.Debug().Str("attribute", value).Msg("attribute already exists")
|
||||
return nil
|
||||
}
|
||||
|
||||
attribute := &UserAttribute{
|
||||
UserID: u.ID,
|
||||
Value: value,
|
||||
db: u.db,
|
||||
}
|
||||
|
||||
result := u.db.Create(attribute)
|
||||
if result.Error != nil {
|
||||
log.Error().Err(result.Error).Msg("unable to add user attribute")
|
||||
return result.Error
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
Str("email", *u.Email).
|
||||
Str("attribute", value).
|
||||
Msg("user attribute added")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) DeleteAttribute(value string) error {
|
||||
value = strings.ToLower(value)
|
||||
result := u.db.Where("user_id = ? AND value = ?", u.ID, value).Delete(&UserAttribute{})
|
||||
if result.Error != nil {
|
||||
log.Error().Err(result.Error).Msg("unable to delete user attribute")
|
||||
return result.Error
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
Str("email", *u.Email).
|
||||
Str("attribute", value).
|
||||
Msg("user attribute deleted")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) HasAttribute(value string) bool {
|
||||
value = strings.ToLower(value)
|
||||
var count int64
|
||||
result := u.db.Model(&UserAttribute{}).Where("user_id = ? AND value = ?", u.ID, value).Count(&count)
|
||||
if result.Error != nil {
|
||||
log.Error().Err(result.Error).Msg("unable to check user attribute")
|
||||
return false
|
||||
}
|
||||
return count > 0
|
||||
}
|
||||
|
||||
func (u *User) GetAttributes() ([]string, error) {
|
||||
var attributes []UserAttribute
|
||||
result := u.db.Where("user_id = ?", u.ID).Find(&attributes)
|
||||
if result.Error != nil {
|
||||
log.Error().Err(result.Error).Msg("unable to get user attributes")
|
||||
return nil, result.Error
|
||||
}
|
||||
|
||||
var attributeValues []string
|
||||
for _, attr := range attributes {
|
||||
attributeValues = append(attributeValues, attr.Value)
|
||||
}
|
||||
|
||||
return attributeValues, nil
|
||||
}
|
||||
51
internal/store/website.go
Normal file
51
internal/store/website.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"time"
|
||||
|
||||
"github.com/oklog/ulid/v2"
|
||||
"github.com/rs/zerolog/log"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Site struct {
|
||||
ID ulid.ULID `gorm:"primaryKey"`
|
||||
Domain string `gorm:"unique"`
|
||||
URL string
|
||||
CreatedAt *time.Time
|
||||
FirstSeen *time.Time
|
||||
LastSeen *time.Time
|
||||
Tags []SiteTag `gorm:"foreignKey:SiteID"`
|
||||
db *gorm.DB `gorm:"-"`
|
||||
}
|
||||
|
||||
func (Site) TableName() string {
|
||||
return "sites"
|
||||
}
|
||||
|
||||
func (s *Store) AddSite(url string) (*Site, error) {
|
||||
ID := ulid.MustNew(ulid.Timestamp(time.Now()), rand.Reader)
|
||||
createdAt := time.Now()
|
||||
site := &Site{
|
||||
ID: ID,
|
||||
URL: url,
|
||||
CreatedAt: &createdAt,
|
||||
db: s.db,
|
||||
}
|
||||
|
||||
result := s.db.Create(site)
|
||||
if result.Error != nil {
|
||||
log.Error().
|
||||
Str("siteid", ID.String()).
|
||||
Err(result.Error).
|
||||
Msg("unable to insert site into db")
|
||||
return nil, result.Error
|
||||
}
|
||||
|
||||
log.Debug().
|
||||
Str("siteid", ID.String()).
|
||||
Str("url", url).
|
||||
Msg("site added to db")
|
||||
return site, nil
|
||||
}
|
||||
Reference in New Issue
Block a user