Compare commits

...

657 Commits

Author SHA1 Message Date
Ferdinand Mütsch
c320afaf3b chore: improved oidc error logging 2025-10-12 00:06:04 +02:00
Ferdinand Mütsch
140f8b2eac test: oidc endpoints 2025-10-11 23:50:00 +02:00
Ferdinand Mütsch
6880d4d524 test: auth middleware oidc methods 2025-10-10 12:21:35 +02:00
Ferdinand Mütsch
a07a4f48b4 feat: finish up oidc implementation (resolve #33) 2025-10-10 08:27:32 +02:00
Ferdinand Mütsch
faa6312cd8 feat(wip): prevent password reset for non-local users 2025-10-05 23:37:46 +02:00
Ferdinand Mütsch
cfa4a7a6ea feat(wip): implement oidc user account creation 2025-10-05 23:37:46 +02:00
Ferdinand Mütsch
e8420f8114 feat(wip): oidc login ui 2025-10-05 23:37:46 +02:00
Ferdinand Mütsch
50b8114959 chore: fix minor rebase errors 2025-10-05 23:37:46 +02:00
Ferdinand Mütsch
76af6981d5 feat(wip): implement openid connect login 2025-10-05 23:37:46 +02:00
Ferdinand Mütsch
78ef6b81db chore: minor refactorings to shared data middleware logic 2025-10-05 23:37:46 +02:00
Ferdinand Mütsch
c952184324 fix: migration for mariadb 2025-10-05 23:37:25 +02:00
Ferdinand Mütsch
068223839c fix: user model consistency checks 2025-10-05 23:07:24 +02:00
Ferdinand Mütsch
aea01fd51d breaking_change: drop mssql support 2025-10-04 21:16:09 +02:00
Ferdinand Mütsch
1b03043ebd chore: upgrade and clean dependencies 2025-10-04 20:57:59 +02:00
Ferdinand Mütsch
8b95cd5d9f fix: nil pointer when accessing non-existing shared data 2025-10-04 20:55:01 +02:00
Ferdinand Mütsch
b2fd840403 refactor: replace principal middleware by generic shared data middleware 2025-10-04 20:45:18 +02:00
Ferdinand Mütsch
3c54f5ecc9 docs: code comments on language mapping resolution (see #845) [skip ci] 2025-09-19 11:50:22 +02:00
Ferdinand Mütsch
213df9e00b chore: minor error logging [skip ci] 2025-09-16 01:02:22 +02:00
Ferdinand Mütsch
3154cd8519 fix: make summaries display actual data range or requested interval if no data present
fix: use unix era start time in wakatime compat endpoints (resolve #843)
2025-09-16 00:40:48 +02:00
Ferdinand Mütsch
849d7a8996 fix: summary from date display (resolve #843) 2025-09-16 00:02:33 +02:00
Ferdinand Mütsch
b12f76ec0f fix: decrease batch insert chunk size for sqlite compatibility (resolve #840) 2025-09-11 20:14:35 +02:00
Ferdinand Mütsch
029d1304f8 docs: update swagger docs [skip ci] 2025-09-11 08:37:26 +02:00
Ferdinand Mütsch
2082624132 chore: replace precomputed user heartbeat ranges by grouping view query 2025-09-07 21:45:42 +02:00
Ferdinand Mütsch
73d7213656 Merge branch 'fork/k2on/add-nix-icon' 2025-09-07 19:22:48 +02:00
Ferdinand Mütsch
74946953c5 fix: prevent overlapping duration regeneration jobs 2025-09-07 08:00:34 +02:00
Max Koon
794d49ad67 Delete wakapi.yml 2025-09-05 23:08:35 -04:00
Max Koon
7aa0773745 fix: bundle new nix icon 2025-09-05 23:05:32 -04:00
Max Koon
e7ff774343 feat: add nix language icon 2025-09-05 16:59:29 -04:00
Ferdinand Mütsch
ae51a762e1 Merge branch 'fork/k2on/theming' 2025-09-05 14:09:21 +02:00
Ferdinand Mütsch
eb169695d7 feat(compat): implement wakatime user agents endpoint (resolve #833) 2025-09-05 14:06:32 +02:00
Max Koon
df81cb34a2 refactor: add alpha value to all colors and fix index digits 2025-09-05 07:49:53 -04:00
Ferdinand Mütsch
57ddbfbd8b ci: build with latest go 1.25 experimental features [skip ci] 2025-09-05 11:08:57 +02:00
Ferdinand Mütsch
72663839eb fix: respect aliases when filtering by project labels (resolve #836) 2025-09-05 10:45:34 +02:00
Ferdinand Mütsch
5f25bd7e99 fix: make db migration script account for unhashed heartbeats [skip ci] 2025-09-05 09:40:11 +02:00
Ferdinand Mütsch
b4aa96ae2f perf: support mysql compressed connections 2025-09-02 21:28:49 +02:00
Ferdinand Mütsch
a060a73098 chore: bash script to export and import mysql tables as tsv [skip ci] 2025-09-02 09:56:15 +02:00
Max Koon
57d7be61fc Merge branch 'master' into theming 2025-09-01 15:41:10 -04:00
Max Koon
92bcfeeb10 refactor: change the rest of the colors 2025-09-01 15:37:44 -04:00
Ferdinand Mütsch
f6ba56e2a5 chore: less verbose info logging for active users [skip ci] 2025-09-01 19:54:25 +02:00
Max Koon
c41ae4a1a6 fix: bg to focused 2025-09-01 13:04:45 -04:00
Max Koon
b88a78da59 fix: add focus var 2025-09-01 13:01:27 -04:00
Max Koon
6d09ce7903 change: gray-800 from card to background 2025-09-01 12:36:12 -04:00
Max Koon
95f465ae30 refactor: color variables 2025-09-01 12:02:52 -04:00
Ferdinand Mütsch
cfe69df9ff chore: upgrade to go 1.25 2025-08-30 22:44:25 +02:00
Ferdinand Mütsch
3a74d4db7f feat: extend db migration script to filter by individual users [skip ci] 2025-08-29 15:33:29 +02:00
Ferdinand Mütsch
191936ec23 fix: proper usage of sqlite driver
fix: improve sqlite date parsing robustness
2025-08-29 15:33:29 +02:00
Ferdinand Mütsch
44bda0f209 docs: update readme [skip ci] 2025-08-29 15:33:29 +02:00
Ferdinand Mütsch
5f42c12933 chore: use wal journal mode for sqlite [skip ci] 2025-08-25 08:21:08 +02:00
Ferdinand Mütsch
e1a62ffc99 chore: ability to toggle certain entity types in db migration script [skip-ci] 2025-08-22 15:57:22 +02:00
Ferdinand Mütsch
9cd322a231 refactor: generic db migrations script
chore: various convenience repository methods
chore: make scripts folder its own module
2025-08-22 15:49:26 +02:00
Ferdinand Mütsch
4050c31b02 Merge branch 'fork/NazmusSayad/start-of-week-customization' 2025-08-22 14:13:29 +02:00
Ferdinand Mütsch
d021f57516 chore: remove unnecessary migration 2025-08-22 14:13:16 +02:00
Ferdinand Mütsch
87e332d5c5 chore: load testing tool [skip-ci] 2025-08-21 08:55:11 +02:00
Nazmus Sayad
bfd137e634 feat: add user preference for start of week 2025-08-18 22:40:45 +06:00
Ferdinand Mütsch
1c47ebf636 Merge remote-tracking branch 'origin/master' 2025-08-15 12:16:10 +02:00
Ferdinand Mütsch
4b02b6bdf5 chore: config option to skip mx dns record validation (resolve #826) 2025-08-15 11:55:52 +02:00
Henri Burau
ae66e69a1d remove debug log entry 2025-08-14 20:02:30 +10:00
Ferdinand Mütsch
850c176b7f chore: consistent indentation [skip-ci] 2025-08-09 11:33:29 +02:00
Ferdinand Mütsch
ad154294c8 fix: accept partially valid batch of heartbeats (resolve #824) 2025-08-08 18:20:11 +02:00
Ferdinand Mütsch
8b8fe21fed fix: assume coding as default category if not specified otherwise (see #817) 2025-08-02 11:06:39 +02:00
Ferdinand Mütsch
7f281184de chore: detect wsl as separate os (resolve #817, #718) 2025-08-02 11:03:15 +02:00
Ferdinand Mütsch
70eafc3144 chore: cockroach deprecation warning 2025-07-18 10:40:27 +02:00
Ferdinand Mütsch
4ffde946d5 chore: disable mysql table optimization 2025-07-18 10:37:54 +02:00
Ferdinand Mütsch
1e0ef43d92 chore: upgrade dependencies 2025-07-18 10:28:11 +02:00
Ferdinand Mütsch
700921406e feat: automatic background database vacuuming and table optimization (resolve #785) 2025-07-18 10:26:41 +02:00
Ferdinand Mütsch
9f925b7f02 ci: pin runner image ubuntu version to 24.04 2025-07-18 08:57:18 +02:00
jstoparczyk
7eab383276 migrate from post-requests tests to actual tests and (mostly) assertions
notable change: expect  fields to be of type array and length of 0, rather than only expecting their length to be 0
2025-06-27 19:00:14 +02:00
jstoparczyk
52925a3532 migrate from post-requests tests to actual tests and (mostly) assertions 2025-06-27 18:55:32 +02:00
jstoparczyk
bf69b81b62 rename default environment from prod to dev; make BASE_URL non-secret and add a default value for it; modify collection documentation accordingly to changes above 2025-06-27 14:18:37 +02:00
jstoparczyk
8212686c64 remove console.log's 2025-06-24 18:00:26 +02:00
jstoparczyk
4d210a719a replace newman with bruno cli and add installation of bruno cli to migration job in ci.yml 2025-06-24 17:41:45 +02:00
jstoparczyk
53743216dc replace newman run with bru run in the run_api_tests.sh script 2025-06-24 17:36:45 +02:00
jstoparczyk
970d9b3276 port wakapi_api_tests.postman_collection.json collections to Bruno format 2025-06-24 17:35:03 +02:00
jstoparczyk
4b0676881d update README 2025-06-24 17:29:47 +02:00
jstoparczyk
80198c87eb port wakapi.postman_collection.json to bruno format 2025-06-23 23:57:42 +02:00
Ferdinand Mütsch
509c96e9a6 feat: allow delegate signup to trusted proxy (resolve #808) 2025-06-17 17:00:22 +02:00
Ferdinand Mütsch
f564c3c6be Merge pull request #805 from muety/ci-docker
fix(ci): ARG for TARGETOS and TARGETARCH
2025-06-13 15:56:20 +02:00
Steven Tang
3f11202e54 fix(ci): ARG for TARGETOS and TARGETARCH
Resolves #804
2025-06-13 19:30:07 +10:00
Ferdinand Mütsch
632020c30b feat: current online users count (resolve #798) 2025-06-13 08:53:48 +02:00
Ferdinand Mütsch
fcf130b7a8 fix: database status in health endpoint [skip ci] 2025-06-10 08:56:17 +02:00
Ferdinand Mütsch
8b9c3e36c9 fix: race condition during summary reaggregation (see #801) 2025-06-10 08:52:55 +02:00
Ferdinand Mütsch
c6b8bf7b67 chore: conditionally return json or plain text from health endpoint 2025-06-10 07:35:32 +02:00
kurtnettle
5891681c24 feat(api): return health check as JSON 2025-06-10 01:32:58 +06:00
Ferdinand Mütsch
be4e59c548 Merge branch 'master' of github.com:muety/wakapi
# Conflicts:
#	coverage/coverage.out
2025-05-30 23:49:53 +02:00
Ferdinand Mütsch
d2d9a2cfa3 fix(wip): duplicate leaderboard entries due to inconsistent language spelling 2025-05-30 23:48:53 +02:00
Ferdinand Mütsch
39e12a2e70 chore: exclude changing assets from server push [skip ci] 2025-05-26 16:52:08 +02:00
Ferdinand Mütsch
50ab1ad26a ci: attempt to properly exclude files from sonarqube checks 2025-05-26 16:43:32 +02:00
Ferdinand Mütsch
7752027dd6 chore: use more aggressive cache busting at dev time (see #795) 2025-05-26 16:23:44 +02:00
Ferdinand Mütsch
8922daca42 chore: cache busting for frequently changing css and scripts (resolve #795) 2025-05-26 16:15:28 +02:00
Ferdinand Mütsch
544e0e4f90 chore: upgrade dependencies
fix: mssql docker testing tls certificate error
2025-05-26 16:09:46 +02:00
Ferdinand Mütsch
a678494f46 fix: panic when attempting to filter by category (resolve #796) 2025-05-26 16:09:24 +02:00
Ferdinand Mütsch
4853e8a264 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	coverage/coverage.out
2025-05-20 21:08:35 +02:00
Ferdinand Mütsch
d42c522c6b feat: auto-redirect api root route to frontpage for browsers (resolve #788) 2025-05-20 21:08:22 +02:00
Ferdinand Mütsch
ff287eaa12 fix: update test fixtures schema (resolve #786) 2025-05-10 17:02:04 +02:00
Ferdinand Mütsch
8c8ae5d7ee fix: purge user durations as part of data cleanup (relates to #785) 2025-05-10 11:16:46 +02:00
Ferdinand Mütsch
ca3035b14b fix: improve robustness of durations key column migration 2025-05-10 10:55:32 +02:00
Ferdinand Mütsch
66f4360b0c fix: include not null constraints for heartbeats customtime columns 2025-05-03 21:06:48 +02:00
Ferdinand Mütsch
78c2cc7593 chore: upgrade dependencies 2025-05-03 20:38:18 +02:00
Ferdinand Mütsch
c75785c6a8 docs: reformat readme 2025-05-03 20:33:10 +02:00
Ferdinand Mütsch
af2011b7c4 chore: minor additions to hourly breakdown chart 2025-05-03 20:28:24 +02:00
Ferdinand Mütsch
aa8a1d933c Merge branch 'fork/justin-jiajia/master' 2025-05-03 20:27:34 +02:00
jiajia
12afba7464 feat: support zooming functionalitiy for hourly breakdown chart 2025-05-01 20:47:12 +08:00
Ferdinand Mütsch
f9f7c3da71 Merge pull request #781 from shyim/improve-docker-build-speed
ci: improve docker build speed
2025-04-30 21:18:53 +02:00
Soner Sayakci
b6df2ce427 ci: improve docker build speed 2025-04-30 17:30:13 +02:00
jiajia
db368eb8c0 fix: update date and time formatting in timeline chart ticks for better readability 2025-04-30 20:53:17 +08:00
Ferdinand Mütsch
f58653f1ba fix: infinite loop caused by timezone weirdness in date range splitting (resolve #779) 2025-04-30 14:39:48 +02:00
Ferdinand Mütsch
06923b6313 fix(ui): make date picker min and max dates reactive (resolve #780) 2025-04-28 20:22:52 +02:00
Ferdinand Mütsch
84365442b0 fix: add non-natural primary key to durations table (resolve #777) 2025-04-25 18:22:02 +02:00
Ferdinand Mütsch
e7da40b307 chore: minor code style-related changes to the timeline chart 2025-04-25 08:22:37 +02:00
Ferdinand Mütsch
afc6d5853c chore: high memory usage debugging script [skip ci] 2025-04-25 07:36:49 +02:00
Ferdinand Mütsch
9843a406cc chore: use docker init system for running wakapi to fight zombies (resolve #713) [skip ci] 2025-04-23 14:46:33 +02:00
jiajia
165467931f support alias for hourly breakdown 2025-04-22 19:12:01 +08:00
jiajia
1f5e3a40be Merge remote-tracking branch 'upstream/master' 2025-04-21 20:22:48 +08:00
jiajia
0d6e9f9c90 Update a few names missed by last commit 2025-04-21 20:22:32 +08:00
jiajia
bd661d58c6 refactor summary view: replace 'dailystats' with 'timeline', while 'timeline' move to 'hourly breakdown' to align with the UI & show only 24 hours of hourly breakdown & refactor the hourly breakdown by only chartjs 2025-04-21 20:01:03 +08:00
jiajia
0f14b745b8 Add support for timeline 2025-04-13 17:50:23 +08:00
Ferdinand Mütsch
a842467067 fix(tests): another attempt to fix smtp tests 2025-04-11 14:58:58 +02:00
Ferdinand Mütsch
4fc605c782 fix: allow to run container as non-root user again (resolve #775) 2025-04-11 14:34:13 +02:00
Ferdinand Mütsch
5a2994fe47 fix: smtp tests with latest smtp4dev version 2025-04-11 14:02:11 +02:00
Ferdinand Mütsch
4ca7d79112 Merge pull request #773 from ycsh-w/msg-usr-name-invalid
Improve error message when signing up a user with invalid name
2025-04-07 07:35:05 +02:00
ycsh
00a45b90f7 specifically deal with invalid user name 2025-04-06 16:28:21 -07:00
ycsh
b25d05c43b auto reformat 2025-04-06 16:22:55 -07:00
Ferdinand Mütsch
b3668085c0 docs: comments [skip ci] 2025-03-26 21:54:37 +01:00
Ferdinand Mütsch
14fae4a3c8 fix: workaround for postgres timestamp issue (resolve #761) 2025-03-26 21:47:57 +01:00
Ferdinand Mütsch
bc2096f411 fix: properly expose docker secrets to wakapi process (resolve #769) 2025-03-23 11:19:27 +01:00
Ferdinand Mütsch
f21441ae1b fix: critical bug in cached durations aggregation (resolve #766) 2025-03-18 16:27:22 +01:00
Ferdinand Mütsch
43cc7ce1b0 fix: parse strange hbulderx user (resolve #765) 2025-03-17 17:06:20 +01:00
Ferdinand Mütsch
04491ec126 fix: duration overlap check (resolve #761) 2025-03-14 14:35:10 +01:00
Ferdinand Mütsch
e8f7d9b789 chore: minor changes to error displaying and page layout 2025-03-14 14:29:28 +01:00
Ferdinand Mütsch
8427c97d18 fix: various minor logging related fixes 2025-03-13 09:36:37 +01:00
Ferdinand Mütsch
9985e8d9de fix: fill missing category of browsing heartbeats (resolve #760) 2025-03-13 08:52:24 +01:00
Ferdinand Mütsch
9856994e74 docs: mention tparse [skip ci] 2025-03-12 17:09:14 +01:00
Ferdinand Mütsch
399cbb4463 chore: upgrade dependencies 2025-03-09 21:55:15 +01:00
Ferdinand Mütsch
70660cf154 chore: thread safety for request enhanced log lines [skip ci] 2025-03-09 21:50:50 +01:00
Ferdinand Mütsch
3f6c3c2c6d Merge pull request #758 from Darneus/master
allow smtp with no auth
2025-03-05 16:15:50 +01:00
Honza Kosák
ded8d2b46c fix: allow smtp with no auth 2025-03-05 12:48:11 +01:00
Ferdinand Mütsch
d37f25e68a fix: resolve faulty data for project details view (resolve #756) 2025-03-03 07:59:42 +01:00
Ferdinand Mütsch
8edb93853a Merge pull request #757 from krishnans2006/master [skip ci]
Add .env to .gitignore
2025-03-03 07:20:07 +01:00
Krishnan Shankar
94fb209639 Add .env to .gitignore 2025-03-02 17:47:55 -06:00
Ferdinand Mütsch
51208a133a fix: panic during summary aggregation (resolve #754) 2025-02-28 08:45:03 +01:00
Ferdinand Mütsch
f5d3f354e3 fix: index error during entity type deref 2025-02-28 07:08:15 +01:00
Ferdinand Mütsch
fde25948ab fix: compute leaderboard with consistent time interval (resolve #749) 2025-02-26 23:45:51 +01:00
Ferdinand Mütsch
1bd00f7209 ci: exclude external code from sonar analyses
chore: minor code changes
2025-02-24 07:48:05 +01:00
Ferdinand Mütsch
898154c5db refactor: language mapping augmentation of durations 2025-02-21 14:25:36 +01:00
Ferdinand Mütsch
3ee63bba65 test: update test database schema 2025-02-21 11:29:13 +01:00
Ferdinand Mütsch
b27e9bb083 fix: deadlock caused by not using open transaction during batch insert
fix: regenerate durations upon language mapping update
fix: minor ui
2025-02-21 11:29:13 +01:00
Ferdinand Mütsch
7b7f5e94bd fix: include most prominent entity for each duration 2025-02-21 11:29:13 +01:00
Ferdinand Mütsch
71b18f1aaf fix: summary regeneration
feat: introduce multi-interval durations
2025-02-21 11:29:13 +01:00
Ferdinand Mütsch
f9835fde71 chore(perf): generate and persist durations incrementally 2025-02-21 11:29:13 +01:00
Ferdinand Mütsch
77bb01020f fix: regenerate durations upon summary regeneration 2025-02-21 11:29:13 +01:00
Ferdinand Mütsch
ee9dd9688c chore: duration service method for regenerating by user 2025-02-21 11:29:13 +01:00
Ferdinand Mütsch
a97c233f0a chore: update migration tool 2025-02-21 11:29:13 +01:00
Ferdinand Mütsch
0ba7a838e8 chore: implement duration streaming repo methods 2025-02-21 11:29:13 +01:00
Ferdinand Mütsch
b210b4d82c fix: durations merging logic
test: cached durations retrieval
2025-02-21 11:29:13 +01:00
Ferdinand Mütsch
48d534ba9e chore(perf): query filtered durations 2025-02-21 11:29:13 +01:00
Ferdinand Mütsch
0351d497fa refactor: make durations a persistent yet ephemeral database entity for query filter speedup (resolve #716) 2025-02-21 11:29:13 +01:00
Ferdinand Mütsch
3fc95be291 fix: tests 2025-02-21 11:29:13 +01:00
Ferdinand Mütsch
dca35946d4 chore: streamed version of filtered heartbeats repo method
chore: minor refactoring
2025-02-21 11:29:13 +01:00
Ferdinand Mütsch
a13b9a96dd chore: minor refactoring of repo methods
chore: replace deprecated hashstructure library
perf: remove double duration hashing
perf: replace flv by xxhash
2025-02-21 11:29:13 +01:00
Ferdinand Mütsch
95a0dd794b refactor: stream heartbeats from database asynchronously for getall 2025-02-21 11:29:13 +01:00
Ferdinand Mütsch
0ce55d26fe chore: migrate user heartbeats timeout preferences 2025-02-21 11:29:13 +01:00
Ferdinand Mütsch
bf6b1f854b docs: update readme to match refactored durations logic 2025-02-21 11:29:13 +01:00
Ferdinand Mütsch
8d3a049f4d refactor(wip): drop heartbeat padding in summary aggregation (see #675) 2025-02-21 11:29:11 +01:00
Ferdinand Mütsch
2b3f1d9ef4 fix: time zone issues in case database and server timezones vary 2025-02-21 10:39:53 +01:00
Ferdinand Mütsch
c48bdfca7e chore: update db migration tool (see #746) [skip ci] 2025-02-20 17:24:10 +01:00
Ferdinand Mütsch
bb484c42f3 chore: go 1.24 2025-02-18 22:27:21 +01:00
Ferdinand Mütsch
58c2fecd9d fix: update fields exposed by heartbeats get compat endpoint
fix: update fields used by download script
feat: provide heartbeats csv upload script (resolve #745)
2025-02-18 22:21:27 +01:00
Steven Tang
2ac63bc391 chore: docker/build-push-action v6, remove workflow_dispatch 2025-02-12 22:22:56 +11:00
Ferdinand Mütsch
9735f11c26 Merge pull request #742 from justin-jiajia/master
make timeline chart respect vibrant colors flag
2025-02-06 14:11:45 +01:00
jiajia
fc56c8595b let calendar view respect "Vibrant Colors" & move the drawDailyProjectChart into draw(subselection) 2025-02-06 17:28:05 +08:00
Ferdinand Mütsch
b9c5db26d6 Merge branch 'fork/justin-jiajia/master' (resolve #338)
# Conflicts:
#	models/view/summary.go
#	routes/summary.go
#	views/summary.tpl.html
2025-02-05 21:36:09 +01:00
Ferdinand Mütsch
674296ce23 Merge branch 'master' of github.com:muety/wakapi 2025-02-05 21:32:21 +01:00
Ferdinand Mütsch
c86419fd41 fix(ui): dashboard grid layout 2025-02-05 21:31:57 +01:00
Ferdinand Mütsch
8dfb30abcd chore: minor refactorings 2025-02-05 21:31:57 +01:00
jiajia
cadf781da5 Fix a minor problem that calendar view shows when there's also a 'no data' warning 2025-02-05 21:31:57 +01:00
jiajia
a7b3e8b78d Fix the HTML structure 2025-02-05 21:31:57 +01:00
jiajia
8310e4acc6 Fix the 'no data' for the calendar view 2025-02-05 21:31:57 +01:00
jiajia
5268b01084 Revert "Fix the activity chart"
This reverts commit e4e3f371a4.
2025-02-05 21:31:57 +01:00
jiajia
5c967ad454 Fix the activity chart 2025-02-05 21:31:57 +01:00
jiajia
0608cfa248 Fix the 'No data' for the Calendar view 2025-02-05 21:31:57 +01:00
jiajia
dc631ed180 Show all vertical bars for the 'Calendar View' 2025-02-05 21:31:57 +01:00
jiajia
cb4a8a8eb4 Limit the interval of the 'Calendar View' & Fix typo 2025-02-05 21:31:57 +01:00
jiajia
a6404b1949 Change the 'Calendar view' to meet the architecture rule & Show 'No Data' for the summary webpage 2025-02-05 21:31:57 +01:00
Zijia Yan
dff08d87b5 Delete my nonsense of the mocks 2025-02-05 21:31:57 +01:00
jiajia
9666d57091 Improve the porformance of the "Calendar view" 2025-02-05 21:31:57 +01:00
jiajia
317cdb01f8 Add a "Calendar view" just like what's on the wakatime dashboard 2025-02-05 21:31:57 +01:00
jiajia
d660deb44a Fix a minor problem that calendar view shows when there's also a 'no data' warning 2025-02-03 21:20:29 +08:00
Ferdinand Mütsch
a75f177759 feat: ability to change username (resolve #739) 2025-02-02 22:19:49 +01:00
Ferdinand Mütsch
8bd23c99ae chore: add check for sqlite cascades before changing user id 2025-02-02 21:56:22 +01:00
Ferdinand Mütsch
2fef990d96 feat: service methods for changing user name (see #739) 2025-02-02 11:52:25 +01:00
jiajia
abcf0e348c Fix the HTML structure 2025-02-02 12:56:17 +08:00
jiajia
da1ac4da3d Fix the 'no data' for the calendar view 2025-02-01 12:24:54 +08:00
jiajia
b3061ca2bd Revert "Fix the activity chart"
This reverts commit e4e3f371a4.
2025-02-01 10:52:45 +08:00
jiajia
e4e3f371a4 Fix the activity chart 2025-01-27 21:04:45 +08:00
jiajia
776ae3f7d8 Fix the 'No data' for the Calendar view 2025-01-27 20:31:30 +08:00
jiajia
03a306c3ac Show all vertical bars for the 'Calendar View' 2025-01-23 21:16:22 +08:00
Ferdinand Mütsch
7c3d4c51bf docs: fix typo in env var (see #738) [skip ci] 2025-01-23 06:38:50 +01:00
Ferdinand Mütsch
820540a2b5 fix: sample data gui headless mode errors [skip ci] 2025-01-22 22:57:59 +01:00
Ferdinand Mütsch
2e4021987b chore: minor fixes to sample data gui 2025-01-22 22:47:11 +01:00
Ferdinand Mütsch
42490015c8 feat: option to require auth for viewing leaderboard (resolve #738) 2025-01-22 22:26:20 +01:00
jiajia
698a99d026 Limit the interval of the 'Calendar View' & Fix typo 2025-01-22 20:05:24 +08:00
Ferdinand Mütsch
cc435eb04f docs: add funding json [skip ci] 2025-01-20 19:03:12 +01:00
Ferdinand Mütsch
b2fd1074c5 chore: upgrade pond dependency [skip ci] 2025-01-18 16:19:19 +01:00
Ferdinand Mütsch
b50b208688 chore: move summary time zone fix into summary model 2025-01-18 00:59:13 +01:00
Ferdinand Mütsch
e70edf1fb3 fix: time zone issue with activity chart (resolve #719) 2025-01-18 00:38:11 +01:00
Ferdinand Mütsch
0957051c79 chore: dependency upgrades 2025-01-17 17:27:23 +01:00
jiajia
5d92d8935d Change the 'Calendar view' to meet the architecture rule & Show 'No Data' for the summary webpage 2025-01-16 21:21:07 +08:00
Ferdinand Mütsch
a02c96ef5f Merge pull request #735 from ricristian/patch-1
Update README.md
2025-01-15 05:49:10 +01:00
Cristian R
c31e8f571e Update README.md
Update the helm repo repository url since the current one is not updated for quite some time. It includes some bugs also config map is not synchronized with current app values. 

I would recommend to wait for few days maybe the current owner of the project will accept my changes. 

In my changes there besides fixes it also includes some workflows that will generate automatically new helm charts versions based on current app releases
2025-01-15 00:04:06 +02:00
Zijia Yan
015fa9f64f Delete my nonsense of the mocks 2025-01-11 20:10:15 +08:00
jiajia
b03ba8a789 Improve the porformance of the "Calendar view" 2025-01-11 20:03:53 +08:00
jiajia
2597d55c55 Merge branch 'master' of https://github.com/justin-jiajia/wakapi 2025-01-10 01:53:41 +00:00
jiajia
5de844602c Add a "Calendar view" just like what's on the wakatime dashboard 2025-01-10 01:53:05 +00:00
Ferdinand Mütsch
1d5f51dcad Update README.md 2024-12-22 23:30:26 +01:00
Ferdinand Mütsch
5fa2515931 fix: readme card default custom title (resolve #715)
chore: add option to configure activity chart sharing separately
2024-12-20 21:16:55 +01:00
Ferdinand Mütsch
946985ef11 perf: user heartbeats query performance improvements [skip ci] 2024-11-25 22:48:01 +01:00
Ferdinand Mütsch
6dc2d8f817 chore: increase user total time counting interval [skip ci] 2024-11-25 20:55:27 +01:00
Ferdinand Mütsch
f8ea8a761d chore: some more logging [skip ci] 2024-11-25 20:49:06 +01:00
Ferdinand Mütsch
f162accfb4 refactor(perf): user first heartbeats query 2024-11-25 20:21:37 +01:00
Ferdinand Mütsch
24751ea2d0 refactor(perf): project stats query 2024-11-25 00:53:30 +01:00
Ferdinand Mütsch
b82a9e8251 Merge remote-tracking branch 'origin/master' 2024-11-22 14:29:37 +01:00
Ferdinand Mütsch
d628020c5c fix: sentry heartbeats tracer sampling 2024-11-22 14:20:34 +01:00
Ferdinand Mütsch
b96db4eafa fix: improve user agent regex
fix: support cursor editor (resolve #712)
2024-11-22 13:57:17 +01:00
Ferdinand Mütsch
1680d0d225 Merge pull request #710 from krishnans2006/master
Update smtp_pass environment variable in compose.yml
2024-11-22 07:39:16 +01:00
Krishnan Shankar
77910ef126 fix: update smtp_pass environment variable in compose.yml 2024-11-22 00:36:42 -06:00
Steven Tang
4f1b2d0a71 ci(release): remove step to delete old releases
Resolves #704
2024-11-17 22:19:21 +11:00
Steven Tang
65ac714866 ci: update softprops/action-gh-release action to v2
See #704
2024-11-17 19:03:36 +11:00
Ferdinand Mütsch
5bd1ffd14d Merge pull request #706
fix(config): reverse proxy ip ranges parse warning when unset
2024-11-11 11:05:57 +01:00
SIMULATAN
35110907b6 fix(config): reverse proxy ip ranges parse warning when unset 2024-11-11 10:39:38 +01:00
Ferdinand Mütsch
f4253678ce Merge pull request #705
Printer-friendly dashboard styling
2024-11-05 11:20:53 +01:00
jiajia
12f2ea6fda Printer-friendly dashboard styling 2024-11-05 09:52:13 +00:00
Ferdinand Mütsch
7e976e607e Merge branch 'master' of github.com:muety/wakapi 2024-11-04 22:53:56 +01:00
Ferdinand Mütsch
354c7f9626 chore: use zero-valued fields
fix: exclude all optional heartbeat fields from hash
2024-11-04 22:52:49 +01:00
Ferdinand Mütsch
d29b11173c docs: contrib guidelines 2024-11-03 23:24:16 +01:00
Ferdinand Mütsch
b04f8c573e chore: show date of first heartbeat in range (resolve #685) 2024-11-03 22:40:57 +01:00
Ferdinand Mütsch
c7b07476d0 fix: time format returned by stats compat endpoints (resolve #698) 2024-11-03 22:10:04 +01:00
Ferdinand Mütsch
a1ebc240b1 fix: do not crash when using dev mode outside project source folder (resolve #703) (also see #693) 2024-11-03 16:46:38 +01:00
Chen, Ron Gang
de91df382a Remove the dependencies field as it may cause errors if the text is too long 2024-11-03 12:59:08 +08:00
Chen, Ron Gang
b2828e7b7b add additional wakatime fields 2024-11-02 23:31:03 +08:00
Ferdinand Mütsch
24044aa4d2 Merge pull request #693 from notarock/master
Remove circular dependency when logging configuration loading error
2024-10-15 07:53:56 +02:00
Roch D'Amour
71b3fd08a9 Remove dependency on config to log config error 2024-10-15 01:19:24 -04:00
Ferdinand Mütsch
830f6c920d chore: upgrade dependencies 2024-10-15 00:26:00 +02:00
Ferdinand Mütsch
96c8143271 merge with latest remote master 2024-10-15 00:25:50 +02:00
Ferdinand Mütsch
763d722738 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	coverage/coverage.out
#	routes/api/heartbeat.go
#	routes/api/heartbeat_test.go
2024-10-15 00:22:38 +02:00
Ferdinand Mütsch
40e01c1b01 fix: make heartbeats endpoint respond to cors preflight requests (resolve #690) 2024-10-15 00:13:53 +02:00
Ferdinand Mütsch
699dab8b27 fix: properly parse user agents sent by desktop app 2024-10-14 23:13:57 +02:00
Ferdinand Mütsch
ef2776f2cd fix: placeholders replacement (fix #687) 2024-10-13 23:46:30 +02:00
Ferdinand Mütsch
65aa688b6b fix: make heartbeats endpoint support browser plugin again (resolve #683, resolve #688) 2024-10-13 23:02:33 +02:00
Ferdinand Mütsch
75c9ef6c10 test: heartbeat id unmarshaling 2024-10-13 22:07:42 +02:00
vlack
341c9a2d07 fix: ignore id in heartbeat post 2024-10-04 22:19:44 +03:00
Ferdinand Mütsch
2568fbd39b chore: user postgres 17 by default in docker compose stack [skip ci] 2024-09-27 15:49:09 +02:00
Ferdinand Mütsch
b6def215c5 chore: use compose secrets by default 2024-09-27 14:49:15 +02:00
Ferdinand Mütsch
dcd36db410 Merge branch 'fork/1fexd/docker-secrets' 2024-09-27 14:48:58 +02:00
1fexd
2135c95fc0 #356 feat: Add support for Docker secrets 2024-09-25 17:56:41 +02:00
Ferdinand Mütsch
6fe801a6fc chore: generate static assets 2024-09-24 17:56:07 +02:00
mylo
455e54c4f2 fix: adjusted KPI spacing, tablet potrait breakpoint 2024-09-24 22:45:23 +08:00
mylo
caa47bcaaf fix: added more breakpoints 2024-09-24 20:56:09 +08:00
mylo
187cf1e9a8 fix: for real this time, mobile, tablet and desktop responsive summary page 2024-09-24 20:51:22 +08:00
mylo
2cfc925085 fix: fixed date picker going out of view in mobile 2024-09-24 11:32:21 +08:00
mylo
81729a20f7 feat: made summary page look better in mobile views 2024-09-24 11:11:04 +08:00
Ferdinand Mütsch
758be88b77 chore: upgrade dependencies 2024-09-08 23:05:31 +02:00
Ferdinand Mütsch
9e97addb1a feat: wildcard aliases (resolve #607) 2024-09-08 22:58:22 +02:00
Ferdinand Mütsch
39ef066ce2 fix: signup rate limiting (resolve #674) 2024-09-08 21:25:52 +02:00
Ferdinand Mütsch
b17096ed3e fix: multi-log to sentry and console in parallel 2024-09-08 21:24:15 +02:00
Ferdinand Mütsch
0580492001 fix: sentry request logging
chore: attach release version info to sentry events
2024-09-07 16:00:14 +02:00
finn
ddffd464e3 Replace SentryLogWrapper by slog-sentry 2024-09-05 11:05:25 +07:00
Peter Oettig
e39c9ecbbf Merge pull request #671 from muety/665-trusted-proxy-ip-ranges
feat: support ip ranges for trusted proxy header auth (resolve #665)
2024-08-25 23:17:36 +02:00
Ferdinand Mütsch
5b5076de7b chore: minor code style [skip ci] 2024-08-25 23:13:53 +02:00
Ferdinand Mütsch
bdb82a8d39 chore: update readme [skip ci] 2024-08-23 09:58:53 +02:00
Ferdinand Mütsch
fdd59aeab1 feat: support ip ranges for trusted proxy header auth (resolve #665) 2024-08-23 09:51:46 +02:00
Ferdinand Mütsch
27bfad1bdc chore: pass sentry environment tag 2024-08-23 08:40:02 +02:00
Ferdinand Mütsch
b59bf0ea63 Merge branch 'master' of github.com:finnng/wakapi into finnng-master 2024-08-23 07:57:01 +02:00
Ferdinand Mütsch
54a780abbf Merge pull request #669 from yuedanlabs/update-docs [skip ci]
docs: Update README.md for docker compose usage
2024-08-23 07:48:05 +02:00
yuedanlabs
3b73b367a1 docs: Update README.md for docker compose usage 2024-08-23 09:19:05 +08:00
finn
34bf742ca8 Use conf.Log().Fatal to replace log.Fatal; Only initial Sentry logger once 2024-08-23 06:11:53 +07:00
yuedanlabs
96905bdf92 docs: Update README.md for docker compose usage 2024-08-22 18:13:16 +08:00
finn
d542ae9602 Update structured log for conf.Log().Request(r), logging middleware, and sentry logger 2024-08-21 09:08:43 +07:00
finn
f665b4497b Tidy go mod to remove logbuch 2024-08-20 12:31:52 +07:00
finn
79f4c03d72 Correct the sentry logs to use the format key:value 2024-08-20 09:20:58 +07:00
finn
b5bb3da9b6 Correct the logs to use the format key:value 2024-08-20 08:45:43 +07:00
finn
118e51139a First round replace logbuch to slog 2024-08-20 05:59:17 +07:00
Ferdinand Mütsch
0702ee25a8 chore: default to go 1.23 [skip ci] 2024-08-14 12:03:00 +02:00
Ferdinand Mütsch
77ec044902 ci: default to latest stable go in dockerfile
ci: sanitize dockerfile to make sonarqube happy [skip ci]
2024-08-11 22:03:15 +02:00
Ferdinand Mütsch
224c28fd93 feat: configurable heartbeats timeout and offset (resolve #156) 2024-08-11 21:48:49 +02:00
Ferdinand Mütsch
7b2f6a3c84 fix(ci): migration tests on mssql 2024-08-11 19:20:22 +02:00
Ferdinand Mütsch
680e3d7036 chore: upgrade dependencies 2024-08-11 19:04:11 +02:00
Ferdinand Mütsch
dada00b57a Merge pull request #660 from twangodev/patch-1 [skip ci]
Fix typo with account deletion
2024-07-22 06:55:37 +02:00
James Ding
cb2e9a7bbf Fix typo with account deletion 2024-07-21 13:59:48 -07:00
Ferdinand Mütsch
a857271b59 Merge pull request #659 from muety/ci/658
ci: run mail tests in workflow
2024-07-12 09:47:06 +02:00
Steven Tang
7a508268dd ci: run mail tests in workflow
Resolves #658
2024-07-11 21:52:01 +10:00
Ferdinand Mütsch
fe672bbfa2 chore: minor code cleanup [skip ci] 2024-07-11 07:50:14 +02:00
Ferdinand Mütsch
f05defc89e chore: upgrade dependencies (resolve #656) 2024-07-10 23:39:41 +02:00
Ferdinand Mütsch
fd6b7542af chore: upgrade to latest smtp lib (resolve #657) 2024-07-10 23:34:12 +02:00
Ferdinand Mütsch
a5565b12ea feat: allow to configure accepting self-signed tls certs for smtp
test: comprehensive smtp tests using smtp4dev
2024-07-10 23:21:11 +02:00
Ferdinand Mütsch
afe8634f0c docs: document projects query parameter as optional (resolve #653) 2024-07-06 08:32:30 +02:00
Ferdinand Mütsch
89550ee5c3 Merge pull request #652 from notarock/master
Improve badge integration sections
2024-06-24 10:35:42 +02:00
Roch D'Amour
cd13e28968 Integrations: show different params for shields.io 2024-06-20 03:55:12 -04:00
Roch D'Amour
9c1e8b9e23 Integrations: url-encode shields label 2024-06-20 03:36:18 -04:00
Ferdinand Mütsch
af85a65d0c Merge remote-tracking branch 'origin/master' 2024-06-13 20:59:46 +02:00
Ferdinand Mütsch
2af6027388 chore(docs): regenerate api docs 2024-06-13 20:59:38 +02:00
Ferdinand Mütsch
d1b940f84a docs: document gnome extension usage [skip ci] 2024-06-03 21:38:51 +02:00
Ferdinand Mütsch
1b230bbf2f fix: make categories stacked bar span full width 2024-05-11 16:32:05 +02:00
Ferdinand Mütsch
0f5bd4185f fix: wakatime summaries view model index error 2024-05-11 08:59:18 +02:00
Ferdinand Mütsch
7acaa83907 chore: update chartjs to latest 3.x version 2024-05-11 08:49:21 +02:00
Ferdinand Mütsch
966e973829 chore: category chart regenerate notice 2024-05-11 08:47:33 +02:00
Ferdinand Mütsch
57b7daae9c feat: implement category chart and summaries (resolve #630) 2024-05-11 08:36:40 +02:00
Ferdinand Mütsch
b22530f083 fix: wakatime dump importer downloading wrong file (resolve #640) 2024-05-10 13:23:08 +02:00
guoguangwu
fd5bdf9920 chore: remove refs to deprecated io/ioutil
Signed-off-by: guoguangwu <guoguangwug@gmail.com>
2024-05-07 21:57:23 +02:00
Ferdinand Mütsch
f2bffe025b chore: add pure postgres sql aggregation script by @cwilby 2024-05-07 21:57:13 +02:00
Ferdinand Mütsch
d4bbf0a2fb fix: make sonarqube happy [skip ci] 2024-04-07 23:13:14 +02:00
Ferdinand Mütsch
7c4145a506 fix: merge relicts 2024-04-07 23:04:37 +02:00
Ferdinand Mütsch
8e30116949 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	README.md
#	config.default.yml
#	config/config.go
#	coverage/coverage.out
#	go.mod
#	go.sum
#	models/user.go
#	models/view/login.go
#	routes/login.go
#	views/signup.tpl.html
2024-04-07 23:01:03 +02:00
Ferdinand Mütsch
9097bc5552 feat: signup captcha (resolve #635) 2024-04-07 22:58:18 +02:00
Ferdinand Mütsch
7f070ae143 chore(test): basic test cases to test for html template being fully rendered (see #634) 2024-04-01 18:10:13 +02:00
Ferdinand Mütsch
6149b0f55e refactor: view models (resolve #634) 2024-04-01 17:55:10 +02:00
Ferdinand Mütsch
724265fb85 ci: update setup-go action 2024-03-31 15:40:59 +02:00
Ferdinand Mütsch
c1cd40ce40 chore: upgrade dependencies (see #632) 2024-03-31 15:36:01 +02:00
Ferdinand Mütsch
1469648796 ci: revert original docker build workflow (resolve #632) [skip ci] 2024-03-31 08:37:33 +02:00
Ferdinand Mütsch
43f6bbde9a Merge pull request #633 from muety/2-11
Special Docker push for 2.11.0
2024-03-31 08:26:26 +02:00
Steven Tang
be54bf2f6f ci: re-enable push 2024-03-31 10:15:34 +11:00
Steven Tang
669f3c4c7e ci: test pushing of 2.11.0 without arm/v7 2024-03-31 10:15:10 +11:00
Ferdinand Mütsch
2ef4111507 fix: make user cleanup opt-out (see #629) 2024-03-30 21:20:55 +01:00
Ferdinand Mütsch
f379e59a25 fix: set default value for vibrant colors picker 2024-03-29 19:33:31 +01:00
Ferdinand Mütsch
34961e7c9a feat: ability to exlclude unknown projects from summaries (resolve #619) 2024-03-29 19:31:09 +01:00
Ferdinand Mütsch
9d50b823ae fix: invite code expiry time
chore: make invite codes shorter
2024-03-29 09:25:54 +01:00
Ferdinand Mütsch
9737d4a775 fix: enable signup button when invite code is used [skip ci] 2024-03-29 00:17:58 +01:00
Ferdinand Mütsch
9015e51dc4 feat: implement invite links (resolve #624) 2024-03-29 00:06:13 +01:00
Ferdinand Mütsch
f9edf093e9 chore(test): add housekeeping unit test [skip ci] 2024-03-27 22:47:56 +01:00
Ferdinand Mütsch
1acbfc6232 chore: inactive users cleanup (resolve #629) 2024-03-27 10:00:35 +01:00
Ferdinand Mütsch
64b6bd72c8 chore: rate limiting for sensitive endpoints (resolve #628) 2024-03-26 15:33:13 +01:00
Ferdinand Mütsch
d087508d3d docs: refer to wakatime integration in initial setup instructions (resolve #611) [skip-ci] 2024-03-26 12:56:05 +01:00
Ferdinand Mütsch
94ae3ab3ff docs: update sponsors 2024-03-26 12:48:25 +01:00
Ferdinand Mütsch
87c034fc48 fix: add apply button to time filter controls (resolve #627) 2024-03-26 12:48:13 +01:00
Ferdinand Mütsch
8e05a40db4 fix: make entities non-clickable (resolve #625) 2024-03-26 12:34:25 +01:00
Ferdinand Mütsch
b1d1dfb31e fix: spacing between wrapped filter controls 2024-03-17 17:26:56 +01:00
Ferdinand Mütsch
ec45c33583 fix: consistently begin weeks on mondays (resolve #623) 2024-03-17 17:25:12 +01:00
Ferdinand Mütsch
997b932b6a Merge pull request #622 from muety/annotations
Add annotations to Dockerfile
2024-03-17 11:56:19 +01:00
Steven Tang
41432c6f21 add annotations to Dockerfile 2024-03-17 14:22:26 +11:00
Ferdinand Mütsch
b71dee2493 fix: settings page responsiveness (resolve #618) 2024-03-15 22:25:59 +01:00
Ferdinand Mütsch
674966dea1 Merge branch 'master' of github.com:muety/wakapi 2024-03-12 22:18:12 +01:00
Ferdinand Mütsch
cd0449a2a2 fix: entity filter dropdown for unknown project (resolve #621) [skip ci] 2024-03-12 22:17:28 +01:00
Ferdinand Mütsch
3b78526ad9 fix: custom date validation 2024-03-12 10:32:09 +01:00
Ferdinand Mütsch
df83e99568 Merge pull request #614 from scarf005/feat-custom-date-format
feat: customizable date format
2024-03-12 10:03:37 +01:00
scarf
7671c1e090 fix: validate date format 2024-03-12 08:14:40 +09:00
scarf
df611b74af docs: update README 2024-03-06 19:07:33 +09:00
scarf
af302ec4d1 docs: update default config 2024-03-06 19:03:24 +09:00
scarf
72d1f8abf6 feat: customize date formatting 2024-03-06 19:03:00 +09:00
scarf
1263860b9b feat: date format config 2024-03-06 19:02:38 +09:00
Ferdinand Mütsch
80dc552d17 fix: wakapi import on clean instance (resolve #610)
fix(perf): make legacy importer much more efficient (see #610)
2024-02-18 16:09:26 +01:00
Ferdinand Mütsch
7dda996b7d chore: upgrade dependencies 2024-02-16 23:36:27 +01:00
Ferdinand Mütsch
1d4f3edbdf feat: add user creation automation script (resolve #604) 2024-02-16 23:35:22 +01:00
Ferdinand Mütsch
303e1a2c24 fix: exclude unknown languages from language specific leaderboard (see #606) 2024-02-16 22:23:29 +01:00
Ferdinand Mütsch
9a9f645562 ci: clear old releases upon new release [skip ci] 2024-02-05 07:40:40 +01:00
Ferdinand Mütsch
0c17a95ce9 fix: prevent empty slice error during summary items insertion 2024-02-02 15:31:59 +01:00
Chen Junda
21e2d5beb2 use transation to create 2024-02-02 22:07:42 +08:00
Chen Junda
b142c1b757 fix 600 2024-02-02 20:54:46 +08:00
Ferdinand Mütsch
2161c885ef fix: dump import (resolve #602) 2024-02-02 11:54:21 +01:00
Ferdinand Mütsch
4ef563c096 docs: add clarifying comments 2024-02-02 10:28:18 +01:00
Chen Junda
0136c3019e try fix 2024-02-02 00:26:58 +08:00
Ferdinand Mütsch
559c23e95f fix: activity chart routing (again) (resolve #599) 2024-01-30 21:43:59 +01:00
Ferdinand Mütsch
56fd7ab345 chore: fix caddyfile [skip ci] 2024-01-30 18:26:35 +01:00
Ferdinand Mütsch
e5b538f7ed chore: dependency upgrades 2024-01-27 08:04:31 +01:00
Ferdinand Mütsch
6efedeca8a Merge remote-tracking branch 'origin/master' 2024-01-26 11:08:14 +01:00
Ferdinand Mütsch
b78b0112a9 fix: activity chart routing for usernames containing dots (resolve #596) 2024-01-26 11:08:04 +01:00
Ferdinand Mütsch
5eb847c70d Merge pull request #597
Fix entity filter
2024-01-25 06:31:47 +01:00
hesstobi
ed8ad01d82 Reset the value by selecting null 2024-01-24 21:57:41 +01:00
Tobias Westerheide
e84a973780 Update the filter selection on event 2024-01-24 15:55:01 +00:00
Ferdinand Mütsch
1ea64f0397 chore: minor formatting [skip ci] 2024-01-16 18:42:44 +01:00
Ferdinand Mütsch
d9aefdde22 Merge pull request #592 from ddadaal/add-mssql-support
Add mssql support
2024-01-16 18:40:28 +01:00
Chen Junda
fd53ee328e fix duplicate heartbeat 2024-01-16 23:41:49 +08:00
Chen Junda
791f5cc5f7 fix foreign key constraints 2024-01-14 17:05:13 +08:00
Ferdinand Mütsch
e70cc59528 chore: add indicator for total number of entity items shown in charts (resolve #588) 2024-01-12 17:03:31 +01:00
Chen Junda
056633c5ec fix some issue 2024-01-12 23:41:18 +08:00
Ferdinand Mütsch
fa2dce0c44 fix: display activity chart when running under subpath (resolve #594) 2024-01-12 16:28:35 +01:00
Ferdinand Mütsch
14a3d2c2af fix: top menu bar on small screens (resolve #593) [skip ci] 2024-01-12 16:14:21 +01:00
Chen Junda
e296886d92 add quotesql function to quote identifiers in sql 2024-01-11 23:02:36 +08:00
Chen Junda
9169560336 revert unnecessary changes 2024-01-10 21:54:52 +08:00
Chen Junda
a467a0a9fd fix 2024-01-10 21:17:48 +08:00
Chen Junda
a7d523b5ce implement 2024-01-10 20:03:38 +08:00
Ferdinand Mütsch
ebc579a48c docs: add community contributions sections [skip ci] 2024-01-09 22:09:43 +01:00
Chen Junda
152767cce5 fix timestamp columns in mssql 2024-01-10 00:16:44 +08:00
Chen Junda
8e6f00b8f1 update 2024-01-09 23:34:42 +08:00
Chen Junda
b8570f6616 fix script 2024-01-09 23:00:52 +08:00
Chen Junda
f57ac9db54 fix script 2024-01-09 22:59:54 +08:00
Chen Junda
10b6281004 add support for mssql 2024-01-09 22:56:05 +08:00
Ferdinand Mütsch
fc483cc35c Merge remote-tracking branch 'origin/master' 2024-01-08 21:59:54 +01:00
Ferdinand Mütsch
7d066fe197 chore(perf): speed up badge endpoints and single-filter summary computation 2024-01-08 21:59:03 +01:00
Ferdinand Mütsch
33d86c7232 fix: auto redirect to summary page for any authentication mechanism (resolve #589) 2024-01-08 21:58:13 +01:00
Ferdinand Mütsch
f04508c3e0 fix: auto redirect to summary page for any authentication mechanism (resolve #589) 2024-01-06 16:26:43 +01:00
Ferdinand Mütsch
fae0128b93 fix(test): badges test 2024-01-02 12:00:34 +01:00
Ferdinand Mütsch
640793fa81 chore: collect additional runtime memory statistics 2024-01-02 11:30:15 +01:00
Ferdinand Mütsch
3cea48bcf5 Merge remote-tracking branch 'origin/master' [skip ci] 2023-12-30 18:12:44 +01:00
Ferdinand Mütsch
d2f1fc7970 fix: clear branch name if no latest branch could be found [skip ci] 2023-12-30 18:12:09 +01:00
Ferdinand Mütsch
dd9bb52666 Merge pull request #586 from muety/compose [skip ci]
rename to compose.yml
2023-12-29 09:42:00 +01:00
Ferdinand Mütsch
a1e27f173d fix: disable ipv6 in docker (resolve #584) 2023-12-29 09:06:23 +01:00
Steven Tang
9787a5ed59 rename to compose.yml 2023-12-29 16:30:18 +11:00
Ferdinand Mütsch
856ee276e4 fix: faulty summary computation with filters enabled (resolve #535) 2023-12-28 18:04:51 +01:00
Ferdinand Mütsch
91e4b9d517 chore: rename variables in auto-install script [skip ci] 2023-12-28 08:42:14 +01:00
Ferdinand Mütsch
da54894f58 chore: upgrade dependencies 2023-12-24 09:05:29 +01:00
Ferdinand Mütsch
36f1fb753b refactor: drop mailwhale support (resolve #581) 2023-12-24 09:03:09 +01:00
Ferdinand Mütsch
846bc15a48 Merge pull request #582 from SupianIDz/master
Updated SALT command syntax [skip-ci]
2023-12-23 22:22:09 +01:00
Supian M
4fea0c9a3a Updated SALT command syntax
Updated SALT command syntax to avoid illegal byte sequence errors.
2023-12-24 03:04:24 +08:00
Ferdinand Mütsch
231f708929 Merge remote-tracking branch 'origin/master' 2023-12-16 11:01:44 +01:00
Ferdinand Mütsch
b23d5145b4 fix: properly parse neovim (resolve #578) 2023-12-16 11:01:33 +01:00
Ferdinand Mütsch
c292b79763 Merge pull request #577 from muety/ci/cache
ci: update actions, cache go dependencies
2023-12-15 13:01:21 +01:00
Steven Tang
2d6bd7340d ci: update actions, cache go dependencies 2023-12-15 22:21:47 +11:00
Ferdinand Mütsch
d933584351 fix: nil pointer in user metrics endpoint with disabled leaderboard (resolve #576) 2023-12-12 08:45:21 +01:00
Ferdinand Mütsch
ae40a3dc20 chore: add marketing logo 2023-12-12 08:45:16 +01:00
Ferdinand Mütsch
e70f45638b fix: faulty parameter in default config (resolve #573) 2023-12-02 19:19:54 +01:00
Ferdinand Mütsch
7c29a9aafb chore: require go 1.21 2023-12-02 17:49:22 +01:00
Ferdinand Mütsch
db90463684 feat: include user rank in prometheus metrics (resolve #566) 2023-12-02 14:28:24 +01:00
Ferdinand Mütsch
f82ab8293b fix: make leaders endpoint publicly accessible 2023-12-02 14:13:25 +01:00
Ferdinand Mütsch
99f2b3ced5 feat: implement language filtering in leaders compat endpoint 2023-12-02 14:02:02 +01:00
Ferdinand Mütsch
cbd476f0de feat: implement leaders compat endpoint (resolve #570) 2023-12-02 13:43:38 +01:00
Ferdinand Mütsch
439b400aed refactor: embed activity chart in dom directly (resolve #569) 2023-12-02 11:27:54 +01:00
Ferdinand Mütsch
29e85b4504 chore: upgrade dependencies 2023-12-01 17:15:47 +01:00
Ferdinand Mütsch
aa489e3366 chore: ability to disable leaderboard (resolve #571) 2023-12-01 17:14:23 +01:00
Ferdinand Mütsch
c6cf4a2dda docs: document leaderboard scope config option 2023-12-01 11:26:41 +01:00
Ferdinand Mütsch
7d156cd3ed feat: configurable leaderboard interval (resolve #568)
chore: hide zero entries on leaderboard
2023-12-01 11:24:17 +01:00
Ferdinand Mütsch
20e442b58f feat: add single project compat endpoint (resolve #562) 2023-12-01 10:30:20 +01:00
Ferdinand Mütsch
8449348ad5 Merge pull request #565 from Juneezee/perf/regexp-matchstring
perf: avoid allocations with `(*regexp.Regexp).MatchString`
2023-11-24 17:20:00 +01:00
Eng Zer Jun
cbdf341361 perf: avoid allocations with (*regexp.Regexp).MatchString
We should use `(*regexp.Regexp).MatchString` instead of
`(*regexp.Regexp).Match([]byte(...))` when matching string to avoid
unnecessary `[]byte` conversions and reduce allocations.

Example benchmark:

func BenchmarkMatch(b *testing.B) {
	for i := 0; i < b.N; i++ {
		if match := emailAddrRegex.Match([]byte("email@example.com")); !match {
			b.Fail()
		}
	}
}

func BenchmarkMatchString(b *testing.B) {
	for i := 0; i < b.N; i++ {
		if match := emailAddrRegex.MatchString("email@example.com"); !match {
			b.Fail()
		}
	}
}

goos: linux
goarch: amd64
pkg: github.com/muety/wakapi/models
cpu: AMD Ryzen 7 PRO 4750U with Radeon Graphics
BenchmarkMatch-16          	 1149106	      1012 ns/op	      24 B/op	       1 allocs/op
BenchmarkMatchString-16    	 1566058	       727.8 ns/op	       0 B/op	       0 allocs/op
PASS
ok  	github.com/muety/wakapi/models	3.521s

Signed-off-by: Eng Zer Jun <engzerjun@gmail.com>
2023-11-24 23:54:28 +08:00
Ferdinand Mütsch
c714085ddb chore: test coverage 2023-11-24 09:03:26 +01:00
InsanePrawn
47a6e09d67 support empty version string in user agent regex 2023-11-24 07:17:28 +01:00
Ferdinand Mütsch
54efd45bb8 Merge pull request #563 from alanhamlett/master
Use api subdomain for load balancing
2023-11-21 23:44:56 +01:00
Alan Hamlett
af040e29b5 Use api subdomain for load balancing 2023-11-21 23:21:40 +01:00
Ferdinand Mütsch
dcb522e032 Merge pull request #561 from GioPan04/fix-external-avatar-api
fix: avatar url wrong url using external api
2023-11-20 18:06:20 +01:00
GioPan04
ba5e3c2603 fix: avatar url wrong url using external api 2023-11-20 16:57:50 +01:00
Ferdinand Mütsch
dbdf44eda6 chore: add utility method to execute functions while turning panics into errors (resolve #560) 2023-11-19 12:52:58 +01:00
Ferdinand Mütsch
0772d73347 chore: make summaries compat view model included fields consistent with wakatime 2023-11-19 11:19:03 +01:00
GioPan04
462b113ef9 fix: crash on project summary 2023-11-19 11:00:42 +01:00
Ferdinand Mütsch
de9a42b792 feat: add ability to disable imports (resolve #552) 2023-11-18 18:04:35 +01:00
Ferdinand Mütsch
48806a40f1 Merge branch 'show-avatar-api' 2023-11-18 17:35:38 +01:00
Ferdinand Mütsch
6b6a0d116e fix: tests 2023-11-18 17:35:29 +01:00
Ferdinand Mütsch
b79e5ca272 Merge pull request #556 from muety/fix/run-tests-script
fix: run api tests ordering
2023-11-18 08:35:53 +01:00
Steven Tang
4bdc4e6325 fix: run api tests ordering 2023-11-18 10:13:37 +11:00
GioPan04
8a3dcd2753 feat: show user id as display_name in user 2023-11-17 13:06:58 +01:00
GioPan04
2ed03ec445 feat: show avatar url in wakatime user model 2023-11-17 13:03:14 +01:00
Ferdinand Mütsch
38cb6dc516 Merge branch 'updated-projects-api' 2023-11-16 20:05:53 +01:00
Ferdinand Mütsch
f68c37c8d8 fix: nil pointer in case of failing db connection setup (see #549) 2023-11-16 19:59:05 +01:00
Gioele Pannetto
63c45ac36f feat: added more info per project 2023-11-15 18:18:20 +01:00
Ferdinand Mütsch
4677687f94 chore: return empty list instead of null for unauthorized stats endpoint requests
fix(test): adapt tests
2023-11-14 08:17:30 +01:00
Gioele Pannetto
f43acc9497 fix: logged user see own stats 2023-11-13 14:00:12 +01:00
Gioele Pannetto
6c742e3d78 fix: ignore permissions when logged in stats api 2023-11-13 13:48:50 +01:00
Ferdinand Mütsch
8929ca0fde chore: upgrade dependencies 2023-11-12 11:38:12 +01:00
Ferdinand Mütsch
a1172f9199 chore: consistently use spaces over tabs 2023-11-12 11:35:09 +01:00
Ferdinand Mütsch
6935d66b30 fix: divide by zero during compat summary generation 2023-11-12 11:33:48 +01:00
Ferdinand Mütsch
898983b4d2 feat: add option to choose legacy wakatime importer (resolve #542) 2023-11-12 11:27:57 +01:00
Ferdinand Mütsch
046f41cbcc feat: implement daily average field for wakatime compat summary (resolve #545) 2023-11-12 10:55:14 +01:00
Ferdinand Mütsch
411df74943 fix: correct prevously intended typo in wakatime summary view model (see #545) 2023-11-12 09:27:26 +01:00
Ferdinand Mütsch
4995fdf072 fix: nil pointer deref in wakatime dump import (resolve #544) 2023-11-05 16:23:12 +01:00
Ferdinand Mütsch
0e66b2d36c fix: always permit badges for logged in user (resolve #533) 2023-10-25 08:16:09 +02:00
Ferdinand Mütsch
329e16e419 docs: fix readme link [skip ci] 2023-10-24 08:46:36 +02:00
Ferdinand Mütsch
bc8e7f1234 chore: minor fix in latest migration 2023-10-23 12:11:26 +02:00
Ferdinand Mütsch
a49fe28fe8 chore: data retention notice on first login 2023-10-23 12:00:38 +02:00
Ferdinand Mütsch
e84ee1aa94 chore: implement support for last branch placeholder (resolve #538) 2023-10-23 11:48:19 +02:00
Ferdinand Mütsch
cee76b115f feat: support for reverse proxy trusted header authentication (resolve #534) 2023-10-15 22:17:06 +02:00
Ferdinand Mütsch
fc07c0611a chore: allow promo codes during checkout [skip ci] 2023-10-06 09:37:45 +02:00
Ferdinand Mütsch
d93af1130b fix: hide attribution in activity chart on summary page (resolve #533) 2023-10-05 21:12:17 +02:00
Ferdinand Mütsch
e22e3df4c7 fix: skip sharing permissions check at badge endpoint for authorized users (see #533) 2023-10-05 21:03:57 +02:00
Ferdinand Mütsch
6946ad5e3c fix: project stats query for postgres (resolve #531) 2023-10-04 20:03:31 +02:00
Ferdinand Mütsch
ce7b07ec75 chore: add alt tag to activity chart [skip ci] 2023-10-03 20:39:29 +02:00
Ferdinand Mütsch
47576b6938 feat: finish and include activity chart (resolve #12) 2023-09-28 22:54:53 +02:00
Ferdinand Mütsch
6135ca0603 feat(wip): implement activity chart generation (see #12) 2023-09-28 13:41:10 +02:00
Ferdinand Mütsch
dff9587bad feat: add ui controls for filtering (resolve #511) 2023-09-27 11:08:04 +02:00
Ferdinand Mütsch
d66c441a82 feat: add color gradient to indicate project intensity (see #302)
fix: project stats query limit
2023-09-27 09:38:12 +02:00
Ferdinand Mütsch
ec2c6b1090 feat: add pagination to projects page (resolve #302) 2023-09-26 23:18:01 +02:00
Ferdinand Mütsch
a06be491d9 feat(wip): link from project page to summaries 2023-09-26 21:33:53 +02:00
Ferdinand Mütsch
10da265acd chore: always log errors to sentry wherever possible [skip ci] 2023-09-26 16:46:07 +02:00
Ferdinand Mütsch
24b34beef1 feat(wip): first draft of projects page implementation [skip ci] 2023-09-26 16:42:24 +02:00
Steven Tang
793d9e01d9 ci: reduce permissions for non-release actions 2023-09-24 14:33:35 +10:00
Ferdinand Mütsch
219a24ca94 chore: pre-warm project stats cache [skip ci] 2023-09-23 15:04:16 +02:00
Ferdinand Mütsch
51b4362993 chore: refactorings and improvements for project stats calculation interfaces [skip ci] 2023-09-22 18:53:58 +02:00
Ferdinand Mütsch
e21d9b24a6 feat(wip): implement query for fetching user project stats (see #302) [skip ci] 2023-09-22 16:16:47 +02:00
Ferdinand Mütsch
c925a6dfc4 ci: remove mapi stuff 2023-09-21 21:53:17 +02:00
Ferdinand Mütsch
510b9fbf7a chore: remove debug output 2023-09-21 21:50:06 +02:00
Ferdinand Mütsch
9668a752d6 refactor: use third-party worker pool inside wakatime heartbeats import 2023-09-21 21:47:23 +02:00
Ferdinand Mütsch
8a740d2920 feat: expose total user activity time in admin metrics (resolve #484) 2023-09-21 21:38:43 +02:00
Ferdinand Mütsch
b2c8712785 chore: warn about overlapping summaries 2023-09-21 17:34:44 +02:00
Ferdinand Mütsch
37ac895bd8 fix: skip summary cache for sub second-level date precision (resolve #522) 2023-09-20 15:17:47 +02:00
Ferdinand Mütsch
7db470caf8 chore: expose gc related metrics [skip ci] 2023-09-20 13:04:26 +02:00
Ferdinand Mütsch
435560b3dd chore: include optional pprof profiling 2023-09-20 12:41:41 +02:00
Ferdinand Mütsch
2561fb39a9 chore: allow specifying connection dsn for mysql 2023-09-20 12:41:21 +02:00
Ferdinand Mütsch
4314be350f chore: introduce config parameter for min time between two consecutive imports 2023-09-13 22:49:26 +02:00
Ferdinand Mütsch
5059f941d5 fix: nil point error after unsuccessful wakatime key validation 2023-09-13 17:22:41 +02:00
Ferdinand Mütsch
7f2544f003 fix: reverse interval resolution 2023-09-02 10:33:26 +02:00
Ferdinand Mütsch
ce115e0812 chore: upgrade dependencies 2023-09-02 10:33:10 +02:00
Ferdinand Mütsch
92703e4271 fix: properly parse edge user agent as editor (resolve #516) 2023-09-02 09:50:20 +02:00
Ferdinand Mütsch
05bc55a488 docs: instructions how to set up browser extension [skip ci] 2023-07-30 08:17:14 +02:00
Ferdinand Mütsch
a9364e3d9e Merge remote-tracking branch 'origin/master' 2023-07-28 12:08:54 +02:00
Ferdinand Mütsch
ec65847d0c fix: make stats endpoint default to user-chosen time range (resolve #508)
chore: include more properties in status model for better compatibility
2023-07-28 12:08:47 +02:00
Ferdinand Mütsch
eca443be35 Merge pull request #507 from cbrand/master
fix failing migration which prohibits startup
2023-07-21 08:50:42 +02:00
Christoph Brand
04ec44dcef fix: failing migration
Fix an issue in migration which results in the following error message
due to wrong and or precedence configuration:
```
panic: runtime error: index out of range [0] with length 0
```
2023-07-20 18:44:26 +02:00
Ferdinand Mütsch
938290b2da Merge remote-tracking branch 'origin/master' 2023-07-19 18:36:35 +02:00
Ferdinand Mütsch
c8b88ccef5 chore: log response body of failed http requests 2023-07-19 18:36:27 +02:00
Ferdinand Mütsch
bc2d05bd85 ci: skip multi-platform build step on pushes and prs [skip ci] 2023-07-14 08:50:23 +02:00
Ferdinand Mütsch
3785867c3a Merge pull request #504 from muety/502-imports
Simplify import checks
2023-07-14 08:47:54 +02:00
Ferdinand Mütsch
56de275781 chore: simplify import checks
fix: minor fixes
2023-07-13 20:48:56 +02:00
Edward
583ddcab7a refactor: remove repeated code in readyPollTimer 2023-07-14 00:33:55 +08:00
Edward
7b0bbcefe6 fix(import): data dump already exists
handle the import when there is already an active data dump exists.

Resolves #502
2023-07-13 23:54:48 +08:00
Ferdinand Mütsch
5f1ca4ed69 ci: set minimum go version to 1.20 [skip-ci] 2023-07-09 20:32:26 +02:00
Ferdinand Mütsch
c06b2b8aca chore: bump to go 1.20 2023-07-09 20:30:00 +02:00
Ferdinand Mütsch
45a003185e chore: minor code style and cleanup 2023-07-09 20:16:34 +02:00
Ferdinand Mütsch
3063e80692 refactor: use wakatime dump api for data imports (resolve #323) 2023-07-09 18:03:41 +02:00
Ferdinand Mütsch
38286c7f3a fix: correctly parse os and editor for chrome plugin
fix: handle last project special keyword
2023-07-09 10:28:23 +02:00
Ferdinand Mütsch
37e6acd058 Merge pull request #501 from muety/ci/runner-version
ci(release): update to ubuntu-latest
2023-07-09 09:26:27 +02:00
Steven Tang
07b24fe3b1 ci: update to ubuntu-latest 2023-07-09 13:42:49 +10:00
Ferdinand Mütsch
78f327dbeb docs: add instructions for using export script 2023-07-08 20:48:38 +02:00
Ferdinand Mütsch
2af82f529a chore: testing scripts for cockroachdb 2023-07-08 20:33:07 +02:00
Ferdinand Mütsch
5278dba4f4 feat: add per weekday stats to report (resolve #493) 2023-07-08 19:54:15 +02:00
Ferdinand Mütsch
35ef323b19 security: migrate to argon2id password hashing
fix: support super long passwords (resolve #494)
2023-07-08 19:15:59 +02:00
Ferdinand Mütsch
a8e2bc671d fix: badge endpoint caching (resolve #496) 2023-07-08 18:44:40 +02:00
Ferdinand Mütsch
7b60c44ac6 chore: update sentry sdk 2023-07-08 18:36:25 +02:00
Ferdinand Mütsch
055d006379 chore: upgrade dependencies 2023-07-08 18:33:08 +02:00
Ferdinand Mütsch
1a6ee55d14 fix: respect errors thrown in misc functions 2023-05-15 21:25:03 +02:00
Miles Liu
74390bfccf fix: invalid search query in Postgres 2023-05-15 14:33:12 +08:00
Ferdinand Mütsch
8de56a4c7b docs: limit readme stats preview to past week [skip ci] 2023-05-08 19:21:19 +02:00
Ferdinand Mütsch
a6915a187a fix: properly handle error responses during wakatime import 2023-04-21 09:43:10 +02:00
Ferdinand Mütsch
df25183035 fix: return ok status even upon subscription event handling failure [skip ci] 2023-04-10 18:49:29 +02:00
Ferdinand Mütsch
b33c71b41f chore: upgrade dependencies 2023-04-10 10:04:48 +02:00
Ferdinand Mütsch
a9e1c4b589 docs: update readme [skip ci] 2023-04-09 17:40:01 +02:00
Ferdinand Mütsch
dc4eefbede fix: exclude unknown languages from leaderboard time 2023-04-09 17:35:55 +02:00
Ferdinand Mütsch
a20456bb8e fix: cancel active subscription upon user account deletion 2023-04-09 17:29:57 +02:00
Steven Tang
44c481b9e0 ci: upgrade docker action 2023-04-04 23:00:06 +10:00
Ferdinand Mütsch
beced39923 fix: include query params with relayed request 2023-04-04 00:15:56 +02:00
Ferdinand Mütsch
083fbf8633 fix: view details of unknown projects on dashboard 2023-04-03 20:30:23 +02:00
Ferdinand Mütsch
ca3320b174 Merge pull request #479 from muety/cli-version
feat: -version flag
2023-04-03 15:10:26 +02:00
Steven Tang
406f5147c8 feat: -version flag 2023-04-03 22:46:31 +10:00
Steven Tang
d061a4ef1b Revert "ci: add workflow_dispatch to docker.yml"
This reverts commit 65c2d9a17f.
2023-03-27 21:53:18 +11:00
Steven Tang
65c2d9a17f ci: add workflow_dispatch to docker.yml 2023-03-27 21:28:47 +11:00
Steven Tang
de2702241b ci: consistent trigger for release docker 2023-03-25 11:53:15 +11:00
Ferdinand Mütsch
31664c25a8 chore: exclude avatar routes from logging [skip ci] 2023-03-24 23:16:36 +01:00
Ferdinand Mütsch
c82186046c chore: add support indicator for users 2023-03-24 23:12:51 +01:00
Ferdinand Mütsch
24fec41ec6 fix: top files picker 2023-03-22 20:49:47 +01:00
Ferdinand Mütsch
4ee3da6f7e feat: implement file statistics (resolve #80) 2023-03-22 20:45:27 +01:00
Ferdinand Mütsch
0cf09a0871 fix: updating subscription renewal date 2023-03-21 18:05:34 +01:00
Ferdinand Mütsch
bfeebafb2f fix: support user agents set by browser plugin (see #237) 2023-03-16 21:02:28 +01:00
Ferdinand Mütsch
c9f2518fbc fix: badges broken (resolve #475) 2023-03-15 21:47:12 +01:00
Ferdinand Mütsch
46a248ac30 docs: add note about necessity to enable public data for github readme integration (resolve #474) [skip-ci] 2023-03-12 19:00:33 +01:00
Ferdinand Mütsch
fde45a5138 fix: include missing mail headers (resolve #472) 2023-03-06 20:31:31 +01:00
Ferdinand Mütsch
ce077f2efc chore: ability to disable home page (resolve #460) 2023-03-04 09:33:36 +01:00
Ferdinand Mütsch
b1dff58025 fix: minor fix for key generation 2023-03-03 22:05:36 +01:00
Ferdinand Mütsch
6c75bb5d21 chore: reuse securecookie keys in dev mode (resolve #463) 2023-03-03 21:44:13 +01:00
Ferdinand Mütsch
a6ef735ba1 refactor: replace gorilla mux with chi 2023-03-03 20:53:18 +01:00
Ferdinand Mütsch
e495468be2 chore: add support for astro files (see #469) 2023-02-19 20:14:22 +01:00
Ferdinand Mütsch
f67115a788 fix: disallow blank space usernames 2023-02-19 19:41:20 +01:00
Ferdinand Mütsch
3512db5ca4 fix: track subscription renewal date 2023-02-19 19:37:03 +01:00
Ferdinand Mütsch
23f9787b69 chore: update license [skip ci] 2023-02-19 11:29:21 +01:00
Ferdinand Mütsch
e21c9ff6be docs: update readme [skip ci] 2023-02-13 23:47:50 +01:00
Steven Tang
e22dda6f7b chore: update actions versions 2023-02-11 13:21:26 +11:00
Ferdinand Mütsch
592e0ed135 chore: add runtime directory param to systemd unit [skip ci] 2023-01-29 16:41:15 +01:00
Ferdinand Mütsch
83ecf43d90 chore: upgrade dependencies 2023-01-26 16:49:35 +01:00
Ferdinand Mütsch
a6ce6725db docs: update readme [skip ci] 2023-01-25 12:35:01 +01:00
Ferdinand Mütsch
dc6985bbb0 Merge pull request #465 from muety/docker-user
Non-root user for Docker
2023-01-25 12:34:34 +01:00
Steven Tang
b04a84ee5a docker: add non-root user 2023-01-25 21:56:54 +11:00
Steven Tang
391a53972b chore: update .dockerignore 2023-01-25 21:54:22 +11:00
Ferdinand Mütsch
7a52a076b1 fix(ci): replace deprecated codeql version 2023-01-25 11:14:22 +01:00
Ferdinand Mütsch
fa70ff6c43 chore: minor ui changes 2023-01-25 09:41:52 +01:00
Diptesh Choudhuri
db89d30cda fix(settings.tpl): remove useless attribute 2023-01-25 05:03:57 +00:00
Diptesh Choudhuri
974e4c39fa feat(settings): add btn to toggle add project btn 2023-01-25 05:00:31 +00:00
Diptesh Choudhuri
3be80896bb fix(settings): change background color for options 2023-01-25 03:20:42 +00:00
Diptesh Choudhuri
56c35daed4 fix(settings): parse select as list 2023-01-25 03:14:25 +00:00
Diptesh Choudhuri
69f59a9a16 feat(settings): handle bulk associations in db 2023-01-24 16:31:46 +00:00
Diptesh Choudhuri
fb02916b1e feat(settings): add UI for bulk associations 2023-01-24 16:30:18 +00:00
Diptesh Choudhuri
6855539315 fix(settings): remove useless commas 2023-01-24 15:07:46 +00:00
Diptesh Choudhuri
998ff009f5 chore(settings): remove duplicated code 2023-01-24 13:41:12 +00:00
Diptesh Choudhuri
0b8181acab feat: insert label into database 2023-01-24 13:23:41 +00:00
Diptesh Choudhuri
57c1582ed1 feat: add UI changes to add project to label 2023-01-24 12:14:17 +00:00
Ferdinand Mütsch
749782b15b chore: error handling in mailing script [skip ci] 2023-01-23 16:23:18 +01:00
Ferdinand Mütsch
c42985a279 Merge pull request #461 from muety/tz-test
test: remove api call for timezone
2023-01-22 14:09:25 +01:00
Steven Tang
71563c2c33 test: remove api call for timezone 2023-01-21 19:39:28 +11:00
Ferdinand Mütsch
0160a9ae52 Merge pull request #458 from xxchan/xxchan/perfect-bobolink
fix: when importing from wakapi, os & editor are reversed
2023-01-20 10:12:36 +01:00
xxchan
9751f8d11d fix: when importing from wakapi, os & editor are reversed 2023-01-19 23:52:53 +01:00
Ferdinand Mütsch
a1048d480a chore: introduce dry run flag for data cleanup [skip ci] 2023-01-18 09:26:01 +01:00
Ferdinand Mütsch
8ccfcef8e3 chore: show warning message when data about to expire 2023-01-18 01:27:07 +01:00
Ferdinand Mütsch
934178412e fix: default cleanup job cron expression 2023-01-18 01:26:22 +01:00
Ferdinand Mütsch
9d384e5d1c fix: respect requested user in summary compat endpoint (resolve #455) 2023-01-17 10:39:41 +01:00
Ferdinand Mütsch
1615a35628 Merge pull request #456 from Kichiyaki/master
fix(metrics): warning - postgres - failed to get database size (expected 0 arguments, got 1)
2023-01-16 18:31:43 +01:00
Dawid Wysokiński
2e0e791853 fix(metrics): warning - postgres - failed to get database size (expected 0 arguments, got 1) 2023-01-16 17:23:39 +01:00
Ferdinand Mütsch
97a10cc08a chore: add tests for heartbeat hashing 2023-01-15 20:41:09 +01:00
Ferdinand Mütsch
3922c3767d chore: fix log line [ci-skip] 2023-01-14 17:08:48 +01:00
Ferdinand Mütsch
efbfd5c231 fix: adapt csp header for subscriptions [ci-skip] 2023-01-13 14:51:35 +01:00
Ferdinand Mütsch
91b89645ae feat: add heartbeats download script 2023-01-12 23:43:39 +01:00
Ferdinand Mütsch
c0a0da2170 chore: ability to configure socket mode 2023-01-08 17:14:43 +01:00
Ferdinand Mütsch
41311a8b06 fix: sentry logging without user authentication [ci-skip] 2023-01-08 15:52:36 +01:00
Ferdinand Mütsch
0c51d8682b chore: serve static assets without compression in dev mode 2023-01-07 23:59:50 +01:00
Ferdinand Mütsch
0cb1afc5b5 fix: leaderboard ui on small screens 2023-01-07 23:59:21 +01:00
Ferdinand Mütsch
75cc071222 fix: sign up api test 2023-01-02 18:28:41 +01:00
Ferdinand Mütsch
e8310cfa69 fix: ci tests 2023-01-02 18:18:58 +01:00
Ferdinand Mütsch
746608c062 refactor: flash messages framework (resolve #446) 2023-01-02 18:05:28 +01:00
Ferdinand Mütsch
a1444bca8c chore: validate email addresses with dns 2023-01-02 15:31:28 +01:00
Ferdinand Mütsch
ef5b49ebd8 chore: clear user cache upon logout 2023-01-02 14:53:21 +01:00
Ferdinand Mütsch
fb5b2f52c7 fix: make wakatime relay middleware accept single heartbeat format (resolve #445) 2023-01-02 11:33:47 +01:00
Ferdinand Mütsch
a49abfe0de docs: update readme [ci-skip] 2023-01-02 11:16:39 +01:00
Ferdinand Mütsch
cf5a515952 fix: sentry middleware interface conversion 2023-01-02 10:55:57 +01:00
Ferdinand Mütsch
c1f1b05fa8 chore: add data privacy notice 2023-01-01 22:08:10 +01:00
Ferdinand Mütsch
9166c98df7 chore: script to send mass mail via mailwhale 2022-12-31 16:36:33 +01:00
Ferdinand Mütsch
bfd2832846 fix: minor fixes 2022-12-31 16:03:44 +01:00
Ferdinand Mütsch
814e74a41e fix: don't require db param for api test script 2022-12-30 14:07:43 +01:00
Ferdinand Mütsch
f755275309 fix: tests 2022-12-30 13:41:27 +01:00
Ferdinand Mütsch
731598fa38 fix: critical bug with data retention / cleanup 2022-12-30 13:32:05 +01:00
Ferdinand Mütsch
8e521741f8 refactor(subscriptions): store stripe customer id with user 2022-12-30 13:14:24 +01:00
Ferdinand Mütsch
3aac5e9062 fix: tests 2022-12-29 17:26:15 +01:00
Ferdinand Mütsch
50c54685ec feat: subscription expiry notification mails 2022-12-29 17:12:34 +01:00
Ferdinand Mütsch
dc0bcbe65d chore: cap data import according to max data retention time 2022-12-29 12:33:21 +01:00
Ferdinand Mütsch
bafbc34706 refactor: minor code refactorings 2022-12-29 11:55:09 +01:00
Ferdinand Mütsch
8ca1404f8b fix: dont clean data for subscribed users 2022-12-29 11:17:24 +01:00
Ferdinand Mütsch
195755581b chore: require email address for subscriptions 2022-12-29 11:17:24 +01:00
Ferdinand Mütsch
8a94fef06b feat: implement computation of users first heartbeats data time 2022-12-29 11:17:24 +01:00
Ferdinand Mütsch
ebcf87ea93 feat(wip): polish settings ui for subscriptions 2022-12-29 11:17:24 +01:00
Ferdinand Mütsch
0e83ab02fa feat(wip): implement stripe webhooks 2022-12-29 11:17:24 +01:00
Ferdinand Mütsch
05ea05cdf4 feat(wip): implement stripe webhooks 2022-12-29 11:17:24 +01:00
Ferdinand Mütsch
f39ecc46bd feat(wip): stripe integration for subscriptions 2022-12-29 11:17:24 +01:00
Ferdinand Mütsch
333c1b5dd0 feat(subscriptions): introduce config options and user attribute to support subscriptions 2022-12-29 11:17:24 +01:00
Ferdinand Mütsch
52d45d4644 Merge pull request #448 from muety/test-migrations
Basic migration tests for mysql/mariadb/postgres
2022-12-27 15:35:45 +01:00
Steven Tang
f46f24f0be test: ref the config conditional 2022-12-27 20:58:17 +11:00
Steven Tang
9cce0ac2e1 test: revert to docker compose 2022-12-27 20:42:55 +11:00
Steven Tang
497046d0a4 test: address pr comments 2022-12-27 20:41:18 +11:00
Steven Tang
03af194385 test: more efficient database ready detection 2022-12-27 15:35:11 +11:00
Steven Tang
ad704cef5c test: migration testing for mysql/mariadb/postgres 2022-12-27 15:19:30 +11:00
Ferdinand Mütsch
cd5c511474 fix: enable experimental column altering for cockroachdb (see #442) 2022-12-16 12:33:01 +01:00
Ferdinand Mütsch
8a26e24081 chore: minor changes 2022-12-06 21:01:21 +01:00
Ferdinand Mütsch
db6dde32cd Merge branch 'upgrade-testing' 2022-12-06 21:00:04 +01:00
Ferdinand Mütsch
394215e53b Merge branch 'allow-mysql-socket' 2022-12-06 20:50:08 +01:00
Ferdinand Mütsch
27586f3a54 Merge branch 'remove-config-file-requirement' 2022-12-06 20:46:35 +01:00
Soner Sayakci
9e9e9fbef9 check still for empty string 2022-12-06 12:19:18 +00:00
Soner Sayakci
bc9132f84d fix: remove config file requirement, fixes #435 2022-12-06 12:17:28 +00:00
Soner Sayakci
e7b6a87153 feat: allow using mysql socket, fixes #433 2022-12-06 12:10:18 +00:00
Soner Sayakci
0a2cba647c fix: disabling tcp webserver sockets, fixes #434 2022-12-06 11:55:33 +00:00
Steven Tang
f5395e36ad ci: SQLite upgrade testing comments 2022-12-06 18:39:09 +11:00
Ferdinand Mütsch
9f38246fe2 Merge pull request #425 from Daste745/persistent-summary-interval
Persistent summary time interval
2022-12-05 19:27:07 +01:00
Steven Tang
5242df2b7d ci: upgrade testing for SQLite 2022-12-04 18:06:48 +11:00
Daste
10648d66ad Remove redundant logic in client-side javascript 2022-12-03 12:51:33 +01:00
Daste
97fab3e109 Redirect to correct summary page if interval cookie is set
This adds an additional 302 redirect when the user doesn't specify an
`interval` as a query param, but has the `wakapi_summary_interval`
cookie set.
2022-12-03 12:47:05 +01:00
Ferdinand Mütsch
0f3b41c2dd fix(ci): adapt docker and gha build to use go 1.19 2022-12-03 00:32:30 +01:00
Ferdinand Mütsch
5ae7527b7b feat: implement data retention mechanism 2022-12-01 20:26:03 +01:00
Ferdinand Mütsch
2db065d47a Merge branch 'muety/427-job-processing' 2022-12-01 15:55:40 +01:00
Ferdinand Mütsch
0e5c5a56d2 chore: dependency upgrades 2022-12-01 15:31:19 +01:00
Ferdinand Mütsch
a4b89d3a69 fix: concurrency bugs with summary aggregation and user counting 2022-12-01 14:13:52 +01:00
Ferdinand Mütsch
aab9e98ebd fix: error handling for user counting
fix: make user counting thread-safe
2022-12-01 13:46:06 +01:00
Ferdinand Mütsch
d4945c982f fix: tests 2022-12-01 11:11:45 +01:00
Ferdinand Mütsch
964405f349 chore: refine report scheduling 2022-12-01 10:57:51 +01:00
Ferdinand Mütsch
21f6809f05 refactor: split utility functions into utils and helpers 2022-12-01 10:57:07 +01:00
Ferdinand Mütsch
c5fda02900 docs: update default cron expressions 2022-12-01 10:10:39 +01:00
Ferdinand Mütsch
10e432c185 Merge pull request #431 from rummik/patch-1
Increase avatar entropy
2022-11-29 23:07:28 +01:00
*Kim Zick
f121112d09 Increase avatar entropy 2022-11-26 16:26:03 -05:00
Ferdinand Mütsch
c13fc96a16 refactor: use job queue for data imports 2022-11-20 11:09:51 +01:00
Ferdinand Mütsch
61f13fce20 fix: prometheus metrics types 2022-11-20 10:59:06 +01:00
Ferdinand Mütsch
99e50b1062 chore: logging 2022-11-20 10:12:34 +01:00
Ferdinand Mütsch
4ce75c2acb chore: clean up dependencies 2022-11-20 10:11:23 +01:00
Ferdinand Mütsch
fcca881cfc refactor: move more background jobs to using job queue 2022-11-20 10:10:24 +01:00
Ferdinand Mütsch
e2ef54152d refactor(wip): introduce job processing system
refactor: adapt report generation scheduling
2022-11-19 22:21:51 +01:00
Daste
ebe1836ac6 Write a Set-Cookie header with the last used summary interval 2022-11-19 09:52:44 +01:00
Ferdinand Mütsch
b1a12a5759 Merge pull request #430 from xiecang/bugfix/send_smtp_mail
fix: ignore error "starttls command has been already sent"
2022-11-17 08:31:24 +01:00
xiecang
ae407fffca fix: ignore error "starttls command has been already sent" 2022-11-16 12:29:20 +08:00
Daste
e89ce076fd Read the persisted summary interval from a cookie
This cookie will be read only if the `interval` or `from` query params
are not set. If the cookie is also unset, it will still default to
the "today" interval.

TODO: The cookie still needs to be set on the client
with a `Set-Cookie` response header.
2022-11-05 19:30:42 +01:00
Daste
ba81c07345 Display persistent summary interval into the front-end time picker 2022-11-05 19:23:39 +01:00
427 changed files with 27805 additions and 10824 deletions

View File

@@ -5,6 +5,9 @@ config*.yml
*.exe
wakapi
Dockerfile
docker-compose.yml
compose.yml
.dockerignore
.git*
node_modules/
testing/
coverage/

BIN
.github/assets/frachtwerk_logo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

BIN
.github/assets/screenshot_gnome.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
.github/assets/servercamp_logo.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -6,98 +6,55 @@ on:
jobs:
test:
name: 'Unit- & API tests'
runs-on: ubuntu-latest
# because of https://github.com/usebruno/bruno/issues/5003, we currently need Node.js v20, so pinning runner image at ubuntu 24.04 for now
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v4
- name: Set up Go 1.x
uses: actions/setup-go@v2
uses: actions/setup-go@v5
with:
go-version: ^1.18
go-version: ^1.25
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Get dependencies
run: go get
- name: Unit Tests
run: go test ./... -run ./...
run: CGO_ENABLED=0 go test `go list ./... | grep -v 'github.com/muety/wakapi/scripts'` -run ./... # skip scripts package, because not actually a package
- name: API Tests
run: |
npm -g install newman
npm install -g @usebruno/cli
./testing/run_api_tests.sh
mapi:
name: 'Automated pen-tests with Mayhem for API'
runs-on: ubuntu-latest
env:
CGO_ENABLED: 0
- name: Mail Tests
run: ./testing/run_mail_tests.sh
steps:
- name: Set up Go 1.x
uses: actions/setup-go@v2
with:
go-version: ^1.18
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Get dependencies
run: go get
- name: Build
run: go build -v .
- name: start wakapi
run: ./wakapi --config config.default.yml &
- name: create a trivial testing user
run: sqlite3 wakapi_db.db "insert into users (id, api_key) values ('mapi', 'test-api-key')"
- name: Run Mayhem for API
uses: ForAllSecure/mapi-action@v1
continue-on-error: true
with:
mapi-token: ${{ secrets.MAPI_TOKEN }}
api-url: http://localhost:3000/api/
api-spec: static/docs/swagger.yaml
target: muety/wakapi
duration: 1min
sarif-report: mapi.sarif
run-args: |
--header-auth
Authorization: Basic dGVzdC1hcGkta2V5
- name: Upload SARIF file
uses: github/codeql-action/upload-sarif@v1
with:
sarif_file: mapi.sarif
build:
name: 'Build (Win, Linux, Mac)'
migration:
name: Migration tests
runs-on: ubuntu-24.04
permissions:
contents: read
strategy:
fail-fast: false
matrix:
platform: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.platform }}
env:
CGO_ENABLED: 0
db: [sqlite, postgres, mysql, mariadb]
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v4
- name: Set up Go 1.x
uses: actions/setup-go@v2
uses: actions/setup-go@v5
with:
go-version: ^1.18
go-version: ^1.25
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v2
- name: Get dependencies
run: go get
- name: Build
run: go build -v .
- run: npm install -g @usebruno/cli
- run: ./testing/run_api_tests.sh ${{ matrix.db }} --migration

View File

@@ -1,10 +1,9 @@
name: Publish Docker Image
on:
push:
tags:
- '*.*.*'
- '!*.*.*-*'
release:
types:
- published
jobs:
docker-publish:
@@ -12,7 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set version
run: |
@@ -21,19 +20,19 @@ jobs:
|| git rev-parse --short HEAD) > version.txt 2> /dev/null
- name: Set up QEMU
uses: docker/setup-qemu-action@v1
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1
uses: docker/setup-buildx-action@v3
- name: Login to DockerHub
uses: docker/login-action@v1
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Log in to the Container registry
uses: docker/login-action@v1
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
@@ -41,7 +40,7 @@ jobs:
- name: Docker Metadata
id: meta
uses: docker/metadata-action@v3
uses: docker/metadata-action@v5
with:
images: |
ghcr.io/${{ github.repository }}
@@ -54,12 +53,10 @@ jobs:
type=semver,pattern={{version}}
- name: Build and push
uses: docker/build-push-action@v2
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile
push: true
platforms: linux/amd64,linux/arm64,linux/arm/v7
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=registry,ref=n1try/wakapi:buildcache-alpine
cache-to: type=registry,ref=n1try/wakapi:buildcache-alpine,mode=max

View File

@@ -12,10 +12,10 @@ jobs:
fail-fast: false
matrix:
include:
- platform: ubuntu-18.04
- platform: ubuntu-latest
GOOS: linux
GOARCH: amd64
- platform: ubuntu-18.04
- platform: ubuntu-latest
GOOS: linux
GOARCH: arm64
- platform: windows-latest
@@ -32,13 +32,14 @@ jobs:
steps:
- name: Set up Go 1.x
uses: actions/setup-go@v2
uses: actions/setup-go@v5
with:
go-version: ^1.18
go-version: ^1.25
cache: false
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v2
uses: actions/checkout@v4
- name: Set version
shell: bash
@@ -56,7 +57,7 @@ jobs:
working-directory: ./dist
shell: bash
run: |
GOOS=${{ matrix.GOOS }} GOARCH=${{ matrix.GOARCH }} CGO_ENABLED=0 \
GOOS=${{ matrix.GOOS }} GOARCH=${{ matrix.GOARCH }} CGO_ENABLED=0 GOEXPERIMENT=greenteagc,jsonv2 \
go build -v -ldflags '-w -s' ../
- name: Compress working folder (Windows PowerShell)
@@ -72,6 +73,6 @@ jobs:
zip -9 wakapi_${{ matrix.GOOS }}_${{ matrix.GOARCH }}.zip *
- name: Upload built executable to Release
uses: softprops/action-gh-release@v1
uses: softprops/action-gh-release@v2
with:
files: ./dist/*.zip

13
.gitignore vendored
View File

@@ -5,9 +5,18 @@ wakapi
build
*.exe
*.db
*.zip
config*.yml
!config.default.yml
!testing/config.testing.yml
!testing/config.*.yml
pkged.go
package-lock.json
node_modules
node_modules
.DS_Store
.venv
venv
.env
scripts/mysql_to_*.yml
*.db-journal
*.db-shm
*.db-wal

2
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,2 @@
# Contributing
Please refer to our [contribution guidelines](https://github.com/muety/wakapi/wiki/Contributing) and [design goals](https://github.com/muety/wakapi/wiki/Design-Goals).

View File

@@ -1,22 +1,25 @@
FROM golang:1.18-alpine AS build-env
FROM --platform=$BUILDPLATFORM golang:alpine AS build-env
WORKDIR /src
RUN wget "https://raw.githubusercontent.com/vishnubob/wait-for-it/master/wait-for-it.sh" -O wait-for-it.sh && \
chmod +x wait-for-it.sh
ADD ./go.mod ./go.sum ./
COPY ./go.mod ./go.sum ./
RUN go mod download
ADD . .
COPY . .
RUN CGO_ENABLED=0 go build -ldflags "-s -w" -v -o wakapi main.go
ARG TARGETOS
ARG TARGETARCH
RUN GOOS=$TARGETOS GOARCH=$TARGETARCH CGO_ENABLED=0 GOEXPERIMENT=greenteagc,jsonv2 go build -ldflags "-s -w" -v -o wakapi main.go
WORKDIR /staging
RUN mkdir ./data ./app && \
cp /src/wakapi app/ && \
cp /src/config.default.yml app/config.yml && \
sed -i 's/listen_ipv6: ::1/listen_ipv6: /g' app/config.yml && \
sed -i 's/listen_ipv6: ::1/listen_ipv6: "-"/g' app/config.yml && \
cp /src/wait-for-it.sh app/ && \
cp /src/entrypoint.sh app/
cp /src/entrypoint.sh app/ && \
chown 1000:1000 ./data
# Run Stage
@@ -27,7 +30,9 @@ RUN mkdir ./data ./app && \
FROM alpine:3
WORKDIR /app
RUN apk add --no-cache bash ca-certificates tzdata
RUN addgroup -g 1000 app && \
adduser -u 1000 -G app -s /bin/sh -D app && \
apk add --no-cache bash ca-certificates tzdata
# See README.md and config.default.yml for all config options
ENV ENVIRONMENT=prod \
@@ -43,6 +48,15 @@ ENV ENVIRONMENT=prod \
COPY --from=build-env /staging /
LABEL org.opencontainers.image.url="https://github.com/muety/wakapi" \
org.opencontainers.image.documentation="https://github.com/muety/wakapi" \
org.opencontainers.image.source="https://github.com/muety/wakapi" \
org.opencontainers.image.title="Wakapi" \
org.opencontainers.image.licenses="MIT" \
org.opencontainers.image.description="A minimalist, self-hosted WakaTime-compatible backend for coding statistics"
USER app
EXPOSE 3000
ENTRYPOINT /app/entrypoint.sh

695
LICENSE
View File

@@ -1,674 +1,21 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
Awesome and simple movie downloader, for NZB and Torrents.
Copyright (C) 2011 - Ruud Burger
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
CouchPotato - Copyright (C) 2011 - Ruud Burger
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<http://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
The MIT License (MIT)
Copyright (c) 2016
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

291
README.md
View File

@@ -4,7 +4,6 @@
<p align="center">
<img src="https://badges.fw-web.space/github/license/muety/wakapi">
<a href="#-treeware"><img src="https://badges.fw-web.space:/treeware/trees/muety/wakapi?color=%234EC820&label=%F0%9F%8C%B3%20trees"></a>
<a href="https://liberapay.com/muety/"><img src="https://badges.fw-web.space/liberapay/receives/muety.svg?logo=liberapay"></a>
<img src="https://wakapi.dev/api/badge/n1try/interval:any/project:wakapi?label=wakapi">
<img src="https://badges.fw-web.space/github/languages/code-size/muety/wakapi">
@@ -32,11 +31,12 @@
<img src="static/assets/images/screenshot.webp" width="500px">
</p>
Installation instructions can be found below and in the [Wiki](https://github.com/muety/wakapi/wiki).
> [!IMPORTANT]
> Upvote Wakapi on [AlternativeTo](https://alternativeto.net/software/wakapi/about/) and [ProductHunt](https://www.producthunt.com/posts/wakapi-coding-statistics) to support the project 🌈.
## 🚀 Features
*100 % free and open-source
*Free and open-source
* ✅ Built by developers for developers
* ✅ Statistics for projects, languages, editors, hosts and operating systems
* ✅ Badges
@@ -48,10 +48,6 @@ Installation instructions can be found below and in the [Wiki](https://github.co
* ✅ Lightning fast
* ✅ Self-hosted
## 🚧 Roadmap
Plans for the near future mainly include, besides usual improvements and bug fixes, a UI redesign as well as additional types of charts and statistics (see [#101](https://github.com/muety/wakapi/issues/101), [#76](https://github.com/muety/wakapi/issues/76), [#12](https://github.com/muety/wakapi/issues/12)). If you have feature requests or any kind of improvement proposals feel free to open an issue or share them in our [user survey](https://github.com/muety/wakapi/issues/82).
## ⌨️ How to use?
There are different options for how to use Wakapi, ranging from our hosted cloud service to self-hosting it. Regardless of which option choose, you will always have to do the [client setup](#-client-setup) in addition.
@@ -67,6 +63,7 @@ $ curl -L https://wakapi.dev/get | bash
```
**Alternatively** using [eget](https://github.com/zyedidia/eget):
```bash
$ eget muety/wakapi
```
@@ -77,10 +74,11 @@ $ eget muety/wakapi
# Create a persistent volume
$ docker volume create wakapi-data
$ SALT="$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w ${1:-32} | head -n 1)"
$ SALT="$(cat /dev/urandom | LC_ALL=C tr -dc 'a-zA-Z0-9' | fold -w ${1:-32} | head -n 1)"
# Run the container
$ docker run -d \
--init \
-p 3000:3000 \
-e "WAKAPI_PASSWORD_SALT=$SALT" \
-v wakapi-data:/data \
@@ -90,7 +88,25 @@ $ docker run -d \
**Note:** By default, SQLite is used as a database. To run Wakapi in Docker with MySQL or Postgres, see [Dockerfile](https://github.com/muety/wakapi/blob/master/Dockerfile) and [config.default.yml](https://github.com/muety/wakapi/blob/master/config.default.yml) for further options.
If you want to run Wakapi on **Kubernetes**, there is [wakapi-helm-chart](https://github.com/andreymaznyak/wakapi-helm-chart) for quick and easy deployment.
If you want to run Wakapi on **Kubernetes**, there is [wakapi-helm-chart](https://github.com/ricristian/wakapi-helm-chart) for quick and easy deployment.
#### Docker Compose
Alternatively, you can use Docker Compose for an even more straightforward deployment. See [compose.yml](https://github.com/muety/wakapi/blob/master/compose.yml) for configuration details.
Wakapi supports [Docker Secrets](https://docs.docker.com/compose/how-tos/use-secrets/) for the following variables: `WAKAPI_PASSWORD_SALT`, `WAKAPI_DB_PASSWORD`, `WAKAPI_MAIL_SMTP_PASS`. You can set these either by having them mounted as a secret file, or directly pass them as environment variables.
##### Example
```bash
export WAKAPI_PASSWORD_SALT=changeme
export WAKAPI_DB_PASSWORD=changeme
export WAKAPI_MAIL_SMTP_PASS=changeme
docker compose up -d
```
If you prefer to persist data in a local directory while using SQLite as the database, make sure to set the correct `user` option in the Docker Compose configuration to avoid permission issues.
### 🧑‍💻 Option 4: Compile and run from source
@@ -109,11 +125,11 @@ $ ./wakapi -config wakapi.yml
**Note:** Check the comments in `config.yml` for best practices regarding security configuration and more.
💡 When running Wakapi standalone (without Docker), it is recommended to run it as a [SystemD service](etc/wakapi.service).
💡 When running Wakapi standalone (without Docker), it is recommended to run it as a [SystemD service](etc/wakapi.service).
### 💻 Client setup
Wakapi relies on the open-source [WakaTime](https://github.com/wakatime/wakatime) client tools. In order to collect statistics for Wakapi, you need to set them up.
Wakapi relies on the open-source [WakaTime](https://github.com/wakatime/wakatime-cli) client tools. In order to collect statistics for Wakapi, you need to set them up.
1. **Set up WakaTime** for your specific IDE or editor. Please refer to the respective [plugin guide](https://wakatime.com/plugins)
2. **Edit your local `~/.wakatime.cfg`** file as follows.
@@ -134,56 +150,90 @@ Optionally, you can set up a [client-side proxy](https://github.com/muety/wakapi
You can specify configuration options either via a config file (default: `config.yml`, customizable through the `-c` argument) or via environment variables. Here is an overview of all options.
| YAML key / Env. variable | Default | Description |
|------------------------------------------------------------------------------|--------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `env` /<br>`ENVIRONMENT` | `dev` | Whether to use development- or production settings |
| `app.aggregation_time` /<br>`WAKAPI_AGGREGATION_TIME` | `02:15` | Time of day at which to periodically run summary generation for all users |
| `app.report_time_weekly` /<br>`WAKAPI_REPORT_TIME_WEEKLY` | `fri,18:00` | Week day and time at which to send e-mail reports |
| `app.import_batch_size` /<br>`WAKAPI_IMPORT_BATCH_SIZE` | `50` | Size of batches of heartbeats to insert to the database during importing from external services |
| `app.inactive_days` /<br>`WAKAPI_INACTIVE_DAYS` | `7` | Number of days after which to consider a user inactive (only for metrics) |
| `app.heartbeat_max_age /`<br>`WAKAPI_HEARTBEAT_MAX_AGE` | `4320h` | Maximum acceptable age of a heartbeat (see [`ParseDuration`](https://pkg.go.dev/time#ParseDuration)) |
| `app.custom_languages` | - | Map from file endings to language names |
| `app.avatar_url_template` /<br>`WAKAPI_AVATAR_URL_TEMPLATE` | (see [`config.default.yml`](config.default.yml)) | URL template for external user avatar images (e.g. from [Dicebear](https://dicebear.com) or [Gravatar](https://gravatar.com)) |
| `server.port` /<br> `WAKAPI_PORT` | `3000` | Port to listen on |
| `server.listen_ipv4` /<br> `WAKAPI_LISTEN_IPV4` | `127.0.0.1` | IPv4 network address to listen on (leave blank to disable IPv4) |
| `server.listen_ipv6` /<br> `WAKAPI_LISTEN_IPV6` | `::1` | IPv6 network address to listen on (leave blank to disable IPv6) |
| `server.listen_socket` /<br> `WAKAPI_LISTEN_SOCKET` | - | UNIX socket to listen on (leave blank to disable UNIX socket) |
| `server.timeout_sec` /<br> `WAKAPI_TIMEOUT_SEC` | `30` | Request timeout in seconds |
| `server.tls_cert_path` /<br> `WAKAPI_TLS_CERT_PATH` | - | Path of SSL server certificate (leave blank to not use HTTPS) |
| `server.tls_key_path` /<br> `WAKAPI_TLS_KEY_PATH` | - | Path of SSL server private key (leave blank to not use HTTPS) |
| `server.base_path` /<br> `WAKAPI_BASE_PATH` | `/` | Web base path (change when running behind a proxy under a sub-path) |
| `server.public_url` /<br> `WAKAPI_PUBLIC_URL` | `http://localhost:3000` | URL at which your Wakapi instance can be found publicly |
| `security.password_salt` /<br> `WAKAPI_PASSWORD_SALT` | - | Pepper to use for password hashing |
| `security.insecure_cookies` /<br> `WAKAPI_INSECURE_COOKIES` | `false` | Whether or not to allow cookies over HTTP |
| `security.cookie_max_age` /<br> `WAKAPI_COOKIE_MAX_AGE` | `172800` | Lifetime of authentication cookies in seconds or `0` to use [Session](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#Define_the_lifetime_of_a_cookie) cookies |
| `security.allow_signup` /<br> `WAKAPI_ALLOW_SIGNUP` | `true` | Whether to enable user registration |
| `security.expose_metrics` /<br> `WAKAPI_EXPOSE_METRICS` | `false` | Whether to expose Prometheus metrics under `/api/metrics` |
| `db.host` /<br> `WAKAPI_DB_HOST` | - | Database host |
| `db.port` /<br> `WAKAPI_DB_PORT` | - | Database port |
| `db.user` /<br> `WAKAPI_DB_USER` | - | Database user |
| `db.password` /<br> `WAKAPI_DB_PASSWORD` | - | Database password |
| `db.name` /<br> `WAKAPI_DB_NAME` | `wakapi_db.db` | Database name |
| `db.dialect` /<br> `WAKAPI_DB_TYPE` | `sqlite3` | Database type (one of `sqlite3`, `mysql`, `postgres`, `cockroach`) |
| `db.charset` /<br> `WAKAPI_DB_CHARSET` | `utf8mb4` | Database connection charset (for MySQL only) |
| `db.max_conn` /<br> `WAKAPI_DB_MAX_CONNECTIONS` | `2` | Maximum number of database connections |
| `db.ssl` /<br> `WAKAPI_DB_SSL` | `false` | Whether to use TLS encryption for database connection (Postgres and CockroachDB only) |
| `db.automgirate_fail_silently` /<br> `WAKAPI_DB_AUTOMIGRATE_FAIL_SILENTLY` | `false` | Whether to ignore schema auto-migration failures when starting up |
| `mail.enabled` /<br> `WAKAPI_MAIL_ENABLED` | `true` | Whether to allow Wakapi to send e-mail (e.g. for password resets) |
| `mail.sender` /<br> `WAKAPI_MAIL_SENDER` | `Wakapi <noreply@wakapi.dev>` | Default sender address for outgoing mails (ignored for MailWhale) |
| `mail.provider` /<br> `WAKAPI_MAIL_PROVIDER` | `smtp` | Implementation to use for sending mails (one of [`smtp`, `mailwhale`]) |
| `mail.smtp.host` /<br> `WAKAPI_MAIL_SMTP_HOST` | - | SMTP server address for sending mail (if using `smtp` mail provider) |
| `mail.smtp.port` /<br> `WAKAPI_MAIL_SMTP_PORT` | - | SMTP server port (usually 465) |
| `mail.smtp.username` /<br> `WAKAPI_MAIL_SMTP_USER` | - | SMTP server authentication username |
| `mail.smtp.password` /<br> `WAKAPI_MAIL_SMTP_PASS` | - | SMTP server authentication password |
| `mail.smtp.tls` /<br> `WAKAPI_MAIL_SMTP_TLS` | `false` | Whether the SMTP server requires TLS encryption (`false` for STARTTLS or no encryption) |
| `mail.mailwhale.url` /<br> `WAKAPI_MAIL_MAILWHALE_URL` | - | URL of [MailWhale](https://mailwhale.dev) instance (e.g. `https://mailwhale.dev`) (if using `mailwhale` mail provider) |
| `mail.mailwhale.client_id` /<br> `WAKAPI_MAIL_MAILWHALE_CLIENT_ID` | - | MailWhale API client ID |
| `mail.mailwhale.client_secret` /<br> `WAKAPI_MAIL_MAILWHALE_CLIENT_SECRET` | - | MailWhale API client secret |
| `sentry.dsn` /<br> `WAKAPI_SENTRY_DSN` | | DSN for to integrate [Sentry](https://sentry.io) for error logging and tracing (leave empty to disable) |
| `sentry.enable_tracing` /<br> `WAKAPI_SENTRY_TRACING` | `false` | Whether to enable Sentry request tracing |
| `sentry.sample_rate` /<br> `WAKAPI_SENTRY_SAMPLE_RATE` | `0.75` | Probability of tracing a request in Sentry |
| `sentry.sample_rate_heartbeats` /<br> `WAKAPI_SENTRY_SAMPLE_RATE_HEARTBEATS` | `0.1` | Probability of tracing a heartbeat request in Sentry |
| `quick_start` /<br> `WAKAPI_QUICK_START` | `false` | Whether to skip initial boot tasks. Use only for development purposes! |
| YAML key / Env. variable | Default | Description |
|---------------------------------------------------------------------------------------------|--------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `env` /<br>`ENVIRONMENT` | `dev` | Whether to use development- or production settings |
| `app.leaderboard_enabled` /<br>`WAKAPI_LEADERBOARD_ENABLED` | `true` | Whether to enable the public leaderboard |
| `app.leaderboard_scope` /<br>`WAKAPI_LEADERBOARD_SCOPE` | `7_days` | Aggregation interval for public leaderboard (see [here](https://github.com/muety/wakapi/blob/7d156cd3edeb93af2997bd95f12933b0aabef0c9/config/config.go#L71) for allowed values) |
| `app.leaderboard_generation_time` /<br>`WAKAPI_LEADERBOARD_GENERATION_TIME` | `0 0 6 * * *,0 0 18 * * *` | One or multiple times of day at which to re-calculate the leaderboard |
| `app.leaderboard_require_auth` /<br>`WAKAPI_LEADERBOARD_REQUIRE_AUTH` | `false` | Restrict leaderboard access to logged in users only |
| `app.aggregation_time` /<br>`WAKAPI_AGGREGATION_TIME` | `0 15 2 * * *` | Time of day at which to periodically run summary generation for all users |
| `app.report_time_weekly` /<br>`WAKAPI_REPORT_TIME_WEEKLY` | `0 0 18 * * 5` | Week day and time at which to send e-mail reports |
| `app.data_cleanup_time` /<br>`WAKAPI_DATA_CLEANUP_TIME` | `0 0 6 * * 0` | When to perform data cleanup operations (see `app.data_retention_months`) |
| `app.optimize_database_time` /<br>`WAKAPI_OPTIMIZE_DATABASE_TIME` | `0 0 8 1 * *` | When to perform database vacuuming (SQLite, Postgres) or table optimization (MySQL) |
| `app.import_enabled` /<br>`WAKAPI_IMPORT_ENABLED` | `true` | Whether data imports from WakaTime or other Wakapi instances are permitted |
| `app.import_batch_size` /<br>`WAKAPI_IMPORT_BATCH_SIZE` | `50` | Size of batches of heartbeats to insert to the database during importing from external services |
| `app.import_backoff_min` /<br>`WAKAPI_IMPORT_BACKOFF_MIN` | `5` | "Cooldown" period in minutes before user may attempt another data import |
| `app.import_max_rate` /<br>`WAKAPI_IMPORT_MAX_RATE` | `24` | Minimum number of hours to wait after a successful data import before user may attempt another one |
| `app.inactive_days` /<br>`WAKAPI_INACTIVE_DAYS` | `7` | Number of days after which to consider a user inactive (only for metrics) |
| `app.heartbeat_max_age /`<br>`WAKAPI_HEARTBEAT_MAX_AGE` | `4320h` | Maximum acceptable age of a heartbeat (see [`ParseDuration`](https://pkg.go.dev/time#ParseDuration)) |
| `app.warm_caches /`<br>`WAKAPI_WARM_CACHES` | `true` | Whether to perform some initial cache warming upon startup |
| `app.custom_languages` | - | Map from file endings to language names |
| `app.avatar_url_template` /<br>`WAKAPI_AVATAR_URL_TEMPLATE` | (see [`config.default.yml`](config.default.yml)) | URL template for external user avatar images (e.g. from [Dicebear](https://dicebear.com) or [Gravatar](https://gravatar.com)) |
| `app.date_format` /<br>`WAKAPI_DATE_FORMAT` | `Mon, 02 Jan 2006` | Go time format strings to format human-readable date (see [`Time.Format`](https://pkg.go.dev/time#Time.Format)) |
| `app.datetime_format` /<br>`WAKAPI_DATETIME_FORMAT` | `Mon, 02 Jan 2006 15:04` | Go time format strings to format human-readable datetime (see [`Time.Format`](https://pkg.go.dev/time#Time.Format)) |
| `app.support_contact` /<br>`WAKAPI_SUPPORT_CONTACT` | `hostmaster@wakapi.dev` | E-Mail address to display as a support contact on the page |
| `app.data_retention_months` /<br>`WAKAPI_DATA_RETENTION_MONTHS` | `-1` | Maximum retention period in months for user data (heartbeats) (-1 for unlimited) |
| `app.max_inactive_months` /<br>`WAKAPI_MAX_INACTIVE_MONTHS` | `12` | Maximum number of inactive months after which to delete user accounts without data (-1 for unlimited) |
| `server.port` /<br> `WAKAPI_PORT` | `3000` | Port to listen on |
| `server.listen_ipv4` /<br> `WAKAPI_LISTEN_IPV4` | `127.0.0.1` | IPv4 network address to listen on (set to `'-'` to disable IPv4) |
| `server.listen_ipv6` /<br> `WAKAPI_LISTEN_IPV6` | `::1` | IPv6 network address to listen on (set to `'-'` to disable IPv6) |
| `server.listen_socket` /<br> `WAKAPI_LISTEN_SOCKET` | - | UNIX socket to listen on (set to `'-'` to disable UNIX socket) |
| `server.listen_socket_mode` /<br> `WAKAPI_LISTEN_SOCKET_MODE` | `0666` | Permission mode to create UNIX socket with |
| `server.timeout_sec` /<br> `WAKAPI_TIMEOUT_SEC` | `30` | Request timeout in seconds |
| `server.tls_cert_path` /<br> `WAKAPI_TLS_CERT_PATH` | - | Path of SSL server certificate (leave blank to not use HTTPS) |
| `server.tls_key_path` /<br> `WAKAPI_TLS_KEY_PATH` | - | Path of SSL server private key (leave blank to not use HTTPS) |
| `server.base_path` /<br> `WAKAPI_BASE_PATH` | `/` | Web base path (change when running behind a proxy under a sub-path) |
| `server.public_url` /<br> `WAKAPI_PUBLIC_URL` | `http://localhost:3000` | URL at which your Wakapi instance can be found publicly |
| `security.password_salt` /<br> `WAKAPI_PASSWORD_SALT` | - | Pepper to use for password hashing |
| `security.insecure_cookies` /<br> `WAKAPI_INSECURE_COOKIES` | `false` | Whether or not to allow cookies over HTTP |
| `security.cookie_max_age` /<br> `WAKAPI_COOKIE_MAX_AGE` | `172800` | Lifetime of authentication cookies in seconds or `0` to use [Session](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#Define_the_lifetime_of_a_cookie) cookies |
| `security.allow_signup` /<br> `WAKAPI_ALLOW_SIGNUP` | `true` | Whether to enable user registration |
| `security.signup_captcha` /<br> `WAKAPI_SIGNUP_CAPTCHA` | `false` | Whether the registration form requires solving a CAPTCHA |
| `security.invite_codes` /<br> `WAKAPI_INVITE_CODES` | `true` | Whether to enable registration by invite codes. Primarily useful if registration is disabled (invite-only server). |
| `security.disable_frontpage` /<br> `WAKAPI_DISABLE_FRONTPAGE` | `false` | Whether to disable landing page (useful for personal instances) |
| `security.expose_metrics` /<br> `WAKAPI_EXPOSE_METRICS` | `false` | Whether to expose Prometheus metrics under `/api/metrics` |
| `security.trusted_header_auth` /<br> `WAKAPI_TRUSTED_HEADER_AUTH` | `false` | Whether to enable trusted header authentication for reverse proxies (see [#534](https://github.com/muety/wakapi/issues/534)). **Use with caution!** |
| `security.trusted_header_auth_key` /<br> `WAKAPI_TRUSTED_HEADER_AUTH_KEY` | `Remote-User` | Header field for trusted header authentication. **Caution:** proxy must be configured to strip this header from client requests! |
| `security.trusted_header_auth_allow_signup` /<br> `WAKAPI_TRUSTED_HEADER_AUTH_ALLOW_SIGNUP` | `false` | Whether to allow creation of new users based on upstream trusted header authentication (see [#808](https://github.com/muety/wakapi/issues/808)) |
| `security.trust_reverse_proxy_ips` /<br> `WAKAPI_TRUST_REVERSE_PROXY_IPS` | - | Comma-separated list of IPv4 or IPv6 addresses or CIDRs of reverse proxies to trust to handle authentication (e.g. `172.17.0.1`, `192.168.0.0/24`, `[::1]`). |
| `security.signup_max_rate` /<br> `WAKAPI_SIGNUP_MAX_RATE` | `5/1h` | Rate limiting config for signup endpoint in format `<max_req>/<multiplier><unit>`, where `unit` is one of `s`, `m` or `h`. |
| `security.login_max_rate` /<br> `WAKAPI_LOGIN_MAX_RATE` | `10/1m` | Rate limiting config for login endpoint in format `<max_req>/<multiplier><unit>`, where `unit` is one of `s`, `m` or `h`. |
| `security.password_reset_max_rate` /<br> `WAKAPI_PASSWORD_RESET_MAX_RATE` | `5/1h` | Rate limiting config for password reset endpoint in format `<max_req>/<multiplier><unit>`, where `unit` is one of `s`, `m` or `h`. |
| `security.oidc` | `[]` | List of OpenID Connect provider configurations (for details, see [wiki](https://github.com/muety/wakapi/wiki/OpenID-Connect-login-(SSO))) |
| `security.oidc[0].name` /<br> `WAKAPI_OIDC_PROVIDER_NAME` | - | Name / identifier for the OpenID Connect provider (e.g. `gitlab`) |
| `security.oidc[0].client_id` /<br> `WAKAPI_OIDC_PROVIDER_CLIENT_ID` | - | OAuth client name with this provider |
| `security.oidc[0].client_secret` /<br> `WAKAPI_OIDC_PROVIDER_CLIENT_SECRET` | - | OAuth client secret with this provider |
| `security.oidc[0].endpoint` /<br> `WAKAPI_OIDC_PROVIDER_ENDPOINT` | - | OpenID Connect provider API entrypoint (for [discovery](https://openid.net/specs/openid-connect-discovery-1_0.html)) |
| `db.host` /<br> `WAKAPI_DB_HOST` | - | Database host |
| `db.port` /<br> `WAKAPI_DB_PORT` | - | Database port |
| `db.socket` /<br> `WAKAPI_DB_SOCKET` | - | Database UNIX socket (alternative to `host`) (for MySQL only) |
| `db.user` /<br> `WAKAPI_DB_USER` | - | Database user |
| `db.password` /<br> `WAKAPI_DB_PASSWORD` | - | Database password |
| `db.name` /<br> `WAKAPI_DB_NAME` | `wakapi_db.db` | Database name |
| `db.dialect` /<br> `WAKAPI_DB_TYPE` | `sqlite3` | Database type (one of `sqlite3`, `mysql`, `postgres`) |
| `db.charset` /<br> `WAKAPI_DB_CHARSET` | `utf8mb4` | Database connection charset (for MySQL only) |
| `db.max_conn` /<br> `WAKAPI_DB_MAX_CONNECTIONS` | `2` | Maximum number of database connections |
| `db.ssl` /<br> `WAKAPI_DB_SSL` | `false` | Whether to use TLS encryption for database connection (Postgres only) |
| `db.compress` /<br> `WAKAPI_DB_COMPRESS` | `false` | Whether to enable compression for database connection (MySQL only) |
| `db.automgirate_fail_silently` /<br> `WAKAPI_DB_AUTOMIGRATE_FAIL_SILENTLY` | `false` | Whether to ignore schema auto-migration failures when starting up |
| `mail.enabled` /<br> `WAKAPI_MAIL_ENABLED` | `true` | Whether to allow Wakapi to send e-mail (e.g. for password resets) |
| `mail.sender` /<br> `WAKAPI_MAIL_SENDER` | `Wakapi <noreply@wakapi.dev>` | Default sender address for outgoing mails |
| `mail.skip_verify_mx_record` /<br> `WAKAPI_MAIL_SKIP_VERIFY_MX_RECORD` | `false` | Whether to skip validating MX DNS record for user email addresses |
| `mail.provider` /<br> `WAKAPI_MAIL_PROVIDER` | `smtp` | Implementation to use for sending mails (one of [`smtp`]) |
| `mail.smtp.host` /<br> `WAKAPI_MAIL_SMTP_HOST` | - | SMTP server address for sending mail (if using `smtp` mail provider) |
| `mail.smtp.port` /<br> `WAKAPI_MAIL_SMTP_PORT` | - | SMTP server port (usually 465) |
| `mail.smtp.username` /<br> `WAKAPI_MAIL_SMTP_USER` | - | SMTP server authentication username |
| `mail.smtp.password` /<br> `WAKAPI_MAIL_SMTP_PASS` | - | SMTP server authentication password |
| `mail.smtp.tls` /<br> `WAKAPI_MAIL_SMTP_TLS` | `false` | Whether the SMTP server requires TLS encryption (`false` for STARTTLS or no encryption) |
| `mail.smtp.skip_verify` /<br> `WAKAPI_MAIL_SMTP_SKIP_VERIFY` | `false` | Whether to allow invalid or self-signed certificates for TLS-encrypted SMTP |
| `sentry.dsn` /<br> `WAKAPI_SENTRY_DSN` | | DSN for to integrate [Sentry](https://sentry.io) for error logging and tracing (leave empty to disable) |
| `sentry.environment` /<br> `WAKAPI_SENTRY_ENVIRONMENT` | (`env`) | Sentry [environment](https://docs.sentry.io/concepts/key-terms/environments/) tag (defaults to `env` / `ENV`) |
| `sentry.enable_tracing` /<br> `WAKAPI_SENTRY_TRACING` | `false` | Whether to enable Sentry request tracing |
| `sentry.sample_rate` /<br> `WAKAPI_SENTRY_SAMPLE_RATE` | `0.75` | Probability of tracing a request in Sentry |
| `sentry.sample_rate_heartbeats` /<br> `WAKAPI_SENTRY_SAMPLE_RATE_HEARTBEATS` | `0.1` | Probability of tracing a heartbeat request in Sentry |
| `quick_start` /<br> `WAKAPI_QUICK_START` | `false` | Whether to skip initial boot tasks. Use only for development purposes! |
| `enable_pprof` /<br> `WAKAPI_ENABLE_PPROF` | `false` | Whether to expose [pprof](https://pkg.go.dev/runtime/pprof) profiling data as an endpoint for debugging |
### Supported databases
@@ -193,7 +243,22 @@ Wakapi uses [GORM](https://gorm.io) as an ORM. As a consequence, a set of differ
* [MySQL](https://hub.docker.com/_/mysql) (_recommended, because most extensively tested_)
* [MariaDB](https://hub.docker.com/_/mariadb) (_open-source MySQL alternative_)
* [Postgres](https://hub.docker.com/_/postgres) (_open-source as well_)
* [CockroachDB](https://www.cockroachlabs.com/docs/stable/install-cockroachdb-linux.html) (_cloud-native, distributed, Postgres-compatible API_)
## 🔐 Authentication
Wakapi supports different types of user authentication.
* **Cookie:** This method is used in the browser. Users authenticate by sending along an encrypted, secure, HTTP-only cookie (`wakapi_auth`) that was set in the server's response upon login.
* **API key:**
* **Via header:** This method is inspired by [WakaTime's auth. mechanism](https://wakatime.com/developers/#authentication) and is the common way to authenticate against API endpoints. Users set the `Authorization` header to `Basic <BASE64_TOKEN>`, where the latter part corresponds to your base64-hashed API key.
* **Vis query param:** Alternatively, users can also pass their plain API key as a query parameter (e.g. `?api_key=86648d74-19c5-452b-ba01-fb3ec70d4c2f`) in the URL with every request.
* **Trusted header:** This mechanism allows to delegate authentication to a **reverse proxy** (e.g. for SSO), that Wakapi will then trust blindly. See [#534](https://github.com/muety/wakapi/issues/534) for details.
* Must be enabled via `trusted_header_auth` and configuring `trust_reverse_proxy_ip` in the config
* Warning: This type of authentication is quite prone to misconfiguration. Make sure that your reverse proxy properly strips relevant headers from client requests.
### Single Sign-On / OpenID Connect
Wakapi supports login via external identity providers via OpenID Connect. See [our wiki](https://github.com/muety/wakapi/wiki/OpenID-Connect-login-(SSO)) for details.
## 🔧 API endpoints
@@ -235,7 +300,7 @@ scrape_configs:
metrics_path: '/api/metrics'
bearer_token: '<YOUR_BASE64_HASHED_TOKEN>'
static_configs:
- targets: ['localhost:3000']
- targets: [ 'localhost:3000' ]
```
#### Grafana
@@ -250,9 +315,7 @@ Wakapi plays well together with [WakaTime](https://wakatime.com). For one thing,
### GitHub Readme Stats integrations
Wakapi also integrates with [GitHub Readme Stats](https://github.com/anuraghazra/github-readme-stats#wakatime-week-stats) to generate fancy cards for you. Here is an example.
![](https://github-readme-stats.vercel.app/api/wakatime?username=n1try&api_domain=wakapi.dev&bg_color=2D3748&title_color=2F855A&icon_color=2F855A&text_color=ffffff&custom_title=Wakapi%20Week%20Stats&layout=compact)
Wakapi also integrates with [GitHub Readme Stats](https://github.com/anuraghazra/github-readme-stats#wakatime-week-stats) to generate fancy cards for you. Here is an example. To use this, don't forget to **enable public data** under [Settings -> Permissions](https://wakapi.dev/settings#permissions).
<details>
<summary>Click to view code</summary>
@@ -264,9 +327,9 @@ Wakapi also integrates with [GitHub Readme Stats](https://github.com/anuraghazra
</details>
<br>
### Github Readme Metrics integration
### GitHub Readme Metrics integration
There is a [WakaTime plugin](https://github.com/lowlighter/metrics/tree/master/source/plugins/wakatime) for GitHub [Metrics](https://github.com/lowlighter/metrics/) that is also compatible with Wakapi.
There is a [WakaTime plugin](https://github.com/lowlighter/metrics/tree/master/source/plugins/wakatime) for GitHub [Metrics](https://github.com/lowlighter/metrics/) that is also compatible with Wakapi. To use this, don't forget to **enable public data** under [Settings -> Permissions](https://wakapi.dev/settings#permissions).
Preview:
@@ -292,6 +355,59 @@ Preview:
</details>
<br>
### Browser Plugin (Chrome & Firefox)
The [browser-wakatime](https://github.com/wakatime/browser-wakatime) plugin enables you to track your web surfing in WakaTime (and Wakapi, of course). Visited websites will appear as "files" in the summary. Follow these instructions to get started:
1. Install the browser extension from the official store ([Firefox](https://addons.mozilla.org/en-US/firefox/addon/wakatimes), [Chrome](https://chrome.google.com/webstore/detail/wakatime/jnbbnacmeggbgdjgaoojpmhdlkkpblgi?hl=de))
2. Open the extension settings dialog
3. Configure it like so (see screenshot below):
* API Key: Your personal API key (get it at [wakapi.dev](https://wakapi.dev))
* Logging Type: _Only the domain_
* API URL: `https://wakapi.dev/api/compat/wakatime/v1` (alternatively, replace _wakapi.dev_ with your self-hosted instance hostname)
4. Save
5. Start browsing!
![](.github/assets/screenshot_browser_plugin.png)
Note: the plugin will only sync heartbeats once in a while, so it might take some time for them to appear on Wakapi. To "force" it to sync, simply bring up the plugin main dialog.
### Gnome Extension
If you're using the GNOME desktop, there is a quick way to display your today's coding statistics in the status bar.
![](.github/assets/screenshot_gnome.png)
Simply install the [Executor](https://extensions.gnome.org/extension/2932/executor/) extension and add the following command as a status bar indicator:
```bash
~/.wakatime/wakatime-cli-linux-amd64 --today
```
## 📦 Data Export
You can export your coding activity from Wakapi to CSV in the form of raw heartbeats. While there is no way to achieve this directly through the web UI, we provide an easy-to-use Python [script](scripts/download_heartbeats.py)
instead.
```bash
$ pip install requests tqdm
$ python scripts/download_heartbeats.py --api_key API_KEY [--url URL] [--from FROM] [--to TO] [--output OUTPUT]
```
<details>
<summary>Example</summary>
```bash
python scripts/download_heartbeats.py --api_key 04648d14-15c9-432b-b901-dbeec70d4eaf \
--url https://wakapi.dev/api \
--from 2023-01-01 \
--to 2023-01-31 \
--output wakapi_export.csv
```
</details>
## 👍 Best practices
It is recommended to use wakapi behind a **reverse proxy**, like [Caddy](https://caddyserver.com) or [nginx](https://www.nginx.com/), to enable **TLS encryption** (HTTPS).
@@ -307,16 +423,17 @@ Unit tests are supposed to test business logic on a fine-grained level. They are
#### How to run
```bash
$ CGO_ENABLED=0 go test -json -coverprofile=coverage/coverage.out ./... -run ./...
$ go install github.com/mfridman/tparse@latest # optional
$ CGO_ENABLED=0 go test -json -coverprofile=coverage/coverage.out ./... -run ./... | tparse -all
```
### API tests
API tests are implemented as black box tests, which interact with a fully-fledged, standalone Wakapi through HTTP requests. They are supposed to check Wakapi's web stack and endpoints, including response codes, headers and data on a syntactical level, rather than checking the actual content that is returned.
Our API (or end-to-end, in some way) tests are implemented as a [Postman](https://www.postman.com/) collection and can be run either from inside Postman, or using [newman](https://www.npmjs.com/package/newman) as a command-line runner.
Our API (or end-to-end, in some way) tests are implemented as a [Bruno](https://www.usebruno.com/) collection and can be run either from inside Bruno, or using [Bruno CLI](https://www.npmjs.com/package/@usebruno/cli) as a command-line runner.
To get a predictable environment, tests are run against a fresh and clean Wakapi instance with a SQLite database that is populated with nothing but some seed data (see [data.sql](testing/data.sql)). It is usually recommended for software tests to be [safe](https://www.restapitutorial.com/lessons/idempotency.html), stateless and without side effects. In contrary to that paradigm, our API tests strictly require a fixed execution order (which Postman assures) and their assertions may rely on specific previous tests having succeeded.
To get a predictable environment, tests are run against a fresh and clean Wakapi instance with a SQLite database that is populated with nothing but some seed data (see [data.sql](testing/data.sql)). It is usually recommended for software tests to be [safe](https://www.restapitutorial.com/lessons/idempotency.html), stateless and without side effects. In contrary to that paradigm, our API tests strictly require a fixed execution order (which Bruno assures) and their assertions may rely on specific previous tests having succeeded.
#### Prerequisites (Linux only)
@@ -324,8 +441,8 @@ To get a predictable environment, tests are run against a fresh and clean Wakapi
# 1. sqlite (cli)
$ sudo apt install sqlite # Fedora: sudo dnf install sqlite
# 2. newman
$ npm install -g newman
# 2. bruno cli
$ npm install -g @usebruno/cli
```
#### How to run (Linux only)
@@ -420,7 +537,7 @@ WakaTime is worth the price. However, if you only need basic statistics and like
<details>
<summary><b>How are durations calculated?</b></summary>
Inferring a measure for your coding time from heartbeats works a bit differently than in WakaTime. While WakaTime has <a href="https://wakatime.com/faq#timeout">timeout intervals</a>, Wakapi essentially just pads every heartbeat that occurs after a longer pause with 2 extra minutes.
Inferring a measure for your coding time from heartbeats works similar to WakaTime, see their [docs](https://wakatime.com/faq#timeout). Traditionally, Wakapi used to _pad_ every heartbeat before a `<timeout>`-long break with a padding of 2 minutes by default. Now, after the refactoring addressed in [#675](https://github.com/muety/wakapi/issues/675), Wakapi's logic is prtty much the same as WakaTime's, including a manually configurable timeout (default is 10 minutes).
Here is an example (circles are heartbeats):
@@ -433,30 +550,34 @@ Here is an example (circles are heartbeats):
It is unclear how to handle the three minutes in between. Did the developer do a 3-minute break, or were just no heartbeats being sent, e.g. because the developer was staring at the screen trying to find a solution, but not actually typing code?
<ul>
<li><b>WakaTime</b> (with 5 min timeout): 3 min 20 sec
<li><b>WakaTime</b> (with 2 min timeout): 20 sec
<li><b>Wakapi:</b> 10 sec + 2 min + 10 sec = 2 min 20 sec</li>
<li><b>With 10 min timeout:</b>: 3 min 20 sec
<li><b>With 2 min timeout:</b> 20 sec
<li><b>Previously (with 2 min timeout + padding):</b> 10 sec + 2 min + 10 sec = 2 min 20 sec</li>
</ul>
Wakapi adds a "padding" of two minutes before the third heartbeat. This is why total times will slightly vary between Wakapi and WakaTime.
</details>
## 🌳 Treeware
See [this comment](https://github.com/muety/wakapi/issues/716#issuecomment-2668887035) for another example.
This package is [Treeware](https://treeware.earth). If you use it in production, then we ask that you [**buy the world a tree**](https://plant.treeware.earth/muety/wakapi) to thank us for our work. By contributing to the Treeware forest youll be creating employment for local families and restoring wildlife habitats.
## 👥 Community contributions
* 💻 [Code] Image generator from Wakapi stats [LacazeThomas/wakapi-stats](https://github.com/LacazeThomas/wakapi-stats) (`Go`)
* 💻 [Code] Discord integration for Wakapi - [LLoneDev6/Wakapi-Discord](https://github.com/LoneDev6/Wakapi-Discord) (`JavaScript`)
* 💻 [Code] Alternative heartbeats export script - [wakapiexporter.nim](https://github.com/theAkito/mini-tools-nim/tree/master/generic/web/wakapiexporter) (`Nim`)
* 💻 [Code] Wakapi Helm chart for K8s deployments - [andreymaznyak/wakapi-helm-chart](https://github.com/andreymaznyak/wakapi-helm-chart) (`YAML`)
* 🗒 [Article] [Wakamonth: hours reporting tool](https://bitstillery.com/2024/01/09/wakamonth-hours-reporting-tool/)
## 👏 Support
Coding in open source is my passion and I would love to do it on a full-time basis and make a living from it one day. So if you like this project, please consider supporting it 🙂. You can donate either through [buying me a coffee](https://buymeacoff.ee/n1try) or becoming a GitHub sponsor. Every little donation is highly appreciated and boosts my motivation to keep improving Wakapi!
Coding in open source is my passion, and I would love to do it on a full-time basis and make a living from it one day. So if you like this project, please consider supporting it 🙂. You can donate either through [buying me a coffee](https://buymeacoff.ee/n1try) or becoming a GitHub sponsor. Every little donation is highly appreciated and boosts my motivation to keep improving Wakapi!
## 🙏 Thanks
I highly appreciate the efforts of **[@alanhamlett](https://github.com/alanhamlett)** and the WakaTime team and am thankful for their software being open source.
Moreover, thanks to **[JetBrains](https://jb.gg/OpenSource)** for supporting this project as part of their open-source program.
Moreover, thanks to **[server.camp](https://server.camp)** for sponsoring server infrastructure for Wakapi.dev.
![](static/assets/images/jetbrains-logo.png)
<img src=".github/assets/servercamp_logo.png" width="220px" />
## 📓 License
GPL-v3 @ [Ferdinand Mütsch](https://muetsch.io)
MIT @ [Ferdinand Mütsch](https://muetsch.io)

View File

@@ -0,0 +1,43 @@
meta {
name: Create heartbeat
type: http
seq: 1
}
post {
url: {{BASE_URL}}/api/heartbeat
body: json
auth: none
}
headers {
Authorization: Basic {{TOKEN}}
X-Machine-Name: devmachine
User-Agent: wakatime/13.0.7 (Linux-4.15.0-91-generic-x86_64-with-glibc2.4) Python3.8.0.final.0 generator/1.42.1 generator-wakatime/4.0.0
Content-Type: application/json
}
body:json {
[
{
"entity": "/home/user1/dev/proejct1/main.go",
"project": "Project 1",
"language": "Go",
"is_write": true,
"type": "file",
"category": null,
"branch": null,
"time": {{tsNow}}
},
{
"entity": "/home/user1/dev/proejct1/main.go",
"project": "Project 1",
"language": "Go",
"is_write": true,
"type": "file",
"category": null,
"branch": null,
"time": {{ts1MonthAgo}}
}
]
}

View File

@@ -0,0 +1,8 @@
meta {
name: Heartbeats
seq: 2
}
auth {
mode: none
}

11
bruno/Misc/Get health.bru Normal file
View File

@@ -0,0 +1,11 @@
meta {
name: Get health
type: http
seq: 1
}
get {
url: {{BASE_URL}}/api/health
body: none
auth: inherit
}

View File

@@ -0,0 +1,15 @@
meta {
name: Get metrics
type: http
seq: 2
}
get {
url: {{BASE_URL}}/api/metrics
body: none
auth: none
}
headers {
Authorization: Basic {{TOKEN}}
}

View File

@@ -0,0 +1,28 @@
meta {
name: Send diagnostics
type: http
seq: 3
}
post {
url: {{BASE_URL}}/api/plugins/errors
body: json
auth: none
}
headers {
Authorization: Basic {{TOKEN}}
X-Machine-Name: devmachine
User-Agent: wakatime/13.0.7 (Linux-4.15.0-91-generic-x86_64-with-glibc2.4) Python3.8.0.final.0 generator/1.42.1 generator-wakatime/4.0.0
}
body:json {
{
"platform": "unset",
"architecture": "unset",
"plugin": "",
"cli_version": "unset",
"logs": "{\"caller\":\"/home/ferdinand/dev/wakatime-cli/cmd/legacy/run.go:189\",\"func\":\"runCmd\",\"level\":\"error\",\"message\":\"failed to run command: failed to send heartbeat(s) due to api error: failed to send heartbeats via api client: invalid response status from \\\"https://bin.muetsch.io/n7jnywu/users/current/heartbeats.bulk\\\". got: 404, want: 201/202. body: \\\"\\\"\",\"now\":\"2021-08-07T00:33:26+02:00\",\"version\":\"unset\"}\n",
"stacktrace": "goroutine 1 [running]:\nruntime/debug.Stack(0x0, 0xc0001f8680, 0x196)\n\t/opt/go/src/runtime/debug/stack.go:24 +0x9f\ngithub.com/wakatime/wakatime-cli/cmd/legacy.runCmd(0xc000103680, 0xc33c60, 0x0)\n\t/home/ferdinand/dev/wakatime-cli/cmd/legacy/run.go:194 +0x26c\ngithub.com/wakatime/wakatime-cli/cmd/legacy.RunCmdWithOfflineSync(0xc000103680, 0xc33c60)\n\t/home/ferdinand/dev/wakatime-cli/cmd/legacy/run.go:163 +0x35\ngithub.com/wakatime/wakatime-cli/cmd/legacy.Run(0xc0000be2c0, 0xc000103680)\n\t/home/ferdinand/dev/wakatime-cli/cmd/legacy/run.go:90 +0x62e\ngithub.com/wakatime/wakatime-cli/cmd.NewRootCMD.func1(0xc0000be2c0, 0xc00028bd40, 0x0, 0x2)\n\t/home/ferdinand/dev/wakatime-cli/cmd/root.go:31 +0x34\ngithub.com/spf13/cobra.(*Command).execute(0xc0000be2c0, 0xc000020190, 0x2, 0x2, 0xc0000be2c0, 0xc000020190)\n\t/home/ferdinand/go/pkg/mod/github.com/spf13/cobra@v1.1.1/command.go:854 +0x2c2\ngithub.com/spf13/cobra.(*Command).ExecuteC(0xc0000be2c0, 0xc000000180, 0xc0006bff78, 0x407d65)\n\t/home/ferdinand/go/pkg/mod/github.com/spf13/cobra@v1.1.1/command.go:958 +0x375\ngithub.com/spf13/cobra.(*Command).Execute(...)\n\t/home/ferdinand/go/pkg/mod/github.com/spf13/cobra@v1.1.1/command.go:895\ngithub.com/wakatime/wakatime-cli/cmd.Execute()\n\t/home/ferdinand/dev/wakatime-cli/cmd/root.go:227 +0x2b\nmain.main()\n\t/home/ferdinand/dev/wakatime-cli/main.go:6 +0x25\n"
}
}

8
bruno/Misc/folder.bru Normal file
View File

@@ -0,0 +1,8 @@
meta {
name: Misc
seq: 1
}
auth {
mode: inherit
}

View File

@@ -0,0 +1,15 @@
meta {
name: Get Shields data
type: http
seq: 1
}
get {
url: {{BASE_URL}}/api/compat/shields/v1/n1try/interval:today/language:Go
body: none
auth: none
}
headers {
Authorization: Basic {{TOKEN}}
}

8
bruno/Shields/folder.bru Normal file
View File

@@ -0,0 +1,8 @@
meta {
name: Shields
seq: 4
}
auth {
mode: none
}

View File

@@ -0,0 +1,19 @@
meta {
name: Get summary
type: http
seq: 1
}
get {
url: {{BASE_URL}}/api/summary?interval=last_7_days
body: none
auth: none
}
params:query {
interval: last_7_days
}
headers {
Authorization: Basic {{TOKEN}}
}

8
bruno/Summary/folder.bru Normal file
View File

@@ -0,0 +1,8 @@
meta {
name: Summary
seq: 3
}
auth {
mode: none
}

View File

@@ -0,0 +1,15 @@
meta {
name: Get all time
type: http
seq: 1
}
get {
url: {{BASE_URL}}/api/compat/wakatime/v1/users/current/all_time_since_today
body: none
auth: none
}
headers {
Authorization: Basic {{TOKEN}}
}

View File

@@ -0,0 +1,19 @@
meta {
name: Get heartbeats
type: http
seq: 2
}
get {
url: {{BASE_URL}}/api/compat/wakatime/v1/users/current/heartbeats?date=2021-02-10
body: none
auth: none
}
params:query {
date: 2021-02-10
}
headers {
Authorization: Basic {{TOKEN}}
}

View File

@@ -0,0 +1,15 @@
meta {
name: Get stats with range
type: http
seq: 4
}
get {
url: {{BASE_URL}}/api/compat/wakatime/v1/users/current/stats/last_7_days
body: none
auth: none
}
headers {
Authorization: Basic {{TOKEN}}
}

View File

@@ -0,0 +1,15 @@
meta {
name: Get stats
type: http
seq: 3
}
get {
url: {{BASE_URL}}/api/compat/wakatime/v1/users/current/stats
body: none
auth: none
}
headers {
Authorization: Basic {{TOKEN}}
}

View File

@@ -0,0 +1,15 @@
meta {
name: Get statusbar
type: http
seq: 6
}
get {
url: {{BASE_URL}}/api/compat/wakatime/v1/users/current/statusbar/today
body: none
auth: none
}
headers {
Authorization: Basic {{TOKEN}}
}

View File

@@ -0,0 +1,20 @@
meta {
name: Get summaries
type: http
seq: 5
}
get {
url: {{BASE_URL}}/api/compat/wakatime/v1/users/current/summaries?start=2020-03-01T15:04:05Z&end=2020-03-31T15:04:05Z
body: none
auth: none
}
params:query {
start: 2020-03-01T15:04:05Z
end: 2020-03-31T15:04:05Z
}
headers {
Authorization: Basic {{TOKEN}}
}

View File

@@ -0,0 +1,8 @@
meta {
name: WakaTime
seq: 5
}
auth {
mode: none
}

9
bruno/bruno.json Normal file
View File

@@ -0,0 +1,9 @@
{
"version": "1",
"name": "wakapi",
"type": "collection",
"ignore": [
"node_modules",
".git"
]
}

37
bruno/collection.bru Normal file
View File

@@ -0,0 +1,37 @@
meta {
name: Wakapi
seq: 1
}
auth {
mode: none
}
script:pre-request {
const apiKey = bru.getEnvVar('API_KEY')
if (!apiKey) {
throw new Error('no api key given')
}
const token = base64encode(apiKey)
bru.setVar('TOKEN', token)
function base64encode(str) {
return Buffer.from(str, 'utf-8').toString('base64')
}
bru.setVar('tsNow', parseInt(new Date().getTime() / 1000))
bru.setVar('ts1MinAgo', parseInt((new Date().getTime() - 1000*60*1) / 1000))
bru.setVar('ts2MinAgo', parseInt((new Date().getTime() - 1000*60*2) / 1000))
bru.setVar('ts1HourAgo', parseInt((new Date().getTime() - 1000*60*60*1) / 1000))
bru.setVar('ts1MonthAgo', parseInt((new Date().getTime() - 1000*60*60*24*30*1) / 1000))
}
docs {
# Wakapi basic API routes
Start by selecting an environment (there is a premade one called "dev") and configuring your `API_KEY`.
For a complete list of API routes, refer to <https://wakapi.dev/swagger-ui>.
}

View File

@@ -0,0 +1,6 @@
vars {
BASE_URL: http://localhost:3000
}
vars:secret [
API_KEY
]

60
compose.yml Normal file
View File

@@ -0,0 +1,60 @@
services:
wakapi:
build: .
init: true
ports:
- 3000:3000
restart: unless-stopped
environment:
# See README.md and config.default.yml for all config options
WAKAPI_DB_TYPE: "postgres"
WAKAPI_DB_NAME: "wakapi"
WAKAPI_DB_USER: "wakapi"
WAKAPI_DB_HOST: "db"
WAKAPI_DB_PORT: "5432"
WAKAPI_DB_PASSWORD_FILE: "/run/secrets/db_password" # alternatively, set WAKAPI_DB_PASSWORD directly without the use of secrets
WAKAPI_PASSWORD_SALT_FILE: "/run/secrets/password_salt" # alternatively, set WAKAPI_PASSWORD_SALT directly without the use of secrets
WAKAPI_MAIL_SMTP_PASS_FILE: "/run/secrets/smtp_pass" # alternatively, set WAKAPI_MAIL_SMTP_PASS directly without the use of secrets
secrets:
- source: password_salt
target: password_salt
uid: '1000'
gid: '1000'
mode: '0400'
- source: smtp_pass
target: smtp_pass
uid: '1000'
gid: '1000'
mode: '0400'
- source: db_password
target: db_password
uid: '1000'
gid: '1000'
mode: '0400'
db:
image: postgres:17
restart: unless-stopped
environment:
POSTGRES_USER: "wakapi"
POSTGRES_PASSWORD_FILE: "/run/secrets/db_password" # alternatively, set POSTGRES_PASSWORD directly without the use of secrets
POSTGRES_DB: "wakapi"
volumes:
- wakapi-db-data:/var/lib/postgresql/data
secrets:
- db_password
# secrets can be defined either from a local file or from an environment variable defined on the client host (the one that runs `docker compose` command)
# see https://docs.docker.com/compose/how-tos/use-secrets/ for details
secrets:
password_salt:
environment: WAKAPI_PASSWORD_SALT
smtp_pass:
environment: WAKAPI_MAIL_SMTP_PASS
db_password:
environment: WAKAPI_DB_PASSWORD
volumes:
wakapi-db-data: {}

View File

@@ -1,9 +1,13 @@
env: production
quick_start: false # whether to skip initial tasks on application startup, like summary generation
skip_migrations: false # whether to intentionally not run database migrations, only use for dev purposes
enable_pprof: false # whether to expose pprof (https://pkg.go.dev/runtime/pprof) profiling data as an endpoint for debugging
server:
listen_ipv4: 127.0.0.1 # leave blank to disable ipv4
listen_ipv6: ::1 # leave blank to disable ipv6
listen_socket: # leave blank to disable unix sockets
listen_ipv4: 127.0.0.1 # set to '-' to disable ipv4
listen_ipv6: ::1 # set to '-' to disable ipv6
listen_socket: # set to '-' to disable unix sockets
listen_socket_mode: 0666 # permission mode to create unix socket with
timeout_sec: 30 # request timeout
tls_cert_path: # leave blank to not use https
tls_key_path: # leave blank to not use https
@@ -12,12 +16,23 @@ server:
public_url: http://localhost:3000 # required for links (e.g. password reset) in e-mail
app:
aggregation_time: '02:15' # time at which to run daily aggregation batch jobs
leaderboard_generation_time: '06:00;18:00' # time at which to run daily aggregation batch jobs
report_time_weekly: 'fri,18:00' # time at which to fan out weekly reports (format: '<weekday)>,<daytime>')
inactive_days: 7 # time of previous days within a user must have logged in to be considered active
import_batch_size: 50 # maximum number of heartbeats to insert into the database within one transaction
heartbeat_max_age: '4320h' # maximum acceptable age of a heartbeat (see https://pkg.go.dev/time#ParseDuration)
leaderboard_enabled: true # whether to enable public leaderboards
leaderboard_scope: 7_days # leaderboard time interval (e.g. 14_days, 6_months, ...)
leaderboard_generation_time: '0 0 6 * * *,0 0 18 * * *' # times at which to re-calculate the leaderboard
leaderboard_require_auth: false # restrict leaderboard access only to logged in user
aggregation_time: '0 15 2 * * *' # time at which to run daily aggregation batch jobs
report_time_weekly: '0 0 18 * * 5' # time at which to fan out weekly reports (extended cron)
data_cleanup_time: '0 0 6 * * 0' # time at which to run old data cleanup (if enabled through data_retention_months)
optimize_database_time: '0 0 8 1 * *' # time at which to run database vacuuming (sqlite, postgres) or table optimization (mysql)
inactive_days: 7 # time of previous days within a user must have logged in to be considered active
import_enabled: true # whether data import from wakatime or other wakapi instances is allowed
import_backoff_min: 5 # time (in minutes) for "cooldown" before allowing another data import attempt by a user
import_max_rate: 24 # minimum hours to pass after a successful data import by a user before attempting a new one
import_batch_size: 50 # maximum number of heartbeats to insert into the database within one transaction
heartbeat_max_age: '4320h' # maximum acceptable age of a heartbeat (see https://pkg.go.dev/time#ParseDuration)
data_retention_months: -1 # maximum retention period on months for user data (heartbeats) (-1 for infinity)
max_inactive_months: 12 # maximum months of inactivity before deleting user accounts
warm_caches: true # whether to run some initial cache warming upon startup
custom_languages:
vue: Vue
jsx: JSX
@@ -25,31 +40,65 @@ app:
cjs: JavaScript
ipynb: Python
svelte: Svelte
astro: Astro
canonical_language_names:
'java': 'Java'
'ini': 'INI'
'xml': 'XML'
'jsx': 'JSX'
'tsx': 'TSX'
'php': 'PHP'
'yaml': 'YAML'
'toml': 'TOML'
'sql': 'SQL'
'css': 'CSS'
'scss': 'SCSS'
'jsp': 'JSP'
'svg': 'SVG'
'csv': 'CSV'
# url template for user avatar images (to be used with services like gravatar or dicebear)
# available variable placeholders are: username, username_hash, email, email_hash
# defaults to wakapi's internal avatar rendering powered by https://codeberg.org/Codeberg/avatars
avatar_url_template: api/avatar/{username_hash}.svg
# go time format strings to format human-readable dates
# for details, check https://pkg.go.dev/time#Time.Format
date_format: Mon, 02 Jan 2006
datetime_format: Mon, 02 Jan 2006 15:04
db:
host: # leave blank when using sqlite3
port: # leave blank when using sqlite3
socket: # alternative to db.host (leave blank when using sqlite3)
user: # leave blank when using sqlite3
password: # leave blank when using sqlite3
name: wakapi_db.db # database name for mysql / postgres or file path for sqlite (e.g. /tmp/wakapi.db)
dialect: sqlite3 # mysql, postgres, sqlite3
charset: utf8mb4 # only used for mysql connections
max_conn: 2 # maximum number of concurrent connections to maintain
max_conn: 10 # maximum number of concurrent connections to maintain
ssl: false # whether to use tls for db connection (must be true for cockroachdb) (ignored for mysql and sqlite)
automgirate_fail_silently: false # whether to ignore schema auto-migration failures when starting up
compress: false # whether to use compression during transport (mysql only)
automigrate_fail_silently: false # whether to ignore schema auto-migration failures when starting up
security:
password_salt: # change this
insecure_cookies: true # should be set to 'false', except when not running with HTTPS (e.g. on localhost)
password_salt: # change this
insecure_cookies: true # should be set to 'false', except when not running with HTTPS (e.g. on localhost)
cookie_max_age: 172800
allow_signup: true
signup_captcha: false
invite_codes: true # whether to enable invite codes for overriding disabled signups
disable_frontpage: false
expose_metrics: false
enable_proxy: false # only intended for production instance at wakapi.dev
enable_proxy: false # only intended for production instance at wakapi.dev
trusted_header_auth: false # whether to enable trusted header auth for reverse proxies, use with caution!! (https://github.com/muety/wakapi/issues/534)
trusted_header_auth_key: Remote-User # header field for trusted header auth (warning: your proxy must correctly strip this header from client requests!!)
trusted_header_auth_allow_signup: false # whether to allow creation of new users based on upstream trusted header authentication (https://github.com/muety/wakapi/issues/808)
trust_reverse_proxy_ips: # single ip address of the reverse proxy which you trust to pass headers for authentication
signup_max_rate: 5/1h # signup endpoint rate limit pattern
login_max_rate: 10/1m # login endpoint rate limit pattern
password_reset_max_rate: 5/1h # password reset endpoint rate limit pattern
oidc: # list of openid connect providers available for user signup and login, see https://github.com/muety/wakapi/wiki/OpenID-Connect-login-(SSO)
sentry:
dsn: # leave blank to disable sentry integration
@@ -57,10 +106,20 @@ sentry:
sample_rate: 0.75 # probability of tracing a request
sample_rate_heartbeats: 0.1 # probability of tracing a heartbeat request
# only relevant for running wakapi as a hosted service with paid subscriptions and stripe payments
subscriptions:
enabled: false
expiry_notifications: true
stripe_api_key:
stripe_secret_key:
stripe_endpoint_secret:
standard_price_id:
mail:
enabled: true # whether to enable mails (used for password resets, reports, etc.)
provider: smtp # method for sending mails, currently one of ['smtp', 'mailwhale']
sender: Wakapi <noreply@wakapi.dev> # ignored for mailwhale
provider: smtp # method for sending mails, currently one of ['smtp']
sender: Wakapi <noreply@wakapi.dev>
skip_verify_mx_record: false # whether to skip validating mx dns record for user email addresses
# smtp settings when sending mails via smtp
smtp:
@@ -69,12 +128,3 @@ mail:
username:
password:
tls:
# mailwhale.dev settings when using mailwhale as sending service
mailwhale:
url:
client_id:
client_secret:
quick_start: false # whether to skip initial tasks on application startup, like summary generation
skip_migrations: false # whether to intentionally not run database migrations, only use for dev purposes

View File

@@ -2,95 +2,145 @@ package config
import (
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"net"
"net/http"
"os"
"regexp"
"strconv"
"strings"
"time"
"github.com/emvi/logbuch"
"github.com/duke-git/lancet/v2/slice"
"log/slog"
"github.com/gofrs/uuid/v5"
"github.com/gorilla/securecookie"
"github.com/jinzhu/configor"
"github.com/muety/wakapi/data"
"github.com/muety/wakapi/models"
uuid "github.com/satori/go.uuid"
"gorm.io/gorm"
"github.com/muety/wakapi/utils"
"github.com/robfig/cron/v3"
)
const (
defaultConfigPath = "config.yml"
DefaultConfigPath = "config.yml"
SQLDialectMysql = "mysql"
SQLDialectPostgres = "postgres"
SQLDialectSqlite = "sqlite3"
SQLDialectMssql = "mssql"
KeyLatestTotalTime = "latest_total_time"
KeyLatestTotalUsers = "latest_total_users"
KeyLastImportImport = "last_import"
KeyNewsbox = "newsbox"
KeyLatestTotalTime = "latest_total_time"
KeyLatestTotalUsers = "latest_total_users"
KeyLastImport = "last_import" // import attempt
KeyLastImportSuccess = "last_successful_import" // last actual successful import
KeySubscriptionNotificationSent = "sub_reminder"
KeyNewsbox = "newsbox"
KeyInviteCode = "invite"
KeySharedData = "shared_data"
CookieKeySession = "wakapi_session"
CookieKeyAuth = "wakapi_auth"
SessionValueOidcState = "oidc_state"
SessionValueOidcIdTokenPayload = "oidc_id_token"
SimpleDateFormat = "2006-01-02"
SimpleDateTimeFormat = "2006-01-02 15:04:05"
ErrUnauthorized = "401 unauthorized"
ErrBadRequest = "400 bad request"
ErrNotFound = "404 not found"
ErrInternalServerError = "500 internal server error"
)
const (
WakatimeApiUrl = "https://wakatime.com/api/v1"
WakatimeApiUrl = "https://api.wakatime.com/api/v1"
WakatimeApiUserUrl = "/users/current"
WakatimeApiAllTimeUrl = "/users/current/all_time_since_today"
WakatimeApiHeartbeatsUrl = "/users/current/heartbeats"
WakatimeApiHeartbeatsBulkUrl = "/users/current/heartbeats.bulk"
WakatimeApiUserAgentsUrl = "/users/current/user_agents"
WakatimeApiMachineNamesUrl = "/users/current/machine_names"
WakatimeApiDataDumpUrl = "/users/current/data_dumps"
)
const (
MailProviderSmtp = "smtp"
MailProviderMailWhale = "mailwhale"
MailProviderSmtp = "smtp"
)
var emailProviders = []string{
MailProviderSmtp,
MailProviderMailWhale,
}
// first wakatime commit was on this day ;-) so no real heartbeats should exist before
// https://github.com/wakatime/legacy-python-cli/commit/3da94756aa1903c1cca5035803e3f704e818c086
const heartbeatsMinDate = "2013-07-06"
const colorsFile = "data/colors.json"
var leaderboardScopes = []string{"24_hours", "week", "month", "year", "7_days", "14_days", "30_days", "6_months", "12_months", "all_time"}
var appStartTime = time.Now()
var cfg *Config
var cFlag = flag.String("config", defaultConfigPath, "config file location")
var env string
type appConfig struct {
AggregationTime string `yaml:"aggregation_time" default:"02:15" env:"WAKAPI_AGGREGATION_TIME"`
LeaderboardGenerationTime string `yaml:"leaderboard_generation_time" default:"06:00;18:00" env:"WAKAPI_LEADERBOARD_GENERATION_TIME"`
ReportTimeWeekly string `yaml:"report_time_weekly" default:"fri,18:00" env:"WAKAPI_REPORT_TIME_WEEKLY"`
LeaderboardEnabled bool `yaml:"leaderboard_enabled" default:"true" env:"WAKAPI_LEADERBOARD_ENABLED"`
LeaderboardScope string `yaml:"leaderboard_scope" default:"7_days" env:"WAKAPI_LEADERBOARD_SCOPE"`
LeaderboardGenerationTime string `yaml:"leaderboard_generation_time" default:"0 0 6 * * *,0 0 18 * * *" env:"WAKAPI_LEADERBOARD_GENERATION_TIME"`
LeaderboardRequireAuth bool `yaml:"leaderboard_require_auth" default:"false" env:"WAKAPI_LEADERBOARD_REQUIRE_AUTH"`
AggregationTime string `yaml:"aggregation_time" default:"0 15 2 * * *" env:"WAKAPI_AGGREGATION_TIME"`
ReportTimeWeekly string `yaml:"report_time_weekly" default:"0 0 18 * * 5" env:"WAKAPI_REPORT_TIME_WEEKLY"`
DataCleanupTime string `yaml:"data_cleanup_time" default:"0 0 6 * * 0" env:"WAKAPI_DATA_CLEANUP_TIME"`
OptimizeDatabaseTime string `yaml:"optimize_database_time" default:"0 0 8 1 * *" env:"WAKAPI_OPTIMIZE_DATABASE_TIME"`
ImportEnabled bool `yaml:"import_enabled" default:"true" env:"WAKAPI_IMPORT_ENABLED"`
ImportBackoffMin int `yaml:"import_backoff_min" default:"5" env:"WAKAPI_IMPORT_BACKOFF_MIN"`
ImportMaxRate int `yaml:"import_max_rate" default:"24" env:"WAKAPI_IMPORT_MAX_RATE"` // at max one successful import every x hours
ImportBatchSize int `yaml:"import_batch_size" default:"50" env:"WAKAPI_IMPORT_BATCH_SIZE"`
InactiveDays int `yaml:"inactive_days" default:"7" env:"WAKAPI_INACTIVE_DAYS"`
HeartbeatMaxAge string `yaml:"heartbeat_max_age" default:"4320h" env:"WAKAPI_HEARTBEAT_MAX_AGE"`
HeartbeatMaxAge string `yaml:"heartbeat_max_age" default:"168h" env:"WAKAPI_HEARTBEAT_MAX_AGE"`
CountCacheTTLMin int `yaml:"count_cache_ttl_min" default:"30" env:"WAKAPI_COUNT_CACHE_TTL_MIN"`
DataRetentionMonths int `yaml:"data_retention_months" default:"-1" env:"WAKAPI_DATA_RETENTION_MONTHS"`
DataCleanupDryRun bool `yaml:"data_cleanup_dry_run" default:"false" env:"WAKAPI_DATA_CLEANUP_DRY_RUN"` // for debugging only
MaxInactiveMonths int `yaml:"max_inactive_months" default:"-1" env:"WAKAPI_MAX_INACTIVE_MONTHS"`
WarmCaches bool `yaml:"warm_caches" default:"true" env:"WAKAPI_WARM_CACHES"`
AvatarURLTemplate string `yaml:"avatar_url_template" default:"api/avatar/{username_hash}.svg" env:"WAKAPI_AVATAR_URL_TEMPLATE"`
SupportContact string `yaml:"support_contact" default:"hostmaster@wakapi.dev" env:"WAKAPI_SUPPORT_CONTACT"`
DateFormat string `yaml:"date_format" default:"Mon, 02 Jan 2006" env:"WAKAPI_DATE_FORMAT"`
DateTimeFormat string `yaml:"datetime_format" default:"Mon, 02 Jan 2006 15:04" env:"WAKAPI_DATETIME_FORMAT"`
CustomLanguages map[string]string `yaml:"custom_languages"`
CanonicalLanguageNames map[string]string `yaml:"canonical_language_names"` // lower case, compacted representation -> canonical name
Colors map[string]map[string]string `yaml:"-"`
}
type securityConfig struct {
AllowSignup bool `yaml:"allow_signup" default:"true" env:"WAKAPI_ALLOW_SIGNUP"`
ExposeMetrics bool `yaml:"expose_metrics" default:"false" env:"WAKAPI_EXPOSE_METRICS"`
EnableProxy bool `yaml:"enable_proxy" default:"false" env:"WAKAPI_ENABLE_PROXY"` // only intended for production instance at wakapi.dev
AllowSignup bool `yaml:"allow_signup" default:"true" env:"WAKAPI_ALLOW_SIGNUP"`
SignupCaptcha bool `yaml:"signup_captcha" default:"false" env:"WAKAPI_SIGNUP_CAPTCHA"`
InviteCodes bool `yaml:"invite_codes" default:"true" env:"WAKAPI_INVITE_CODES"`
ExposeMetrics bool `yaml:"expose_metrics" default:"false" env:"WAKAPI_EXPOSE_METRICS"`
EnableProxy bool `yaml:"enable_proxy" default:"false" env:"WAKAPI_ENABLE_PROXY"` // only intended for production instance at wakapi.dev
DisableFrontpage bool `yaml:"disable_frontpage" default:"false" env:"WAKAPI_DISABLE_FRONTPAGE"`
// this is actually a pepper (https://en.wikipedia.org/wiki/Pepper_(cryptography))
PasswordSalt string `yaml:"password_salt" default:"" env:"WAKAPI_PASSWORD_SALT"`
InsecureCookies bool `yaml:"insecure_cookies" default:"false" env:"WAKAPI_INSECURE_COOKIES"`
CookieMaxAgeSec int `yaml:"cookie_max_age" default:"172800" env:"WAKAPI_COOKIE_MAX_AGE"`
SecureCookie *securecookie.SecureCookie `yaml:"-"`
PasswordSalt string `yaml:"password_salt" default:"" env:"WAKAPI_PASSWORD_SALT"`
InsecureCookies bool `yaml:"insecure_cookies" default:"false" env:"WAKAPI_INSECURE_COOKIES"`
CookieMaxAgeSec int `yaml:"cookie_max_age" default:"172800" env:"WAKAPI_COOKIE_MAX_AGE"`
TrustedHeaderAuth bool `yaml:"trusted_header_auth" default:"false" env:"WAKAPI_TRUSTED_HEADER_AUTH"`
TrustedHeaderAuthKey string `yaml:"trusted_header_auth_key" default:"Remote-User" env:"WAKAPI_TRUSTED_HEADER_AUTH_KEY"`
TrustedHeaderAuthAllowSignup bool `yaml:"trusted_header_auth_allow_signup" default:"false" env:"WAKAPI_TRUSTED_HEADER_AUTH_ALLOW_SIGNUP"`
TrustReverseProxyIps string `yaml:"trust_reverse_proxy_ips" default:"" env:"WAKAPI_TRUST_REVERSE_PROXY_IPS"` // comma-separated list of trusted reverse proxy ips
SignupMaxRate string `yaml:"signup_max_rate" default:"5/1h" env:"WAKAPI_SIGNUP_MAX_RATE"`
LoginMaxRate string `yaml:"login_max_rate" default:"10/1m" env:"WAKAPI_LOGIN_MAX_RATE"`
PasswordResetMaxRate string `yaml:"password_reset_max_rate" default:"5/1h" env:"WAKAPI_PASSWORD_RESET_MAX_RATE"`
SecureCookie *securecookie.SecureCookie `yaml:"-"`
SessionKey []byte `yaml:"-"`
OidcProviders []oidcProviderConfig `yaml:"oidc"` // TODO: support to read from env.
trustReverseProxyIpsParsed []net.IPNet
}
type dbConfig struct {
Host string `env:"WAKAPI_DB_HOST"`
Socket string `env:"WAKAPI_DB_SOCKET"`
Port uint `env:"WAKAPI_DB_PORT"`
User string `env:"WAKAPI_DB_USER"`
Password string `env:"WAKAPI_DB_PASSWORD"`
@@ -99,50 +149,66 @@ type dbConfig struct {
Charset string `default:"utf8mb4" env:"WAKAPI_DB_CHARSET"`
Type string `yaml:"dialect" default:"sqlite3" env:"WAKAPI_DB_TYPE"`
DSN string `yaml:"DSN" default:"" env:"WAKAPI_DB_DSN"`
MaxConn uint `yaml:"max_conn" default:"2" env:"WAKAPI_DB_MAX_CONNECTIONS"`
MaxConn uint `yaml:"max_conn" default:"10" env:"WAKAPI_DB_MAX_CONNECTIONS"`
Ssl bool `default:"false" env:"WAKAPI_DB_SSL"`
Compress bool `yaml:"compress" default:"false" env:"WAKAPI_DB_COMPRESS"`
MysqlOptimize bool `default:"false" env:"WAKAPI_MYSQL_OPTIMIZE"` // apparently not recommended, because usually has very little effect but takes forever and partially locks the table
AutoMigrateFailSilently bool `yaml:"automigrate_fail_silently" default:"false" env:"WAKAPI_DB_AUTOMIGRATE_FAIL_SILENTLY"`
}
type serverConfig struct {
Port int `default:"3000" env:"WAKAPI_PORT"`
ListenIpV4 string `yaml:"listen_ipv4" default:"127.0.0.1" env:"WAKAPI_LISTEN_IPV4"`
ListenIpV6 string `yaml:"listen_ipv6" default:"::1" env:"WAKAPI_LISTEN_IPV6"`
ListenSocket string `yaml:"listen_socket" default:"" env:"WAKAPI_LISTEN_SOCKET"`
TimeoutSec int `yaml:"timeout_sec" default:"30" env:"WAKAPI_TIMEOUT_SEC"`
BasePath string `yaml:"base_path" default:"/" env:"WAKAPI_BASE_PATH"`
PublicUrl string `yaml:"public_url" default:"http://localhost:3000" env:"WAKAPI_PUBLIC_URL"`
TlsCertPath string `yaml:"tls_cert_path" default:"" env:"WAKAPI_TLS_CERT_PATH"`
TlsKeyPath string `yaml:"tls_key_path" default:"" env:"WAKAPI_TLS_KEY_PATH"`
Port int `default:"3000" env:"WAKAPI_PORT"`
ListenIpV4 string `yaml:"listen_ipv4" default:"127.0.0.1" env:"WAKAPI_LISTEN_IPV4"`
ListenIpV6 string `yaml:"listen_ipv6" default:"::1" env:"WAKAPI_LISTEN_IPV6"`
ListenSocket string `yaml:"listen_socket" default:"" env:"WAKAPI_LISTEN_SOCKET"`
ListenSocketMode uint32 `yaml:"listen_socket_mode" default:"0666" env:"WAKAPI_LISTEN_SOCKET_MODE"`
TimeoutSec int `yaml:"timeout_sec" default:"30" env:"WAKAPI_TIMEOUT_SEC"`
BasePath string `yaml:"base_path" default:"/" env:"WAKAPI_BASE_PATH"`
PublicUrl string `yaml:"public_url" default:"http://localhost:3000" env:"WAKAPI_PUBLIC_URL"`
TlsCertPath string `yaml:"tls_cert_path" default:"" env:"WAKAPI_TLS_CERT_PATH"`
TlsKeyPath string `yaml:"tls_key_path" default:"" env:"WAKAPI_TLS_KEY_PATH"`
}
type subscriptionsConfig struct {
Enabled bool `yaml:"enabled" default:"false" env:"WAKAPI_SUBSCRIPTIONS_ENABLED"`
ExpiryNotifications bool `yaml:"expiry_notifications" default:"true" env:"WAKAPI_SUBSCRIPTIONS_EXPIRY_NOTIFICATIONS"`
StripeApiKey string `yaml:"stripe_api_key" env:"WAKAPI_SUBSCRIPTIONS_STRIPE_API_KEY"`
StripeSecretKey string `yaml:"stripe_secret_key" env:"WAKAPI_SUBSCRIPTIONS_STRIPE_SECRET_KEY"`
StripeEndpointSecret string `yaml:"stripe_endpoint_secret" env:"WAKAPI_SUBSCRIPTIONS_STRIPE_ENDPOINT_SECRET"`
StandardPriceId string `yaml:"standard_price_id" env:"WAKAPI_SUBSCRIPTIONS_STANDARD_PRICE_ID"`
StandardPrice string `yaml:"-"`
}
type sentryConfig struct {
Dsn string `env:"WAKAPI_SENTRY_DSN"`
Environment string `env:"WAKAPI_SENTRY_ENVIRONMENT"`
EnableTracing bool `yaml:"enable_tracing" env:"WAKAPI_SENTRY_TRACING"`
SampleRate float32 `yaml:"sample_rate" default:"0.75" env:"WAKAPI_SENTRY_SAMPLE_RATE"`
SampleRateHeartbeats float32 `yaml:"sample_rate_heartbeats" default:"0.1" env:"WAKAPI_SENTRY_SAMPLE_RATE_HEARTBEATS"`
}
type mailConfig struct {
Enabled bool `env:"WAKAPI_MAIL_ENABLED" default:"true"`
Provider string `env:"WAKAPI_MAIL_PROVIDER" default:"smtp"`
MailWhale MailwhaleMailConfig `yaml:"mailwhale"`
Smtp SMTPMailConfig `yaml:"smtp"`
Sender string `env:"WAKAPI_MAIL_SENDER" yaml:"sender"`
}
type MailwhaleMailConfig struct {
Url string `env:"WAKAPI_MAIL_MAILWHALE_URL"`
ClientId string `yaml:"client_id" env:"WAKAPI_MAIL_MAILWHALE_CLIENT_ID"`
ClientSecret string `yaml:"client_secret" env:"WAKAPI_MAIL_MAILWHALE_CLIENT_SECRET"`
Enabled bool `env:"WAKAPI_MAIL_ENABLED" default:"true"`
Provider string `env:"WAKAPI_MAIL_PROVIDER" default:"smtp"`
Smtp SMTPMailConfig `yaml:"smtp"`
Sender string `env:"WAKAPI_MAIL_SENDER" yaml:"sender"`
SkipVerifyMXRecord bool `yaml:"skip_verify_mx_record" env:"WAKAPI_MAIL_SKIP_VERIFY_MX_RECORD" default:"false"`
}
type SMTPMailConfig struct {
Host string `env:"WAKAPI_MAIL_SMTP_HOST"`
Port uint `env:"WAKAPI_MAIL_SMTP_PORT"`
Username string `env:"WAKAPI_MAIL_SMTP_USER"`
Password string `env:"WAKAPI_MAIL_SMTP_PASS"`
TLS bool `env:"WAKAPI_MAIL_SMTP_TLS"`
Host string `env:"WAKAPI_MAIL_SMTP_HOST"`
Port uint `env:"WAKAPI_MAIL_SMTP_PORT"`
Username string `env:"WAKAPI_MAIL_SMTP_USER"`
Password string `env:"WAKAPI_MAIL_SMTP_PASS"`
TLS bool `env:"WAKAPI_MAIL_SMTP_TLS"`
SkipVerify bool `env:"WAKAPI_MAIL_SMTP_SKIP_VERIFY"`
}
type oidcProviderConfig struct {
Name string `yaml:"name" env:"WAKAPI_OIDC_PROVIDER_NAME"`
ClientID string `yaml:"client_id" env:"WAKAPI_OIDC_PROVIDER_CLIENT_ID"`
ClientSecret string `yaml:"client_secret" env:"WAKAPI_OIDC_PROVIDER_CLIENT_SECRET"`
Endpoint string `yaml:"endpoint" env:"WAKAPI_OIDC_PROVIDER_ENDPOINT"` // base url from which auto-discovery (.well-known/openid-configuration) can be found
}
type Config struct {
@@ -151,14 +217,20 @@ type Config struct {
QuickStart bool `yaml:"quick_start" env:"WAKAPI_QUICK_START"`
SkipMigrations bool `yaml:"skip_migrations" env:"WAKAPI_SKIP_MIGRATIONS"`
InstanceId string `yaml:"-"` // only temporary, changes between runs
EnablePprof bool `yaml:"enable_pprof" env:"WAKAPI_ENABLE_PPROF"`
App appConfig
Security securityConfig
Db dbConfig
Server serverConfig
Subscriptions subscriptionsConfig
Sentry sentryConfig
Mail mailConfig
}
func (c *Config) AppStartTimestamp() string {
return fmt.Sprintf("%d", appStartTime.Unix())
}
func (c *Config) CreateCookie(name, value string) *http.Cookie {
return c.createCookie(name, value, c.Server.BasePath, c.Security.CookieMaxAgeSec)
}
@@ -168,6 +240,10 @@ func (c *Config) GetClearCookie(name string) *http.Cookie {
}
func (c *Config) createCookie(name, value, path string, maxAge int) *http.Cookie {
if path == "" {
path = "/"
}
return &http.Cookie{
Name: name,
Value: value,
@@ -175,7 +251,7 @@ func (c *Config) createCookie(name, value, path string, maxAge int) *http.Cookie
MaxAge: maxAge,
Secure: !c.Security.InsecureCookies,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
SameSite: http.SameSiteLaxMode,
}
}
@@ -187,68 +263,104 @@ func (c *Config) UseTLS() bool {
return c.Server.TlsCertPath != "" && c.Server.TlsKeyPath != ""
}
func (c *Config) GetMigrationFunc(dbDialect string) models.MigrationFunc {
switch dbDialect {
default:
return func(db *gorm.DB) error {
if err := db.AutoMigrate(&models.User{}); err != nil && !c.Db.AutoMigrateFailSilently {
return err
}
if err := db.AutoMigrate(&models.KeyStringValue{}); err != nil && !c.Db.AutoMigrateFailSilently {
return err
}
if err := db.AutoMigrate(&models.Alias{}); err != nil && !c.Db.AutoMigrateFailSilently {
return err
}
if err := db.AutoMigrate(&models.Heartbeat{}); err != nil && !c.Db.AutoMigrateFailSilently {
return err
}
if err := db.AutoMigrate(&models.Summary{}); err != nil && !c.Db.AutoMigrateFailSilently {
return err
}
if err := db.AutoMigrate(&models.SummaryItem{}); err != nil && !c.Db.AutoMigrateFailSilently {
return err
}
if err := db.AutoMigrate(&models.LanguageMapping{}); err != nil && !c.Db.AutoMigrateFailSilently {
return err
}
if err := db.AutoMigrate(&models.ProjectLabel{}); err != nil && !c.Db.AutoMigrateFailSilently {
return err
}
if err := db.AutoMigrate(&models.Diagnostics{}); err != nil && !c.Db.AutoMigrateFailSilently {
return err
}
if err := db.AutoMigrate(&models.LeaderboardItem{}); err != nil && !c.Db.AutoMigrateFailSilently {
return err
}
return nil
}
}
func (c *appConfig) GetCustomLanguages() map[string]string {
return utils.CloneStringMap(c.CustomLanguages, false)
}
func (c *appConfig) GetCustomLanguages() map[string]string {
return cloneStringMap(c.CustomLanguages, false)
func (c *appConfig) GetCanonicalLanguageNames() map[string]string {
if c.CanonicalLanguageNames == nil {
return make(map[string]string)
}
return utils.CloneStringMap(c.CanonicalLanguageNames, false)
}
func (c *appConfig) GetLanguageColors() map[string]string {
return cloneStringMap(c.Colors["languages"], true)
return utils.CloneStringMap(c.Colors["languages"], true)
}
func (c *appConfig) GetEditorColors() map[string]string {
return cloneStringMap(c.Colors["editors"], true)
return utils.CloneStringMap(c.Colors["editors"], true)
}
func (c *appConfig) GetOSColors() map[string]string {
return cloneStringMap(c.Colors["operating_systems"], true)
return utils.CloneStringMap(c.Colors["operating_systems"], true)
}
func (c *appConfig) GetWeeklyReportDay() time.Weekday {
s := strings.Split(c.ReportTimeWeekly, ",")[0]
return parseWeekday(s)
func (c *appConfig) GetAggregationTimeCron() string {
if strings.Contains(c.AggregationTime, ":") {
// old gocron format, e.g. "15:04"
timeParts := strings.Split(c.AggregationTime, ":")
h, err := strconv.Atoi(timeParts[0])
if err != nil {
Log().Fatal(err.Error())
}
m, err := strconv.Atoi(timeParts[1])
if err != nil {
Log().Fatal(err.Error())
}
return fmt.Sprintf("0 %d %d * * *", m, h)
}
return utils.CronPadToSecondly(c.AggregationTime)
}
func (c *appConfig) GetWeeklyReportTime() string {
return strings.Split(c.ReportTimeWeekly, ",")[1]
func (c *appConfig) GetWeeklyReportCron() string {
if strings.Contains(c.ReportTimeWeekly, ",") {
// old gocron format, e.g. "fri,18:00"
split := strings.Split(c.ReportTimeWeekly, ",")
weekday := utils.ParseWeekday(split[0])
timeParts := strings.Split(split[1], ":")
h, err := strconv.Atoi(timeParts[0])
if err != nil {
Log().Fatal(err.Error())
}
m, err := strconv.Atoi(timeParts[1])
if err != nil {
Log().Fatal(err.Error())
}
return fmt.Sprintf("0 %d %d * * %d", m, h, weekday)
}
return utils.CronPadToSecondly(c.ReportTimeWeekly)
}
func (c *appConfig) GetLeaderboardGenerationTimeCron() []string {
crons := []string{}
var parse func(string) string
if strings.Contains(c.LeaderboardGenerationTime, ":") {
// old gocron format, e.g. "15:04"
parse = func(s string) string {
timeParts := strings.Split(s, ":")
h, err := strconv.Atoi(timeParts[0])
if err != nil {
Log().Fatal(err.Error())
}
m, err := strconv.Atoi(timeParts[1])
if err != nil {
Log().Fatal(err.Error())
}
return fmt.Sprintf("0 %d %d * * *", m, h)
}
} else {
parse = func(s string) string {
return utils.CronPadToSecondly(s)
}
}
for _, s := range utils.SplitMulti(c.LeaderboardGenerationTime, ",", ";") {
crons = append(crons, parse(strings.TrimSpace(s)))
}
return crons
}
func (c *appConfig) HeartbeatsMaxAge() time.Duration {
@@ -256,6 +368,87 @@ func (c *appConfig) HeartbeatsMaxAge() time.Duration {
return d
}
func (c *securityConfig) ParseTrustReverseProxyIPs() {
c.trustReverseProxyIpsParsed = make([]net.IPNet, 0)
for _, ip := range strings.Split(c.TrustReverseProxyIps, ",") {
// the config value is empty by default
if ip == "" {
continue
}
// try parse as address range
_, parsedIpNet, err := net.ParseCIDR(ip)
if err == nil {
c.trustReverseProxyIpsParsed = append(c.trustReverseProxyIpsParsed, *parsedIpNet)
continue
}
// try parse as single ip
parsedIp := net.ParseIP(strings.TrimSpace(ip))
if parsedIp != nil {
ipBits := net.IPv4len * 8
if parsedIp.To4() == nil {
ipBits = net.IPv6len * 8
}
ipNet := net.IPNet{IP: parsedIp, Mask: net.CIDRMask(ipBits, ipBits)}
c.trustReverseProxyIpsParsed = append(c.trustReverseProxyIpsParsed, ipNet)
continue
}
slog.Warn("failed to parse reverse proxy ip ranges")
}
}
func (c *securityConfig) TrustReverseProxyIPs() []net.IPNet {
return c.trustReverseProxyIpsParsed
}
func (c *securityConfig) GetSignupMaxRate() (int, time.Duration) {
return c.parseRate(c.SignupMaxRate)
}
func (c *securityConfig) GetLoginMaxRate() (int, time.Duration) {
return c.parseRate(c.LoginMaxRate)
}
func (c *securityConfig) GetPasswordResetMaxRate() (int, time.Duration) {
return c.parseRate(c.PasswordResetMaxRate)
}
func (c *securityConfig) GetOidcProvider(name string) (*OidcProvider, error) {
return GetOidcProvider(name)
}
func (c *securityConfig) ListOidcProviders() []string {
return slice.Map[oidcProviderConfig, string](c.OidcProviders, func(i int, provider oidcProviderConfig) string {
return provider.Name
})
}
func (c *securityConfig) parseRate(rate string) (int, time.Duration) {
pattern := regexp.MustCompile("(\\d+)/(\\d+)([smh])")
matches := pattern.FindStringSubmatch(rate)
if len(matches) != 4 {
Log().Fatal("failed to parse rate pattern", "rate", rate)
}
limit, _ := strconv.Atoi(matches[1])
window, _ := strconv.Atoi(matches[2])
var windowScale time.Duration
switch matches[3] {
case "s":
windowScale = time.Second
case "m":
windowScale = time.Minute
case "h":
windowScale = time.Hour
}
return limit, time.Duration(window) * windowScale
}
func (c *dbConfig) IsSQLite() bool {
return c.Dialect == "sqlite3"
}
@@ -268,6 +461,10 @@ func (c *dbConfig) IsPostgres() bool {
return c.Dialect == "postgres"
}
func (c *dbConfig) IsMssql() bool {
return c.Dialect == SQLDialectMssql
}
func (c *serverConfig) GetPublicUrl() string {
return strings.TrimSuffix(c.PublicUrl, "/")
}
@@ -292,24 +489,21 @@ func readColors() map[string]map[string]string {
raw := data.ColorsFile
if IsDev(env) {
raw, _ = ioutil.ReadFile("data/colors.json")
if _, err := os.Stat(colorsFile); err == nil {
raw, _ = os.ReadFile(colorsFile)
} else {
Log().Warn("attempted to read colors from local fs in dev mode, but failed", "file", colorsFile)
}
}
var colors = make(map[string]map[string]string)
if err := json.Unmarshal(raw, &colors); err != nil {
logbuch.Fatal(err.Error())
Log().Fatal(err.Error())
}
return colors
}
func mustReadConfigLocation() string {
if _, err := os.Stat(*cFlag); err != nil {
logbuch.Fatal("failed to find config file at '%s'", *cFlag)
}
return *cFlag
}
func resolveDbDialect(dbType string) string {
if dbType == "cockroach" {
return "postgres"
@@ -323,35 +517,6 @@ func resolveDbDialect(dbType string) string {
return dbType
}
func findString(needle string, haystack []string, defaultVal string) string {
for _, s := range haystack {
if s == needle {
return s
}
}
return defaultVal
}
func parseWeekday(s string) time.Weekday {
switch strings.ToLower(s) {
case "mon", strings.ToLower(time.Monday.String()):
return time.Monday
case "tue", strings.ToLower(time.Tuesday.String()):
return time.Tuesday
case "wed", strings.ToLower(time.Wednesday.String()):
return time.Wednesday
case "thu", strings.ToLower(time.Thursday.String()):
return time.Thursday
case "fri", strings.ToLower(time.Friday.String()):
return time.Friday
case "sat", strings.ToLower(time.Saturday.String()):
return time.Saturday
case "sun", strings.ToLower(time.Sunday.String()):
return time.Sunday
}
return time.Monday
}
func Set(config *Config) {
cfg = config
}
@@ -360,34 +525,48 @@ func Get() *Config {
return cfg
}
func Load(version string) *Config {
func Load(configFlag string, version string) *Config {
config := &Config{}
flag.Parse()
if err := configor.New(&configor.Config{}).Load(config, mustReadConfigLocation()); err != nil {
logbuch.Fatal("failed to read config: %v", err)
if err := configor.New(&configor.Config{}).Load(config, configFlag); err != nil {
Log().Fatal("failed to read config", err)
}
env = config.Env
InitLogger(config.IsDev())
config.Version = strings.TrimSpace(version)
tagVersionMatch, _ := regexp.MatchString(`\d+\.\d+\.\d+`, version)
tagVersionMatch, _ := regexp.MatchString(`\d+\.\d+\.\d+`, config.Version)
if tagVersionMatch {
config.Version = "v" + config.Version
}
config.InstanceId = uuid.NewV4().String()
config.InstanceId = uuid.Must(uuid.NewV4()).String()
config.App.Colors = readColors()
config.Db.Dialect = resolveDbDialect(config.Db.Type)
config.Security.SecureCookie = securecookie.New(
securecookie.GenerateRandomKey(64),
securecookie.GenerateRandomKey(32),
)
if strings.HasSuffix(config.Server.BasePath, "/") {
config.Server.BasePath = config.Server.BasePath[:len(config.Server.BasePath)-1]
if config.Db.Type == "cockroach" {
slog.Warn("cockroach is not officially supported, it is strongly recommended to migrate to postgres instead")
}
if config.Db.IsMssql() {
slog.Error("mssql is not supported anymore, sorry")
os.Exit(1)
}
hashKey := securecookie.GenerateRandomKey(64)
blockKey := securecookie.GenerateRandomKey(32)
sessionKey := securecookie.GenerateRandomKey(32)
if IsDev(env) {
slog.Warn("using temporary keys to sign and encrypt cookies in dev mode, make sure to set env to production for real-world use")
hashKey, blockKey = getTemporarySecureKeys()
blockKey = hashKey
}
config.Security.SecureCookie = securecookie.New(hashKey, blockKey)
config.Security.SessionKey = sessionKey
config.Security.ParseTrustReverseProxyIPs()
config.Server.BasePath = strings.TrimSuffix(config.Server.BasePath, "/")
for k, v := range config.App.CustomLanguages {
if v == "" {
@@ -396,34 +575,109 @@ func Load(version string) *Config {
}
if config.Sentry.Dsn != "" {
logbuch.Info("enabling sentry integration")
initSentry(config.Sentry, config.IsDev())
if config.Sentry.Environment == "" {
config.Sentry.Environment = config.Env
}
slog.Info("enabling sentry integration", "environment", config.Sentry.Environment)
initSentry(config.Sentry, config.IsDev(), config.Version)
}
if config.App.DataRetentionMonths <= 0 {
slog.Info("disabling data retention policy, keeping data forever")
} else {
dataRetentionWarning := fmt.Sprintf("⚠️ data retention policy will cause user data older than %d months to be deleted", config.App.DataRetentionMonths)
if config.Subscriptions.Enabled {
dataRetentionWarning += " (except for users with active subscriptions)"
}
slog.Warn(dataRetentionWarning)
}
// some validation checks
if config.Server.ListenIpV4 == "" && config.Server.ListenIpV6 == "" && config.Server.ListenSocket == "" {
logbuch.Fatal("either of listen_ipv4 or listen_ipv6 or listen_socket must be set")
if config.Server.ListenIpV4 == "-" && config.Server.ListenIpV6 == "-" && config.Server.ListenSocket == "" {
Log().Fatal("either of listen_ipv4 or listen_ipv6 or listen_socket must be set")
}
if config.Db.MaxConn <= 0 {
logbuch.Fatal("you must allow at least one database connection")
if config.Db.MaxConn < 2 && !config.Db.IsSQLite() {
Log().Warn("you should use a pool of at least 2 database connections")
}
if config.Db.MaxConn > 1 && config.Db.IsSQLite() {
logbuch.Warn("with sqlite, only a single connection is supported") // otherwise 'PRAGMA foreign_keys=ON' would somehow have to be set for every connection in the pool
Log().Warn("with sqlite, only a single connection is supported") // otherwise 'PRAGMA foreign_keys=ON' would somehow have to be set for every connection in the pool
config.Db.MaxConn = 1
}
if config.Mail.Provider != "" && findString(config.Mail.Provider, emailProviders, "") == "" {
logbuch.Fatal("unknown mail provider '%s'", config.Mail.Provider)
}
if _, err := time.Parse("15:04", config.App.GetWeeklyReportTime()); err != nil {
logbuch.Fatal("invalid interval set for report_time_weekly")
}
if _, err := time.Parse("15:04", config.App.AggregationTime); err != nil {
logbuch.Fatal("invalid interval set for aggregation_time")
if config.Mail.Provider != "" && utils.FindString(config.Mail.Provider, emailProviders, "") == "" {
Log().Fatal("unknown mail provider", "provider", config.Mail.Provider)
}
if _, err := time.ParseDuration(config.App.HeartbeatMaxAge); err != nil {
logbuch.Fatal("invalid duration set for heartbeat_max_age")
Log().Fatal("invalid duration set for heartbeat_max_age")
}
if config.Security.TrustedHeaderAuth && len(config.Security.trustReverseProxyIpsParsed) == 0 {
config.Security.TrustedHeaderAuth = false
}
if d, err := time.Parse(config.App.DateFormat, config.App.DateFormat); err != nil || !d.Equal(time.Date(2006, time.January, 2, 0, 0, 0, 0, d.Location())) {
Log().Fatal("invalid date format", "format", config.App.DateFormat)
}
if d, err := time.Parse(config.App.DateTimeFormat, config.App.DateTimeFormat); err != nil || !d.Equal(time.Date(2006, time.January, 2, 15, 4, 0, 0, d.Location())) {
Log().Fatal("invalid datetime format", "format", config.App.DateTimeFormat)
}
cronParser := cron.NewParser(cron.Second | cron.Minute | cron.Hour | cron.Dom | cron.Month | cron.Dow | cron.Descriptor)
if _, err := cronParser.Parse(config.App.GetWeeklyReportCron()); err != nil {
Log().Fatal("invalid cron expression for report_time_weekly")
}
if _, err := cronParser.Parse(config.App.GetAggregationTimeCron()); err != nil {
Log().Fatal("invalid cron expression for aggregation_time")
}
for _, c := range config.App.GetLeaderboardGenerationTimeCron() {
if _, err := cronParser.Parse(c); err != nil {
Log().Fatal("invalid cron expression for leaderboard_generation_time")
}
}
// see models/interval.go
if !slice.Contain[string](leaderboardScopes, config.App.LeaderboardScope) {
Log().Fatal("leaderboard scope is not a valid constant")
}
// deprecation notices
if strings.Contains(config.App.AggregationTime, ":") {
slog.Warn("you're using deprecated syntax for 'aggregation_time', please change it to a valid cron expression")
}
if strings.Contains(config.App.ReportTimeWeekly, ":") {
slog.Warn("you're using deprecated syntax for 'report_time_weekly', please change it to a valid cron expression")
}
if strings.Contains(config.App.LeaderboardGenerationTime, ":") {
slog.Warn("you're using deprecated syntax for 'leaderboard_generation_time', please change it to a semicolon-separated list if valid cron expressions")
}
Set(config)
// post config-load tasks
initOpenIDConnect(config)
return Get()
}
func Empty() *Config {
return &Config{
App: appConfig{},
Security: securityConfig{},
Db: dbConfig{},
Server: serverConfig{},
Subscriptions: subscriptionsConfig{},
Sentry: sentryConfig{},
Mail: mailConfig{},
}
}
func BeginningOfWakatime() time.Time {
t, _ := time.Parse(SimpleDateFormat, heartbeatsMinDate)
return t
}
func initOpenIDConnect(config *Config) {
// openid connect
for _, c := range config.Security.OidcProviders {
RegisterOidcProvider(&c)
slog.Info("registered openid connect provider", "provider", c.Name)
}
}

View File

@@ -2,8 +2,10 @@ package config
import (
"fmt"
"github.com/stretchr/testify/assert"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestConfig_IsDev(t *testing.T) {
@@ -24,10 +26,11 @@ func Test_mysqlConnectionString(t *testing.T) {
Dialect: "mysql",
Charset: "utf8mb4",
MaxConn: 10,
Compress: true,
}
assert.Equal(t, fmt.Sprintf(
"%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true&loc=%s&sql_mode=ANSI_QUOTES",
"%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true&loc=%s&compress=true&sql_mode=ANSI_QUOTES",
c.User,
c.Password,
c.Host,
@@ -37,6 +40,29 @@ func Test_mysqlConnectionString(t *testing.T) {
), mysqlConnectionString(c))
}
func Test_mysqlConnectionStringSocket(t *testing.T) {
c := &dbConfig{
Socket: "/var/run/mysql.sock",
Port: 9999,
User: "test_user",
Password: "test_password",
Name: "test_name",
Dialect: "mysql",
Charset: "utf8mb4",
MaxConn: 10,
Compress: true,
}
assert.Equal(t, fmt.Sprintf(
"%s:%s@unix(%s)/%s?charset=utf8mb4&parseTime=true&loc=%s&compress=true&sql_mode=ANSI_QUOTES",
c.User,
c.Password,
c.Socket,
c.Name,
"Local",
), mysqlConnectionString(c))
}
func Test_postgresConnectionString(t *testing.T) {
c := &dbConfig{
Host: "test_host",
@@ -63,5 +89,6 @@ func Test_sqliteConnectionString(t *testing.T) {
Name: "test_name",
Dialect: "sqlite3",
}
assert.Equal(t, c.Name, sqliteConnectionString(c))
assert.True(t, strings.HasPrefix(sqliteConnectionString(c), c.Name))
assert.Contains(t, strings.ToLower(sqliteConnectionString(c)), "journal_mode=wal")
}

View File

@@ -2,6 +2,7 @@ package config
import (
"fmt"
"github.com/glebarez/sqlite"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
@@ -15,12 +16,17 @@ A quick note to myself including some clarifications about time zones.
- From my understanding, MySQL server tz is only a fallback and can be ignored as long as a connection tz is specified
- All times are currently stored inside TIMESTAMP columns (alternatives would be DATETIME and BIGINT (plain Unix timestamps))
- TIMESTAMP columns, to my understanding, do not keep any time zone information, but only the very time they store
- Setting a `loc` parameter specifies what location parsed time.Time objects will be in, however, does not affect the session time zone setting (https://github.com/go-sql-driver/mysql#loc)
- I.e., when not setting `time_zone` in addition, the session time zone will probably default to the server time zone (UTC in case of Docker)
- Session time zone will result in conversions of inserted times from that time zone to UTC
- From my understanding, TIMESTAMP only stores a plain time value without tz information and then converts it only for retrieval to whatever tz is set for the session
- Setting a `loc` parameter specifies location parsed time.Time objects are interpreted in, however, does not affect the session time zone setting (https://github.com/go-sql-driver/mysql#loc)
- I.e., when not setting `time_zone` in addition, the session time zone will default to the SYSTEM time zone (UTC in case of Docker)
- Session time zone will result in conversions of inserted times from that time zone to UTC (but, again, we don't use that)
- TIMESTAMP only stores a plain time value without tz information and then converts it only for retrieval to whatever tz is set for the session
- E.g., when inserting '2021-04-27 08:26:07' with session tz set to Europe/Berlin and then viewing the database table with UTC tz will return '2021-04-27 06:26:07' instead
- Currently, no session tz is set (only loc), so the database server will assume it receives UTC. However, as no tz is set when retrieving the values either, they are also going to be returned just as is and as long as `loc=Local` is set properly, they are parsed in Go code with the correct time zone
- Loc is a parameter of the driver and used in conjunction with parseTime. It results in time.Time objects being converted to that respective zone before storage and converted back to it during retrieval
- E.g., when inserting '2021-04-27 08:26:07 +01:00' with loc set to UTC results in the actual database column to show '2021-04-27 07:26:07'
- A loc value of "Local" is effective the same as setting loc to the value of the TZ environment variable (or system default otherwise)
- Note that if loc is different from "Local", when using models.CustomTime, values are first received in loc zone and then converted to whatever Wakapi's current zone is (e.g. specified through TZ env.), see https://github.com/muety/wakapi/blob/bc2096f4117275d110a84f5b367aa8fdb4bd87ba/models/shared.go#L95.
- Currently, we don't set a session tz (only loc), so the database server will assume it receives dates in its system time. However, as no tz is set when retrieving the values either, they are also going to be returned just as is and as long as `loc=Local` is set properly, they are parsed in Go code with the correct time zone
- In other words, using loc causes the Go driver to take care of zones, while using a session timezone causes MySQL to take care of zones (?)
- As long as the Wakapi server always runs in the same time zone, it will always parse these dates the same way (i.e. as time.Local, Europe/Berlin in case of Wakapi.dev)
- Using TIMESTAMP columns would only become problematic when either data needs to be migrated to a Wakapi instance in a different tz or if two consumers in different tzs were reading and writing to the same table
- It is important to have same `time_zone` and `loc` parameters set when sending and receiving, no matter what it is (writing / reading in 'UTC' will yield same results as writing / reading in 'Europe/Berlin')
@@ -34,6 +40,14 @@ A quick note to myself including some clarifications about time zones.
- A request with `?from=2021-04-25` from California (PST / UTC-7) would ideally have to be translated into a database query like `from >= 2021-04-25T00:00:00+0900)`, assuming that Wakapi runs at CEST (UTC+2)
- This translation comes from either the user explicitly requesting with a specified tz (i.e. sending `from` as ISO8601 / RFC3999) or them having specified a tz in their profile
- Implicit intervals are tricky, too, as they are generated on the server, but still have to respect the user's tz, as `today` is different for a user in Cali and one in Karlsruhe
For Postgres, the story is even a different one.
- There are `timestamp` and `timestamptz` columns, whereby the former seems to behave very strangely.
- Apparently, when storing dates, they'll just chop off zone information entirely, while for retrieval, all dates are interpreted as UTC
- If Wakapi runs in CEST, '2025-03-25T14:00:00 +02:00' will end up as '2025-03-25T14:00:00' in the database and become '2025-03-25T16:00:00 +02:00' when retrieved back (at least in case of our annoying models.CustomTime, because of https://github.com/muety/wakapi/blob/bc2096f4117275d110a84f5b367aa8fdb4bd87ba/models/shared.go#L95)
- For `timestamptz`, columns don't actually store tz information either (https://github.com/jackc/pgx/issues/520#issuecomment-479692198), but at least allow for correct retrieval. The driver will return dates already in the application's TZ
- Here (https://github.com/go-gorm/gorm/issues/4834) is an interesting discussion on the issue
- According to https://www.cybertec-postgresql.com/en/time-zone-management-in-postgresql/, good practice is to always use timestamptz, leaving conversions to the database itself
*/
func (c *dbConfig) GetDialector() gorm.Dialector {
@@ -50,18 +64,29 @@ func (c *dbConfig) GetDialector() gorm.Dialector {
case SQLDialectSqlite:
return sqlite.Open(sqliteConnectionString(c))
}
return nil
}
func mysqlConnectionString(config *dbConfig) string {
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=true&loc=%s&sql_mode=ANSI_QUOTES",
if len(config.DSN) > 0 {
return config.DSN
}
host := fmt.Sprintf("tcp(%s:%d)", config.Host, config.Port)
if config.Socket != "" {
host = fmt.Sprintf("unix(%s)", config.Socket)
}
return fmt.Sprintf("%s:%s@%s/%s?charset=%s&parseTime=true&loc=%s&compress=%v&sql_mode=ANSI_QUOTES",
config.User,
config.Password,
config.Host,
config.Port,
host,
config.Name,
config.Charset,
"Local",
config.Compress,
)
}
@@ -75,6 +100,9 @@ func postgresConnectionString(config *dbConfig) string {
sslmode = "require"
}
// note: passing a `timezone` param here doesn't seem to have any effect, neither with `timestamp`, not for `timestamptz` columns
// possibly related to https://github.com/go-gorm/postgres/issues/199 ?
return fmt.Sprintf("host=%s port=%d user=%s dbname=%s password=%s sslmode=%s",
config.Host,
config.Port,
@@ -86,5 +114,5 @@ func postgresConnectionString(config *dbConfig) string {
}
func sqliteConnectionString(config *dbConfig) string {
return config.Name
return fmt.Sprintf("%s?busy_timeout=10000&journal_mode=wal", config.Name)
}

35
config/db_opts.go Normal file
View File

@@ -0,0 +1,35 @@
package config
import (
"gorm.io/gorm"
)
type WakapiDBOpts struct {
dbConfig *dbConfig
}
func GetWakapiDBOpts(dbConfig *dbConfig) *WakapiDBOpts {
return &WakapiDBOpts{dbConfig: dbConfig}
}
func (opts WakapiDBOpts) Apply(config *gorm.Config) error {
return nil
}
func (opts WakapiDBOpts) AfterInitialize(db *gorm.DB) error {
// initial session variables
if opts.dbConfig.Type == "cockroach" {
// https://www.cockroachlabs.com/docs/stable/experimental-features.html#alter-column-types
if err := db.Exec("SET enable_experimental_alter_column_type_general = true;").Error; err != nil {
return err
}
}
if opts.dbConfig.IsSQLite() {
if err := db.Exec("PRAGMA foreign_keys = ON;").Error; err != nil {
return err
}
}
return nil
}

View File

@@ -8,17 +8,19 @@ type ApplicationEvent struct {
}
const (
TopicUser = "user.*"
TopicHeartbeat = "heartbeat.*"
TopicProjectLabel = "project_label.*"
EventUserUpdate = "user.update"
EventHeartbeatCreate = "heartbeat.create"
EventProjectLabelCreate = "project_label.create"
EventProjectLabelDelete = "project_label.delete"
EventWakatimeFailure = "wakatime.failure"
FieldPayload = "payload"
FieldUser = "user"
FieldUserId = "user.id"
TopicUser = "user.*"
TopicHeartbeat = "heartbeat.*"
TopicProjectLabel = "project_label.*"
EventUserUpdate = "user.update"
EventUserDelete = "user.delete"
EventHeartbeatCreate = "heartbeat.create"
EventProjectLabelCreate = "project_label.create"
EventProjectLabelDelete = "project_label.delete"
EventWakatimeFailure = "wakatime.failure"
EventLanguageMappingsChanged = "language_mappings.changed"
FieldPayload = "payload"
FieldUser = "user"
FieldUserId = "user.id"
)
var eventHub *hub.Hub

View File

@@ -8,7 +8,10 @@ import (
// ChooseFS returns a local (DirFS) file system when on 'dev' environment and the given go-embed file system otherwise
func ChooseFS(localDir string, embeddedFS fs.FS) fs.FS {
if Get().IsDev() {
return os.DirFS(localDir)
if _, err := os.Stat(localDir); err == nil {
return os.DirFS(localDir)
}
Log().Warn("attempted to use local fs for directory in dev mode, but failed", "path", localDir)
}
return embeddedFS
}

80
config/jobqueue.go Normal file
View File

@@ -0,0 +1,80 @@
package config
import (
"fmt"
"github.com/muety/artifex/v2"
"github.com/muety/wakapi/utils"
"log/slog"
)
var jobQueues map[string]*artifex.Dispatcher
var jobCounts map[string]int
const (
QueueDefault = "wakapi.default"
QueueProcessing = "wakapi.processing"
QueueProcessing2 = "wakapi.processing_2"
QueueReports = "wakapi.reports"
QueueMails = "wakapi.mail"
QueueImports = "wakapi.imports"
QueueHousekeeping = "wakapi.housekeeping"
)
type JobQueueMetrics struct {
Queue string
EnqueuedJobs int
FinishedJobs int
}
func init() {
jobQueues = make(map[string]*artifex.Dispatcher)
}
func StartJobs() {
InitQueue(QueueDefault, 1)
InitQueue(QueueProcessing, utils.HalfCPUs())
InitQueue(QueueProcessing2, utils.HalfCPUs())
InitQueue(QueueReports, 1)
InitQueue(QueueMails, 1)
InitQueue(QueueImports, 1)
InitQueue(QueueHousekeeping, utils.HalfCPUs())
}
func InitQueue(name string, workers int) error {
if _, ok := jobQueues[name]; ok {
return fmt.Errorf("queue '%s' already existing", name)
}
slog.Info("creating job queue", "name", name, "workers", workers)
jobQueues[name] = artifex.NewDispatcher(workers, 4096)
jobQueues[name].Start()
return nil
}
func GetDefaultQueue() *artifex.Dispatcher {
return GetQueue(QueueDefault)
}
func GetQueue(name string) *artifex.Dispatcher {
if _, ok := jobQueues[name]; !ok {
InitQueue(name, 1)
}
return jobQueues[name]
}
func GetQueueMetrics() []*JobQueueMetrics {
metrics := make([]*JobQueueMetrics, 0, len(jobQueues))
for name, queue := range jobQueues {
metrics = append(metrics, &JobQueueMetrics{
Queue: name,
EnqueuedJobs: queue.CountEnqueued(),
FinishedJobs: queue.CountDispatched(),
})
}
return metrics
}
func CloseQueues() {
for _, q := range jobQueues {
q.Stop()
}
}

40
config/key_utils.go Normal file
View File

@@ -0,0 +1,40 @@
package config
import (
"github.com/gorilla/securecookie"
"io"
"os"
"path/filepath"
)
func getTemporarySecureKeys() (hashKey, blockKey []byte) {
keyFile := filepath.Join(os.TempDir(), ".wakapi-dev-keys")
// key file already exists
if _, err := os.Stat(keyFile); err == nil {
file, err := os.Open(keyFile)
if err != nil {
Log().Fatal("failed to open dev keys file", "error", err)
}
defer file.Close()
combinedKey, err := io.ReadAll(file)
if err != nil {
Log().Fatal("failed to read key from file", "error", err)
}
return combinedKey[:32], combinedKey[32:64]
}
// otherwise, generate random keys and save them
file, err := os.OpenFile(keyFile, os.O_CREATE|os.O_WRONLY, 0600)
if err != nil {
Log().Fatal("failed to open dev keys file", "error", err)
}
defer file.Close()
combinedKey := securecookie.GenerateRandomKey(64)
if _, err := file.Write(combinedKey); err != nil {
Log().Fatal("failed to write key to file", "error", err)
}
return combinedKey[:32], combinedKey[32:64]
}

16
config/logging.go Normal file
View File

@@ -0,0 +1,16 @@
package config
import (
"log/slog"
"os"
)
func InitLogger(isDev bool) {
var handler slog.Handler
if isDev {
handler = slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})
} else {
handler = slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})
}
slog.SetDefault(slog.New(handler))
}

90
config/oidc.go Normal file
View File

@@ -0,0 +1,90 @@
package config
import (
"context"
"fmt"
"strings"
"time"
"github.com/coreos/go-oidc/v3/oidc"
"golang.org/x/oauth2"
)
type OidcProvider struct {
Name string
OAuth2 *oauth2.Config
Verifier *oidc.IDTokenVerifier
}
type IdTokenPayload struct {
Issuer string `json:"iss"`
Subject string `json:"sub"`
Expiry int64 `json:"exp"`
Name string `json:"name"`
Nickname string `json:"nickname"`
PreferredUsername string `json:"preferred_username"`
Email string `json:"email"`
EmailVerified bool `json:"email_verified"`
ProviderName string `json:"provider_name"` // custom field, not part of actual id token response
}
func (token *IdTokenPayload) Exp() time.Time {
return time.Unix(token.Expiry, 0)
}
func (token *IdTokenPayload) IsValid() bool {
return token.Exp().After(time.Now())
}
func (token *IdTokenPayload) Username() string {
// https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims
if s := strings.TrimSpace(token.PreferredUsername); s != "" {
return s
}
if s := strings.TrimSpace(token.Nickname); s != "" {
return s
}
if s := strings.TrimSpace(token.Subject); s != "" {
return s
}
return ""
}
var oidcProviders = make(map[string]*OidcProvider)
func RegisterOidcProvider(providerCfg *oidcProviderConfig) {
cfg := Get()
provider, err := oidc.NewProvider(context.Background(), providerCfg.Endpoint)
if err != nil {
Log().Fatal(fmt.Sprintf("failed to initialize oidc provider at %s", providerCfg.Endpoint), "error", err)
return
}
oauth2Conf := oauth2.Config{
ClientID: providerCfg.ClientID,
ClientSecret: providerCfg.ClientSecret,
RedirectURL: fmt.Sprintf("%s/oidc/%s/callback", cfg.Server.GetPublicUrl(), providerCfg.Name),
Endpoint: provider.Endpoint(),
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
}
oidcProviders[providerCfg.Name] = &OidcProvider{
Name: providerCfg.Name,
OAuth2: &oauth2Conf,
Verifier: provider.Verifier(&oidc.Config{ClientID: providerCfg.ClientID}),
}
}
func GetOidcProvider(name string) (*OidcProvider, error) {
provider, ok := oidcProviders[name]
if !ok {
return nil, fmt.Errorf("oidc provider not found: %s", name)
}
return provider, nil
}
func MustGetOidcProvider(name string) *OidcProvider {
provider, _ := GetOidcProvider(name)
return provider
}

View File

@@ -1,105 +1,78 @@
package config
import (
"github.com/emvi/logbuch"
"github.com/getsentry/sentry-go"
"github.com/muety/wakapi/models"
"io"
"log/slog"
"net/http"
"os"
"regexp"
"strings"
"time"
"github.com/getsentry/sentry-go"
slogmulti "github.com/samber/slog-multi"
slogsentry "github.com/samber/slog-sentry/v2"
)
// How to: Logging
// Use logbuch.[Debug|Info|Warn|Error|Fatal]() by default
// Use slog.[Debug|Info|Warn|Error|Fatal]() by default
// Use config.Log().[Debug|Info|Warn|Error|Fatal]() when wanting the log to appear in Sentry as well
type capturingWriter struct {
Writer io.Writer
Message string
// SentryLogger wraps slog.Logger and provides a Fatal method
type SentryLogger struct {
*slog.Logger
}
func (c *capturingWriter) Clear() {
c.Message = ""
}
var sentryLogger *SentryLogger
func (c *capturingWriter) Write(p []byte) (n int, err error) {
c.Message = string(p)
return c.Writer.Write(p)
}
// SentryWrapperLogger is a wrapper around a logbuch.Logger that forwards events to Sentry in addition and optionally allows to attach a request context
type SentryWrapperLogger struct {
*logbuch.Logger
req *http.Request
outWriter *capturingWriter
errWriter *capturingWriter
}
func Log() *SentryWrapperLogger {
ow, ew := &capturingWriter{Writer: os.Stdout}, &capturingWriter{Writer: os.Stderr}
return &SentryWrapperLogger{
Logger: logbuch.NewLogger(ow, ew),
outWriter: ow,
errWriter: ew,
func New() *SentryLogger {
level := slog.LevelInfo
if IsDev(env) {
level = slog.LevelDebug
}
}
func (l *SentryWrapperLogger) Request(req *http.Request) *SentryWrapperLogger {
l.req = req
return l
}
func (l *SentryWrapperLogger) Debug(msg string, params ...interface{}) {
l.outWriter.Clear()
l.Logger.Debug(msg, params...)
l.log(l.errWriter.Message, sentry.LevelDebug)
}
func (l *SentryWrapperLogger) Info(msg string, params ...interface{}) {
l.outWriter.Clear()
l.Logger.Info(msg, params...)
l.log(l.errWriter.Message, sentry.LevelInfo)
}
func (l *SentryWrapperLogger) Warn(msg string, params ...interface{}) {
l.outWriter.Clear()
l.Logger.Warn(msg, params...)
l.log(l.errWriter.Message, sentry.LevelWarning)
}
func (l *SentryWrapperLogger) Error(msg string, params ...interface{}) {
l.errWriter.Clear()
l.Logger.Error(msg, params...)
l.log(l.errWriter.Message, sentry.LevelError)
}
func (l *SentryWrapperLogger) Fatal(msg string, params ...interface{}) {
l.errWriter.Clear()
l.Logger.Fatal(msg, params...)
l.log(l.errWriter.Message, sentry.LevelFatal)
}
func (l *SentryWrapperLogger) log(msg string, level sentry.Level) {
event := sentry.NewEvent()
event.Level = level
event.Message = msg
if l.req != nil {
if h := l.req.Context().Value(sentry.HubContextKey); h != nil {
hub := h.(*sentry.Hub)
hub.Scope().SetRequest(l.req)
if u := getPrincipal(l.req); u != nil {
hub.Scope().SetUser(sentry.User{ID: u.ID})
filterRequestInfo := slogmulti.NewWithAttrsInlineMiddleware(func(attrs []slog.Attr, next func([]slog.Attr) slog.Handler) slog.Handler {
attrsNew := []slog.Attr{}
for _, attr := range attrs {
if attr.Key != "request" {
attrsNew = append(attrsNew, attr)
}
hub.CaptureEvent(event)
return
}
}
return next(attrsNew)
})
sentry.CaptureEvent(event)
return &SentryLogger{Logger: slog.New(
slogmulti.Fanout(
slogmulti.Pipe(filterRequestInfo).Handler(slog.Default().Handler()),
slogsentry.Option{Level: level}.NewSentryHandler(),
),
)}
}
func Log() *SentryLogger {
// note: do not set any state (e.g. request attribute) on this cached logger instance
if sentryLogger != nil {
return sentryLogger
}
sentryLogger = New()
return sentryLogger
}
func (l *SentryLogger) Fatal(msg string, args ...any) {
l.Error(msg, args...)
sentry.Flush(2 * time.Second)
os.Exit(1)
}
func (l *SentryLogger) Request(r *http.Request) *SentryLogger {
ll := New()
ll.Logger = ll.Logger.With("request", r)
if uid := getPrincipal(r); uid != "" {
ll.Logger = ll.Logger.With(slog.Group("user", slog.String("id", uid)))
}
return ll
}
var heartbeatsRouteRegex = regexp.MustCompile(`^POST /api/(?:compat/wakatime/)?(?:v1/)?(?:users/[\w\d-_]+/)?heartbeats?(?:\.bulk)?$`)
var excludedRoutes = []string{
"GET /assets",
"GET /api/health",
@@ -107,50 +80,45 @@ var excludedRoutes = []string{
"GET /docs",
}
func initSentry(config sentryConfig, debug bool) {
func initSentry(config sentryConfig, debug bool, releaseVersion string) {
if err := sentry.Init(sentry.ClientOptions{
Dsn: config.Dsn,
Debug: debug,
Environment: config.Environment,
Release: releaseVersion,
AttachStacktrace: true,
TracesSampler: sentry.TracesSamplerFunc(func(ctx sentry.SamplingContext) sentry.Sampled {
if !config.EnableTracing {
return sentry.SampledFalse
}
hub := sentry.GetHubFromContext(ctx.Span.Context())
txName := hub.Scope().Transaction()
EnableTracing: config.EnableTracing,
TracesSampler: func(ctx sentry.SamplingContext) float64 {
txName := ctx.Span.Name
for _, ex := range excludedRoutes {
if strings.HasPrefix(txName, ex) {
return sentry.SampledFalse
return 0.0
}
}
if txName == "POST /api/heartbeat" {
return sentry.UniformTracesSampler(config.SampleRateHeartbeats).Sample(ctx)
if heartbeatsRouteRegex.Match([]byte(txName)) {
return float64(config.SampleRateHeartbeats)
}
return sentry.UniformTracesSampler(config.SampleRate).Sample(ctx)
}),
return float64(config.SampleRate)
},
BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
if hint.Context != nil {
if req, ok := hint.Context.Value(sentry.RequestContextKey).(*http.Request); ok {
if u := getPrincipal(req); u != nil {
event.User.ID = u.ID
}
}
}
// optional pre-processing before sending the event off
return event
},
}); err != nil {
logbuch.Fatal("failed to initialized sentry - %v", err)
Log().Fatal("failed to initialized sentry", "error", err)
}
}
func getPrincipal(r *http.Request) *models.User {
type principalGetter interface {
GetPrincipal() *models.User
// returns a user id
func getPrincipal(r *http.Request) string {
sharedData := r.Context().Value(KeySharedData)
if sharedData == nil {
Log().Error("request shared data not set while retrieving principal for sentry logging")
return ""
}
if p := r.Context().Value("principal"); p != nil {
return p.(principalGetter).GetPrincipal()
val := sharedData.(*SharedData).MustGet(MiddlewareKeyPrincipalId)
if val == nil {
return ""
}
return nil
return val.(string)
}

23
config/session.go Normal file
View File

@@ -0,0 +1,23 @@
package config
import (
"github.com/gorilla/sessions"
)
// sessions are only used for displaying flash messages
var sessionStore *sessions.CookieStore
func NewSessionStore() *sessions.CookieStore {
return sessions.NewCookieStore(
Get().Security.SessionKey,
Get().Security.SessionKey,
)
}
func GetSessionStore() *sessions.CookieStore {
if sessionStore == nil {
sessionStore = NewSessionStore()
}
return sessionStore
}

20
config/shared_data.go Normal file
View File

@@ -0,0 +1,20 @@
package config
import (
"github.com/muety/wakapi/lib"
)
type SharedDataKey string
const (
MiddlewareKeyPrincipal = SharedDataKey("principal")
MiddlewareKeyPrincipalId = SharedDataKey("principal_identity")
)
type SharedData struct {
*lib.ConcurrentMap[SharedDataKey, interface{}]
}
func NewSharedData() *SharedData {
return &SharedData{lib.NewConcurrentMap[SharedDataKey, interface{}]()}
}

View File

@@ -10,4 +10,5 @@ const (
SettingsTemplate = "settings.tpl.html"
SummaryTemplate = "summary.tpl.html"
LeaderboardTemplate = "leaderboard.tpl.html"
ProjectsTemplate = "projects.tpl.html"
)

13
config/testutils.go Normal file
View File

@@ -0,0 +1,13 @@
package config
func WithOidcProvider(c *Config, name, clientId, clientSecret, Endpoint string) *Config {
providerConf := oidcProviderConfig{
Name: name,
ClientID: clientId,
ClientSecret: clientSecret,
Endpoint: Endpoint,
}
c.Security.OidcProviders = append(c.Security.OidcProviders, providerConf)
RegisterOidcProvider(&providerConf) // config must be Set() for this to work
return c
}

View File

@@ -1,14 +0,0 @@
package config
import "strings"
func cloneStringMap(m map[string]string, keysToLower bool) map[string]string {
m2 := make(map[string]string)
for k, v := range m {
if keysToLower {
k = strings.ToLower(k)
}
m2[k] = v
}
return m2
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,334 +1,605 @@
{
"languages": {
"1C Enterprise": "#814CCC",
"2-Dimensional Array": "#38761D",
"4D": "#004289",
"ABAP": "#E8274B",
"AGS Script": "#B9D9FF",
"AL": "#3AA2B5",
"AMPL": "#E6EFBB",
"ANTLR": "#9DC3FF",
"API Blueprint": "#2ACCA8",
"APL": "#8a0707",
"ASP.NET": "#9400ff",
"ATS": "#1ac620",
"ActionScript": "#e3491a",
"ABAP CDS": "#555e25",
"ActionScript": "#882B0F",
"Ada": "#02f88c",
"Agda": "#467C91",
"Alloy": "#cc5c24",
"Adblock Filter List": "#800000",
"Adobe Font Metrics": "#fa0f00",
"Agda": "#315665",
"AGS Script": "#B9D9FF",
"AIDL": "#34EB6B",
"AL": "#3AA2B5",
"Alloy": "#64C800",
"Alpine Abuild": "#0D597F",
"Altium Designer": "#A89663",
"AMPL": "#E6EFBB",
"AngelScript": "#C7D7DC",
"Ant Build System": "#A9157E",
"Antlers": "#ff269e",
"ANTLR": "#9DC3FF",
"ApacheConf": "#d12127",
"Apex": "#1797c0",
"API Blueprint": "#2ACCA8",
"APL": "#5A8164",
"Apollo Guidance Computer": "#0B3D91",
"AppleScript": "#101F1F",
"Arc": "#ca2afe",
"Arduino": "#bd79d1",
"AspectJ": "#1957b0",
"Arc": "#aa2afe",
"AsciiDoc": "#73a0c5",
"ASL": null,
"ASP.NET": "#9400ff",
"AspectJ": "#a957b0",
"Assembly": "#6E4C13",
"Astro": "#ff5a03",
"Asymptote": "#ff0000",
"Augeas": "#62331f",
"ATS": "#1ac620",
"Augeas": "#9CC134",
"AutoHotkey": "#6594b9",
"AutoIt": "#36699B",
"AutoIt": "#1C3552",
"Avro IDL": "#0040FF",
"Awk": "#c30e9b",
"Ballerina": "#FF5000",
"BASIC": "#ff0000",
"Batchfile": "#C1F12E",
"Beef": "#a52f4e",
"Befunge": null,
"Berry": "#15A13C",
"BibTeX": "#778899",
"Bicep": "#519aba",
"Bikeshed": "#5562ac",
"Bison": "#6A463F",
"BitBake": "#00bce4",
"Blade": "#f7523f",
"BlitzBasic": "#00FFAE",
"BlitzMax": "#cd6400",
"Bluespec": "#12223c",
"Boo": "#d4bec1",
"Boogie": "#c80fa0",
"Brainfuck": "#2F2530",
"BrighterScript": "#66AABB",
"Brightscript": "#662D91",
"Browserslist": "#ffd539",
"C": "#555555",
"C Sharp": "#178600",
"C#": "#5a25a2",
"C#": "#178600",
"C++": "#f34b7d",
"CSON": "#244776",
"CSS": "#563d7c",
"C2hs Haskell": null,
"Cabal Config": "#483465",
"Cadence": "#00ef8b",
"Cairo": "#ff4a48",
"CameLIGO": "#3be133",
"CAP CDS": "#0092d1",
"Cap'n Proto": "#c42727",
"CartoCSS": null,
"Ceylon": "#dfa535",
"Chapel": "#8dc63f",
"Cirru": "#aaaaff",
"Charity": null,
"ChucK": "#3f8000",
"Cirru": "#ccccff",
"Clarion": "#db901e",
"Clarity": "#5546ff",
"Classic ASP": "#6a40fd",
"Clean": "#3a81ad",
"Clean": "#3F85AF",
"Click": "#E4E6F3",
"CLIPS": "#00A300",
"Clojure": "#db5855",
"Closure Templates": "#0d948f",
"Cloud Firestore Security Rules": "#FFA000",
"CMake": "#DA3434",
"COBOL": null,
"CodeQL": "#140f46",
"CoffeeScript": "#244776",
"ColdFusion": "#ed2cd6",
"ColdFusion CFC": "#ed2cd6",
"COLLADA": "#F1A42B",
"Common Lisp": "#3fb68b",
"Common Workflow Language": "#B5314C",
"Component Pascal": "#b0ce4e",
"Component Pascal": "#B0CE4E",
"Cool": null,
"Coq": "#d0b68c",
"Crystal": "#000100",
"CSON": "#244776",
"Csound": "#1a1a1a",
"Csound Document": "#1a1a1a",
"Csound Score": "#1a1a1a",
"CSS": "#563d7c",
"CSV": "#237346",
"Cuda": "#3A4E3A",
"D": "#fcd46d",
"DM": "#075ff1",
"CUE": "#5886E1",
"Curry": "#531242",
"CWeb": "#00007a",
"Cycript": null,
"Cypher": "#34c0eb",
"Cython": "#fedf5b",
"D": "#ba595e",
"Dafny": "#FFEC25",
"Dart": "#98BAD6",
"Darcs Patch": "#8eff23",
"Dart": "#00B4AB",
"DataWeave": "#003a52",
"Denizen": "#faf094",
"Debian Package Control File": "#D70751",
"DenizenScript": "#FBEE96",
"Dhall": "#dfafff",
"DIGITAL Command Language": null,
"DirectX 3D File": "#aace60",
"DM": "#447265",
"Dockerfile": "#384d54",
"Dogescript": "#cca760",
"Dylan": "#3ebc27",
"DTrace": null,
"Dylan": "#6c616e",
"E": "#ccce35",
"Earthly": "#2af0ff",
"Easybuild": "#069406",
"eC": "#913960",
"Ecere Projects": "#913960",
"ECL": "#8a1267",
"ECLiPSe": "#001d9d",
"Ecmarkup": "#eb8131",
"EditorConfig": "#fff1f2",
"Eiffel": "#4d6977",
"EJS": "#a91e50",
"EQ": "#a78649",
"Eagle": "#3994bc",
"Eiffel": "#946d57",
"Elixir": "#6e4a7e",
"Elm": "#60B5CC",
"Elvish": "#55BB55",
"Emacs Lisp": "#c065db",
"EmberScript": "#f64e3e",
"Erlang": "#0faf8d",
"EmberScript": "#FFF4F3",
"EQ": "#a78649",
"Erlang": "#B83998",
"Euphoria": "#FF790B",
"F#": "#b845fc",
"F*": "#572e30",
"FLUX": "#33CCFF",
"FORTRAN": "#4d41b1",
"Factor": "#636746",
"Fancy": "#7b9db4",
"Fantom": "#dbded5",
"Fantom": "#14253c",
"Faust": "#c37240",
"Fennel": "#fff3d7",
"FIGlet Font": "#FFDDBB",
"Filebench WML": "#F6B900",
"Filterscript": null,
"fish": "#4aae47",
"Fluent": "#ffcc33",
"FLUX": "#88ccff",
"Forth": "#341708",
"Fortran": "#4d41b1",
"Fortran Free Form": "#4d41b1",
"FreeBasic": "#867db1",
"FreeMarker": "#0050b2",
"Frege": "#00cafe",
"Futhark": "#5f021f",
"G-code": "#D08CF2",
"Game Maker Language": "#71b417",
"GAML": "#FFC766",
"GAMS": "#f49a22",
"GAP": "#0000cc",
"GCC Machine Description": "#FFCFAB",
"GDB": null,
"GDScript": "#355570",
"Game Maker Language": "#8ad353",
"GEDCOM": "#003058",
"Gemfile.lock": "#701516",
"Gemini": "#ff6900",
"Genero": "#63408e",
"Genero Forms": "#d8df39",
"Genie": "#fb855d",
"Genshi": "#951531",
"Gentoo Ebuild": "#9400ff",
"Gentoo Eclass": "#9400ff",
"Gerber Image": "#d20b00",
"Gherkin": "#5B2063",
"Glyph": "#e4cc98",
"Git Attributes": "#F44D27",
"Git Config": "#F44D27",
"Git Revision List": "#F44D27",
"Gleam": "#ffaff3",
"GLSL": "#5686a5",
"Glyph": "#c1ac7f",
"Gnuplot": "#f0a9f0",
"Go": "#375eab",
"Golo": "#f6a51f",
"Go": "#00ADD8",
"Go Checksums": "#00ADD8",
"Go Module": "#00ADD8",
"Golo": "#88562A",
"Gosu": "#82937f",
"Grace": "#615f8b",
"Gradle": "#02303a",
"Grammatical Framework": "#ff0000",
"GraphQL": "#e10098",
"Groovy": "#e69f56",
"HTML": "#e44b23",
"Graphviz (DOT)": "#2596be",
"Groovy": "#4298b8",
"Groovy Server Pages": "#4298b8",
"GSC": "#FF6800",
"Hack": "#878787",
"Haml": "#ece2a9",
"Handlebars": "#f7931e",
"HAProxy": "#106da9",
"Harbour": "#0e60e3",
"Haskell": "#29b544",
"Haxe": "#f7941e",
"Haskell": "#5e5086",
"Haxe": "#df7900",
"HCL": null,
"HiveQL": "#dce200",
"HLSL": "#aace60",
"HOCON": "#9ff8ee",
"HolyC": "#ffefaf",
"Hy": "#7891b1",
"IDL": "#e3592c",
"IGOR Pro": "#0000cc",
"hoon": "#00b171",
"HTML": "#e34c26",
"HTML+ECR": "#2e1052",
"HTML+EEX": "#6e4a7e",
"HTML+ERB": "#701516",
"HTML+PHP": "#4f5d95",
"HTML+Razor": "#512be4",
"HTTP": "#005C9C",
"HXML": "#f68712",
"Hy": "#7790B2",
"HyPhy": null,
"IDL": "#a3522f",
"Idris": "#b30000",
"Ignore List": "#000000",
"IGOR Pro": "#0000cc",
"ImageJ Macro": "#99AAFF",
"Imba": "#16cec6",
"Inform 7": null,
"INI": "#d1dbe0",
"Ink": null,
"Inno Setup": "#264b99",
"Io": "#a9188d",
"Ioke": "#078193",
"Isabelle": "#fdcd00",
"Isabelle": "#FEFE00",
"Isabelle ROOT": "#FEFE00",
"J": "#9EEDFF",
"JFlex": "#DBCA00",
"JSONiq": "#40d47e",
"Janet": "#0886a5",
"JAR Manifest": "#b07219",
"Jasmin": "#d03600",
"Java": "#b07219",
"Java Properties": "#2A6277",
"Java Server Pages": "#2A6277",
"JavaScript": "#f1e05a",
"JavaScript+ERB": "#f1e05a",
"Jest Snapshot": "#15c213",
"JetBrains MPS": "#21D789",
"JFlex": "#DBCA00",
"Jinja": "#a52a22",
"Jison": "#56b3cb",
"Jison Lex": "#56b3cb",
"Jolie": "#843179",
"jq": "#c7254e",
"JSON": "#292929",
"JSON with Comments": "#292929",
"JSON5": "#267CB9",
"JSONiq": "#40d47e",
"JSONLD": "#0c479c",
"Jsonnet": "#0064bd",
"Julia": "#a270ba",
"Jupyter Notebook": "#DA5B0B",
"KRL": "#f5c800",
"just": "#384d54",
"Kaitai Struct": "#773b37",
"Kotlin": "#F18E33",
"LFE": "#004200",
"LLVM": "#185619",
"LOLCODE": "#cc9900",
"LSL": "#3d9970",
"Lark": "#0b130f",
"Lasso": "#2584c3",
"Latte": "#A8FF97",
"KakouneScript": "#6f8042",
"KiCad Layout": "#2f4aab",
"KiCad Legacy Layout": "#2f4aab",
"KiCad Schematic": "#2f4aab",
"Kotlin": "#A97BFF",
"KRL": "#28430A",
"kvlang": "#1da6e0",
"LabVIEW": "#fede06",
"Lark": "#2980B9",
"Lasso": "#999999",
"Latte": "#f2a542",
"Lean": null,
"Less": "#1d365d",
"Lex": "#DBCA00",
"LFE": "#4C3023",
"LigoLANG": "#0e74ff",
"LilyPond": "#9ccc7c",
"Limbo": null,
"Liquid": "#67b8de",
"Literate Agda": "#315665",
"Literate CoffeeScript": "#244776",
"Literate Haskell": "#5e5086",
"LiveScript": "#499886",
"LLVM": "#185619",
"Logos": null,
"Logtalk": "#295b9a",
"LOLCODE": "#cc9900",
"LookML": "#652B81",
"Lua": "#fa1fa1",
"MATLAB": "#e16737",
"MAXScript": "#00a6a6",
"MLIR": "#5EC8DB",
"MQL4": "#62A8D6",
"MQL5": "#4A76B8",
"MTML": "#0095d9",
"LoomScript": null,
"LSL": "#3d9970",
"Lua": "#000080",
"M": null,
"M4": null,
"M4Sugar": null,
"Macaulay2": "#d8ffff",
"Makefile": "#427819",
"Mako": "#7e858d",
"Markdown": "#083fa1",
"Marko": "#42bff2",
"Mask": "#f97732",
"Matlab": "#bb92ac",
"Max": "#ce279c",
"Mercury": "#abcdef",
"Mathematica": "#dd1100",
"MATLAB": "#e16737",
"Max": "#c4a79c",
"MAXScript": "#00a6a6",
"mcfunction": "#E22837",
"Mercury": "#ff2b2b",
"Mermaid": "#ff3670",
"Meson": "#007800",
"Metal": "#8f14e9",
"MiniD": null,
"MiniYAML": "#ff1111",
"Mint": "#02b046",
"Mirah": "#c7a938",
"mIRC Script": "#3d57c3",
"MLIR": "#5EC8DB",
"Modelica": "#de1d31",
"Modula-2": "#10253f",
"Modula-3": "#223388",
"Module Management System": null,
"Monkey": null,
"Monkey C": "#8D6747",
"Moocode": null,
"MoonScript": "#ff4585",
"Motoko": "#fbb03b",
"Motorola 68K Assembly": "#005daa",
"Move": "#4a137a",
"MQL4": "#62A8D6",
"MQL5": "#4A76B8",
"MTML": "#b7e1f4",
"MUF": null,
"mupad": "#244963",
"Mustache": "#724b3b",
"Myghty": null,
"nanorc": "#2d004d",
"Nasal": "#1d2c4e",
"NASL": null,
"NCL": "#28431f",
"NWScript": "#111522",
"Nearley": "#990000",
"Nemerle": "#0d3c6e",
"Nemerle": "#3d3c6e",
"nesC": "#94B0C7",
"NetLinx": "#0aa0ff",
"NetLinx+ERB": "#747faa",
"NetLogo": "#ff2b2b",
"NewLisp": "#eedd66",
"NetLogo": "#ff6375",
"NewLisp": "#87AED7",
"Nextflow": "#3ac486",
"Nginx": "#009639",
"Nim": "#ffc200",
"Nimrod": "#37775b",
"Nit": "#0d8921",
"Nix": "#7070ff",
"Nit": "#009917",
"Nix": "#7e7eff",
"NPM Config": "#cb3837",
"NSIS": null,
"Nu": "#c9df40",
"NumPy": "#9C8AF9",
"Nunjucks": "#3d8137",
"OCaml": "#3be133",
"ObjectScript": "#424893",
"NWScript": "#111522",
"OASv2-json": "#85ea2d",
"OASv2-yaml": "#85ea2d",
"OASv3-json": "#85ea2d",
"OASv3-yaml": "#85ea2d",
"Objective-C": "#438eff",
"Objective-C++": "#4886FC",
"Objective-C++": "#6866fb",
"Objective-J": "#ff0c5a",
"ObjectScript": "#424893",
"OCaml": "#3be133",
"Odin": "#60AFFE",
"Omgrofl": "#cabbff",
"ooc": "#b0b77e",
"Opa": null,
"Opal": "#f7ede0",
"Open Policy Agent": "#7d9199",
"OpenAPI Specification v2": "#85ea2d",
"OpenAPI Specification v3": "#85ea2d",
"OpenCL": "#ed2e2d",
"OpenEdge ABL": "#5ce600",
"OpenQASM": "#AA70FF",
"OpenRC runscript": null,
"OpenSCAD": "#e5cd45",
"Option List": "#476732",
"Org": "#77aa99",
"Oxygene": "#5a63a3",
"Oz": "#fcaf3e",
"Ox": null,
"Oxygene": "#cdd0e3",
"Oz": "#fab738",
"P4": "#7055b5",
"PAWN": "#dbb284",
"PHP": "#4F5D95",
"PLSQL": "#dad8d8",
"Pan": "#cc0000",
"Papyrus": "#6600cc",
"Parrot": "#f3ca0a",
"Pascal": "#b0ce4e",
"Parrot Assembly": null,
"Parrot Internal Representation": null,
"Pascal": "#E3F171",
"Pawn": "#dbb284",
"PDDL": "#0d00ff",
"PEG.js": "#234d6b",
"Pep8": "#C76F5B",
"Perl": "#0298c3",
"Perl6": "#0298c3",
"PHP": "#4F5D95",
"PicoLisp": "#6067af",
"PigLatin": "#fcd7de",
"Pike": "#066ab2",
"Pike": "#005390",
"PLpgSQL": "#336790",
"PLSQL": "#dad8d8",
"PogoScript": "#d80074",
"Polar": "#ae81ff",
"Pony": null,
"Portugol": "#f8bd00",
"PostCSS": "#dc3a0c",
"PostScript": "#da291c",
"POV-Ray SDL": "#6bac65",
"PowerBuilder": "#8f0f8d",
"PowerShell": "#012456",
"Prisma": "#0c344b",
"Processing": "#2779ab",
"Processing": "#0096D8",
"Procfile": "#3B2F63",
"Prolog": "#74283c",
"Propeller Spin": "#2b446d",
"Promela": "#de0000",
"Propeller Spin": "#7fa2a7",
"Pug": "#a86454",
"Puppet": "#cc5555",
"Pure Data": "#91de79",
"Puppet": "#302B6D",
"PureBasic": "#5a6986",
"PureScript": "#bcdc53",
"Python": "#3581ba",
"PureScript": "#1D222D",
"Python": "#3572A5",
"Python console": "#3572A5",
"Python traceback": "#3572A5",
"q": "#0040cd",
"Q#": "#fed659",
"QMake": null,
"QML": "#44a51c",
"Qt Script": "#00b841",
"Quake": "#882233",
"R": "#198ce7",
"RAML": "#77d9fb",
"RUNOFF": "#665a4e",
"Racket": "#ae17ff",
"R": "#198CE7",
"Racket": "#3c5caa",
"Ragel": "#9d5200",
"Ragel in Ruby Host": "#ff9c2e",
"Raku": "#0000fb",
"RAML": "#77d9fb",
"Rascal": "#fffaa0",
"ReScript": "#ed5051",
"RDoc": "#701516",
"REALbasic": null,
"Reason": "#ff5847",
"ReasonLIGO": "#ff5847",
"Rebol": "#358a5b",
"Record Jar": "#0673ba",
"Red": "#ee0000",
"Red": "#f50000",
"Redcode": null,
"Regular Expression": "#009a00",
"Ren'Py": "#ff7f7f",
"RenderScript": null,
"ReScript": "#ed5051",
"reStructuredText": "#141414",
"REXX": "#d90e09",
"Ring": "#2D54CB",
"Riot": "#A71E49",
"RMarkdown": "#198ce7",
"RobotFramework": "#00c0b5",
"Roff": "#ecdebe",
"Roff Manpage": "#ecdebe",
"Rouge": "#cc0088",
"RouterOS Script": "#DE3941",
"RPC": null,
"RPGLE": "#2BDE21",
"Ruby": "#701516",
"RUNOFF": "#665a4e",
"Rust": "#dea584",
"SAS": "#1E90FF",
"SCSS": "#c6538c",
"SQF": "#FFCB1F",
"SRecode Template": "#348a34",
"SVG": "#ff9900",
"Sage": null,
"SaltStack": "#646464",
"SAS": "#B34936",
"Sass": "#a53b70",
"Scala": "#7dd3b0",
"Scala": "#c22d40",
"Scaml": "#bd181a",
"Scenic": "#fdc700",
"Scheme": "#1e4aec",
"Scilab": "#ca0f21",
"SCSS": "#c6538c",
"sed": "#64b970",
"Self": "#0579aa",
"Shell": "#5861ce",
"ShaderLab": "#222c37",
"Shell": "#89e051",
"ShellCheck Config": "#cecfcb",
"ShellSession": null,
"Shen": "#120F14",
"Sieve": null,
"Simple File Verification": "#C9BFED",
"Singularity": "#64E6AD",
"Slash": "#007eff",
"Slice": "#003fa2",
"Slim": "#ff8877",
"SmPL": "#c94949",
"Slim": "#2b2b2b",
"Smali": null,
"Smalltalk": "#596706",
"Smarty": "#f0c040",
"Smithy": "#c44536",
"SmPL": "#c94949",
"SMT": null,
"Solidity": "#AA6746",
"SourcePawn": "#f69e1d",
"SPARQL": "#0C4597",
"SQF": "#3F3F3F",
"SQL": "#e38c00",
"SQLPL": "#e38c00",
"Squirrel": "#800000",
"SRecode Template": "#348a34",
"Stan": "#b2011d",
"Standard ML": "#dc566d",
"Starlark": "#76d275",
"Stata": "#1a5f91",
"STL": "#373b5e",
"StringTemplate": "#3fb34f",
"Stylus": "#ff6347",
"SubRip Text": "#9e0101",
"SugarSS": "#2fcc9f",
"SuperCollider": "#46390b",
"Svelte": "#ff3e00",
"Swift": "#ffac45",
"SystemVerilog": "#343761",
"TI Program": "#A0AA87",
"SVG": "#ff9900",
"Swift": "#F05138",
"SWIG": null,
"SystemVerilog": "#DAE1C2",
"Talon": "#333333",
"Tcl": "#e4cc98",
"TeX": "#3D6117",
"Tcsh": null,
"Terra": "#00004c",
"Turing": "#45f715",
"TeX": "#3D6117",
"Textile": "#ffe7ac",
"TextMate Properties": "#df66e4",
"Thrift": "#D12127",
"TI Program": "#A0AA87",
"TLA": "#4b0079",
"TOML": "#9c4221",
"TSQL": "#e38c00",
"TSV": "#237346",
"TSX": "#3178c6",
"Turing": "#cf142b",
"Twig": "#c1d026",
"TypeScript": "#31859c",
"Unified Parallel C": "#755223",
"TXL": "#0178b8",
"TypeScript": "#3178c6",
"Unified Parallel C": "#4e3617",
"Unity3D Asset": "#222c37",
"Unix Assembly": null,
"Uno": "#9933cc",
"UnrealScript": "#a54c4d",
"UrWeb": "#ccccee",
"V": "#4f87c4",
"Vala": "#a56de2",
"Valve Data Format": "#f26025",
"VBA": "#867db1",
"VBScript": "#15dcdc",
"VCL": "#0298c3",
"VHDL": "#543978",
"Vala": "#ee7d06",
"Verilog": "#848bf3",
"Vim script": "#199f4b",
"VimL": "#199c4b",
"Visual Basic": "#945db7",
"VCL": "#148AA8",
"Velocity Template Language": "#507cff",
"Verilog": "#b2b7f8",
"VHDL": "#adb2cb",
"Vim Help File": "#199f4b",
"Vim Script": "#199f4b",
"Vim Snippet": "#199f4b",
"Visual Basic .NET": "#945db7",
"Volt": "#0098db",
"Vue": "#2c3e50",
"Web Ontology Language": "#3994bc",
"Visual Basic 6.0": "#2c6353",
"Volt": "#1F1F1F",
"Vue": "#41b883",
"Vyper": "#2980b9",
"wdl": "#42f1f4",
"Web Ontology Language": "#5b70bd",
"WebAssembly": "#04133b",
"WebIDL": null,
"Whiley": "#d5c397",
"Wikitext": "#fc5757",
"Windows Registry Entries": "#52d5ff",
"wisp": "#7582D1",
"Witcher Script": "#ff0000",
"Wollok": "#a23738",
"World of Warcraft Addon Data": "#f7e43f",
"Wren": "#383838",
"X10": "#4B6BEF",
"xBase": "#403a40",
"XC": "#99DA07",
"XQuery": "#2700e2",
"XML": "#0060ac",
"XML Property List": "#0060ac",
"Xojo": "#81bd41",
"Xonsh": "#285EEF",
"XProc": null,
"XQuery": "#5232e7",
"XS": null,
"XSLT": "#EB8CEB",
"Xtend": "#24255d",
"Yacc": "#4B6C4B",
"YAML": "#cb171e",
"YARA": "#220000",
"YASnippet": "#32AB90",
"Yacc": "#4B6C4B",
"Yul": "#794932",
"ZAP": "#0d665e",
"ZIL": "#dc75e5",
"Zeek": null,
"ZenScript": "#00BCD1",
"Zephir": "#118f9e",
"Zig": "#ec915c",
"cpp": "#f34b7d",
"eC": "#913960",
"edn": "#db5855",
"mIRC Script": "#3d57c3",
"mcfunction": "#E22837",
"nesC": "#ffce3b",
"ooc": "#b0b77e",
"q": "#0040cd",
"sed": "#64b970",
"wdl": "#42f1f4",
"wisp": "#7582D1",
"xBase": "#3a4040",
"Other": "#1f9aef"
"ZIL": "#dc75e5",
"Zimpl": "#d67711"
},
"editors": {
"Adobe XD": "#fd27bc",

View File

@@ -1,29 +0,0 @@
version: '3.7'
services:
wakapi:
build: .
ports:
- 3000:3000
restart: always
environment:
# See README.md and config.default.yml for all config options
WAKAPI_DB_TYPE: "postgres"
WAKAPI_DB_NAME: "wakapi"
WAKAPI_DB_USER: "wakapi"
WAKAPI_DB_PASSWORD: "choose-a-password"
WAKAPI_DB_HOST: "db"
WAKAPI_DB_PORT: "5432"
ENVIRONMENT: "prod"
db:
image: postgres:12.3
environment:
POSTGRES_USER: "wakapi"
POSTGRES_PASSWORD: "choose-a-password"
POSTGRES_DB: "wakapi"
volumes:
- wakapi-db-data:/var/lib/postgresql/data
volumes:
wakapi-db-data: {}

View File

@@ -1,8 +1,36 @@
#!/bin/bash
# usage: file_env VAR [DEFAULT]
# ie: file_env 'XYZ_DB_PASSWORD' 'example'
# (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of
# "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature)
file_env() {
local var="$1"
local fileVar="${var}_FILE"
local def="${2:-}"
if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then
printf >&2 'error: both %s and %s are set (but are exclusive)\n' "$var" "$fileVar"
exit 1
fi
local val="$def"
if [ "${!var:-}" ]; then
val="${!var}"
elif [ "${!fileVar:-}" ]; then
val="$(< "${!fileVar}")"
fi
export "$var"="$val"
unset "$fileVar"
}
file_env "WAKAPI_PASSWORD_SALT"
file_env "WAKAPI_DB_PASSWORD"
file_env "WAKAPI_MAIL_SMTP_PASS"
file_env "WAKAPI_SUBSCRIPTIONS_STRIPE_SECRET_KEY"
file_env "WAKAPI_SUBSCRIPTIONS_STRIPE_ENDPOINT_SECRET"
if [ "$WAKAPI_DB_TYPE" == "sqlite3" ] || [ "$WAKAPI_DB_TYPE" == "" ]; then
exec ./wakapi
else
echo "Waiting for database to come up"
exec ./wait-for-it.sh "$WAKAPI_DB_HOST:$WAKAPI_DB_PORT" -s -t 60 -- ./wakapi
exec ./wait-for-it.sh "$WAKAPI_DB_HOST:$WAKAPI_DB_PORT" -s -t 60 -- ./wakapi
fi

View File

@@ -7,7 +7,6 @@ wakapi.yourdomain.tld {
log {
output file /var/log/caddy/wakapi.dev.access.log
format single_field common_log
}
reverse_proxy http://[::1]:3000
@@ -16,11 +15,8 @@ wakapi.yourdomain.tld {
@notapi not path_regexp "^/api.*"
push @notapi /assets/vendor/source-sans-3.css
push @notapi /assets/css/app.dist.css
push @notapi /assets/vendor/petite-vue.min.js
push @notapi /assets/vendor/chart.min.js
push @notapi /assets/vendor/iconify.basic.min.js
push @notapi /assets/js/icons.dist.js
push @notapi /assets/js/base.js
push @notapi /assets/images/logo.svg
}

View File

@@ -28,6 +28,7 @@ Environment=WAKAPI_PASSWORD_SALT=somerandomstring
# sudo useradd -g wakapi wakapi
User=wakapi
Group=wakapi
RuntimeDirectory=wakapi # creates /run/wakapi, useful to place your socket file there
Restart=on-failure
RestartSec=90
@@ -50,4 +51,4 @@ ProtectHostname=true
ProtectProc=invisible
[Install]
WantedBy=multi-user.target
WantedBy=multi-user.target

88
funding.json Normal file
View File

@@ -0,0 +1,88 @@
{
"version": "v1.0.0",
"entity": {
"type": "individual",
"role": "owner",
"name": "Ferdinand Mütsch",
"email": "ferdinand@muetsch.io",
"description": "Author and maintainer of Wakapi and many other projects.",
"webpageUrl": {
"url": "https://muetsch.io",
"wellKnown": "https://muetsch.io/.well-known/funding-manifest-urls"
}
},
"projects": [
{
"guid": "wakapi",
"name": "Wakapi",
"description": "A minimalist, self-hosted WakaTime-compatible backend for coding statistics.",
"webpageUrl": {
"url": "https://wakapi.dev",
"wellKnown": "https://wakapi.dev/.well-known/funding-manifest-urls"
},
"repositoryUrl": {
"url": "https://wakapi.dev/github",
"wellKnown": "https://wakapi.dev/.well-known/funding-manifest-urls"
},
"licenses": [
"spdx:MIT"
],
"tags": [
"productivity",
"self-hosted",
"developer-tools",
"time-tracker",
"wakatime",
"coding-stats"
]
}
],
"funding": {
"channels": [
{
"guid": "github-sponsors",
"type": "other",
"address": "https://github.com/sponsors/muety",
"description": "Use GitHub Sponsors to fund my work. This is most convenient for me, but feel free to reach out for alternatives."
}
],
"plans": [
{
"guid": "hosting-wakapi-monthly",
"status": "active",
"name": "Hosting support for Wakapi",
"description": "This will cover the monthly server hosting costs for Wakapi.",
"amount": 5,
"currency": "EUR",
"frequency": "monthly",
"channels": [
"github-sponsors"
]
},
{
"guid": "devtime-wakapi-monthly",
"status": "active",
"name": "Developer compensation for Wakapi",
"description": "This will cover the cost of one developer (myself) working occasionally on Wakapi.",
"amount": 50,
"currency": "EUR",
"frequency": "monthly",
"channels": [
"github-sponsors"
]
},
{
"guid": "angel-plan",
"status": "active",
"name": "Goodwill plan",
"description": "Pay anything you wish to show your goodwill for any of my projects.",
"amount": 0,
"currency": "EUR",
"frequency": "one-time",
"channels": [
"github-sponsors"
]
}
]
}
}

154
go.mod
View File

@@ -1,82 +1,114 @@
module github.com/muety/wakapi
go 1.18
go 1.25
require (
codeberg.org/Codeberg/avatars v1.0.0
github.com/duke-git/lancet/v2 v2.1.6
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead
github.com/emersion/go-smtp v0.15.0
github.com/emvi/logbuch v1.2.0
github.com/getsentry/sentry-go v0.14.0
github.com/glebarez/sqlite v1.5.0
github.com/go-co-op/gocron v1.17.0
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
github.com/gorilla/schema v1.2.0
github.com/gorilla/securecookie v1.1.1
github.com/hashicorp/golang-lru v0.5.4
github.com/jinzhu/configor v1.2.1
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b
github.com/alexedwards/argon2id v1.0.0
github.com/alitto/pond/v2 v2.5.0
github.com/cespare/xxhash/v2 v2.3.0
github.com/coreos/go-oidc/v3 v3.16.0
github.com/dchest/captcha v1.1.0
github.com/duke-git/lancet/v2 v2.3.7
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6
github.com/emersion/go-smtp v0.24.0
github.com/getsentry/sentry-go v0.35.3
github.com/glebarez/sqlite v1.11.0
github.com/go-chi/chi/v5 v5.2.3
github.com/go-chi/httprate v0.15.0
github.com/gofrs/uuid/v5 v5.3.2
github.com/gohugoio/hashstructure v0.5.0
github.com/gorilla/schema v1.4.1
github.com/gorilla/securecookie v1.1.2
github.com/gorilla/sessions v1.4.0
github.com/hashicorp/golang-lru v1.0.2
github.com/jinzhu/configor v1.2.2
github.com/leandro-lugaresi/hub v1.1.1
github.com/lpar/gzipped/v2 v2.1.0
github.com/mitchellh/hashstructure/v2 v2.0.2
github.com/narqo/go-badge v0.0.0-20220127184443-140af28a266e
github.com/mileusna/useragent v1.3.5
github.com/muety/artifex/v2 v2.0.1-0.20221201142708-74e7d3f6feaf
github.com/narqo/go-badge v0.0.0-20230821190521-c9a75c019a59
github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/satori/go.uuid v1.2.0
github.com/stretchr/testify v1.8.0
github.com/swaggo/http-swagger v1.3.3
github.com/swaggo/swag v1.8.6
go.uber.org/atomic v1.10.0
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0
gorm.io/driver/mysql v1.4.1
gorm.io/driver/postgres v1.4.4
gorm.io/driver/sqlite v1.4.2
gorm.io/gorm v1.24.0
github.com/robfig/cron/v3 v3.0.1
github.com/stretchr/testify v1.11.1
github.com/stripe/stripe-go/v74 v74.30.0
github.com/swaggo/http-swagger v1.3.4
github.com/swaggo/swag v1.16.6
go.uber.org/atomic v1.11.0
golang.org/x/crypto v0.42.0
golang.org/x/oauth2 v0.31.0
gorm.io/driver/mysql v1.6.0
gorm.io/driver/postgres v1.6.0
gorm.io/driver/sqlserver v1.6.1
gorm.io/gorm v1.31.0
)
require (
github.com/BurntSushi/toml v1.2.0 // indirect
github.com/go-jose/go-jose/v3 v3.0.1 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
github.com/go-openapi/swag/conv v0.25.1 // indirect
github.com/go-openapi/swag/jsonname v0.25.1 // indirect
github.com/go-openapi/swag/jsonutils v0.25.1 // indirect
github.com/go-openapi/swag/loading v0.25.1 // indirect
github.com/go-openapi/swag/stringutils v0.25.1 // indirect
github.com/go-openapi/swag/typeutils v0.25.1 // indirect
github.com/go-openapi/swag/yamlutils v0.25.1 // indirect
github.com/golang-jwt/jwt/v5 v5.2.2 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/zeebo/xxh3 v1.0.2 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.28.0 // indirect
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/becheran/wildmatch-go v1.0.0
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/glebarez/go-sqlite v1.19.1 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.20.0 // indirect
github.com/go-openapi/spec v0.20.7 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-sql-driver/mysql v1.6.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/glebarez/go-sqlite v1.22.0 // indirect
github.com/go-openapi/jsonpointer v0.22.1 // indirect
github.com/go-openapi/jsonreference v0.21.2 // indirect
github.com/go-openapi/spec v0.22.0 // indirect
github.com/go-sql-driver/mysql v1.9.3 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.13.0 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.1 // indirect
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
github.com/jackc/pgtype v1.12.0 // indirect
github.com/jackc/pgx/v4 v4.17.2 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.6 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/kevinpollet/nego v0.0.0-20211010160919-a65cd48cee43 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/microsoft/go-mssqldb v1.9.3 // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/stretchr/objx v0.4.0 // indirect
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a // indirect
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 // indirect
golang.org/x/net v0.0.0-20221004154528-8021a29435af // indirect
golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/tools v0.1.12 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rs/cors v1.11.1
github.com/samber/lo v1.51.0 // indirect
github.com/samber/slog-common v0.19.0 // indirect
github.com/samber/slog-multi v1.5.0
github.com/samber/slog-sentry/v2 v2.9.3
github.com/stretchr/objx v0.5.2 // indirect
github.com/swaggo/files v1.0.1 // indirect
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 // indirect
golang.org/x/image v0.31.0 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/tools v0.37.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
modernc.org/libc v1.20.3 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.4.0 // indirect
modernc.org/sqlite v1.19.1 // indirect
modernc.org/libc v1.66.10 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
modernc.org/sqlite v1.39.0 // indirect
)
godebug x509negativeserial=1 // https://stackoverflow.com/a/79062183/3112139

687
go.sum
View File

@@ -1,410 +1,443 @@
codeberg.org/Codeberg/avatars v1.0.0 h1:MRx5QxuT/oVCcPvC5rXwgwWKD7hc6J0GnZ0Kl67lYEM=
codeberg.org/Codeberg/avatars v1.0.0/go.mod h1:ML/htpPRb3+owhkm4+qG2ZrXnk5WXaQLASOZ5GLCPi8=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.0/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.7.1/go.mod h1:bjGvMhVMb+EEm3VRNQawDMUyMMjo+S5ewNjflkep/0Q=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.11.1/go.mod h1:a6xsAQUZg+VsS3TJ05SRp524Hs4pZ/AeFSr5ENf0Yjo=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U=
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.3.1/go.mod h1:uE9zaUfEQT/nbQjVi2IblCG9iaLtZsuYZ8ne+PuQ02M=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.6.0/go.mod h1:9kIvujWAA58nmPmWB1m23fyWic1kYZMxD9CxaWn4Qpg=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.2/go.mod h1:yInRyqWXAuaPrgI7p70+lDDgh3mlBohis29jGMISnmc=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.8.0/go.mod h1:4OG6tQ9EOP/MT0NMjDlRzWoVFxfu9rN9B2X+tlSVktg=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.1/go.mod h1:GpPjLhVR9dnUoJMyHWSPy71xY9/lcmpzIPZXmF0FCVY=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1 h1:Wgf5rZba3YZqeTNJPtvqZoBu1sBN/L4sry+u2U3Y75w=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.3.1/go.mod h1:xxCBG/f/4Vbmh2XQJBsOmNdxWUY5j/s27jujKPbQf14=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww=
github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.2.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs=
github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0=
github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=
github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY=
github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk=
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw=
github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w=
github.com/alexedwards/argon2id v1.0.0/go.mod h1:tYKkqIjzXvZdzPvADMWOEZ+l6+BD6CtBXMj5fnJppiw=
github.com/alitto/pond/v2 v2.5.0 h1:vPzS5GnvSDRhWQidmj2djHllOmjFExVFbDGCw1jdqDw=
github.com/alitto/pond/v2 v2.5.0/go.mod h1:xkjYEgQ05RSpWdfSd1nM3OVv7TBhLdy7rMp3+2Nq+yE=
github.com/becheran/wildmatch-go v1.0.0 h1:mE3dGGkTmpKtT4Z+88t8RStG40yN9T+kFEGj2PZFSzA=
github.com/becheran/wildmatch-go v1.0.0/go.mod h1:gbMvj0NtVdJ15Mg/mH9uxk2R1QCistMyU7d9KFzroX4=
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-oidc/v3 v3.16.0 h1:qRQUCFstKpXwmEjDQTIbyY/5jF00+asXzSkmkoa/mow=
github.com/coreos/go-oidc/v3 v3.16.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/duke-git/lancet/v2 v2.1.6 h1:zRWZkK3IAoGnzEonbrkmUP2NyHqtH9qIlW0AaSQrzmY=
github.com/duke-git/lancet/v2 v2.1.6/go.mod h1:5Nawyf/bK783rCiHyVkZLx+jj8028oVVjLOrC21ZONA=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y=
github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.15.0 h1:3+hMGMGrqP/lqd7qoxZc1hTU8LY8gHV9RFGWlqSDmP8=
github.com/emersion/go-smtp v0.15.0/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ=
github.com/emvi/logbuch v1.2.0 h1:Bw0jQH1Dbs+oIygZBNx/2Ub1igXRFtKQrIMRrZdVFJM=
github.com/emvi/logbuch v1.2.0/go.mod h1:hFxe0XQOFl76SkE/f0Pt5oQbXRZtyGa8EroBrrbQHuc=
github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk=
github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/getsentry/sentry-go v0.13.0 h1:20dgTiUSfxRB/EhMPtxcL9ZEbM1ZdR+W/7f7NWD+xWo=
github.com/getsentry/sentry-go v0.13.0/go.mod h1:EOsfu5ZdvKPfeHYV6pTVQnsjfp30+XA7//UooKNumH0=
github.com/getsentry/sentry-go v0.14.0 h1:rlOBkuFZRKKdUnKO+0U3JclRDQKlRu5vVQtkWSQvC70=
github.com/getsentry/sentry-go v0.14.0/go.mod h1:RZPJKSw+adu8PBNygiri/A98FqVr2HtRckJk9XVxJ9I=
github.com/glebarez/go-sqlite v1.18.2 h1:ck3PQVaEzzzapP0g7pfhzbB3Jw4rNk+IldLMy/lgdeQ=
github.com/glebarez/go-sqlite v1.18.2/go.mod h1:/kOdnnt5T0ztYXqBPdjRVM8JwMpFtyAQp1mtRoNxziM=
github.com/glebarez/go-sqlite v1.19.1 h1:o2XhjyR8CQ2m84+bVz10G0cabmG0tY4sIMiCbrcUTrY=
github.com/glebarez/go-sqlite v1.19.1/go.mod h1:9AykawGIyIcxoSfpYWiX1SgTNHTNsa/FVc75cDkbp4M=
github.com/glebarez/sqlite v1.4.7 h1:tIBxEWLJOPkekuQcwfenNfh13itj9GoVJYxp7GidJAo=
github.com/glebarez/sqlite v1.4.7/go.mod h1:UY1smw9rBTSGnJE0He8pVRPvlxCP1C8hlB8Z24K8fG4=
github.com/glebarez/sqlite v1.5.0 h1:+8LAEpmywqresSoGlqjjT+I9m4PseIM3NcerIJ/V7mk=
github.com/glebarez/sqlite v1.5.0/go.mod h1:0wzXzTvfVJIN2GqRhCdMbnYd+m+aH5/QV7B30rM6NgY=
github.com/go-co-op/gocron v1.17.0 h1:IixLXsti+Qo0wMvmn6Kmjp2csk2ykpkcL+EmHmST18w=
github.com/go-co-op/gocron v1.17.0/go.mod h1:IpDBSaJOVfFw7hXZuTag3SCSkqazXBBUkbQ1m1aesBs=
github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w=
github.com/dchest/captcha v1.1.0 h1:2kt47EoYUUkaISobUdTbqwx55xvKOJxyScVfw25xzhQ=
github.com/dchest/captcha v1.1.0/go.mod h1:7zoElIawLp7GUMLcj54K9kbw+jEyvz2K0FDdRRYhvWo=
github.com/dnaeon/go-vcr v1.1.0/go.mod h1:M7tiix8f0r6mKKJ3Yq/kqU1OYf3MnfmBWVbPx/yU9ko=
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
github.com/duke-git/lancet/v2 v2.3.7 h1:nnNBA9KyoqwbPm4nFmEFVIbXeAmpqf6IDCH45+HHHNs=
github.com/duke-git/lancet/v2 v2.3.7/go.mod h1:zGa2R4xswg6EG9I6WnyubDbFO/+A/RROxIbXcwryTsc=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.24.0 h1:g6AfoF140mvW0vLNPD/LuCBLEAdlxOjIXqbIkJIS6Wk=
github.com/emersion/go-smtp v0.24.0/go.mod h1:ZtRRkbTyp2XTHCA+BmyTFTrj8xY4I+b4McvHxCU2gsQ=
github.com/getsentry/sentry-go v0.35.3 h1:u5IJaEqZyPdWqe/hKlBKBBnMTSxB/HenCqF3QLabeds=
github.com/getsentry/sentry-go v0.35.3/go.mod h1:mdL49ixwT2yi57k5eh7mpnDyPybixPzlzEJFu0Z76QA=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE=
github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-chi/httprate v0.15.0 h1:j54xcWV9KGmPf/X4H32/aTH+wBlrvxL7P+SdnRqxh5g=
github.com/go-chi/httprate v0.15.0/go.mod h1:rzGHhVrsBn3IMLYDOZQsSU4fJNWcjui4fWKJcCId1R4=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA=
github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo=
github.com/go-openapi/spec v0.20.7 h1:1Rlu/ZrOCCob0n+JKKJAWhNWMPW8bOZRg8FJaY+0SKI=
github.com/go-openapi/spec v0.20.7/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA=
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw=
github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-openapi/jsonpointer v0.22.1 h1:sHYI1He3b9NqJ4wXLoJDKmUmHkWy/L7rtEo92JUxBNk=
github.com/go-openapi/jsonpointer v0.22.1/go.mod h1:pQT9OsLkfz1yWoMgYFy4x3U5GY5nUlsOn1qSBH5MkCM=
github.com/go-openapi/jsonreference v0.21.2 h1:Wxjda4M/BBQllegefXrY/9aq1fxBA8sI5M/lFU6tSWU=
github.com/go-openapi/jsonreference v0.21.2/go.mod h1:pp3PEjIsJ9CZDGCNOyXIQxsNuroxm8FAJ/+quA0yKzQ=
github.com/go-openapi/spec v0.22.0 h1:xT/EsX4frL3U09QviRIZXvkh80yibxQmtoEvyqug0Tw=
github.com/go-openapi/spec v0.22.0/go.mod h1:K0FhKxkez8YNS94XzF8YKEMULbFrRw4m15i2YUht4L0=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag/conv v0.25.1 h1:+9o8YUg6QuqqBM5X6rYL/p1dpWeZRhoIt9x7CCP+he0=
github.com/go-openapi/swag/conv v0.25.1/go.mod h1:Z1mFEGPfyIKPu0806khI3zF+/EUXde+fdeksUl2NiDs=
github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
github.com/go-openapi/swag/jsonutils v0.25.1 h1:AihLHaD0brrkJoMqEZOBNzTLnk81Kg9cWr+SPtxtgl8=
github.com/go-openapi/swag/jsonutils v0.25.1/go.mod h1:JpEkAjxQXpiaHmRO04N1zE4qbUEg3b7Udll7AMGTNOo=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1 h1:DSQGcdB6G0N9c/KhtpYc71PzzGEIc/fZ1no35x4/XBY=
github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.1/go.mod h1:kjmweouyPwRUEYMSrbAidoLMGeJ5p6zdHi9BgZiqmsg=
github.com/go-openapi/swag/loading v0.25.1 h1:6OruqzjWoJyanZOim58iG2vj934TysYVptyaoXS24kw=
github.com/go-openapi/swag/loading v0.25.1/go.mod h1:xoIe2EG32NOYYbqxvXgPzne989bWvSNoWoyQVWEZicc=
github.com/go-openapi/swag/stringutils v0.25.1 h1:Xasqgjvk30eUe8VKdmyzKtjkVjeiXx1Iz0zDfMNpPbw=
github.com/go-openapi/swag/stringutils v0.25.1/go.mod h1:JLdSAq5169HaiDUbTvArA2yQxmgn4D6h4A+4HqVvAYg=
github.com/go-openapi/swag/typeutils v0.25.1 h1:rD/9HsEQieewNt6/k+JBwkxuAHktFtH3I3ysiFZqukA=
github.com/go-openapi/swag/typeutils v0.25.1/go.mod h1:9McMC/oCdS4BKwk2shEB7x17P6HmMmA6dQRtAkSnNb8=
github.com/go-openapi/swag/yamlutils v0.25.1 h1:mry5ez8joJwzvMbaTGLhw8pXUnhDK91oSJLDPF1bmGk=
github.com/go-openapi/swag/yamlutils v0.25.1/go.mod h1:cm9ywbzncy3y6uPm/97ysW8+wZ09qsks+9RS8fLWKqg=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
github.com/gofrs/uuid/v5 v5.3.2 h1:2jfO8j3XgSwlz/wHqemAEugfnTlikAYHhnqQ8Xh4fE0=
github.com/gofrs/uuid/v5 v5.3.2/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
github.com/gohugoio/hashstructure v0.5.0 h1:G2fjSBU36RdwEJBWJ+919ERvOVqAg9tfcYp47K9swqg=
github.com/gohugoio/hashstructure v0.5.0/go.mod h1:Ser0TniXuu/eauYmrwM4o64EBvySxNzITEOLlm4igec=
github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
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.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/handlers v1.5.1 h1:9lRY6j8DEeeBT10CvO9hGW0gmky0BprnvDI5vfhUHH4=
github.com/gorilla/handlers v1.5.1/go.mod h1:t8XrUpc4KVXb7HGyJ4/cEnwQiaxrX/hz1Zv/4g96P1Q=
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc=
github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/jackc/chunkreader v1.0.0 h1:4s39bBR8ByfqH+DKm8rQA3E1LHZWB9XWcrz8fqaZbe0=
github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo=
github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8=
github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk=
github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA=
github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE=
github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s=
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
github.com/jackc/pgconn v1.13.0 h1:3L1XMNV2Zvca/8BYhzcRFS70Lr0WlDg16Di6SFGAbys=
github.com/jackc/pgconn v1.13.0/go.mod h1:AnowpAqO4CMIIJNZl2VJp+KrkAZciAkhEl0W0JIobpI=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc=
github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c=
github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
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/pgproto3 v1.1.0 h1:FYYE4yRw+AgI8wXIinMlNjBbp/UitDJwfj5LqqewP1A=
github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA=
github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg=
github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.3.1 h1:nwj7qwf0S+Q7ISFfBndqeLwSwxs+4DPsbRFjECT1Y4Y=
github.com/jackc/pgproto3/v2 v2.3.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg=
github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc=
github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw=
github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM=
github.com/jackc/pgtype v1.12.0 h1:Dlq8Qvcch7kiehm8wPGIW0W3KsCCHJnRacKW0UM8n5w=
github.com/jackc/pgtype v1.12.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4=
github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y=
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
github.com/jackc/pgx/v4 v4.17.2 h1:0Ut0rpeKwvIVbMQ1KbMBU4h6wxehBI535LK6Flheh8E=
github.com/jackc/pgx/v4 v4.17.2/go.mod h1:lcxIZN44yMIrWI78a5CpucdD14hX0SBDbNRvjDBItsw=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jinzhu/configor v1.2.1 h1:OKk9dsR8i6HPOCZR8BcMtcEImAFjIhbJFZNyn5GCZko=
github.com/jinzhu/configor v1.2.1/go.mod h1:nX89/MOmDba7ZX7GCyU/VIaQ2Ar2aizBl2d3JLF/rDc=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jinzhu/configor v1.2.2 h1:sLgh6KMzpCmaQB4e+9Fu/29VErtBUqsS2t8C9BNIVsA=
github.com/jinzhu/configor v1.2.2/go.mod h1:iFFSfOBKP3kC2Dku0ZGB3t3aulfQgTGJknodhFavsU8=
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.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kevinpollet/nego v0.0.0-20211010160919-a65cd48cee43 h1:Pdirg1gwhEcGjMLyuSxGn9664p+P8J9SrfMgpFwrDyg=
github.com/kevinpollet/nego v0.0.0-20211010160919-a65cd48cee43/go.mod h1:ahLMuLCUyDdXqtqGyuwGev7/PGtO7r7ocvdwDuEN/3E=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leandro-lugaresi/hub v1.1.1 h1:zqp0HzFvj4HtqjMBXM2QF17o6PNmR8MJOChgeKl/aw8=
github.com/leandro-lugaresi/hub v1.1.1/go.mod h1:XEFWanhHv6Rt3XlteHMxuNDYi8dJcpJjodpqkU+BtIo=
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8=
github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lpar/gzipped/v2 v2.1.0 h1:87/ug239roEqXLVOnXZg6NjDfFvMwmkGTKnFWJPUA9U=
github.com/lpar/gzipped/v2 v2.1.0/go.mod h1:G3UlFoFYzjCx6NV4zDmD1BIWMNBaJuKoUvxrEWJuZ3Y=
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
github.com/narqo/go-badge v0.0.0-20220127184443-140af28a266e h1:bR8DQ4ZfItytLJwRlrLOPUHd5z18V6tECwYQFy8W+8g=
github.com/narqo/go-badge v0.0.0-20220127184443-140af28a266e/go.mod h1:m9BzkaxwU4IfPQi9ko23cmuFltayFe8iS0dlRlnEWiM=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/microsoft/go-mssqldb v1.8.2/go.mod h1:vp38dT33FGfVotRiTmDo3bFyaHq+p3LektQrjTULowo=
github.com/microsoft/go-mssqldb v1.9.3 h1:hy4p+LDC8LIGvI3JATnLVmBOLMJbmn5X400mr5j0lPs=
github.com/microsoft/go-mssqldb v1.9.3/go.mod h1:GBbW9ASTiDC+mpgWDGKdm3FnFLTUsLYN3iFL90lQ+PA=
github.com/mileusna/useragent v1.3.5 h1:SJM5NzBmh/hO+4LGeATKpaEX9+b4vcGg2qXGLiNGDws=
github.com/mileusna/useragent v1.3.5/go.mod h1:3d8TOmwL/5I8pJjyVDteHtgDGcefrFUX4ccGOMKNYYc=
github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3PzxT8aQXRPkAt8xlV/e7d7w8GM5g0fa5F0D8=
github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow=
github.com/muety/artifex/v2 v2.0.1-0.20221201142708-74e7d3f6feaf h1:zd7IU9rxVMl2FBwSwiWCUh6s0TkPKgOU6GyVBciNdlo=
github.com/muety/artifex/v2 v2.0.1-0.20221201142708-74e7d3f6feaf/go.mod h1:eElbcdMwTDc7Wzl7A46IopgkC6a9nV7jOB6Mw8r0waE=
github.com/narqo/go-badge v0.0.0-20230821190521-c9a75c019a59 h1:kbREB9muGo4sHLoZJD/E/IV8yK3Y15eEA9mYi/ztRsk=
github.com/narqo/go-badge v0.0.0-20230821190521-c9a75c019a59/go.mod h1:m9BzkaxwU4IfPQi9ko23cmuFltayFe8iS0dlRlnEWiM=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25 h1:9bCMuD3TcnjeqjPT2gSlha4asp8NvgcFRYExCaikCxk=
github.com/oauth2-proxy/mockoidc v0.0.0-20240214162133-caebfff84d25/go.mod h1:eDjgYHYDJbPLBLsyZ6qRaugP0mX8vePOhZ5id1fdzJw=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa h1:tEkEyxYeZ43TR55QU/hsIt9aRGBxbgGuz9CGykjvogY=
github.com/remyoudompheng/bigfft v0.0.0-20220927061507-ef77025ab5aa/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ=
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI=
github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
github.com/samber/slog-common v0.19.0 h1:fNcZb8B2uOLooeYwFpAlKjkQTUafdjfqKcwcC89G9YI=
github.com/samber/slog-common v0.19.0/go.mod h1:dTz+YOU76aH007YUU0DffsXNsGFQRQllPQh9XyNoA3M=
github.com/samber/slog-multi v1.5.0 h1:UDRJdsdb0R5vFQFy3l26rpX3rL3FEPJTJ2yKVjoiT1I=
github.com/samber/slog-multi v1.5.0/go.mod h1:im2Zi3mH/ivSY5XDj6LFcKToRIWPw1OcjSVSdXt+2d0=
github.com/samber/slog-sentry/v2 v2.9.3 h1:2/PZa78BFe0FuW/wm6Q3kBcd1phb1dBFHsCWZ4wX8Ko=
github.com/samber/slog-sentry/v2 v2.9.3/go.mod h1:HGQRgN11HkZqSw/X493Zr65yIRx9ZpjZ2T5v2Dx/REc=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
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 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a h1:kAe4YSu0O0UFn1DowNo2MY5p6xzqtJ/wQ7LZynSvGaY=
github.com/swaggo/files v0.0.0-20220728132757-551d4a08d97a/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w=
github.com/swaggo/http-swagger v1.3.3 h1:Hu5Z0L9ssyBLofaama21iYaF2VbWyA8jdohaaCGpHsc=
github.com/swaggo/http-swagger v1.3.3/go.mod h1:sE+4PjD89IxMPm77FnkDz0sdO+p5lbXzrVWT6OTVVGo=
github.com/swaggo/swag v1.8.6 h1:2rgOaLbonWu1PLP6G+/rYjSvPg0jQE0HtrEKuE380eg=
github.com/swaggo/swag v1.8.6/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBnseg=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/stripe/stripe-go/v74 v74.30.0 h1:0Kf0KkeFnY7iRhOwvTerX0Ia1BRw+eV1CVJ51mGYAUY=
github.com/stripe/stripe-go/v74 v74.30.0/go.mod h1:f9L6LvaXa35ja7eyvP6GQswoaIPaBRvGAimAO+udbBw=
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
github.com/swaggo/http-swagger v1.3.4 h1:q7t/XLx0n15H1Q9/tk3Y9L4n210XzJF5WtnDX64a5ww=
github.com/swaggo/http-swagger v1.3.4/go.mod h1:9dAh0unqMBAlbp1uE2Uc2mQTxNMU/ha4UbucIg1MFkQ=
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA=
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be h1:fmw3UbQh+nxngCAHrDCCztao/kbYFnWjoqop8dHx05A=
golang.org/x/crypto v0.0.0-20220926161630-eccd6366d1be/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b h1:huxqepDufQpLLIRXiVkTvnxrzJlpwmIWAObmcCcUFr0=
golang.org/x/crypto v0.0.0-20221005025214-4161e89ecf1b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69 h1:Lj6HJGCSn5AjxRAH2+r35Mir4icalbqku+CLUtjnvXY=
golang.org/x/image v0.0.0-20220902085622-e7cb96979f69/go.mod h1:doUCurBvlfPMKfmIpRIywoHmhN3VyhnoFDbvIEWF4hY=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9 h1:TQwNpfvNkxAVlItJf6Cr5JTsVZoC/Sj7K3OZv2Pc14A=
golang.org/x/exp v0.0.0-20251002181428-27f1f14c8bb9/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
golang.org/x/image v0.31.0 h1:mLChjE2MV6g1S7oqbXC0/UcKijjm5fnJLUYKIYrLESA=
golang.org/x/image v0.31.0/go.mod h1:R9ec5Lcp96v9FTF+ajwaH3uGxPH4fKfHHAVbUILxghA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
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/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220927171203-f486391704dc h1:FxpXZdoBqT8RjqTy6i1E8nXHhW21wK7ptQ/EPIGxzPQ=
golang.org/x/net v0.0.0-20220927171203-f486391704dc/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.0.0-20221004154528-8021a29435af h1:wv66FM3rLZGPdxpYL+ApnDe2HzHcTFta3z5nsc13wI4=
golang.org/x/net v0.0.0-20221004154528-8021a29435af/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/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.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo=
golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0 h1:cu5kTvlzcw1Q5S9f5ip1/cpiB4nXvw1XYzFPGgzLUOY=
golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
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.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/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-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec h1:BkDtF2Ih9xZ7le9ndzTA7KJow28VbQW3odyk/8drmuI=
golang.org/x/sys v0.0.0-20220928140112-f11e5e49a4ec/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875 h1:AzgQNqF+FKwyQ5LbVrVqOcuuFB67N47F9+htZYH0wFM=
golang.org/x/sys v0.0.0-20221006211917-84dc82d7e875/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/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.1.0/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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
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.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
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.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12 h1:VveCTK38A2rkS8ZqFY25HIDFscX5X9OoEhJd3quQmXU=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/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/mysql v1.3.6 h1:BhX1Y/RyALb+T9bZ3t07wLnPZBukt+IRkMn8UZSNbGM=
gorm.io/driver/mysql v1.3.6/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c=
gorm.io/driver/mysql v1.4.1 h1:4InA6SOaYtt4yYpV1NF9B2kvUKe9TbvUd1iWrvxnjic=
gorm.io/driver/mysql v1.4.1/go.mod h1:sSIebwZAVPiT+27jK9HIwvsqOGKx3YMPmrA3mBJR10c=
gorm.io/driver/postgres v1.3.10 h1:Fsd+pQpFMGlGxxVMUPJhNo8gG8B1lKtk8QQ4/VZZAJw=
gorm.io/driver/postgres v1.3.10/go.mod h1:whNfh5WhhHs96honoLjBAMwJGYEuA3m1hvgUbNXhPCw=
gorm.io/driver/postgres v1.4.4 h1:zt1fxJ+C+ajparn0SteEnkoPg0BQ6wOWXEQ99bteAmw=
gorm.io/driver/postgres v1.4.4/go.mod h1:whNfh5WhhHs96honoLjBAMwJGYEuA3m1hvgUbNXhPCw=
gorm.io/driver/sqlite v1.3.6 h1:Fi8xNYCUplOqWiPa3/GuCeowRNBRGTf62DEmhMDHeQQ=
gorm.io/driver/sqlite v1.3.6/go.mod h1:Sg1/pvnKtbQ7jLXxfZa+jSHvoX8hoZA8cn4xllOMTgE=
gorm.io/driver/sqlite v1.4.2 h1:F6vYJcmR4Cnh0ErLyoY8JSfabBGyR0epIGuhgHJuNws=
gorm.io/driver/sqlite v1.4.2/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
gorm.io/gorm v1.23.4/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.23.7/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
gorm.io/gorm v1.23.10 h1:4Ne9ZbzID9GUxRkllxN4WjJKpsHx8YbKvekVdgyWh24=
gorm.io/gorm v1.23.10/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
gorm.io/gorm v1.24.0 h1:j/CoiSm6xpRpmzbFJsQHYj+I8bGYWLXVHeYEyyKlF74=
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI=
modernc.org/cc/v3 v3.37.0/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20=
modernc.org/cc/v3 v3.38.1/go.mod h1:vtL+3mdHx/wcj3iEGz84rQa8vEqR6XM84v5Lcvfph20=
modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc=
modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw=
modernc.org/ccgo/v3 v3.0.0-20220904174949-82d86e1b6d56/go.mod h1:YSXjPL62P2AMSxBphRHPn7IkzhVHqkvOnRKAKh+W6ZI=
modernc.org/ccgo/v3 v3.0.0-20220910160915-348f15de615a/go.mod h1:8p47QxPkdugex9J4n9P2tLZ9bK01yngIVp00g4nomW0=
modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ=
modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws=
modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo=
modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA=
modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A=
modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU=
modernc.org/libc v1.16.17/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU=
modernc.org/libc v1.16.19/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA=
modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0=
modernc.org/libc v1.17.4/go.mod h1:WNg2ZH56rDEwdropAJeZPQkXmDwh+JCA1s/htl6r2fA=
modernc.org/libc v1.18.0/go.mod h1:vj6zehR5bfc98ipowQOM2nIDUZnVew/wNC/2tOGS+q0=
modernc.org/libc v1.19.0/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0=
modernc.org/libc v1.20.0 h1:MEbCfCKpuDC/LRb3HOCM9fZOqnPx8le3kzTJVmUGDbU=
modernc.org/libc v1.20.0/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0=
modernc.org/libc v1.20.3 h1:BodaDPuUse7taQchAClMmbE/yZp3T2ZBiwCDFyBLEXw=
modernc.org/libc v1.20.3/go.mod h1:ZRfIaEkgrYgZDl6pa4W39HgN5G/yDW+NRmNKZBDFrk0=
modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw=
modernc.org/memory v1.3.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/memory v1.4.0 h1:crykUfNSnMAXaOJnnxcSzbUGMqkLWjklJKkBK2nwZwk=
modernc.org/memory v1.4.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
modernc.org/sqlite v1.18.2/go.mod h1:kvrTLEWgxUcHa2GfHBQtanR1H9ht3hTJNtKpzH9k1u0=
modernc.org/sqlite v1.19.1 h1:8xmS5oLnZtAK//vnd4aTVj8VOeTAccEFOtUnIzfSw+4=
modernc.org/sqlite v1.19.1/go.mod h1:UfQ83woKMaPW/ZBruK0T7YaFCrI+IE0LeWVY6pmnVms=
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
modernc.org/tcl v1.13.2/go.mod h1:7CLiGIPo1M8Rv1Mitpv5akc2+8fxUd2y2UzC/MfMzy0=
modernc.org/tcl v1.14.0/go.mod h1:gQ7c1YPMvryCHCcmf8acB6VPabE59QBeuRQLL7cTUlM=
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8=
modernc.org/z v1.6.0/go.mod h1:hVdgNMh8ggTuRG1rGU8x+xGRFfiQUIAw0ZqlPy8+HyQ=
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/driver/sqlserver v1.6.1 h1:XWISFsu2I2pqd1KJhhTZNJMx1jNQ+zVL/Q8ovDcUjtY=
gorm.io/driver/sqlserver v1.6.1/go.mod h1:VZeNn7hqX1aXoN5TPAFGWvxWG90xtA8erGn2gQmpc6U=
gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=
modernc.org/cc/v4 v4.26.5 h1:xM3bX7Mve6G8K8b+T11ReenJOT+BmVqQj0FY5T4+5Y4=
modernc.org/cc/v4 v4.26.5/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.28.1 h1:wPKYn5EC/mYTqBO373jKjvX2n+3+aK7+sICCv4Fjy1A=
modernc.org/ccgo/v4 v4.28.1/go.mod h1:uD+4RnfrVgE6ec9NGguUNdhqzNIeeomeXf6CL0GTE5Q=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.66.10 h1:yZkb3YeLx4oynyR+iUsXsybsX4Ubx7MQlSYEw4yj59A=
modernc.org/libc v1.66.10/go.mod h1:8vGSEwvoUoltr4dlywvHqjtAqHBaw0j1jI7iFBTAr2I=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.39.0 h1:6bwu9Ooim0yVYA7IZn9demiQk/Ejp0BtTjBWFLymSeY=
modernc.org/sqlite v1.39.0/go.mod h1:cPTJYSlgg3Sfg046yBShXENNtPrWrDX8bsbAQBzgQ5E=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=

View File

@@ -1,9 +1,9 @@
package utils
package helpers
import (
"errors"
"fmt"
"github.com/muety/wakapi/config"
"regexp"
"strings"
"time"
)
@@ -34,29 +34,25 @@ func FormatDateTime(date time.Time) string {
}
func FormatDateTimeHuman(date time.Time) string {
return date.Format("Mon, 02 Jan 2006 15:04")
return date.Format(config.Get().App.DateTimeFormat)
}
func FormatDateTimeHumanTZ(date time.Time) string {
format := config.Get().App.DateTimeFormat
if !strings.HasSuffix(format, "MST") {
format += " MST"
}
return date.Format(format)
}
func FormatDateHuman(date time.Time) string {
return date.Format("Mon, 02 Jan 2006")
return date.Format(config.Get().App.DateFormat)
}
func Add(i, j int) int {
return i + j
}
func ParseUserAgent(ua string) (string, string, error) {
re := regexp.MustCompile(`(?iU)^wakatime\/(?:v?[\d+.]+|unset)\s\((\w+)-.*\)\s.+\s([^\/\s]+)-wakatime\/.+$`)
groups := re.FindAllStringSubmatch(ua, -1)
if len(groups) == 0 || len(groups[0]) != 3 {
return "", "", errors.New("failed to parse user agent string")
}
return groups[0][1], groups[0][2], nil
}
func SubSlice[T any](slice []T, from, to uint) []T {
if int(to) > len(slice) {
to = uint(len(slice))
}
return slice[from:int(to)]
func FmtWakatimeDuration(d time.Duration) string {
d = d.Round(time.Minute)
h := d / time.Hour
d -= h * time.Hour
m := d / time.Minute
return fmt.Sprintf("%d hrs %d mins", h, m)
}

4
helpers/helpers.go Normal file
View File

@@ -0,0 +1,4 @@
package helpers
// helpers are different from utils in that they contain wakapi-specific utility functions
// also, helpers may depend on the config package, while utils must be entirely static

30
helpers/http.go Normal file
View File

@@ -0,0 +1,30 @@
package helpers
import (
"encoding/json"
"errors"
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"net/http"
)
func ExtractCookieAuth(r *http.Request, config *config.Config) (username *string, err error) {
cookie, err := r.Cookie(models.AuthCookieKey)
if err != nil {
return nil, errors.New("missing authentication")
}
if err := config.Security.SecureCookie.Decode(models.AuthCookieKey, cookie.Value, &username); err != nil {
return nil, errors.New("cookie is invalid")
}
return username, nil
}
func RespondJSON(w http.ResponseWriter, r *http.Request, status int, object interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if err := json.NewEncoder(w).Encode(object); err != nil {
config.Log().Request(r).Error("error while writing json response", "error", err)
}
}

108
helpers/interval.go Normal file
View File

@@ -0,0 +1,108 @@
package helpers
import (
"errors"
"time"
"github.com/muety/wakapi/models"
"github.com/muety/wakapi/utils"
)
func ParseInterval(interval string) (*models.IntervalKey, error) {
for _, i := range models.AllIntervals {
if i.HasAlias(interval) {
return i, nil
}
}
return nil, errors.New("not a valid interval")
}
func MustParseInterval(interval string) *models.IntervalKey {
key, _ := ParseInterval(interval)
return key
}
func MustResolveIntervalRawTZ(interval string, tz *time.Location, startOfWeek time.Weekday) (from, to time.Time) {
_, from, to = ResolveIntervalRawTZ(interval, tz, startOfWeek)
return from, to
}
func ResolveIntervalRawTZ(interval string, tz *time.Location, startOfWeek time.Weekday) (err error, from, to time.Time) {
parsed, err := ParseInterval(interval)
if err != nil {
return err, time.Time{}, time.Time{}
}
return ResolveIntervalTZ(parsed, tz, startOfWeek)
}
func ResolveIntervalTZ(interval *models.IntervalKey, tz *time.Location, startOfWeek time.Weekday) (err error, from, to time.Time) {
now := time.Now().In(tz)
to = now
switch interval {
case models.IntervalToday:
from = utils.BeginOfToday(tz)
case models.IntervalYesterday:
from = utils.BeginOfToday(tz).Add(-24 * time.Hour)
to = utils.BeginOfToday(tz)
case models.IntervalPastDay:
from = now.Add(-24 * time.Hour)
case models.IntervalThisWeek:
from = utils.BeginOfThisWeek(tz, startOfWeek)
case models.IntervalLastWeek:
from = utils.BeginOfThisWeek(tz, startOfWeek).AddDate(0, 0, -7)
to = utils.BeginOfThisWeek(tz, startOfWeek)
case models.IntervalThisMonth:
from = utils.BeginOfThisMonth(tz)
case models.IntervalLastMonth:
from = utils.BeginOfThisMonth(tz).AddDate(0, -1, 0)
to = utils.BeginOfThisMonth(tz)
case models.IntervalThisYear:
from = utils.BeginOfThisYear(tz)
case models.IntervalPast7Days:
from = now.AddDate(0, 0, -7)
case models.IntervalPast7DaysYesterday:
from = utils.BeginOfToday(tz).AddDate(0, 0, -1).AddDate(0, 0, -7)
to = utils.BeginOfToday(tz).AddDate(0, 0, -1)
case models.IntervalPast14Days:
from = now.AddDate(0, 0, -14)
case models.IntervalPast30Days:
from = now.AddDate(0, 0, -30)
case models.IntervalPast6Months:
from = now.AddDate(0, -6, 0)
case models.IntervalPast12Months:
from = now.AddDate(0, -12, 0)
case models.IntervalAny:
from = utils.UnixEra()
default:
err = errors.New("invalid interval")
}
return err, from, to
}
// ResolveMaximumRange returns the interval label (e.g. "last_7_days") of the maximum allowed range when having opted to share this many days or an error for days == 0.
func ResolveMaximumRange(days int) (error, *models.IntervalKey) {
if days == 0 {
return errors.New("no matching interval"), nil
}
if days < 0 {
return nil, models.IntervalAny
}
if days < 7 {
return nil, models.IntervalPastDay
}
if days < 14 {
return nil, models.IntervalPast7Days
}
if days < 30 {
return nil, models.IntervalPast14Days
}
if days < 184 { // 4*31 + 2*30 (longest possible 6 months are from jul to dec)
return nil, models.IntervalPast30Days
}
if days < 366 { // 7*31 + 4*30 + 1*29
return nil, models.IntervalPast6Months
}
return nil, models.IntervalPast12Months
}

28
helpers/interval_test.go Normal file
View File

@@ -0,0 +1,28 @@
package helpers
import (
"testing"
"time"
"github.com/muety/wakapi/models"
"github.com/stretchr/testify/assert"
)
func TestResolveMaximumRange_Default(t *testing.T) {
for i := 1; i <= 367; i++ {
err1, maximumInterval := ResolveMaximumRange(i)
err2, from, to := ResolveIntervalTZ(maximumInterval, time.UTC, time.Monday)
assert.Nil(t, err1)
assert.Nil(t, err2)
assert.LessOrEqual(t, to.Sub(from), time.Duration(i*24)*time.Hour)
}
}
func TestResolveMaximumRange_EdgeCases(t *testing.T) {
err, _ := ResolveMaximumRange(0)
assert.NotNil(t, err)
_, maximumInterval := ResolveMaximumRange(-1)
assert.Equal(t, models.IntervalAny, maximumInterval)
}

91
helpers/summary.go Normal file
View File

@@ -0,0 +1,91 @@
package helpers
import (
"errors"
"net/http"
"time"
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
)
func ParseSummaryParams(r *http.Request) (*models.SummaryParams, error) {
user := extractUser(r)
params := r.URL.Query()
var err error
var from, to time.Time
if interval := params.Get("interval"); interval != "" {
err, from, to = ResolveIntervalRawTZ(interval, user.TZ(), user.StartOfWeekDay())
} else if start := params.Get("start"); start != "" {
err, from, to = ResolveIntervalRawTZ(start, user.TZ(), user.StartOfWeekDay())
} else {
from, err = ParseDateTimeTZ(params.Get("from"), user.TZ())
if err != nil {
return nil, errors.New("missing or invalid 'from' parameter")
}
to, err = ParseDateTimeTZ(params.Get("to"), user.TZ())
if err != nil {
return nil, errors.New("missing or invalid 'to' parameter")
}
}
recompute := params.Get("recompute") != "" && params.Get("recompute") != "false"
filters := ParseSummaryFilters(r)
return &models.SummaryParams{
From: from,
To: to,
User: user,
Recompute: recompute,
Filters: filters,
}, nil
}
func ParseSummaryFilters(r *http.Request) *models.Filters {
filters := &models.Filters{}
if q := r.URL.Query().Get("project"); q != "" {
filters.With(models.SummaryProject, q)
}
if q := r.URL.Query().Get("language"); q != "" {
filters.With(models.SummaryLanguage, q)
}
if q := r.URL.Query().Get("editor"); q != "" {
filters.With(models.SummaryEditor, q)
}
if q := r.URL.Query().Get("machine"); q != "" {
filters.With(models.SummaryMachine, q)
}
if q := r.URL.Query().Get("operating_system"); q != "" {
filters.With(models.SummaryOS, q)
}
if q := r.URL.Query().Get("label"); q != "" {
filters.With(models.SummaryLabel, q)
}
if q := r.URL.Query().Get("branch"); q != "" {
filters.With(models.SummaryBranch, q)
}
if q := r.URL.Query().Get("entity"); q != "" {
filters.With(models.SummaryBranch, q)
}
if q := r.URL.Query().Get("category"); q != "" {
filters.With(models.SummaryCategory, q)
}
return filters
}
func extractUser(r *http.Request) *models.User {
sharedData := r.Context().Value(config.KeySharedData)
if sharedData == nil {
config.Log().Error("request shared data not set while retrieving principal")
return nil
}
val := sharedData.(*config.SharedData).MustGet(config.MiddlewareKeyPrincipal)
if val == nil {
return nil
}
return val.(*models.User)
}

44
lib/concurrent_map.go Normal file
View File

@@ -0,0 +1,44 @@
package lib
import "sync"
type ConcurrentMap[K comparable, V any] struct {
mu sync.RWMutex
items map[K]V
}
func NewConcurrentMap[K comparable, V any]() *ConcurrentMap[K, V] {
return &ConcurrentMap[K, V]{
items: make(map[K]V),
}
}
func (m *ConcurrentMap[K, V]) Set(key K, value V) {
m.mu.Lock()
defer m.mu.Unlock()
m.items[key] = value
}
func (m *ConcurrentMap[K, V]) Get(key K) (V, bool) {
m.mu.RLock()
defer m.mu.RUnlock()
value, ok := m.items[key]
return value, ok
}
func (m *ConcurrentMap[K, V]) MustGet(key K) V {
val, _ := m.Get(key)
return val
}
func (m *ConcurrentMap[K, V]) Delete(key K) {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.items, key)
}
func (m *ConcurrentMap[K, V]) Len() int {
m.mu.RLock()
defer m.mu.RUnlock()
return len(m.items)
}

229
main.go
View File

@@ -2,20 +2,27 @@ package main
import (
"embed"
"github.com/muety/wakapi/static/docs"
"flag"
"io/fs"
"log"
"log/slog"
"net"
"net/http"
"os"
"strconv"
"time"
"github.com/emvi/logbuch"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/duke-git/lancet/v2/condition"
_ "github.com/glebarez/sqlite"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/lpar/gzipped/v2"
"github.com/swaggo/http-swagger"
httpSwagger "github.com/swaggo/http-swagger"
_ "gorm.io/driver/mysql"
_ "gorm.io/driver/postgres"
_ "gorm.io/driver/sqlserver"
"gorm.io/gorm"
"gorm.io/gorm/logger"
conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/middlewares"
@@ -28,15 +35,10 @@ import (
"github.com/muety/wakapi/routes/relay"
"github.com/muety/wakapi/services"
"github.com/muety/wakapi/services/mail"
"github.com/muety/wakapi/static/docs"
fsutils "github.com/muety/wakapi/utils/fs"
_ "gorm.io/driver/mysql"
_ "gorm.io/driver/postgres"
_ "gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
_ "github.com/muety/wakapi/static/docs"
_ "net/http/pprof"
)
// Embed version.txt
@@ -65,6 +67,7 @@ var (
keyValueRepository repositories.IKeyValueRepository
diagnosticsRepository repositories.IDiagnosticsRepository
metricsRepository *repositories.MetricsRepository
durationRepository *repositories.DurationRepository
)
var (
@@ -80,7 +83,9 @@ var (
mailService services.IMailService
keyValueService services.IKeyValueService
reportService services.IReportService
activityService services.IActivityService
diagnosticsService services.IDiagnosticsService
housekeepingService services.IHousekeepingService
miscService services.IMiscService
)
@@ -106,17 +111,20 @@ var (
// @name Authorization
func main() {
config = conf.Load(version)
var versionFlag = flag.Bool("version", false, "print version")
var configFlag = flag.String("config", conf.DefaultConfigPath, "config file location")
flag.Parse()
if *versionFlag {
print(version)
os.Exit(0)
}
config = conf.Load(*configFlag, version)
// Configure Swagger docs
docs.SwaggerInfo.BasePath = config.Server.BasePath + "/api"
// Set log level
if config.IsDev() {
logbuch.SetLevel(logbuch.LevelDebug)
} else {
logbuch.SetLevel(logbuch.LevelInfo)
}
slog.Info("Wakapi", "version", config.Version)
// Set up GORM
gormLogger := logger.New(
@@ -130,25 +138,21 @@ func main() {
// Connect to database
var err error
db, err = gorm.Open(config.Db.GetDialector(), &gorm.Config{Logger: gormLogger})
slog.Info("starting with database", "dialect", config.Db.Dialect)
db, err = gorm.Open(config.Db.GetDialector(), &gorm.Config{Logger: gormLogger}, conf.GetWakapiDBOpts(&config.Db))
if err != nil {
logbuch.Error(err.Error())
logbuch.Fatal("could not open database")
}
if config.Db.IsSQLite() {
db.Exec("PRAGMA foreign_keys = ON;")
conf.Log().Fatal("could not connect to database", "error", err)
}
if config.IsDev() {
db = db.Debug()
}
sqlDb, err := db.DB()
if err != nil {
conf.Log().Fatal("could not connect to database", "error", err)
}
sqlDb.SetMaxIdleConns(int(config.Db.MaxConn))
sqlDb.SetMaxOpenConns(int(config.Db.MaxConn))
if err != nil {
logbuch.Error(err.Error())
logbuch.Fatal("could not connect to database")
}
defer sqlDb.Close()
// Migrate database schema
@@ -167,39 +171,53 @@ func main() {
keyValueRepository = repositories.NewKeyValueRepository(db)
diagnosticsRepository = repositories.NewDiagnosticsRepository(db)
metricsRepository = repositories.NewMetricsRepository(db)
durationRepository = repositories.NewDurationRepository(db)
// Services
mailService = mail.NewMailService()
aliasService = services.NewAliasService(aliasRepository)
userService = services.NewUserService(mailService, userRepository)
keyValueService = services.NewKeyValueService(keyValueRepository)
userService = services.NewUserService(keyValueService, mailService, userRepository)
languageMappingService = services.NewLanguageMappingService(languageMappingRepository)
projectLabelService = services.NewProjectLabelService(projectLabelRepository)
heartbeatService = services.NewHeartbeatService(heartbeatRepository, languageMappingService)
durationService = services.NewDurationService(heartbeatService)
summaryService = services.NewSummaryService(summaryRepository, durationService, aliasService, projectLabelService)
leaderboardService = services.NewLeaderboardService(leaderboardRepository, summaryService, userService)
aggregationService = services.NewAggregationService(userService, summaryService, heartbeatService)
keyValueService = services.NewKeyValueService(keyValueRepository)
durationService = services.NewDurationService(durationRepository, heartbeatService, userService, languageMappingService)
summaryService = services.NewSummaryService(summaryRepository, heartbeatService, durationService, aliasService, projectLabelService)
aggregationService = services.NewAggregationService(userService, summaryService, heartbeatService, durationService)
reportService = services.NewReportService(summaryService, userService, mailService)
activityService = services.NewActivityService(summaryService)
diagnosticsService = services.NewDiagnosticsService(diagnosticsRepository)
miscService = services.NewMiscService(userService, summaryService, keyValueService)
housekeepingService = services.NewHousekeepingService(userService, heartbeatService, summaryService, aliasRepository) // can pass any repo here
miscService = services.NewMiscService(userService, heartbeatService, summaryService, keyValueService, mailService)
if config.App.LeaderboardEnabled {
leaderboardService = services.NewLeaderboardService(leaderboardRepository, summaryService, userService)
}
// Schedule background tasks
go conf.StartJobs()
go aggregationService.Schedule()
go leaderboardService.ScheduleDefault()
go miscService.ScheduleCountTotalTime()
go reportService.Schedule()
go housekeepingService.Schedule()
go miscService.Schedule()
if config.App.LeaderboardEnabled {
go leaderboardService.Schedule()
}
routes.Init()
// API Handlers
rootApiHandler := api.NewApiRootHandler()
healthApiHandler := api.NewHealthApiHandler(db)
heartbeatApiHandler := api.NewHeartbeatApiHandler(userService, heartbeatService, languageMappingService)
summaryApiHandler := api.NewSummaryApiHandler(userService, summaryService)
metricsHandler := api.NewMetricsHandler(userService, summaryService, heartbeatService, keyValueService, metricsRepository)
metricsHandler := api.NewMetricsHandler(userService, summaryService, heartbeatService, leaderboardService, keyValueService, metricsRepository)
diagnosticsHandler := api.NewDiagnosticsApiHandler(userService, diagnosticsService)
avatarHandler := api.NewAvatarHandler()
activityHandler := api.NewActivityApiHandler(userService, activityService)
badgeHandler := api.NewBadgeHandler(userService, summaryService)
captchaHandler := api.NewCaptchaHandler()
// Compat Handlers
wakatimeV1StatusBarHandler := wtV1Routes.NewStatusBarHandler(userService, summaryService)
@@ -209,57 +227,72 @@ func main() {
wakatimeV1UsersHandler := wtV1Routes.NewUsersHandler(userService, heartbeatService)
wakatimeV1ProjectsHandler := wtV1Routes.NewProjectsHandler(userService, heartbeatService)
wakatimeV1HeartbeatsHandler := wtV1Routes.NewHeartbeatHandler(userService, heartbeatService)
wakatimeV1LeadersHandler := wtV1Routes.NewLeadersHandler(userService, leaderboardService)
wakatimeV1UserAgentsHandler := wtV1Routes.NewUserAgentsHandler(userService, heartbeatService)
shieldV1BadgeHandler := shieldsV1Routes.NewBadgeHandler(summaryService, userService)
// MVC Handlers
summaryHandler := routes.NewSummaryHandler(summaryService, userService)
settingsHandler := routes.NewSettingsHandler(userService, heartbeatService, summaryService, aliasService, aggregationService, languageMappingService, projectLabelService, keyValueService, mailService)
leaderboardHandler := routes.NewLeaderboardHandler(userService, leaderboardService)
homeHandler := routes.NewHomeHandler(keyValueService)
loginHandler := routes.NewLoginHandler(userService, mailService)
summaryHandler := routes.NewSummaryHandler(summaryService, userService, heartbeatService, durationService, aliasService)
settingsHandler := routes.NewSettingsHandler(userService, heartbeatService, durationService, summaryService, aliasService, aggregationService, languageMappingService, projectLabelService, keyValueService, mailService)
subscriptionHandler := routes.NewSubscriptionHandler(userService, mailService, keyValueService)
projectsHandler := routes.NewProjectsHandler(userService, heartbeatService)
homeHandler := routes.NewHomeHandler(userService, keyValueService)
loginHandler := routes.NewLoginHandler(userService, mailService, keyValueService)
imprintHandler := routes.NewImprintHandler(keyValueService)
leaderboardHandler := condition.Ternary[bool, routes.Handler](config.App.LeaderboardEnabled, routes.NewLeaderboardHandler(userService, leaderboardService), routes.NewNoopHandler())
// Other Handlers
relayHandler := relay.NewRelayHandler()
// Setup Routers
router := mux.NewRouter()
rootRouter := router.PathPrefix("/").Subrouter()
apiRouter := router.PathPrefix("/api").Subrouter().StrictSlash(true)
// https://github.com/gorilla/mux/issues/416
router.NotFoundHandler = router.NewRoute().BuildOnly().HandlerFunc(http.NotFound).GetHandler()
router.NotFoundHandler = middlewares.NewLoggingMiddleware(logbuch.Info, []string{
"/assets",
"/favicon",
"/service-worker.js",
})(router.NotFoundHandler)
// Globally used middlewares
router.Use(middlewares.NewPrincipalMiddleware())
router.Use(middlewares.NewLoggingMiddleware(logbuch.Info, []string{"/assets", "/api/health"}))
router.Use(handlers.RecoveryHandler())
// Setup Routing
router := chi.NewRouter()
router.Use(
middleware.CleanPath,
middleware.StripSlashes,
middleware.Recoverer,
middlewares.NewSharedDataMiddleware(),
middlewares.NewLoggingMiddleware(slog.Info, []string{
"/assets",
"/favicon",
"/service-worker.js",
"/api/health",
"/api/avatar",
}),
)
if config.Sentry.Dsn != "" {
router.Use(middlewares.NewSentryMiddleware())
}
// Setup Sub Routers
rootRouter := chi.NewRouter()
rootRouter.Use(middlewares.NewSecurityMiddleware())
apiRouter := chi.NewRouter()
// Hook sub routers
router.Mount("/", rootRouter)
router.Mount("/api", apiRouter)
// Route registrations
homeHandler.RegisterRoutes(rootRouter)
loginHandler.RegisterRoutes(rootRouter)
imprintHandler.RegisterRoutes(rootRouter)
summaryHandler.RegisterRoutes(rootRouter)
leaderboardHandler.RegisterRoutes(rootRouter)
projectsHandler.RegisterRoutes(rootRouter)
settingsHandler.RegisterRoutes(rootRouter)
subscriptionHandler.RegisterRoutes(rootRouter)
relayHandler.RegisterRoutes(rootRouter)
// API route registrations
rootApiHandler.RegisterRoutes(apiRouter)
summaryApiHandler.RegisterRoutes(apiRouter)
healthApiHandler.RegisterRoutes(apiRouter)
heartbeatApiHandler.RegisterRoutes(apiRouter)
metricsHandler.RegisterRoutes(apiRouter)
diagnosticsHandler.RegisterRoutes(apiRouter)
avatarHandler.RegisterRoutes(apiRouter)
activityHandler.RegisterRoutes(apiRouter)
badgeHandler.RegisterRoutes(apiRouter)
wakatimeV1StatusBarHandler.RegisterRoutes(apiRouter)
wakatimeV1AllHandler.RegisterRoutes(apiRouter)
@@ -268,24 +301,34 @@ func main() {
wakatimeV1UsersHandler.RegisterRoutes(apiRouter)
wakatimeV1ProjectsHandler.RegisterRoutes(apiRouter)
wakatimeV1HeartbeatsHandler.RegisterRoutes(apiRouter)
wakatimeV1LeadersHandler.RegisterRoutes(apiRouter)
wakatimeV1UserAgentsHandler.RegisterRoutes(apiRouter)
shieldV1BadgeHandler.RegisterRoutes(apiRouter)
captchaHandler.RegisterRoutes(apiRouter)
// Static Routes
// https://github.com/golang/go/issues/43431
embeddedStatic, _ := fs.Sub(staticFiles, "static")
static := conf.ChooseFS("static", embeddedStatic)
assetsFileServer := gzipped.FileServer(fsutils.NewExistsHttpFS(
fsutils.NewExistsFS(static).WithCache(!config.IsDev()),
))
staticFileServer := http.FileServer(http.FS(
fsutils.NeuteredFileSystem{FS: static},
))
assetsStaticFs := fsutils.NewExistsHttpFS(fsutils.NewExistsFS(static).WithCache(!config.IsDev()))
assetsFileServer := http.FileServer(assetsStaticFs)
if !config.IsDev() {
assetsFileServer = gzipped.FileServer(assetsStaticFs)
}
staticFileServer := http.FileServer(http.FS(fsutils.NeuteredFileSystem{FS: static}))
router.PathPrefix("/contribute.json").Handler(staticFileServer)
router.PathPrefix("/assets").Handler(assetsFileServer)
router.Path("/swagger-ui").Handler(http.RedirectHandler("swagger-ui/", http.StatusMovedPermanently)) // https://github.com/swaggo/http-swagger/issues/44
router.PathPrefix("/swagger-ui").Handler(httpSwagger.WrapHandler)
router.Get("/contribute.json", staticFileServer.ServeHTTP)
router.Get("/assets/*", assetsFileServer.ServeHTTP)
router.Get("/swagger-ui", http.RedirectHandler("swagger-ui/", http.StatusMovedPermanently).ServeHTTP) // https://github.com/swaggo/http-swagger/issues/44
router.Get("/swagger-ui/*", httpSwagger.WrapHandler)
if config.EnablePprof {
slog.Info("profiling enabled, exposing pprof data", "url", "http://127.0.0.1:6060/debug/pprof")
go func() {
_ = http.ListenAndServe("127.0.0.1:6060", nil)
}()
}
// Listen HTTP
listen(router)
@@ -295,7 +338,7 @@ func listen(handler http.Handler) {
var s4, s6, sSocket *http.Server
// IPv4
if config.Server.ListenIpV4 != "" {
if config.Server.ListenIpV4 != "-" && config.Server.ListenIpV4 != "" {
bindString4 := config.Server.ListenIpV4 + ":" + strconv.Itoa(config.Server.Port)
s4 = &http.Server{
Handler: handler,
@@ -306,7 +349,7 @@ func listen(handler http.Handler) {
}
// IPv6
if config.Server.ListenIpV6 != "" {
if config.Server.ListenIpV6 != "-" && config.Server.ListenIpV6 != "" {
bindString6 := "[" + config.Server.ListenIpV6 + "]:" + strconv.Itoa(config.Server.Port)
s6 = &http.Server{
Handler: handler,
@@ -317,12 +360,12 @@ func listen(handler http.Handler) {
}
// UNIX domain socket
if config.Server.ListenSocket != "" {
if config.Server.ListenSocket != "-" && config.Server.ListenSocket != "" {
// Remove if exists
if _, err := os.Stat(config.Server.ListenSocket); err == nil {
logbuch.Info("--> Removing unix socket %s", config.Server.ListenSocket)
slog.Info("👉 Removing unix socket", "listenSocket", config.Server.ListenSocket)
if err := os.Remove(config.Server.ListenSocket); err != nil {
logbuch.Fatal(err.Error())
conf.Log().Fatal(err.Error())
}
}
sSocket = &http.Server{
@@ -334,59 +377,65 @@ func listen(handler http.Handler) {
if config.UseTLS() {
if s4 != nil {
logbuch.Info("--> Listening for HTTPS on %s... ✅", s4.Addr)
slog.Info("👉 Listening for HTTPS... ✅", "address", s4.Addr)
go func() {
if err := s4.ListenAndServeTLS(config.Server.TlsCertPath, config.Server.TlsKeyPath); err != nil {
logbuch.Fatal(err.Error())
conf.Log().Fatal(err.Error())
}
}()
}
if s6 != nil {
logbuch.Info("--> Listening for HTTPS on %s... ✅", s6.Addr)
slog.Info("👉 Listening for HTTPS... ✅", "address", s6.Addr)
go func() {
if err := s6.ListenAndServeTLS(config.Server.TlsCertPath, config.Server.TlsKeyPath); err != nil {
logbuch.Fatal(err.Error())
conf.Log().Fatal(err.Error())
}
}()
}
if sSocket != nil {
logbuch.Info("--> Listening for HTTPS on %s... ✅", config.Server.ListenSocket)
slog.Info("👉 Listening for HTTPS... ✅", "address", config.Server.ListenSocket)
go func() {
unixListener, err := net.Listen("unix", config.Server.ListenSocket)
if err != nil {
logbuch.Fatal(err.Error())
conf.Log().Fatal(err.Error())
}
if err := os.Chmod(config.Server.ListenSocket, os.FileMode(config.Server.ListenSocketMode)); err != nil {
slog.Warn("failed to set user permissions for unix socket", "error", err)
}
if err := sSocket.ServeTLS(unixListener, config.Server.TlsCertPath, config.Server.TlsKeyPath); err != nil {
logbuch.Fatal(err.Error())
conf.Log().Fatal(err.Error())
}
}()
}
} else {
if s4 != nil {
logbuch.Info("--> Listening for HTTP on %s... ✅", s4.Addr)
slog.Info("👉 Listening for HTTP... ✅", "address", s4.Addr)
go func() {
if err := s4.ListenAndServe(); err != nil {
logbuch.Fatal(err.Error())
conf.Log().Fatal(err.Error())
}
}()
}
if s6 != nil {
logbuch.Info("--> Listening for HTTP on %s... ✅", s6.Addr)
slog.Info("👉 Listening for HTTP... ✅", "address", s6.Addr)
go func() {
if err := s6.ListenAndServe(); err != nil {
logbuch.Fatal(err.Error())
conf.Log().Fatal(err.Error())
}
}()
}
if sSocket != nil {
logbuch.Info("--> Listening for HTTP on %s... ✅", config.Server.ListenSocket)
slog.Info("👉 Listening for HTTP... ✅", "address", config.Server.ListenSocket)
go func() {
unixListener, err := net.Listen("unix", config.Server.ListenSocket)
if err != nil {
logbuch.Fatal(err.Error())
conf.Log().Fatal(err.Error())
}
if err := os.Chmod(config.Server.ListenSocket, os.FileMode(config.Server.ListenSocketMode)); err != nil {
slog.Warn("failed to set user permissions for unix socket", "error", err)
}
if err := sSocket.Serve(unixListener); err != nil {
logbuch.Fatal(err.Error())
conf.Log().Fatal(err.Error())
}
}()
}

View File

@@ -1,10 +1,17 @@
package middlewares
import (
"errors"
"fmt"
"net"
"net/http"
"strings"
"github.com/duke-git/lancet/v2/slice"
"github.com/gofrs/uuid/v5"
"github.com/muety/wakapi/helpers"
routeutils "github.com/muety/wakapi/routes/utils"
conf "github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"github.com/muety/wakapi/services"
@@ -21,30 +28,43 @@ var (
)
type AuthenticateMiddleware struct {
config *conf.Config
userSrvc services.IUserService
optionalForPaths []string
redirectTarget string // optional
config *conf.Config
userSrvc services.IUserService
optionalForPaths []string
optionalForMethods []string
redirectTarget string // optional
redirectErrorMessage string // optional
}
func NewAuthenticateMiddleware(userService services.IUserService) *AuthenticateMiddleware {
return &AuthenticateMiddleware{
config: conf.Get(),
userSrvc: userService,
optionalForPaths: []string{},
config: conf.Get(),
userSrvc: userService,
optionalForPaths: []string{},
optionalForMethods: []string{},
}
}
func (m *AuthenticateMiddleware) WithOptionalFor(paths []string) *AuthenticateMiddleware {
func (m *AuthenticateMiddleware) WithOptionalFor(paths ...string) *AuthenticateMiddleware {
m.optionalForPaths = paths
return m
}
func (m *AuthenticateMiddleware) WithOptionalForMethods(methods ...string) *AuthenticateMiddleware {
m.optionalForMethods = methods
return m
}
func (m *AuthenticateMiddleware) WithRedirectTarget(path string) *AuthenticateMiddleware {
m.redirectTarget = path
return m
}
func (m *AuthenticateMiddleware) WithRedirectErrorMessage(message string) *AuthenticateMiddleware {
m.redirectErrorMessage = message
return m
}
func (m *AuthenticateMiddleware) Handler(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
m.ServeHTTP(w, r, h.ServeHTTP)
@@ -53,17 +73,26 @@ func (m *AuthenticateMiddleware) Handler(h http.Handler) http.Handler {
func (m *AuthenticateMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
var user *models.User
user, err := m.tryGetUserByCookie(r)
if m.tryHandleOidc(w, r) {
// user has expired oidc token, thus is redirected to provider and will come back to callback endpoint
// notably, if user does have a valid, non-expired id token, they will also have a valid auth cookie, so proceed as usual
return
}
user, err := m.tryGetUserByCookie(r)
if err != nil {
user, err = m.tryGetUserByApiKeyHeader(r)
}
if err != nil {
user, err = m.tryGetUserByApiKeyQuery(r)
}
if err != nil && m.config.Security.TrustedHeaderAuth {
user, err = m.tryGetUserByTrustedHeader(r, m.config.Security.TrustedHeaderAuthAllowSignup)
}
if err != nil || user == nil {
if m.isOptional(r.URL.Path) {
if m.isOptional(r) {
next(w, r)
return
}
@@ -72,6 +101,11 @@ func (m *AuthenticateMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Reques
w.WriteHeader(http.StatusUnauthorized)
w.Write([]byte(conf.ErrUnauthorized))
} else {
if m.redirectErrorMessage != "" {
session, _ := conf.GetSessionStore().Get(r, conf.CookieKeySession)
session.AddFlash(m.redirectErrorMessage, "error")
session.Save(r, w)
}
http.SetCookie(w, m.config.GetClearCookie(models.AuthCookieKey))
http.Redirect(w, r, m.redirectTarget, http.StatusFound)
}
@@ -82,9 +116,14 @@ func (m *AuthenticateMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Reques
next(w, r)
}
func (m *AuthenticateMiddleware) isOptional(requestPath string) bool {
func (m *AuthenticateMiddleware) isOptional(r *http.Request) bool {
for _, p := range m.optionalForPaths {
if strings.HasPrefix(requestPath, p) || requestPath == p {
if strings.HasPrefix(r.URL.Path, p) || r.URL.Path == p {
return true
}
}
for _, m := range m.optionalForMethods {
if r.Method == strings.ToUpper(m) {
return true
}
}
@@ -120,8 +159,46 @@ func (m *AuthenticateMiddleware) tryGetUserByApiKeyQuery(r *http.Request) (*mode
return user, nil
}
func (m *AuthenticateMiddleware) tryGetUserByTrustedHeader(r *http.Request, create bool) (*models.User, error) {
if !m.config.Security.TrustedHeaderAuth {
return nil, errors.New("trusted header auth disabled")
}
create = create && m.config.Security.TrustedHeaderAuthAllowSignup // double-check
remoteUser := r.Header.Get(m.config.Security.TrustedHeaderAuthKey)
if remoteUser == "" {
return nil, errors.New("trusted header field empty")
}
if addr, err := net.ResolveTCPAddr("tcp", r.RemoteAddr); err != nil || !slice.ContainBy[net.IPNet](m.config.Security.TrustReverseProxyIPs(), func(ipNet net.IPNet) bool {
return ipNet.Contains(addr.IP) // if err != nil, addr is nil
}) {
return nil, errors.New("reverse proxy not trusted")
}
user, err := m.userSrvc.GetUserById(remoteUser)
if err == nil {
return user, nil
}
if err.Error() != "record not found" || !create {
return nil, err
}
// register new user solely based on upstream provided username (see https://github.com/muety/wakapi/issues/808)
signup := &models.Signup{
Username: remoteUser,
Password: uuid.Must(uuid.NewV4()).String(), // throwaway random string as password
}
conf.Log().Request(r).Warn("registering new remotely authenticated user based on trusted header auth", "user_id", remoteUser)
if _, _, err := m.userSrvc.CreateOrGet(signup, false); err != nil {
return nil, err
}
return m.userSrvc.GetUserById(remoteUser)
}
func (m *AuthenticateMiddleware) tryGetUserByCookie(r *http.Request) (*models.User, error) {
username, err := utils.ExtractCookieAuth(r, m.config)
username, err := helpers.ExtractCookieAuth(r, m.config)
if err != nil {
return nil, err
}
@@ -136,3 +213,31 @@ func (m *AuthenticateMiddleware) tryGetUserByCookie(r *http.Request) (*models.Us
return user, nil
}
// redirect if oidc id token was found, but expired
// returns true if further authentication can be skipped
func (m *AuthenticateMiddleware) tryHandleOidc(w http.ResponseWriter, r *http.Request) bool {
idToken := routeutils.GetOidcIdTokenPayload(r)
if idToken == nil {
return false
}
if !idToken.IsValid() { // expired
provider, err := m.config.Security.GetOidcProvider(idToken.ProviderName)
if err != nil {
conf.Log().Request(r).Error("failed to get provider from id token", "provider", idToken.ProviderName, "sub", idToken.Subject)
return false
}
if _, err := m.userSrvc.GetUserByOidc(provider.Name, idToken.Subject); err != nil {
conf.Log().Request(r).Error("got expired oidc token for non-oidc user", "provider", idToken.ProviderName, "sub", idToken.Subject)
return false
}
state := routeutils.SetNewOidcState(r, w)
http.Redirect(w, r, provider.OAuth2.AuthCodeURL(state), http.StatusFound)
return true
}
return false
}

View File

@@ -2,10 +2,19 @@ package middlewares
import (
"encoding/base64"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
"github.com/muety/wakapi/config"
routeutils "github.com/muety/wakapi/routes/utils"
"github.com/oauth2-proxy/mockoidc"
"github.com/stretchr/testify/mock"
"github.com/muety/wakapi/mocks"
"github.com/muety/wakapi/models"
@@ -100,4 +109,253 @@ func TestAuthenticateMiddleware_tryGetUserByApiKeyQuery_Invalid(t *testing.T) {
assert.Nil(t, result)
}
func TestAuthenticateMiddleware_tryGetUserByTrustedHeader_Disabled(t *testing.T) {
cfg := config.Empty()
cfg.Security.TrustedHeaderAuth = false
cfg.Security.TrustedHeaderAuthKey = "Remote-User"
cfg.Security.TrustReverseProxyIps = "127.0.0.1,::1"
cfg.Security.ParseTrustReverseProxyIPs()
config.Set(cfg)
testUser := &models.User{ID: "user01"}
mockRequest := &http.Request{
Header: http.Header{"Remote-User": []string{testUser.ID}},
RemoteAddr: "127.0.0.1:54654",
}
userServiceMock := new(mocks.UserServiceMock)
userServiceMock.On("GetUserById", testUser.ID).Return(testUser, nil)
sut := NewAuthenticateMiddleware(userServiceMock)
result, actualErr := sut.tryGetUserByTrustedHeader(mockRequest, false)
assert.Error(t, actualErr)
assert.Nil(t, result)
}
func TestAuthenticateMiddleware_tryGetUserByTrustedHeader_Untrusted(t *testing.T) {
cfg := config.Empty()
cfg.Security.TrustedHeaderAuth = true
cfg.Security.TrustedHeaderAuthKey = "Remote-User"
cfg.Security.TrustReverseProxyIps = "127.0.0.1,::1,192.168.0.1,192.168.178.0/24,33b7:08d8:c07a:c2ee:0fac:cb95:dadc:dafb,1ddc:e2d6:dcce:ab6c::1/64"
cfg.Security.ParseTrustReverseProxyIPs()
config.Set(cfg)
testIps := []string{"192.168.0.2", "192.168.179.35", "[33b7:08d8:c07a:c2ee:0fac:cb95:dadc:dafa]", "[1ddc:e2d6:dcce:ab6b:1ba7:7aaa:58dc:a42b]"} // none of these should be authorized
testUser := &models.User{ID: "user01"}
for _, ip := range testIps {
mockRequest := &http.Request{
Header: http.Header{
"Remote-User": []string{testUser.ID},
"X-Forwarded-For": []string{"192.168.0.1"}, // forward for some trusted ip -> header should be ignored for auth. checks, because only actual reverse proxy must be legitimized
},
RemoteAddr: fmt.Sprintf("%s:54654", ip),
}
userServiceMock := new(mocks.UserServiceMock)
userServiceMock.On("GetUserById", testUser.ID).Return(testUser, nil)
sut := NewAuthenticateMiddleware(userServiceMock)
result, actualErr := sut.tryGetUserByTrustedHeader(mockRequest, false)
assert.Error(t, actualErr)
assert.Nil(t, result)
}
}
func TestAuthenticateMiddleware_tryGetUserByTrustedHeader_Success(t *testing.T) {
cfg := config.Empty()
cfg.Security.TrustedHeaderAuth = true
cfg.Security.TrustedHeaderAuthKey = "Remote-User"
cfg.Security.TrustReverseProxyIps = "127.0.0.1,::1,192.168.0.1,192.168.178.0/24,33b7:08d8:c07a:c2ee:0fac:cb95:dadc:dafb,1ddc:e2d6:dcce:ab6c::1/64"
cfg.Security.ParseTrustReverseProxyIPs()
config.Set(cfg)
testIps := []string{"127.0.0.1", "[::1]", "192.168.0.1", "192.168.178.1", "192.168.178.35", "[33b7:08d8:c07a:c2ee:0fac:cb95:dadc:dafb]", "[1ddc:e2d6:dcce:ab6c:2ba7:7aaa:58dc:a42b]", "[1ddc:e2d6:dcce:ab6c:1ba7:7aaa:58dc:a42b]"} // all of these should be authorized
testUser := &models.User{ID: "user01"}
for _, ip := range testIps {
mockRequest := &http.Request{
Header: http.Header{"Remote-User": []string{testUser.ID}},
RemoteAddr: fmt.Sprintf("%s:54654", ip),
}
userServiceMock := new(mocks.UserServiceMock)
userServiceMock.On("GetUserById", testUser.ID).Return(testUser, nil)
sut := NewAuthenticateMiddleware(userServiceMock)
result, actualErr := sut.tryGetUserByTrustedHeader(mockRequest, false)
assert.Equal(t, testUser, result)
assert.Nil(t, actualErr)
}
}
func TestAuthenticateMiddleware_tryGetUserByTrustedHeader_NoSignup(t *testing.T) {
cfg := config.Empty()
cfg.Security.TrustedHeaderAuth = true
cfg.Security.TrustedHeaderAuthAllowSignup = false
cfg.Security.TrustedHeaderAuthKey = "Remote-User"
cfg.Security.TrustReverseProxyIps = "127.0.0.1,::1,192.168.0.1,192.168.178.0/24,33b7:08d8:c07a:c2ee:0fac:cb95:dadc:dafb,1ddc:e2d6:dcce:ab6c::1/64"
cfg.Security.ParseTrustReverseProxyIPs()
config.Set(cfg)
testIps := []string{"127.0.0.1", "[::1]", "192.168.0.1", "192.168.178.1", "192.168.178.35", "[33b7:08d8:c07a:c2ee:0fac:cb95:dadc:dafb]", "[1ddc:e2d6:dcce:ab6c:2ba7:7aaa:58dc:a42b]", "[1ddc:e2d6:dcce:ab6c:1ba7:7aaa:58dc:a42b]"} // all of these should be authorized
testUser := &models.User{ID: "nonexisting"}
for _, ip := range testIps {
mockRequest := &http.Request{
Header: http.Header{"Remote-User": []string{testUser.ID}},
RemoteAddr: fmt.Sprintf("%s:54654", ip),
}
userServiceMock := new(mocks.UserServiceMock)
userServiceMock.On("GetUserById", testUser.ID).Return(nil, errors.New("record not found"))
sut := NewAuthenticateMiddleware(userServiceMock)
result, actualErr := sut.tryGetUserByTrustedHeader(mockRequest, false)
assert.Error(t, actualErr)
assert.Nil(t, result)
userServiceMock.AssertNumberOfCalls(t, "GetUserById", 1)
}
}
func TestAuthenticateMiddleware_tryGetUserByTrustedHeader_Signup(t *testing.T) {
cfg := config.Empty()
cfg.Security.TrustedHeaderAuth = true
cfg.Security.TrustedHeaderAuthAllowSignup = true
cfg.Security.TrustedHeaderAuthKey = "Remote-User"
cfg.Security.TrustReverseProxyIps = "127.0.0.1,::1,192.168.0.1,192.168.178.0/24,33b7:08d8:c07a:c2ee:0fac:cb95:dadc:dafb,1ddc:e2d6:dcce:ab6c::1/64"
cfg.Security.ParseTrustReverseProxyIPs()
config.Set(cfg)
testIps := []string{"127.0.0.1", "[::1]", "192.168.0.1", "192.168.178.1", "192.168.178.35", "[33b7:08d8:c07a:c2ee:0fac:cb95:dadc:dafb]", "[1ddc:e2d6:dcce:ab6c:2ba7:7aaa:58dc:a42b]", "[1ddc:e2d6:dcce:ab6c:1ba7:7aaa:58dc:a42b]"} // all of these should be authorized
testUser := &models.User{ID: "tobecreated"}
for _, ip := range testIps {
mockRequest := &http.Request{
Header: http.Header{"Remote-User": []string{testUser.ID}},
RemoteAddr: fmt.Sprintf("%s:54654", ip),
}
userServiceMock := new(mocks.UserServiceMock)
userServiceMock.On("GetUserById", testUser.ID).Return(nil, errors.New("record not found"))
userServiceMock.On("CreateOrGet", mock.Anything, false).Return(testUser, true, nil)
userServiceMock.On("GetUserById", testUser.ID).Return(testUser, nil)
sut := NewAuthenticateMiddleware(userServiceMock)
result, actualErr := sut.tryGetUserByTrustedHeader(mockRequest, true)
assert.Error(t, actualErr)
assert.Nil(t, result)
userServiceMock.AssertNumberOfCalls(t, "GetUserById", 2)
}
}
func TestAuthenticateMiddleware_tryHandleOidc_NoToken(t *testing.T) {
config.Set(config.Empty())
userServiceMock := new(mocks.UserServiceMock)
r := httptest.NewRequest(http.MethodGet, "/summary", nil)
w := httptest.NewRecorder()
sut := NewAuthenticateMiddleware(userServiceMock)
assert.False(t, sut.tryHandleOidc(w, r))
assert.NotEqual(t, w.Code, http.StatusTemporaryRedirect)
assert.NotEqual(t, w.Code, http.StatusFound)
}
func TestAuthenticateMiddleware_tryHandleOidc_InvalidToken_ExistingUser(t *testing.T) {
const (
testProvider = "mock"
testSub = "testsub"
)
var testUser = &models.User{ID: "testuser"}
oidcMock, _ := mockoidc.Run()
defer oidcMock.Shutdown()
cfg := config.Empty()
config.Set(cfg)
config.WithOidcProvider(cfg, testProvider, oidcMock.ClientID, oidcMock.ClientSecret, oidcMock.Addr()+"/oidc")
r := httptest.NewRequest(http.MethodGet, "/summary", nil)
w := httptest.NewRecorder()
testIdToken := &config.IdTokenPayload{
Subject: testSub,
Expiry: time.Now().Add(-time.Minute).Unix(),
ProviderName: testProvider,
}
routeutils.SetOidcIdTokenPayload(testIdToken, r, w)
userServiceMock := new(mocks.UserServiceMock)
userServiceMock.On("GetUserByOidc", testProvider, testSub).Return(testUser, nil)
sut := NewAuthenticateMiddleware(userServiceMock)
assert.True(t, sut.tryHandleOidc(w, r))
assert.Equal(t, w.Code, http.StatusFound)
assert.True(t, strings.HasPrefix(w.Header().Get("Location"), oidcMock.AuthorizationEndpoint()))
assert.NotEmpty(t, routeutils.GetOidcState(r))
assert.Contains(t, w.Header().Get("Location"), fmt.Sprintf("state=%s", routeutils.GetOidcState(r)))
}
func TestAuthenticateMiddleware_tryHandleOidc_InvalidToken_NonExistingUser(t *testing.T) {
const (
testProvider = "mock"
testSub = "testsub"
)
oidcMock, _ := mockoidc.Run()
defer oidcMock.Shutdown()
cfg := config.Empty()
config.Set(cfg)
config.WithOidcProvider(cfg, testProvider, oidcMock.ClientID, oidcMock.ClientSecret, oidcMock.Addr()+"/oidc")
r := httptest.NewRequest(http.MethodGet, "/summary", nil)
w := httptest.NewRecorder()
testIdToken := &config.IdTokenPayload{
Subject: testSub,
Expiry: time.Now().Add(-time.Minute).Unix(),
ProviderName: testProvider,
}
routeutils.SetOidcIdTokenPayload(testIdToken, r, w)
userServiceMock := new(mocks.UserServiceMock)
userServiceMock.On("GetUserByOidc", testProvider, testSub).Return(nil, errors.New(""))
sut := NewAuthenticateMiddleware(userServiceMock)
assert.False(t, sut.tryHandleOidc(w, r))
assert.NotEqual(t, w.Code, http.StatusTemporaryRedirect)
assert.NotEqual(t, w.Code, http.StatusFound)
}
func TestAuthenticateMiddleware_tryHandleOidc_ValidToken(t *testing.T) {
config.Set(config.Empty())
userServiceMock := new(mocks.UserServiceMock)
r := httptest.NewRequest(http.MethodGet, "/summary", nil)
w := httptest.NewRecorder()
routeutils.SetOidcIdTokenPayload(&config.IdTokenPayload{
Expiry: time.Now().Add(1 * time.Minute).Unix(),
}, r, w)
sut := NewAuthenticateMiddleware(userServiceMock)
assert.False(t, sut.tryHandleOidc(w, r))
assert.NotEqual(t, w.Code, http.StatusTemporaryRedirect)
assert.NotEqual(t, w.Code, http.StatusFound)
}
// TODO: somehow test cookie auth function

View File

@@ -6,7 +6,6 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/emvi/logbuch"
"github.com/leandro-lugaresi/hub"
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/middlewares"
@@ -14,7 +13,7 @@ import (
routeutils "github.com/muety/wakapi/routes/utils"
"github.com/patrickmn/go-cache"
"io"
"io/ioutil"
"log/slog"
"net/http"
"time"
)
@@ -63,13 +62,13 @@ func (m *WakatimeRelayMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Reque
err := m.filterByCache(r)
if err != nil {
logbuch.Warn("%v", err)
slog.Warn("filter cache error", "error", err)
return
}
body, _ := ioutil.ReadAll(r.Body)
body, _ := io.ReadAll(r.Body)
r.Body.Close()
r.Body = ioutil.NopCloser(bytes.NewBuffer(body))
r.Body = io.NopCloser(bytes.NewBuffer(body))
// prevent cycles
downstreamInstanceId := ownInstanceId
@@ -105,7 +104,7 @@ func (m *WakatimeRelayMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Reque
func (m *WakatimeRelayMiddleware) send(method, url string, body io.Reader, headers http.Header, forUser *models.User) {
request, err := http.NewRequest(method, url, body)
if err != nil {
logbuch.Warn("error constructing relayed request - %v", err)
slog.Warn("error constructing relayed request", "error", err)
return
}
@@ -117,12 +116,12 @@ func (m *WakatimeRelayMiddleware) send(method, url string, body io.Reader, heade
response, err := m.httpClient.Do(request)
if err != nil {
logbuch.Warn("error executing relayed request - %v", err)
slog.Warn("error executing relayed request", "error", err)
return
}
if response.StatusCode < 200 || response.StatusCode >= 300 {
logbuch.Warn("failed to relay request for user %s, got status %d", forUser.ID, response.StatusCode)
slog.Warn("failed to relay request for user", "userID", forUser.ID, "statusCode", response.StatusCode)
// TODO: use leaky bucket instead of expiring cache?
if _, found := m.failureCache.Get(forUser.ID); !found {
@@ -134,7 +133,7 @@ func (m *WakatimeRelayMiddleware) send(method, url string, body io.Reader, heade
Fields: map[string]interface{}{config.FieldUser: forUser, config.FieldPayload: n},
})
} else if n%10 == 0 {
logbuch.Warn("%d / %d failed wakatime heartbeat relaying attempts for user %s within last 24 hours", n, maxFailuresPerDay, forUser.ID)
slog.Warn("failed wakatime heartbeat relaying attempts for user", "failedCount", n, "maxFailures", maxFailuresPerDay, "userID", forUser.ID)
}
}
}
@@ -149,42 +148,48 @@ func (m *WakatimeRelayMiddleware) filterByCache(r *http.Request) error {
return err
}
body, _ := ioutil.ReadAll(r.Body)
body, _ := io.ReadAll(r.Body)
r.Body.Close()
r.Body = ioutil.NopCloser(bytes.NewBuffer(body))
r.Body = io.NopCloser(bytes.NewBuffer(body))
var rawData interface{}
if err := json.NewDecoder(ioutil.NopCloser(bytes.NewBuffer(body))).Decode(&rawData); err != nil {
if err := json.NewDecoder(io.NopCloser(bytes.NewBuffer(body))).Decode(&rawData); err != nil {
return err
}
newData := make([]interface{}, 0, len(heartbeats))
for i, hb := range heartbeats {
hb = hb.Hashed()
process := func(heartbeat *models.Heartbeat, rawData interface{}) {
heartbeat = heartbeat.Hashed()
// we didn't see this particular heartbeat before
if _, found := m.hashCache.Get(hb.Hash); !found {
m.hashCache.SetDefault(hb.Hash, true)
newData = append(newData, rawData.([]interface{})[i])
continue
if _, found := m.hashCache.Get(heartbeat.Hash); !found {
m.hashCache.SetDefault(heartbeat.Hash, true)
newData = append(newData, rawData)
}
}
if _, isList := rawData.([]interface{}); isList {
for i, hb := range heartbeats {
process(hb, rawData.([]interface{})[i])
}
} else if len(heartbeats) > 0 {
process(heartbeats[0], rawData.(interface{}))
}
if len(newData) == 0 {
return errors.New("no new heartbeats to relay")
}
if len(newData) != len(heartbeats) {
user := middlewares.GetPrincipal(r)
logbuch.Warn("only relaying %d of %d heartbeats for user %s", len(newData), len(heartbeats), user.ID)
slog.Warn("only relaying partial heartbeats for user", "relayedCount", len(newData), "totalCount", len(heartbeats), "userID", user.ID)
}
buf := bytes.Buffer{}
if err := json.NewEncoder(&buf).Encode(newData); err != nil {
return err
}
r.Body = ioutil.NopCloser(&buf)
r.Body = io.NopCloser(&buf)
return nil
}

View File

@@ -1,6 +1,7 @@
package middlewares
// Borrowed from https://gist.github.com/elithrar/887d162dfd0c539b700ab4049c76e22b
// Alternatively, we could use https://github.com/samber/slog-chi, however, it pulls in another bunch of dependencies and log messages are more verbose and feel almost little bloated
import (
"io"
@@ -42,15 +43,14 @@ func (lg *LoggingMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
lg.logFunc(
"[request] status=%d, method=%s, uri=%s, duration=%v, bytes=%d, addr=%s, user=%s",
ww.Status(),
r.Method,
r.URL.String(),
duration,
ww.BytesWritten(),
readUserIP(r),
readUserID(r),
lg.logFunc("[request]",
"status", ww.Status(),
"method", r.Method,
"uri", r.URL.String(),
"duration", duration,
"bytes", ww.BytesWritten(),
"addr", readUserIP(r),
"user", readUserID(r),
)
}

View File

@@ -1,64 +1,16 @@
package middlewares
import (
"context"
"github.com/muety/wakapi/models"
routeutils "github.com/muety/wakapi/routes/utils"
"net/http"
)
const keyPrincipal = "principal"
type PrincipalContainer struct {
principal *models.User
}
func (c *PrincipalContainer) SetPrincipal(user *models.User) {
c.principal = user
}
func (c *PrincipalContainer) GetPrincipal() *models.User {
return c.principal
}
// This middleware is a bit of a dirty workaround to the fact that a http.Request's context
// does not allow to pass values from an inner to an outer middleware. Calling WithContext() on a
// request shallow-copies the whole request itself and therefore, in a chain of handler1(handler2()),
// handler 1 will not have access to values handler 2 writes to its context. In addition, Context.WithValue
// returns a new context with the old context as a parent.
//
// As a concrete example, SentryMiddleware as well as LoggingMiddleware should be quite the outer layers,
// while AuthenticationMiddleware is on the very inside of the chain. However, we still want sentry or the
// logger to have access to the user object populated by the auth. middleware, if present.
//
// This middleware shall be included as the outermost layers and it injects a stateful container that does
// nothing but conditionally hold a reference to an authenticated user object.
//
// Other reference: https://stackoverflow.com/questions/55972869/send-errors-to-sentry-with-golang-and-mux
type PrincipalMiddleware struct {
handler http.Handler
}
func NewPrincipalMiddleware() func(handler http.Handler) http.Handler {
return func(h http.Handler) http.Handler {
return &PrincipalMiddleware{handler: h}
}
}
func (p *PrincipalMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
ctx := context.WithValue(r.Context(), keyPrincipal, &PrincipalContainer{})
p.handler.ServeHTTP(w, r.WithContext(ctx))
}
func SetPrincipal(r *http.Request, user *models.User) {
if p := r.Context().Value(keyPrincipal); p != nil {
p.(*PrincipalContainer).SetPrincipal(user)
}
routeutils.SetPrincipal(r, user)
}
func GetPrincipal(r *http.Request) *models.User {
if p := r.Context().Value(keyPrincipal); p != nil {
return p.(*PrincipalContainer).GetPrincipal()
}
return nil
return routeutils.GetPrincipal(r)
}

View File

@@ -6,7 +6,7 @@ import (
var securityHeaders = map[string]string{
"Cross-Origin-Opener-Policy": "same-origin",
"Content-Security-Policy": "default-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' https: data:; form-action 'self'; block-all-mixed-content;",
"Content-Security-Policy": "default-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' https: data:; form-action 'self' *.stripe.com; block-all-mixed-content;",
"X-Frame-Options": "DENY",
"X-Content-Type-Options": "nosniff",
}

View File

@@ -0,0 +1,42 @@
package middlewares
import (
"context"
"net/http"
"github.com/muety/wakapi/config"
)
const key = config.KeySharedData
// This middleware is a bit of a dirty workaround to the fact that a http.Request's context
// does not allow to pass values from an inner to an outer middleware. Calling WithContext() on a
// request shallow-copies the whole request itself and therefore, in a chain of handler1(handler2()),
// handler 1 will not have access to values handler 2 writes to its context. In addition, Context.WithValue
// returns a new context with the old context as a parent.
//
// As a concrete example, SentryMiddleware as well as LoggingMiddleware should be quite the outer layers,
// while AuthenticationMiddleware is on the very inside of the chain. However, we still want sentry or the
// logger to have access to the user object populated by the auth. middleware, if present.
//
// This middleware shall be included as the outermost layers and it injects a stateful container that does
// nothing but conditionally hold a reference to an authenticated user object.
//
// Other reference: https://stackoverflow.com/questions/55972869/send-errors-to-sentry-with-golang-and-mux
type SharedDataMiddleware struct {
Data *config.SharedData
handler http.Handler
}
func NewSharedDataMiddleware() func(http.Handler) http.Handler {
return func(h http.Handler) http.Handler {
return &SharedDataMiddleware{handler: h}
}
}
func (s *SharedDataMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
data := config.NewSharedData()
ctx := context.WithValue(r.Context(), key, data)
s.handler.ServeHTTP(w, r.WithContext(ctx))
}

View File

@@ -1,10 +1,10 @@
package migrations
import (
"github.com/emvi/logbuch"
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"gorm.io/gorm"
"log/slog"
)
func init() {
@@ -16,12 +16,12 @@ func init() {
oldIndexName, newIndexName := "idx_customrule_user", "idx_language_mapping_user"
if migrator.HasTable(oldTableName) {
logbuch.Info("renaming '%s' table to '%s'", oldTableName, newTableName)
slog.Info("renaming table", "oldName", oldTableName, "newName", newTableName)
if err := migrator.RenameTable(oldTableName, &models.LanguageMapping{}); err != nil {
return err
}
logbuch.Info("renaming '%s' index to '%s'", oldIndexName, newIndexName)
slog.Info("renaming index", "oldName", oldIndexName, "newName", newIndexName)
return migrator.RenameIndex(&models.LanguageMapping{}, oldIndexName, newIndexName)
}
return nil

View File

@@ -1,10 +1,10 @@
package migrations
import (
"github.com/emvi/logbuch"
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"gorm.io/gorm"
"log/slog"
)
func init() {
@@ -22,12 +22,12 @@ func init() {
// https://stackoverflow.com/a/1884893/3112139
// unfortunately, we can't migrate existing sqlite databases to the newly introduced cascade settings
// things like deleting all summaries won't work in those cases unless an entirely new db is created
logbuch.Info("not attempting to drop and regenerate constraints on sqlite")
slog.Info("not attempting to drop and regenerate constraints on sqlite")
return nil
}
if !migrator.HasTable(&models.KeyStringValue{}) {
logbuch.Info("key-value table not yet existing")
slog.Info("key-value table not yet existing")
return nil
}
@@ -51,7 +51,7 @@ func init() {
for name, table := range constraints {
if migrator.HasConstraint(table, name) {
logbuch.Info("dropping constraint '%s'", name)
slog.Info("dropping constraint", "name", name)
if err := migrator.DropConstraint(table, name); err != nil {
return err
}

View File

@@ -1,10 +1,10 @@
package migrations
import (
"github.com/emvi/logbuch"
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"gorm.io/gorm"
"log/slog"
)
func init() {
@@ -17,12 +17,12 @@ func init() {
if cfg.Db.Dialect == config.SQLDialectSqlite {
// see 20201106_migration_cascade_constraints
logbuch.Info("not attempting to drop and regenerate constraints on sqlite")
slog.Info("not attempting to drop and regenerate constraints on sqlite")
return nil
}
if !migrator.HasTable(&models.KeyStringValue{}) {
logbuch.Info("key-value table not yet existing")
slog.Info("key-value table not yet existing")
return nil
}
@@ -31,7 +31,7 @@ func init() {
}
if migrator.HasConstraint(&models.Alias{}, "fk_aliases_user") {
logbuch.Info("dropping constraint 'fk_aliases_user'")
slog.Info("dropping constraint", "name", "fk_aliases_user")
if err := migrator.DropConstraint(&models.Alias{}, "fk_aliases_user"); err != nil {
return err
}

View File

@@ -1,10 +1,10 @@
package migrations
import (
"github.com/emvi/logbuch"
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"gorm.io/gorm"
"log/slog"
)
func init() {
@@ -38,14 +38,14 @@ func init() {
}
if cfg.Db.Dialect == config.SQLDialectSqlite {
logbuch.Info("not attempting to drop column 'badges_enabled' on sqlite")
slog.Info("not attempting to drop column on sqlite", "column", "badges_enabled")
return nil
}
if err := migrator.DropColumn(&models.User{}, "badges_enabled"); err != nil {
return err
}
logbuch.Info("dropped column 'badges_enabled' after substituting it by sharing indicators")
slog.Info("dropped column after substituting it by sharing indicators", "column", "badges_enabled")
return nil
},

View File

@@ -14,7 +14,13 @@ func init() {
return nil
}
if err := db.Exec("UPDATE users SET has_data = TRUE WHERE TRUE").Error; err != nil {
statement := "UPDATE users SET has_data = TRUE"
if cfg.Db.IsMssql() {
statement = "UPDATE users SET has_data = 1"
}
if err := db.Exec(statement).Error; err != nil {
return err
}

View File

@@ -14,7 +14,7 @@ func init() {
return nil
}
if err := db.Exec("UPDATE heartbeats SET created_at = time WHERE TRUE").Error; err != nil {
if err := db.Exec("UPDATE heartbeats SET created_at = time").Error; err != nil {
return err
}

View File

@@ -3,6 +3,7 @@ package migrations
import (
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"github.com/muety/wakapi/utils"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
@@ -16,10 +17,7 @@ func init() {
return nil
}
condition := "key = ?"
if cfg.Db.Dialect == config.SQLDialectMysql {
condition = "`key` = ?"
}
condition := utils.QuoteSql(db, "%s = ?", "key")
imprintKv := &models.KeyStringValue{Key: "imprint", Value: "no content here"}
if err := db.

View File

@@ -1,9 +1,9 @@
package migrations
import (
"github.com/emvi/logbuch"
"github.com/muety/wakapi/config"
"gorm.io/gorm"
"log/slog"
)
func init() {
@@ -12,7 +12,7 @@ func init() {
name: name,
f: func(db *gorm.DB, cfg *config.Config) error {
if err := db.Migrator().DropTable("gorp_migrations"); err == nil {
logbuch.Info("dropped table 'gorp_migrations'")
slog.Info("dropped table", "table", "gorp_migrations")
}
return nil
},

View File

@@ -2,10 +2,10 @@ package migrations
import (
"fmt"
"github.com/emvi/logbuch"
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"gorm.io/gorm"
"log/slog"
)
func init() {
@@ -19,14 +19,14 @@ func init() {
rawDb, err := db.DB()
if err != nil {
logbuch.Error("failed to retrieve raw sql db instance")
slog.Error("failed to retrieve raw sql db instance")
return err
}
if _, err := rawDb.Exec(fmt.Sprintf("delete from summary_items where type = %d", models.SummaryLabel)); err != nil {
logbuch.Error("failed to delete project label summary items")
slog.Error("failed to delete project label summary items")
return err
}
logbuch.Info("successfully deleted project label summary items")
slog.Info("successfully deleted project label summary items")
setHasRun(name, db)
return nil

View File

@@ -1,9 +1,9 @@
package migrations
import (
"github.com/emvi/logbuch"
"github.com/muety/wakapi/config"
"gorm.io/gorm"
"log/slog"
)
func init() {
@@ -15,7 +15,7 @@ func init() {
return nil
}
logbuch.Info("this may take a while!")
slog.Info("this may take a while!")
if cfg.Db.IsMySQL() {
tx := db.Begin()
@@ -40,7 +40,7 @@ func init() {
} else {
// sqlite doesn't allow for changing column type easily
// https://stackoverflow.com/a/2083562/3112139
logbuch.Warn("unable to migrate id columns to bigint on %s", cfg.Db.Dialect)
slog.Warn("unable to migrate id columns to bigint", "dialect", cfg.Db.Dialect)
}
setHasRun(name, db)

View File

@@ -2,10 +2,10 @@ package migrations
import (
"database/sql"
"github.com/emvi/logbuch"
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"gorm.io/gorm"
"log/slog"
)
func init() {
@@ -17,7 +17,7 @@ func init() {
return nil
}
logbuch.Info("this may take a while!")
slog.Info("this may take a while!")
// this turns out to actually be way faster than using joins and instead has the benefit of being cross-dialect compatible
@@ -35,7 +35,7 @@ func init() {
}
if err := tx.Commit().Error; err != nil {
tx.Rollback()
logbuch.Error("failed to retroactively determine total summary heartbeats")
slog.Error("failed to retroactively determine total summary heartbeats")
return err
}

View File

@@ -1,10 +1,10 @@
package migrations
import (
"github.com/emvi/logbuch"
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"gorm.io/gorm"
"log/slog"
)
func init() {
@@ -16,7 +16,7 @@ func init() {
return nil
}
logbuch.Info("this may take a while!")
slog.Info("this may take a while!")
// find all summaries whose num_heartbeats is zero even though they have items
var faultyIds []uint
@@ -30,22 +30,22 @@ func init() {
// update their heartbeats counter
result := db.
Table("summaries AS s1").
Where("s1.id IN ?", faultyIds).
Table("summaries").
Where("summaries.id IN ?", faultyIds).
Update(
"num_heartbeats",
db.
Model(&models.Heartbeat{}).
Select("COUNT(*)").
Where("user_id = ?", gorm.Expr("s1.user_id")).
Where("time BETWEEN ? AND ?", gorm.Expr("s1.from_time"), gorm.Expr("s1.to_time")),
Where("user_id = ?", gorm.Expr("summaries.user_id")).
Where("time BETWEEN ? AND ?", gorm.Expr("summaries.from_time"), gorm.Expr("summaries.to_time")),
)
if err := result.Error; err != nil {
return err
}
logbuch.Info("corrected heartbeats counter of %d summaries", result.RowsAffected)
slog.Info("corrected heartbeats counter of summaries", "count", result.RowsAffected)
setHasRun(name, db)
return nil

View File

@@ -1,9 +1,9 @@
package migrations
import (
"github.com/emvi/logbuch"
"github.com/muety/wakapi/config"
"gorm.io/gorm"
"log/slog"
)
func init() {
@@ -16,7 +16,7 @@ func init() {
}
if cfg.Db.IsMySQL() {
logbuch.Info("altering heartbeats table, this may take a while (up to hours)")
slog.Info("altering heartbeats table, this may take a while (up to hours)")
db.Exec("SET foreign_key_checks=0;")
db.Exec("SET unique_checks=0;")
@@ -29,7 +29,7 @@ func init() {
db.Exec("SET foreign_key_checks=1;")
db.Exec("SET unique_checks=1;")
logbuch.Info("migrated timestamp columns to millisecond precision")
slog.Info("migrated timestamp columns to millisecond precision")
}
setHasRun(name, db)

View File

@@ -1,10 +1,10 @@
package migrations
import (
"github.com/emvi/logbuch"
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"gorm.io/gorm"
"log/slog"
)
func init() {
@@ -19,14 +19,14 @@ func init() {
migrator := db.Migrator()
if migrator.HasColumn(&models.Diagnostics{}, "user_id") {
logbuch.Info("running migration '%s'", name)
slog.Info("running migration", "name", name)
if err := migrator.DropConstraint(&models.Diagnostics{}, "fk_diagnostics_user"); err != nil {
logbuch.Warn("failed to drop 'fk_diagnostics_user' constraint (%v)", err)
slog.Warn("failed to drop constraint", "constraint", "fk_diagnostics_user", "error", err)
}
if err := migrator.DropColumn(&models.Diagnostics{}, "user_id"); err != nil {
logbuch.Warn("failed to drop user_id column of diagnostics (%v)", err)
slog.Warn("failed to drop column", "table", "diagnostics", "column", "user_id", "error", err)
}
}

View File

@@ -1,10 +1,10 @@
package migrations
import (
"github.com/emvi/logbuch"
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"gorm.io/gorm"
"log/slog"
)
// migration to fix https://github.com/muety/wakapi/issues/346
@@ -25,9 +25,9 @@ func init() {
}
if cfg.Db.IsSQLite() && db.Migrator().HasIndex(&models.Heartbeat{}, idxName) {
logbuch.Info("running migration '%s'", name)
slog.Info("running migration", "name", name)
if err := db.Migrator().DropIndex(&models.Heartbeat{}, idxName); err != nil {
logbuch.Warn("failed to drop %s", idxName)
slog.Warn("failed to drop index", "indexName", idxName)
}
}

View File

@@ -1,10 +1,10 @@
package migrations
import (
"github.com/emvi/logbuch"
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"gorm.io/gorm"
"log/slog"
)
func init() {
@@ -19,10 +19,10 @@ func init() {
migrator := db.Migrator()
if migrator.HasTable(&models.LeaderboardItem{}) && migrator.HasColumn(&models.LeaderboardItem{}, "rank") {
logbuch.Info("running migration '%s'", name)
slog.Info("running migration", "name", name)
if err := migrator.DropColumn(&models.LeaderboardItem{}, "rank"); err != nil {
logbuch.Warn("failed to drop 'rank' column (%v)", err)
slog.Warn("failed to drop column", "column", "rank", "error", err)
}
}

View File

@@ -1,12 +1,13 @@
package migrations
import (
"github.com/emvi/logbuch"
"regexp"
"strings"
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"gorm.io/gorm"
"regexp"
"strings"
"log/slog"
)
// due to an error in the model definition, idx_time_user used to only cover 'user_id', but not time column
@@ -23,6 +24,11 @@ func init() {
}
var drop bool
if cfg.Db.IsMssql() {
// mssql migrator doesn't support GetIndexes() currently
// mssql is implemented after this migration, so ignore it.
return nil
}
if cfg.Db.IsSQLite() {
// sqlite migrator doesn't support GetIndexes() currently
var ddl string
@@ -37,7 +43,7 @@ func init() {
}
matches := regexp.MustCompile("(?i)\\((.+)\\)$").FindStringSubmatch(ddl)
if len(matches) > 0 && !strings.Contains(matches[0], "`user_id`") || !strings.Contains(matches[0], "`time`") {
if len(matches) > 0 && (!strings.Contains(matches[0], "`user_id`") || !strings.Contains(matches[0], "`time`")) {
drop = true
}
} else {
@@ -61,7 +67,7 @@ func init() {
if err := migrator.DropIndex(&models.Heartbeat{}, "idx_time_user"); err != nil {
return err
}
logbuch.Info("index 'idx_time_user' needs to be recreated, this may take a while")
slog.Info("index 'idx_time_user' needs to be recreated, this may take a while")
return nil
},

View File

@@ -0,0 +1,30 @@
package migrations
import (
"github.com/muety/wakapi/config"
"gorm.io/gorm"
"log/slog"
)
func init() {
const name = "20230219-add_astro_language"
f := migrationFunc{
name: name,
f: func(db *gorm.DB, cfg *config.Config) error {
if hasRun(name, db) {
return nil
}
slog.Info("running migration", "name", name)
if err := db.Exec("UPDATE heartbeats SET language = 'Astro' where language = '' and entity like '%.astro'").Error; err != nil {
return err
}
setHasRun(name, db)
return nil
},
}
registerPostMigration(f)
}

View File

@@ -0,0 +1,35 @@
package migrations
import (
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"gorm.io/gorm"
"log/slog"
)
func init() {
const name = "20230219-add_subscription_renewal"
f := migrationFunc{
name: name,
f: func(db *gorm.DB, cfg *config.Config) error {
if hasRun(name, db) {
return nil
}
migrator := db.Migrator()
if migrator.HasColumn(&models.User{}, "subscription_renewal") {
slog.Info("running migration", "name", name)
if err := db.Exec("UPDATE users SET subscription_renewal = subscribed_until WHERE subscribed_until is not null").Error; err != nil {
return err
}
}
setHasRun(name, db)
return nil
},
}
registerPostMigration(f)
}

View File

@@ -0,0 +1,55 @@
package migrations
import (
"github.com/alitto/pond/v2"
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"github.com/muety/wakapi/utils"
"gorm.io/gorm"
)
func init() {
const name = "20231023-fill_last_branch"
f := migrationFunc{
name: name,
f: func(db *gorm.DB, cfg *config.Config) error {
if hasRun(name, db) {
return nil
}
var heartbeats []*models.Heartbeat
if err := db.Where(&models.Heartbeat{Branch: "<<LAST_BRANCH>>"}).Find(&heartbeats).Error; err != nil {
return err
}
wp := pond.NewPool(utils.AllCPUs())
// this is the most inefficient way to perform the update, but i couldn't find a way to do this is a single query
for _, h := range heartbeats {
h := h
wp.Submit(func() {
var latest models.Heartbeat
if err := db.
Where(&models.Heartbeat{UserID: h.UserID, Project: h.Project}).
Not("branch", "<<LAST_BRANCH>>").
Where("time < ?", h.Time).
Order("time desc").
First(&latest).Error; err != nil {
return
}
db.
Model(&models.Heartbeat{}).
Where("id", h.ID).
Update("branch", latest.Branch)
})
}
wp.StopAndWait()
setHasRun(name, db)
return nil
},
}
registerPostMigration(f)
}

View File

@@ -0,0 +1,30 @@
package migrations
import (
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"gorm.io/gorm"
)
func init() {
const name = "20241220-share_activity_chart_flag"
f := migrationFunc{
name: name,
f: func(db *gorm.DB, cfg *config.Config) error {
if hasRun(name, db) {
return nil
}
db.
Model(&models.User{}).
Where("share_data_max_days < ?", 0).
Or("share_data_max_days >= ?", 365).
Update("share_activity_chart", true)
setHasRun(name, db)
return nil
},
}
registerPostMigration(f)
}

View File

@@ -0,0 +1,44 @@
package migrations
import (
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"gorm.io/gorm"
)
func init() {
const name = "20250219-update_heartbeats_timeout"
f := migrationFunc{
name: name,
f: func(db *gorm.DB, cfg *config.Config) error {
if hasRun(name, db) {
return nil
}
minTimeout := models.MinHeartbeatsTimeout.Seconds()
maxTimeout := models.MaxHeartbeatsTimeout.Seconds()
defaultTimeout := models.DefaultHeartbeatsTimeout.Seconds()
defaultTimeoutLegacy := models.DefaultHeartbeatsTimeoutLegacy.Seconds()
db.
Model(&models.User{}).
Where("heartbeats_timeout_sec < ?", minTimeout).
Update("heartbeats_timeout_sec", minTimeout)
db.
Model(&models.User{}).
Where("heartbeats_timeout_sec > ?", maxTimeout).
Update("heartbeats_timeout_sec", maxTimeout)
db.
Model(&models.User{}).
Where("heartbeats_timeout_sec = ?", defaultTimeoutLegacy).
Update("heartbeats_timeout_sec", defaultTimeout)
setHasRun(name, db)
return nil
},
}
registerPostMigration(f)
}

View File

@@ -0,0 +1,33 @@
package migrations
import (
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"gorm.io/gorm"
)
func init() {
const name = "20250313-fix_browsing_category"
f := migrationFunc{
name: name,
background: true,
f: func(db *gorm.DB, cfg *config.Config) error {
if hasRun(name, db) {
return nil
}
db.
Model(&models.Heartbeat{}).
Where("category = ?", "").
Where(db.
Where("type = ?", "domain").
Or("type = ?", "url")).
Update("category", "browsing")
setHasRun(name, db)
return nil
},
}
registerPostMigration(f)
}

View File

@@ -0,0 +1,66 @@
package migrations
import (
"github.com/muety/wakapi/config"
"github.com/muety/wakapi/models"
"gorm.io/gorm"
)
// In the context if https://github.com/muety/wakapi/issues/777, we retroactively added a primary key column to the durations table.
// However, SQLite doesn't allow to alter an existing table that way. Workaround is to create a new one and copy its contents.
func init() {
const name = "20250425-add_durations_primary_key"
f := migrationFunc{
name: name,
background: true,
f: func(db *gorm.DB, cfg *config.Config) error {
if hasRun(name, db) {
return nil
}
if cfg.Db.Dialect != config.SQLDialectSqlite {
return nil
}
if !db.Migrator().HasTable("durations") {
return nil
}
if db.Migrator().HasColumn(&models.Duration{}, "id") {
return nil
}
if err := db.Transaction(func(tx *gorm.DB) error {
if tx.Migrator().HasColumn(&models.Duration{}, "interval") { // legacy stuff
if err := tx.Migrator().DropColumn(&models.Duration{}, "interval"); err != nil {
return err
}
}
if err := tx.Migrator().RenameTable("durations", "durations_old"); err != nil {
return err
}
if tx.Migrator().HasIndex(&models.Duration{}, "idx_time_duration_user") {
if err := tx.Migrator().DropIndex(&models.Duration{}, "idx_time_duration_user"); err != nil {
return err
}
}
if err := tx.Migrator().CreateTable(&models.Duration{}); err != nil {
return err
}
if err := tx.Exec("insert into durations(user_id, time, duration, project, language, editor, operating_system, machine, category, branch, entity, num_heartbeats, group_hash, timeout) select * from durations_old").Error; err != nil {
return err
}
if err := tx.Migrator().DropTable("durations_old"); err != nil {
return err
}
return nil
}); err != nil {
return err
}
setHasRun(name, db)
return nil
},
}
registerPreMigration(f)
}

View File

@@ -0,0 +1,30 @@
package migrations
import (
"github.com/muety/wakapi/config"
"gorm.io/gorm"
)
// see https://github.com/muety/wakapi/issues/817#issuecomment-3146365708
func init() {
const name = "20250802_fix_default_coding_category"
f := migrationFunc{
name: name,
background: true,
f: func(db *gorm.DB, cfg *config.Config) error {
if hasRun(name, db) {
return nil
}
if err := db.Exec("update heartbeats set category = 'coding' where category = '' and type = 'file' and language != ''").Error; err != nil {
return err
}
setHasRun(name, db)
return nil
},
}
registerPostMigration(f)
}

Some files were not shown because too many files have changed in this diff Show More