Compare commits

...

1154 Commits

Author SHA1 Message Date
Derrick Hammer 1b394f3ca1
chore: remove usage of buf and rust in docker image 2024-03-30 14:01:01 -04:00
Derrick Hammer 8d98f131d5
refactor: implement native bao support 2024-03-30 13:59:53 -04:00
Derrick Hammer ba67d19299
fix: reset file after seeking 2024-03-29 15:59:01 -04:00
Derrick Hammer d5575fbf4f
fix: unpin needs to return 200, not 201 2024-03-29 14:46:54 -04:00
Derrick Hammer 86669d9f08
dep: update dashboard 2024-03-29 13:22:00 -04:00
Derrick Hammer 818ee60aea
dep: update dashboard 2024-03-29 12:33:39 -04:00
Derrick Hammer d913e0f7b2
fix: seek after mime read 2024-03-29 12:29:57 -04:00
Derrick Hammer 8ccd90825b
fix: wrong error handling 2024-03-29 12:28:46 -04:00
Derrick Hammer 09c9ab8614
refactor: if no db mimetype, manually use mimetype.DetectReader, and if none found fall back to application/octet-stream 2024-03-29 11:56:30 -04:00
Derrick Hammer 8e9d2d0398
refactor: switch to github.com/gabriel-vasile/mimetype 2024-03-29 11:51:58 -04:00
Derrick Hammer aeba225c87
fix: only set the Content-Type header if we actually have a mime type, else let ServeContent detect it. 2024-03-29 00:49:54 -04:00
Derrick Hammer 5ad1aaeb6a
refactor: pass mimetype to tus upload record and check a list of meta fields for it 2024-03-29 00:47:33 -04:00
Derrick Hammer 9d259925f8
dep: update dashboard 2024-03-29 00:19:23 -04:00
Derrick Hammer 277c3ff23f
dep: update dashboard 2024-03-28 23:40:01 -04:00
Derrick Hammer 161d71bfda
dep: update dashboard 2024-03-28 22:53:25 -04:00
Derrick Hammer bb41d70c10
fix: UploadExists check needs to be before UploadCompleted call 2024-03-28 22:35:05 -04:00
Derrick Hammer 82532b01eb
fix: use lock on CompleteUploads 2024-03-28 22:18:45 -04:00
Derrick Hammer 9254dc20c8
fix: use decodedHash.HashBytes() 2024-03-28 21:48:13 -04:00
Derrick Hammer fffe769ccf
fix: use mapKey as a string 2024-03-28 21:47:48 -04:00
Derrick Hammer 775d3f7e6f
fix: bad mapKey typing 2024-03-28 21:42:12 -04:00
Derrick Hammer 0679a7cc3b
refactor: add a sync.Map, and lock parallel uploads in a mutex keyed by the hash and tus upload id, check if it already exists, and if so, abort. 2024-03-28 21:20:44 -04:00
Derrick Hammer 0f1360c6df
dep: update dashboard 2024-03-28 20:41:39 -04:00
Derrick Hammer cb6ba5a24b
fix: remove PinByHash logic 2024-03-28 18:31:40 -04:00
Derrick Hammer d810cf0848
fix: don't error if IsFinal 2024-03-28 18:08:13 -04:00
Derrick Hammer a16390ccc1
refactor: add tus uploads to the pins response 2024-03-28 17:52:01 -04:00
Derrick Hammer a54238d4b6
feat: add TusHandler::Uploads 2024-03-28 17:43:32 -04:00
Derrick Hammer b267ace017
fix: ignore exists check if we are in partial/parallel upload mode 2024-03-28 17:41:17 -04:00
Derrick Hammer 90c8ce39eb
dep: update dashboard 2024-03-28 17:20:17 -04:00
Derrick Hammer 76b8def51d
dep: update dashboard 2024-03-28 16:56:33 -04:00
Derrick Hammer 87477ad559
dep: update dashboard 2024-03-28 16:31:40 -04:00
Derrick Hammer 804f32e2be
dep: update dashboard 2024-03-28 16:20:08 -04:00
Derrick Hammer af5c7dc40d
dep: update dashboard 2024-03-28 15:45:11 -04:00
Derrick Hammer 12caef5f9b
dep: update dashboard 2024-03-28 10:01:45 -04:00
Derrick Hammer d078999108
dep: update portal-sdk 2024-03-28 00:02:35 -04:00
Derrick Hammer 3331f1b1c1
refactor: update progress logic 2024-03-26 23:40:11 -04:00
Derrick Hammer 6a9eede07d
fix: invert check 2024-03-26 23:31:17 -04:00
Derrick Hammer 989ed70265
fix: client does not return ErrBucketNotFound, only the string error 2024-03-26 23:14:50 -04:00
Derrick Hammer d03a781e18
fix: correct checking the download size based on LimitedReader 2024-03-26 22:46:06 -04:00
Derrick Hammer 48da5d3fd4
Revert "chore: temp disable email verification for testing"
This reverts commit f6b28b0ee0.
2024-03-26 20:40:26 -04:00
Derrick Hammer 7ba826490e
dep: update dashboard 2024-03-26 20:33:15 -04:00
Derrick Hammer 0a5e52ec16
refactor: use ErrKeySecurityInvalidToken 2024-03-26 20:26:59 -04:00
Derrick Hammer adc5bc213e
Revert "fix: add error for email verification"
This reverts commit ea5a97c6
2024-03-26 20:24:55 -04:00
Derrick Hammer 501de26793
fix: update error 2024-03-26 20:22:53 -04:00
Derrick Hammer 5a9f4a5940
fix: don't pass db error 2024-03-26 20:21:34 -04:00
Derrick Hammer ea5a97c613
fix: add error for email verification 2024-03-26 20:19:38 -04:00
Derrick Hammer 64eea68a84
fix: if user is already verified, abort 2024-03-26 20:17:41 -04:00
Derrick Hammer 12a9ad28fd
fix: check if email or token is empty 2024-03-26 17:04:16 -04:00
Derrick Hammer 4c2baf164f
fix: bad verify url, remove email query var 2024-03-26 16:49:53 -04:00
Derrick Hammer d40a954b67
fix: add required to AccountInfoResponse 2024-03-26 16:44:03 -04:00
Derrick Hammer c2d4ea7847
refactor: add verified to account info 2024-03-26 16:43:43 -04:00
Derrick Hammer 23f462773f
fix: add cors route for /api/account/verify-email/resend 2024-03-26 16:38:51 -04:00
Derrick Hammer cf83dc6767
refactor: switch to a verification link 2024-03-26 15:12:31 -04:00
Derrick Hammer 90cdcd16af
fix: put verify-email behind auth 2024-03-26 15:03:56 -04:00
Derrick Hammer ccf1707f11
feat: add /api/account/verify-email/resend route 2024-03-26 15:03:20 -04:00
Derrick Hammer 98c576e2e8
refactor: update SendEmailVerification to take a user id and look it up 2024-03-26 15:00:29 -04:00
Derrick Hammer 6d12ef9b94
Revert "fix: send, echo, and remove a copy of the cookie without the domain to try and work better with localhost"
This reverts commit ad23104700.
2024-03-26 11:23:20 -04:00
Derrick Hammer ad23104700
fix: send, echo, and remove a copy of the cookie without the domain to try and work better with localhost 2024-03-26 11:04:15 -04:00
Derrick Hammer 1f183c5052
dep: update dashboard 2024-03-26 10:51:12 -04:00
Derrick Hammer 8b1c2f1065
dep: update dashboard 2024-03-26 01:18:22 -04:00
Derrick Hammer c73b5c4d47
dep: update dashboard 2024-03-26 01:00:01 -04:00
Derrick Hammer 46f81443c6
dep: update dashboard 2024-03-26 00:46:36 -04:00
Derrick Hammer 1185a2a56e
feat: add /api/meta endpoint 2024-03-26 00:46:11 -04:00
Derrick Hammer 73247a86fd
fix: handle edge case where the Protocol config map has no configs, but we are creating a default configuration for a protocol. 2024-03-26 00:18:42 -04:00
Derrick Hammer c9531bc588
fix: update error messages to reflect full config keys 2024-03-26 00:00:04 -04:00
Derrick Hammer fc6240c149
fix: use errors.As as viper uses a custom error struct 2024-03-25 22:05:24 -04:00
Derrick Hammer eef49c34be
dep: update dashboard 2024-03-25 21:09:11 -04:00
Derrick Hammer f6be51b942
refactor: move away from a granular tracking to a simpler, stage based tracking 2024-03-23 10:43:18 -04:00
Derrick Hammer 334fa4d788
feat: add UpdateProgress and UpdateStatus 2024-03-23 10:36:20 -04:00
Derrick Hammer f2c68857f2
refactor: fix pinning logic if file exists 2024-03-22 20:31:44 -04:00
Derrick Hammer 0caa54f028
fix: if i > 0 add to bytesRead 2024-03-22 20:23:51 -04:00
Derrick Hammer 96713e3538
fix: always pass 0 to ReadBytes 2024-03-22 20:23:37 -04:00
Derrick Hammer ba60a6c729
fix: if eof, return the byte count 2024-03-22 20:23:14 -04:00
Derrick Hammer 35b64515d7
fix: if a field changed, set its changes 2024-03-22 19:35:11 -04:00
Derrick Hammer 8d20659a5b
fix: if a field changed, set its changes 2024-03-22 19:34:40 -04:00
Derrick Hammer e3022b8587
fix: skipExisting should be false 2024-03-22 19:19:31 -04:00
Derrick Hammer 66ddf67337
fix: wrong model registered 2024-03-22 18:49:56 -04:00
Derrick Hammer 085c4d69d6
fix: add cors to pin api 2024-03-22 18:47:41 -04:00
Derrick Hammer 5b2a86275f
refactor: if the user has the file pinned and no import exists, assume a completed status 2024-03-22 18:30:02 -04:00
Derrick Hammer 7665937196
refactor: add ImportStatusCompleted 2024-03-22 18:29:09 -04:00
Derrick Hammer 0a85711ead
feat: implement /s5/pin/:cid/status 2024-03-22 18:01:12 -04:00
Derrick Hammer 7696997e53
refactor: implement import reader in s5 pinning 2024-03-22 17:39:17 -04:00
Derrick Hammer 5523d5e60d
feat: add ImportReader class to wrap and track the reading of the import and update at every read, and support a stage number to act as an offset so it can be used for both s3 upload and sia upload stages 2024-03-22 17:32:30 -04:00
Derrick Hammer 0e3a25aa8a
feat: add import metadata db service based off the upload metadata service 2024-03-22 16:36:59 -04:00
Derrick Hammer f22506b413
fix: add interface type check 2024-03-22 15:43:02 -04:00
Derrick Hammer d06f436fa1
refactor: rewrite getHandler 2024-03-21 16:56:29 -04:00
Derrick Hammer 74cc88540d
fix: add rw mutex lock to getHandler 2024-03-21 16:53:48 -04:00
Derrick Hammer 755aff15da
feat: add pinned_at field to account pins 2024-03-21 16:28:48 -04:00
Derrick Hammer 62867f26a9
refactor: use CIDFromHash and extract the hash 2024-03-21 15:57:05 -04:00
Derrick Hammer a48b10e50c
fix: we aren't pinning small files after uploading 2024-03-21 15:50:11 -04:00
Derrick Hammer a8f62fd666
refactor: add option to SaveUpload so we have the option not to change an upload record if we are uploading or importing a file 2024-03-21 15:46:02 -04:00
Derrick Hammer 8bf4887dae
fix: add GET and DELETE to cors 2024-03-21 14:58:26 -04:00
Derrick Hammer 3e3f539a8b
refactor: change cookie approach to broadcast cookies across all protocols based on the root domain, so they can be shared. 2024-03-21 14:54:17 -04:00
Derrick Hammer a289828c6f
fix: add cors to /s5/account/pins 2024-03-21 14:39:06 -04:00
Derrick Hammer d895d047b3
refactor: add upload size to account pin response 2024-03-21 12:35:45 -04:00
Derrick Hammer abec1877cd
fix: set hash and mime_type as required 2024-03-21 11:01:03 -04:00
Derrick Hammer 4dec430c69
fix: set pins as required 2024-03-21 10:54:15 -04:00
Derrick Hammer facea33e0e
fix: need to pass api name 2024-03-20 17:28:25 -04:00
Derrick Hammer be7a7977ac
refactor: need to pass api name to SetAuthCookie 2024-03-20 17:13:58 -04:00
Derrick Hammer ee8fa2b98d
refactor: dont pass a domain 2024-03-20 17:13:31 -04:00
Derrick Hammer ad8de8f5a1
refactor: switch to using MaxAge 2024-03-20 17:13:13 -04:00
Derrick Hammer 9587ef4941
refactor: require an api name to be provided and skip if not matched 2024-03-20 17:12:31 -04:00
Derrick Hammer 4a3028f61a
fix: cookies sent to us don't include the expiry time, so we need to parse from thr jwt echo it. 2024-03-20 14:56:18 -04:00
Derrick Hammer da19a2e287
fix: use StatusUnauthorized 2024-03-20 14:44:28 -04:00
Derrick Hammer 52a1f18c60
fix: if claim is empty and ExpiredAllowed on, abort early and pass through 2024-03-20 14:41:54 -04:00
Derrick Hammer 7df6bb245b
fix: return in wrong spot 2024-03-20 14:34:48 -04:00
Derrick Hammer cfce7348d4
fix: move error conditional outside if 2024-03-20 14:30:03 -04:00
Derrick Hammer 053a55c1f3
fix: invert check 2024-03-20 14:28:46 -04:00
Derrick Hammer 6c0ae8c0e6
refactor: if unauthorized is true, audList may be nil, and we may have to manually parse out the aud unverified to test 2024-03-20 14:27:02 -04:00
Derrick Hammer 9e170bae0d
fix: capture aud in JWTVerifyToken 2024-03-20 14:13:59 -04:00
Derrick Hammer 7616d9f7c9
refactor: set ExpiredAllowed on loginAuthMw2fa 2024-03-20 14:11:21 -04:00
Derrick Hammer 2528fd0afe
refactor: add optionExpiredAllowed to AuthMiddlewareOptions, add jwtPurposeEqual helper, don't error if expired with ExpiredAllowed and the purposes are different 2024-03-20 14:09:41 -04:00
Derrick Hammer bee80a9981
Revert "refactor: if the token doesn't match our purpose only error if EmptyAllowed is off"
This reverts commit b1fcc7f7ae.
2024-03-20 13:55:35 -04:00
Derrick Hammer b1fcc7f7ae
refactor: if the token doesn't match our purpose only error if EmptyAllowed is off 2024-03-20 13:52:25 -04:00
Derrick Hammer b6c92a6348
fix: check for a mysql.MySQLError and error no 1062 explicitly 2024-03-20 13:39:13 -04:00
Derrick Hammer f9c834752f
fix: update AllowedHeaders and add Content-Type 2024-03-19 18:25:20 -04:00
Derrick Hammer 30aac94468
fix: update AllowedHeaders and add Authorization 2024-03-19 17:23:35 -04:00
Derrick Hammer 79425b76fc
fix: update AllowedMethods 2024-03-19 15:47:54 -04:00
Derrick Hammer 85738c1065
refactor: replace AllowedOrigins with AllowOriginFunc 2024-03-19 15:45:02 -04:00
Derrick Hammer 7ed63d94cc
refactor: add proxy middleware to add routes 2024-03-19 15:40:03 -04:00
Derrick Hammer c667c9509c
refactor: update cors options handling for S5 2024-03-19 15:37:38 -04:00
Derrick Hammer 4988368b7c
fix: add explicit options routes for all API methods 2024-03-19 15:32:40 -04:00
Derrick Hammer b55c1f7d48
refactor: set MaxAge 2024-03-19 11:43:11 -04:00
Derrick Hammer ce93591ff8
Revert "refactor: change ClearAuthCookie to set value to deleted"
This reverts commit ac61279081.
2024-03-19 11:40:34 -04:00
Derrick Hammer 6ac37cfe65
refactor: add no cache headers 2024-03-19 11:05:51 -04:00
Derrick Hammer ac61279081
refactor: change ClearAuthCookie to set value to deleted 2024-03-19 10:59:38 -04:00
Derrick Hammer d1bbe7c158
refactor: change ClearAuthCookie to set expires date to epoch 2024-03-19 10:57:49 -04:00
Derrick Hammer aff371a844
feat: add update password api route 2024-03-19 10:04:27 -04:00
Derrick Hammer 3473551f6c
fix: ensure exists check only matches if it is a different account id 2024-03-19 09:48:45 -04:00
Derrick Hammer 080bef354d
refactor: check if the email is the same and return a new error for it 2024-03-19 09:44:44 -04:00
Derrick Hammer 1d60cbf532
fix: cast to user, no pointer 2024-03-19 09:38:05 -04:00
Derrick Hammer 3b3faaa1e6
fix: use tx.Statement.Dest 2024-03-19 09:36:50 -04:00
Derrick Hammer 9ea77fb5c3
fix: check to ensure error is something other than ErrRecordNotFound, or that exists is true 2024-03-19 09:29:29 -04:00
Derrick Hammer 9e52d35d2f
Revert "fix: use AccountExists"
This reverts commit 0bbb89e02c.
2024-03-19 09:26:36 -04:00
Derrick Hammer 0bbb89e02c
fix: use AccountExists 2024-03-19 09:24:19 -04:00
Derrick Hammer ebb19df217
fix: update email endpoint needs auth mw 2024-03-19 09:09:25 -04:00
Derrick Hammer bf6264b01d
fix: malformed routes 2024-03-19 07:53:24 -04:00
Derrick Hammer 080a4a1a85
feat: add /api/account/update-email route 2024-03-19 07:49:15 -04:00
Derrick Hammer 9bfdef1519
feat: add UpdateAccountEmail 2024-03-19 07:46:58 -04:00
Derrick Hammer fddc64799e
refactor: put some account endpoints under account instead of auth 2024-03-19 07:41:53 -04:00
Derrick Hammer 4391e9fc31
fix: define new cookie in EchoAuthCookie but use the existing cookies values 2024-03-19 05:02:48 -04:00
Derrick Hammer b2b6102216
fix: add Content-Type to tus cors allowed headers 2024-03-18 18:13:24 -04:00
Derrick Hammer 2067c68a72
fix: ctx's not property nested 2024-03-18 17:29:49 -04:00
Derrick Hammer d1c5bde5c1
refactor: add token to ping response message 2024-03-18 17:19:52 -04:00
Derrick Hammer 26a6bda053
feat: add GetAuthTokenFromContext 2024-03-18 17:18:26 -04:00
Derrick Hammer 93105fe5af
refactor: add auth token to the request context 2024-03-18 17:17:52 -04:00
Derrick Hammer 040c662826
refactor: echo the auth cookie back if any exist 2024-03-18 17:03:17 -04:00
Derrick Hammer 66f73d1a53
feat: add EchoAuthCookie 2024-03-18 17:02:16 -04:00
Derrick Hammer 48dc1b9be0
Revert "fix: remove Secure property"
This reverts commit 9e5d996f20.
2024-03-18 16:54:42 -04:00
Derrick Hammer 9e5d996f20
fix: remove Secure property 2024-03-18 16:40:29 -04:00
Derrick Hammer 649e0e0011
Revert "fix: remove Secure property"
This reverts commit 044604d863.
2024-03-18 16:40:18 -04:00
Derrick Hammer 044604d863
fix: remove Secure property 2024-03-18 16:31:42 -04:00
Derrick Hammer fd53b98633
fix: allow Upload-Metadata in cors 2024-03-18 15:53:40 -04:00
Derrick Hammer 5b8a7f79f0
fix: use AllowOriginFunc in tus 2024-03-18 15:45:29 -04:00
Derrick Hammer 228cabd83b
fix: cid needs to be lowercase 2024-03-18 14:26:45 -04:00
Derrick Hammer 99d47a4d9c
fix: need to enable AllowCredentials 2024-03-18 14:19:34 -04:00
Derrick Hammer 6b51e7196c
fix: need to whitelist Authorization and Content-Type headers 2024-03-18 14:16:04 -04:00
Derrick Hammer dcab0b46cd
fix: upload cors does not support wildcard methods 2024-03-18 14:13:40 -04:00
Derrick Hammer 65278cb046
fix: add options routes for basic upload 2024-03-18 14:06:58 -04:00
Derrick Hammer 455b793db6
dep: update dashboard 2024-03-18 13:51:29 -04:00
Derrick Hammer 3b01c8642d
refactor: add cors mw for account 2024-03-18 13:50:21 -04:00
Derrick Hammer c68dc51732
refactor: add cors mw for uploads 2024-03-18 13:39:50 -04:00
Derrick Hammer e864bcb098
feat: add upload limit endpoint 2024-03-17 11:16:50 -04:00
Derrick Hammer e73ab26ebf
feat: add logout endpoint 2024-03-17 09:27:57 -04:00
Derrick Hammer 0e18f695cf
fix: name needs to be AuthTokenName, actually use jwt 2024-03-17 09:22:47 -04:00
Derrick Hammer fd75ec3f6a
fix: bad alias for authCookieName 2024-03-17 09:19:33 -04:00
Derrick Hammer 9306051812
fix: use account.SetAuthCookie 2024-03-17 09:18:31 -04:00
Derrick Hammer d893216831
fix: SetAuthCookie needs the jwt 2024-03-17 09:18:01 -04:00
Derrick Hammer 675a583422
refactor: update SetAuthCookie to use routeableApi.AuthTokenName() 2024-03-17 09:15:47 -04:00
Derrick Hammer 5861e95fb5
refactor: update use of SetAuthCookie 2024-03-17 09:15:24 -04:00
Derrick Hammer e7393085b4
refactor: implement new methods for RoutableAPI 2024-03-17 09:13:20 -04:00
Derrick Hammer 4bd2b028b7
chore: remove local setAuthCookie 2024-03-17 09:10:14 -04:00
Derrick Hammer 107118febc
refactor: switch to account.SetAuthCookie 2024-03-17 09:09:57 -04:00
Derrick Hammer 244aa89d71
refactor: use jape.Context 2024-03-17 09:09:29 -04:00
Derrick Hammer 9a899317c1
refactor: implement Domain and AuthTokenName 2024-03-17 09:02:48 -04:00
Derrick Hammer b4b211d003
feat: add BuildSubdomain helper 2024-03-17 09:01:28 -04:00
Derrick Hammer 2a8c036dc6
refactor: set auth cookie for every api, and optionally pass a specific api name to only set instead 2024-03-17 08:59:34 -04:00
Derrick Hammer 325a368dea
refactor: add AuthTokenName to RoutableAPI 2024-03-17 08:53:56 -04:00
Derrick Hammer 5223a44790
refactor: move DEFAULT_AUTH_COOKIE_NAME to account 2024-03-17 08:51:59 -04:00
Derrick Hammer 33e644f5c7
refactor: add Domain method to RoutableAPI 2024-03-17 08:51:23 -04:00
Derrick Hammer 3dfdd2d2f4
refactor: add GetAllAPIs 2024-03-17 08:42:53 -04:00
Derrick Hammer 3e48593675
refactor: register api after init 2024-03-17 08:42:25 -04:00
Derrick Hammer ae37a186a9
feat: add dedicated registry for api objects 2024-03-17 08:41:40 -04:00
Derrick Hammer a85ced7c62
refactor: change Registry name to EntryRegistry 2024-03-17 08:36:32 -04:00
Derrick Hammer 61012ae394
fix: change property name 2024-03-17 08:33:24 -04:00
Derrick Hammer 1bd4527300
fix: use middleware.DEFAULT_AUTH_COOKIE_NAME for authCookieName 2024-03-17 08:30:55 -04:00
Derrick Hammer 6c58f6bd6c
refactor: use SetAuthCookie and set both DEFAULT_AUTH_COOKIE_NAME and s5 authCookieName 2024-03-17 08:17:56 -04:00
Derrick Hammer 51c7211c39
feat: add SetAuthCookie helper 2024-03-17 08:15:27 -04:00
Derrick Hammer b03e6815e2
refactor: fall back and check for DEFAULT_AUTH_COOKIE_NAME 2024-03-17 08:13:50 -04:00
Derrick Hammer 193871f083
fix: error if user is nil 2024-03-16 21:31:09 -04:00
Derrick Hammer 3dc5c72840
fix: auth cookie needs to be at the root path 2024-03-16 21:14:43 -04:00
Derrick Hammer 1ca8d78c8e
fix: need to use develop branch for dashboard submodule 2024-03-16 21:07:57 -04:00
Derrick Hammer 57a455a17e
refactor: need to handle get requests as nested jape routers with special routing checks to ensure virtual client side app routing returns the static bundle 2024-03-16 21:05:42 -04:00
Derrick Hammer 5431cac73d
feat: initial docker file 2024-03-16 18:57:01 -04:00
Derrick Hammer 41edceb11c
chore: tidy 2024-03-16 18:16:14 -04:00
Derrick Hammer fd6c3e8604
fix: wrong embed path 2024-03-16 17:46:36 -04:00
Derrick Hammer cc1efd5d85
feat: add dashboard as a submodule and register embed and routing in account api 2024-03-16 17:13:29 -04:00
Derrick Hammer f6b28b0ee0
chore: temp disable email verification for testing 2024-03-16 15:43:52 -04:00
Derrick Hammer 971c72ada9
fix: add 401 status code 2024-03-16 11:37:42 -04:00
Derrick Hammer f558d87b36
fix: update /s5/account/pins response 2024-03-16 11:36:38 -04:00
Derrick Hammer ccae147398
feat: implement a basic account pins json api without paging 2024-03-16 11:14:06 -04:00
Derrick Hammer 8a2f501e8e
refactor: rename accountPins to accountPinsBinary 2024-03-16 11:00:27 -04:00
Derrick Hammer 86c53d3c54
refactor: add default for auth_type 2024-03-15 07:39:43 -04:00
Derrick Hammer cafe863350
fix: add missing mapstructure tags 2024-03-15 07:39:18 -04:00
Derrick Hammer 41f9947429
fix: ensure AuthType is uppercase 2024-03-15 07:37:44 -04:00
Derrick Hammer 8b687d506f
fix: set to and from 2024-03-15 07:31:47 -04:00
Derrick Hammer 52f462e03e
refactor: add from to mail config 2024-03-15 07:30:39 -04:00
Derrick Hammer 2d571e3484
fix: remove minutes from template copy 2024-03-15 07:27:45 -04:00
Derrick Hammer ba0d32bb63
fix: update how ExpireTime is computed 2024-03-15 07:27:24 -04:00
Derrick Hammer 7c6fec61b6
fix: remove tpl extension 2024-03-15 07:03:35 -04:00
Derrick Hammer fae98f3d52
refactor: fix gob paths and template prefix/suffixes 2024-03-15 07:01:22 -04:00
Derrick Hammer e11340ad2b
refactor: ensure we set a cookie, auth header and json response in both login and otp validate 2024-03-14 12:54:41 -04:00
Derrick Hammer e380dacced
fix: loop needs to start at 1 so it's not outside the history range, else it tries to always create a date on boot 2024-03-14 07:40:12 -04:00
Derrick Hammer 37708c91f2
fix: don't compute by usd the contract price 2024-03-14 07:20:11 -04:00
Derrick Hammer 7d87ed6ad7
refactor: switch to using newRat 2024-03-14 07:19:14 -04:00
Derrick Hammer 19afa09c4d
refactor: split computeByRate to newRat 2024-03-14 07:17:58 -04:00
Derrick Hammer cc63ff2c6e
dep: update libs5 2024-03-14 06:54:54 -04:00
Derrick Hammer 358d5fdf60
feat: add account info endpoint 2024-03-14 06:42:38 -04:00
Derrick Hammer d946e969bc
fix: update to check for ErrDuplicatedKey and return a more specific but generic error if so 2024-03-13 18:53:45 -04:00
Derrick Hammer 6ff84bbc1a
refactor: use dedicated auth mw for ping 2024-03-13 18:45:14 -04:00
Derrick Hammer d5118beb58
refactor: allow purpose to be none 2024-03-13 18:44:09 -04:00
Derrick Hammer 749a932663
fix: give login a dedicated authMiddleware instance 2024-03-13 17:35:26 -04:00
Derrick Hammer 4f891e067c
refactor: rename pong to ping 2024-03-13 17:34:10 -04:00
Derrick Hammer 2e3ec1408e
refactor: add PingResponse and link to /api/auth/ping 2024-03-13 17:26:25 -04:00
Derrick Hammer 2f0a538033
fix: return user 2024-03-13 14:09:17 -04:00
Derrick Hammer 0efcd35d65
refactor: add EmptyAllowed to authMw2fa 2024-03-13 14:01:08 -04:00
Derrick Hammer bf8d909a3c
refactor: add option to allow jwt to be bypassed if there is no token 2024-03-13 14:00:19 -04:00
Derrick Hammer ca12b99438
fix: first/last name are snake case 2024-03-13 13:54:30 -04:00
Derrick Hammer 36c614c483
fix: correct swagger for ping 2024-03-13 12:30:31 -04:00
Derrick Hammer c416b40d00
feat: add ping endpoint to check auth status 2024-03-13 12:26:38 -04:00
Derrick Hammer 06f37bf3d8
refactor: add 415 response to /s5/metadata 2024-03-12 11:23:15 -04:00
Derrick Hammer a1014acf15
refactor: use StatusUnsupportedMediaType 2024-03-12 11:21:25 -04:00
Derrick Hammer d5782c7e86
fix: bad error message 2024-03-12 07:00:57 -04:00
Derrick Hammer 296f7b611e
dep: update libs5 2024-03-11 17:51:42 -04:00
Derrick Hammer 9529c71b3c
dep: update libs5 2024-03-11 17:49:54 -04:00
Derrick Hammer d5bed19c5d
dep: update libs5 2024-03-11 17:46:28 -04:00
Derrick Hammer f33b73b533
dep: update libs5 2024-03-11 17:40:58 -04:00
Derrick Hammer edfce8b181
dep: update libs5 2024-03-11 17:25:02 -04:00
Derrick Hammer 87fa117df3
dep: update libs5 2024-03-11 16:41:11 -04:00
Derrick Hammer d99b994175
dep: update libs5 2024-03-11 11:51:20 -04:00
Derrick Hammer 4941949f22
refactor: rewrite importPrices to track and import the last x days 2024-03-11 11:12:48 -04:00
Derrick Hammer c63f7ef50b
fix: use DailyJob 2024-03-11 11:00:34 -04:00
Derrick Hammer 96ec3b8501
fix: dont compute by rate the max rpc price 2024-03-11 08:01:56 -04:00
Derrick Hammer 944cb868b5
refactor: increase decimal to 30,20 2024-03-11 07:57:09 -04:00
Derrick Hammer 0d9d7d4d98
refactor: store to 20 decimal places 2024-03-11 07:54:52 -04:00
Derrick Hammer 973c40afb4
refactor: switch to using decimals in db, a fork of the siacentral api to return decimals, strings for currency settings, and rats to do big number math safety 2024-03-11 07:51:48 -04:00
Derrick Hammer a4bb3eadaa
fix: wrong debug output 2024-03-11 06:49:10 -04:00
Derrick Hammer e7caa50932
fix: don't store MaxRPCSCPrice as a float, and use big.Rat to compute it 2024-03-11 06:42:51 -04:00
Derrick Hammer 7f9887bdcc
fix: don't divide maxDownloadPrice by redundancy 2024-03-10 17:32:37 -04:00
Derrick Hammer 6edf872664
fix: need to divide by block time (blocks in a month) 2024-03-10 17:01:55 -04:00
Derrick Hammer 20a273a0dd
refactor: change maxStoragePrice math for readability 2024-03-10 14:50:25 -04:00
Derrick Hammer d628ede1ee
fix: use maxStoragePrice 2024-03-10 14:48:09 -04:00
Derrick Hammer 9d17ae1065
fix: don't compute maxRPCPrice based on usd rate 2024-03-10 14:42:00 -04:00
Derrick Hammer 01cd30b6f8
fix: divide maxRPCPrice by 1 million 2024-03-10 14:38:59 -04:00
Derrick Hammer 22a5c661bc
refactor: update max_rpc_sc_price default to 0.1 2024-03-10 14:36:45 -04:00
Derrick Hammer 51d74b6483
refactor: add max_contract_sc_price to defaults 2024-03-10 14:35:37 -04:00
Derrick Hammer 57c5088a1f
fix: max_rpc_price needs to be max_rpc_sc_price 2024-03-10 14:35:05 -04:00
Derrick Hammer 6eeed24707
fix: need to divide maxStoragePrice by a tib unit 2024-03-10 14:33:46 -04:00
Derrick Hammer af29081a3a
refactor: have rpc and contract prices be in SC as they are expected to change very rarely 2024-03-10 14:26:17 -04:00
Derrick Hammer 1568aa9007
fix: redundancy applies to storage not downloads, and add debug logging per rate computed 2024-03-10 14:14:36 -04:00
Derrick Hammer 5687e72a32
refactor: change SCPriceHistory table name 2024-03-10 13:58:45 -04:00
Derrick Hammer e425c038e1
fix: use config PriceHistoryDays 2024-03-10 13:56:41 -04:00
Derrick Hammer effb341418
refactor: don't block price updates, but do fatal on error 2024-03-10 13:52:49 -04:00
Derrick Hammer 8c05180703
refactor: move price tracker init to a lifecycle 2024-03-10 13:29:43 -04:00
Derrick Hammer 21ae6d093d
refactor: fetch redundancy settings and divide on it so price is always the total 2024-03-10 13:25:33 -04:00
Derrick Hammer c9683453c0
feat: add RedundancySettings getter 2024-03-10 13:13:25 -04:00
Derrick Hammer ae178f003f
refactor: put sia config under storage 2024-03-10 13:07:44 -04:00
Derrick Hammer b6683a5744
fix: default to debug if no config is passed, we will then error on config parsing after 2024-03-10 13:02:31 -04:00
Derrick Hammer 39a39b00b7
fix: need prefix on setDefaultsForObject for core to be core 2024-03-10 12:54:15 -04:00
Derrick Hammer dd296bd78a
feat: add initial price tracker 2024-03-10 12:43:18 -04:00
Derrick Hammer 736dc8aa9d
refactor: simplify sia upload to not try to parallel process as it corrupts data. We will come back to this optimization in the future. 2024-03-10 11:54:32 -04:00
Derrick Hammer 6c60dae743
fix: if the render end is 0, ensure the range header is valid syntax and skip it 2024-03-10 11:53:24 -04:00
Derrick Hammer f56377df2b
dep: update libs5 2024-03-10 09:17:46 -04:00
Derrick Hammer 8c0a2451e4
dep: update libs5 2024-03-10 09:11:26 -04:00
Derrick Hammer 89bbd8b061
dep: update libs5 2024-03-10 08:58:23 -04:00
Derrick Hammer ce977da5c1
dep: update libs5 2024-03-10 08:56:29 -04:00
Derrick Hammer 770e3335d1
dep: update libs5 2024-03-10 08:41:21 -04:00
Derrick Hammer ab5356f024
dep: update libs5 2024-03-10 07:39:23 -04:00
Derrick Hammer 673cd535a0
dep: update libs5 2024-03-10 07:26:05 -04:00
Derrick Hammer 1f1f204b35
fix: only return noop is we are SeekStart and want the beginning, otherwise this is likely actually needing to start a partial content seek 2024-03-09 17:46:18 -05:00
Derrick Hammer e89a9450e9
fix: don't tie contexts to tus http context 2024-03-09 16:55:10 -05:00
Derrick Hammer 0c90924f31
refactor: add checking tus for metadata before uploads 2024-03-09 15:37:58 -05:00
Derrick Hammer f3040399e4
feat: add GetUploadSize 2024-03-09 15:37:16 -05:00
Derrick Hammer 887f51f88d
fix: need to set upload id on siaUpload 2024-03-09 14:45:31 -05:00
Derrick Hammer 4020b9f7c7
fix: S5File::Exists needs to check tus before uploads 2024-03-09 14:27:07 -05:00
Derrick Hammer d0d67d2ae5
dep: update libs5 2024-03-09 13:23:07 -05:00
Derrick Hammer 1a1d34ef7a
dep: update libs5 2024-03-09 07:17:13 -05:00
Derrick Hammer 1da600f1ca
dep: update libs5 2024-03-09 07:00:55 -05:00
Derrick Hammer 2bff5d2b9e
refactor: switch to new libs5 kv db interface 2024-03-09 06:58:32 -05:00
Derrick Hammer 5e31dc2f02
dep: update libs5 2024-03-09 06:55:13 -05:00
Derrick Hammer 4261bb6a5d
refactor: if the file exists locally, pull from out own node before fetching it from the net 2024-03-07 16:59:50 -05:00
Derrick Hammer e489de1e86
dep: update libs5 2024-03-07 16:54:57 -05:00
Derrick Hammer 8a6516e157
fix: if root return a file with that set 2024-03-07 16:48:56 -05:00
Derrick Hammer 6e97b582ba
fix: pass Name to override using CID 2024-03-07 16:48:20 -05:00
Derrick Hammer 3e4eed12ae
fix: typo 2024-03-07 04:38:56 -05:00
Derrick Hammer 91d58ee87f
refactor: change login to respond with the jwt token in the body 2024-03-06 18:13:34 -05:00
Derrick Hammer 550398c701
feat: add swagger spec and support to account api 2024-03-06 04:58:04 -05:00
Derrick Hammer 6fb77d102a
refactor: add logic to find any unpinned files of a child manifest if the root is pinned but the children aren't 2024-03-05 16:27:29 -05:00
Derrick Hammer 111d0a7ead
dep: update libs5 2024-03-05 15:22:29 -05:00
Derrick Hammer dcbc54cec5
refactor: set p2p.max_connection_attempts to 10 2024-03-05 15:16:31 -05:00
Derrick Hammer 1f008c40b4
dep: update libs5 2024-03-05 15:14:43 -05:00
Derrick Hammer 0896a70cfb
dep: update libs5 2024-03-05 15:07:21 -05:00
Derrick Hammer 6479104d5a
dep: update libs5 2024-03-05 15:02:52 -05:00
Derrick Hammer c4a124b521
dep: update libs5 2024-03-05 14:26:31 -05:00
Derrick Hammer caac09cc6f
feat: add CtxAborted and use in account pin 2024-03-05 13:41:47 -05:00
Derrick Hammer e6d51cd9e7
dep: update libs5 2024-03-05 13:13:16 -05:00
Derrick Hammer 0df51358d6
chore: add todo 2024-03-05 12:59:52 -05:00
Derrick Hammer 14cd9fda7f
dep: don't need jape fork again 2024-03-05 12:46:47 -05:00
Derrick Hammer cfc8ea4eac
dep: security fix 2024-03-05 12:45:07 -05:00
Derrick Hammer fdfffb897c
fix: if not a manifest, call pinEntity directly 2024-03-05 12:41:28 -05:00
Derrick Hammer a5c1356847
fix: don't rely on content length, but do a basic heuristic by reading 1 byte past the max upload, if if we haven't hit the limit and the sizes don't match, then error, but otherwise take an optimistic stance 2024-03-05 12:38:41 -05:00
Derrick Hammer c0fa8d4ea3
dep: update libs5 2024-03-03 13:04:10 -05:00
Derrick Hammer 70e399cdd5
dep: update libs5 2024-03-03 12:58:57 -05:00
Derrick Hammer 56e076512b
dep: update libs5 2024-03-03 12:56:45 -05:00
Derrick Hammer a169e4c3be
dep: update libs5 2024-03-03 12:43:23 -05:00
Derrick Hammer 7791399fe4
dep: update libs5 2024-03-03 12:28:21 -05:00
Derrick Hammer a340967dd1
dep: update libs5 2024-03-03 12:13:37 -05:00
Derrick Hammer c6feee1351
fix: check if entry returns nil 2024-03-03 11:29:37 -05:00
Derrick Hammer ac3f0f8bdb
dep: update libs5 2024-03-03 11:29:12 -05:00
Derrick Hammer 7d16ec5b7d
dep: update libs5 2024-03-03 11:01:44 -05:00
Derrick Hammer 73623b8c36
fix: add CIDTypeDirectory to isCidManifest 2024-03-03 10:43:01 -05:00
Derrick Hammer f20b4ee916
fix: update usage of GetCachedStorageLocations 2024-03-03 09:33:21 -05:00
Derrick Hammer f4799e2b7d
dep: update libs5 2024-03-03 09:31:23 -05:00
Derrick Hammer 080fcbd559
fix: add special case to check for an index.html, and pass root and root type 2024-03-03 08:29:07 -05:00
Derrick Hammer bd5544198e
refactor: optionally track a "root" cid which will be used for webapp manifests to start. Have IsDir run heuristics to verify if it is a dir by checking the manifest tryfiles 2024-03-03 08:28:39 -05:00
Derrick Hammer 756a01d52f
fix: if the filename is a . treat as a special case for a dir 2024-03-03 07:39:18 -05:00
Derrick Hammer bd29ab4612
fix: if we are being requested the root while is a dot, create a file based on the root cid with the . name 2024-03-03 07:38:34 -05:00
Derrick Hammer 9d378f4197
fix: ignore / and . not existing 2024-03-03 07:18:03 -05:00
Derrick Hammer 201c9b992f
fix: need to deref r to replace it 2024-03-03 07:17:46 -05:00
Derrick Hammer 4483ad7ec5
dep: update libs5 2024-03-03 07:17:14 -05:00
Derrick Hammer 9d25784a6e
fix: we need to pin the files after upload 2024-03-03 06:50:24 -05:00
Derrick Hammer d2c9f8e38a
fix: strip out any port 2024-03-03 06:40:50 -05:00
Derrick Hammer 53f9a8fb32
refactor: add UploadPinned and have DNSLinkExists ensure the upload exists, the dnslink exists, and the upload is pinned 2024-03-03 06:16:01 -05:00
Derrick Hammer 82ae0d3ef7
refactor: add UploadID to UploadMetadata 2024-03-03 06:10:03 -05:00
Derrick Hammer ecf2532974
dep: update libs5 2024-03-03 05:36:47 -05:00
Derrick Hammer b03af65418
fix: need to manually cast errorPages to webappErrorPages 2024-03-03 05:24:03 -05:00
Derrick Hammer c2483aa7a9
dep: update libs5 2024-03-03 05:23:40 -05:00
Derrick Hammer c5b03ffd9e
dep: update libs5 2024-03-03 04:43:00 -05:00
Derrick Hammer 4c91b0c2e7
dep: update libs5 2024-03-03 04:34:40 -05:00
Derrick Hammer 1688cf39f8
dep: update libs5 2024-03-03 04:29:32 -05:00
Derrick Hammer e71a160f46
dep: update libs5 2024-03-03 04:01:54 -05:00
Derrick Hammer 3d55254916
fix: update use of webApp.Paths 2024-03-03 03:41:30 -05:00
Derrick Hammer 9f65b1b455
dep: update libs5 2024-03-03 03:41:03 -05:00
Derrick Hammer c8124c0739
dep: update libs5 2024-03-03 03:36:08 -05:00
Derrick Hammer b2a2e13496
dep: update libs5 2024-03-03 03:28:22 -05:00
Derrick Hammer 37514a742f
fix: errorFiles is really errorPages 2024-03-03 03:03:15 -05:00
Derrick Hammer 4db07d5170
fix: need to pass tus 2024-03-02 06:24:12 -05:00
Derrick Hammer 2e9694149b
fix: wrong tag option for unique index 2024-03-02 06:19:31 -05:00
Derrick Hammer f98b0f48bd
fix: use hash provided by constructor 2024-03-02 06:12:29 -05:00
Derrick Hammer dde5d255a6
fix: debug api needs cors support 2024-03-02 06:08:23 -05:00
Derrick Hammer 845de39049
dep: update libs5 2024-03-02 05:54:36 -05:00
Derrick Hammer b5c3d99568
dep: update libs5 2024-03-02 05:42:50 -05:00
Derrick Hammer ca06919764
dep: update libs5 2024-03-02 05:24:57 -05:00
Derrick Hammer 3654607f3f
refactor: replace debug apu auth middlewares with proxy middlewares 2024-03-02 05:01:21 -05:00
Derrick Hammer a96fc8682f
fix: return error not S5Error 2024-03-02 04:55:04 -05:00
Derrick Hammer 7315f8e694
fix: need to manually extract the multipart filename because goes internals strips file paths 2024-03-02 04:54:29 -05:00
Derrick Hammer 0c0cdfd2b1
fix: fix parsing of app upload settings, as they are json, and errorPages needs to be errorFiles 2024-03-02 04:51:28 -05:00
Derrick Hammer a9c2ecade0
fix: if we get an ErrUnexpectedEOF due to being less than512 bytes, read it all instead 2024-03-02 04:45:09 -05:00
Derrick Hammer 8a112a8c12
refactor: make S5File implement fs.ReadDirFile and fs.DirEntry 2024-03-01 23:30:06 -05:00
Derrick Hammer 1dff84accf
refactor: make resolveDirCid a utility function 2024-03-01 23:28:05 -05:00
Derrick Hammer b87ba1e6bf
feat: add support for webapp and dir cid's in dnslink 2024-03-01 22:50:22 -05:00
Derrick Hammer 8e2adba1eb
feat: add initial fs interfaces for webapp and dir 2024-03-01 22:49:47 -05:00
Derrick Hammer 3dbd791314
feat: add S5FileInfo struct and refactorings needed to be used as a fs.File and in a fs.Fs 2024-03-01 22:49:17 -05:00
Derrick Hammer 022d6a94cc
dep: update libs5 2024-03-01 22:46:51 -05:00
Derrick Hammer f79a0dd448
fix: need to use hashBytes and an optional type 2024-03-01 22:46:42 -05:00
Derrick Hammer a1f36df8b9
refactor: change newFile to take the params struct directly and fill in the blanks 2024-03-01 21:59:49 -05:00
Derrick Hammer 65548e8ec7
refactor: pass cidType to new S5File 2024-03-01 21:44:02 -05:00
Derrick Hammer f63d567b53
refactor: add support for using a passed cid type 2024-03-01 21:32:06 -05:00
Derrick Hammer 217bb78b3b
fix: ExistingUploadID no longer needed 2024-03-01 20:56:22 -05:00
Derrick Hammer ddc21014a8
dep: update libs5 2024-03-01 20:55:16 -05:00
Derrick Hammer a28bd6b11e
refactor: add db index on bucket/key 2024-03-01 20:44:55 -05:00
Derrick Hammer 454deeae21
refactor: change renter UploadObjectMultipart to use a db table like we do with s3 to track multipart uploads persistently and generically so we can resume later 2024-03-01 20:42:42 -05:00
Derrick Hammer aefe9efaaa
fix: export struct and add json tags 2024-03-01 10:30:08 -05:00
Derrick Hammer aacdd48428
fix: we need to track when the queue is finished ourselves 2024-03-01 10:11:03 -05:00
Derrick Hammer b272e32185
fix: dont send an empty buffer to bao 2024-03-01 06:23:31 -05:00
Derrick Hammer d5e2770135
refactor: don't use a buffered chan 2024-03-01 05:14:47 -05:00
Derrick Hammer b46e12b972
fix: if for some edge case reason an upload id no longer exists, start over, but use db Save so we can update the record if we have an ID 2024-03-01 05:09:57 -05:00
Derrick Hammer 56100e5d50
fix: shadow cid 2024-03-01 04:55:46 -05:00
Derrick Hammer 866739007c
refactor: use golang-queue to parallel process pinning checks 2024-03-01 04:27:24 -05:00
Derrick Hammer 8f6e7d1acc
fix: dont add nil cids 2024-03-01 04:14:11 -05:00
Derrick Hammer 7ec4f26142
fix: only try to fetch proof file if we are over the post limit 2024-03-01 04:01:22 -05:00
Derrick Hammer a1572b256b
fix: init results 2024-03-01 04:00:39 -05:00
Derrick Hammer 6427d24b0f
dep: update libs5 2024-03-01 03:41:48 -05:00
Derrick Hammer 76cb45a3f4
dep: update libs5 2024-03-01 03:31:14 -05:00
Derrick Hammer 61832022f9
dep: update libs5 2024-03-01 03:26:05 -05:00
Derrick Hammer 823a1c68fc
dep: update libs5 2024-03-01 03:22:17 -05:00
Derrick Hammer 32fed7e495
dep: update libs5 2024-03-01 03:06:35 -05:00
Derrick Hammer c19290c0fc
dep: update libs5 2024-03-01 03:02:16 -05:00
Derrick Hammer 016ac72b8d
dep: update libs5 2024-03-01 02:50:39 -05:00
Derrick Hammer a8c40fb998
dep: update libs5 2024-03-01 02:46:49 -05:00
Derrick Hammer 59247c5128
dep: update libs5 2024-03-01 02:37:42 -05:00
Derrick Hammer 038a1f0a88
dep: update libs5 2024-03-01 02:30:20 -05:00
Derrick Hammer fe793b33be
dep: update libs5 2024-03-01 02:20:28 -05:00
Derrick Hammer 69de0440c1
dep: update libs5 2024-03-01 01:55:27 -05:00
Derrick Hammer cc41f87b14
dep: update libs5 2024-03-01 01:49:53 -05:00
Derrick Hammer eb26063015
dep: update libs5 2024-03-01 01:42:05 -05:00
Derrick Hammer 984e33d4a7
dep: update libs5 2024-03-01 01:37:02 -05:00
Derrick Hammer 652a80db3a
dep: update libs5 2024-03-01 01:32:23 -05:00
Derrick Hammer 39beb89c77
dep: update libs5 2024-03-01 01:25:26 -05:00
Derrick Hammer 2245a9b24e
dep: update libs5 2024-03-01 00:20:04 -05:00
Derrick Hammer bde35d28bd
dep: update libs5 2024-02-29 12:54:29 -05:00
Derrick Hammer d2832cc67a
dep: update libs5 2024-02-29 12:27:02 -05:00
Derrick Hammer 3f2757fb18
feat: add support for resolving a registry entry when pinning 2024-02-29 12:08:04 -05:00
Derrick Hammer b3df326980
feat: add support for recursively pinning a manifest and all its children 2024-02-29 12:02:49 -05:00
Derrick Hammer 4126c06cd8
dep: update libs5 2024-02-29 11:33:40 -05:00
Derrick Hammer 99a6f3a2f6
fix: StorageLocationTypeFile/StorageLocationTypeFull expiry should be an hour 2024-02-29 10:16:16 -05:00
Derrick Hammer e32a9b2070
fix: only say we have the file if no error 2024-02-29 09:55:51 -05:00
Derrick Hammer 64f52a87bd
fix: return on any error 2024-02-29 09:55:07 -05:00
Derrick Hammer 754ab390f0
refactor: add build tag to s5 specific tables 2024-02-28 21:24:06 -05:00
Derrick Hammer bb167ea360
refactor: use a dynamic model registration process so we can use build tags to exclude/include 2024-02-28 21:20:05 -05:00
Derrick Hammer 4fe856fd03
dep: update libs5 2024-02-28 14:16:50 -05:00
Derrick Hammer 7abba4ac8e
refactor: move averageReadTime out of loop 2024-02-28 12:59:11 -05:00
Derrick Hammer 68d82390ed
refactor: add timing tracking for reading 2024-02-28 12:56:39 -05:00
Derrick Hammer dd9fdabf47
fix: move verification time logging outside loop and only record average 2024-02-28 12:41:03 -05:00
Derrick Hammer 0b000bfc89
fix: track after buffer write 2024-02-28 12:37:32 -05:00
Derrick Hammer c984d72cfd
refactor: add performance logging for bao 2024-02-28 12:33:13 -05:00
Derrick Hammer 0d0ec43125
refactor: add eta to logging 2024-02-28 12:20:16 -05:00
Derrick Hammer a49da3fdfe
fix: partStartTime needs to include reading 2024-02-28 12:16:47 -05:00
Derrick Hammer 5f0e2d2e15
refactor: add performance tracking and logging 2024-02-28 12:13:26 -05:00
Derrick Hammer b030de9714
feat: add generic s3 upload tracking 2024-02-28 11:36:53 -05:00
Derrick Hammer b2325eb9af
refactor: keep filename singular 2024-02-28 11:17:31 -05:00
Derrick Hammer 55203fa466
refactor: add more debug info 2024-02-28 11:08:05 -05:00
Derrick Hammer fba98da0e0
refactor: enable tracking the called line 2024-02-28 11:06:35 -05:00
Derrick Hammer 844d3a0a5f
refactor: add debug log line 2024-02-28 10:59:44 -05:00
Derrick Hammer e45ab26a09
refactor: add error message support to verify 2024-02-28 10:49:15 -05:00
Derrick Hammer 72fed662e8
fix: use io.ReadFull 2024-02-28 10:42:01 -05:00
Derrick Hammer b756ad73e9
fix: skip unexported fields 2024-02-28 09:50:42 -05:00
Derrick Hammer 6b7724eb51
fix: skip unexported fields 2024-02-28 09:48:20 -05:00
Derrick Hammer 0ae69e5ba1
fix: ensure m.root.Core.Clustered is not nil 2024-02-28 09:45:34 -05:00
Derrick Hammer 80d34000a0
refactor: store protocol config in root 2024-02-28 09:43:06 -05:00
Derrick Hammer bfb8559e32
fix: hack: make Protocol an interface map 2024-02-28 09:42:06 -05:00
Derrick Hammer 586a6dc205
fix: maybeConfigureCluster called in the wrong place 2024-02-28 09:36:01 -05:00
Derrick Hammer fcf43e1f79
refactor: add cluster configuration support to set db cache based on the cluster settings 2024-02-28 09:21:20 -05:00
Derrick Hammer d71849493b
chore: unneeded method 2024-02-28 09:16:35 -05:00
Derrick Hammer 9b82da72ca
refactor: move redis to its own file and add in defaults 2024-02-28 09:14:11 -05:00
Derrick Hammer 960c2b01d9
refactor: move all config defaults and add some validations. remove initCheckRequiredConfig. 2024-02-28 09:04:47 -05:00
Derrick Hammer cb558cdfc3
feat: add reflection-based system to manage defaults and validations per struct 2024-02-28 08:47:33 -05:00
Derrick Hammer 1a20a7d35f
refactor: define a cluster config with redis and etcd support 2024-02-28 08:03:36 -05:00
Derrick Hammer 7edab13afe
refactor: define Protocol under Config for completeness 2024-02-28 06:11:04 -05:00
Derrick Hammer d9d85f2804
fix: casing 2024-02-27 11:14:38 -05:00
Derrick Hammer 5878d1557f
refactor: switch using hkdf for child key 2024-02-27 11:10:46 -05:00
Derrick Hammer 097e29aa94
refactor: switch to using All api then filter to find valid locations 2024-02-27 10:58:15 -05:00
Derrick Hammer cdb23540ca
dep: update libs5 2024-02-27 10:51:09 -05:00
Derrick Hammer 9782907c90
fix: increase iterations to 600k 2024-02-27 09:54:11 -05:00
Derrick Hammer 1ca46dddd4
fix: use proto.identity 2024-02-27 09:15:18 -05:00
Derrick Hammer b5509f11d1
fix: use a deterministic child key 2024-02-27 09:11:17 -05:00
Derrick Hammer 944c5f01b9
refactor: add support for ExcludeNodes via excludeSelf in newStorageLocationProvider 2024-02-27 07:31:59 -05:00
Derrick Hammer f9e8d4e2fc
dep: update libs5 2024-02-27 07:28:43 -05:00
Derrick Hammer d002c56ffe
fix: set no limit on message size 2024-02-27 07:16:00 -05:00
Derrick Hammer c8a6570e8f
refactor: bao verifier needs to manage a data buffer to allow any number of bytes read while verifying in chunks 2024-02-27 06:37:49 -05:00
Derrick Hammer 9f5b676f47
refactor: use S3MultipartUpload if greater than S3_MULTIPART_MIN_PART_SIZE 2024-02-27 06:14:27 -05:00
Derrick Hammer 7411228106
feat: add S3MultipartUpload api 2024-02-27 06:12:41 -05:00
Derrick Hammer 01eda4aa23
fix: need to pass content length 2024-02-27 04:31:55 -05:00
Derrick Hammer 9b6a253313
fix: need to use Get, head isn't always supported 2024-02-27 04:26:55 -05:00
Derrick Hammer 218c0a1b1a
fix: used a named sub-logger 2024-02-27 04:14:18 -05:00
Derrick Hammer 355e8a3d22
dep: update libs5 2024-02-27 04:11:21 -05:00
Derrick Hammer b51f28ea64
fix: remove import cycle 2024-02-27 03:57:17 -05:00
Derrick Hammer 6e3e096be9
refactor: change NewLogger to use config manager, and have CM pass nil to newConfig 2024-02-27 03:54:00 -05:00
Derrick Hammer 8f8944a645
fix: pass ctx 2024-02-27 03:43:41 -05:00
Derrick Hammer 89c03f9cee
fix: pass context.Background() 2024-02-27 03:43:34 -05:00
Derrick Hammer 86dc8c8b9a
fix: pass ctx on protocol Stop 2024-02-27 03:36:09 -05:00
Derrick Hammer 43eb8c3e9a
refactor: add ctx to protocol Init 2024-02-27 03:35:43 -05:00
Derrick Hammer 6061cffcfd
dep: update libs5 2024-02-27 03:32:45 -05:00
Derrick Hammer 5dfedeb66a
fix: need to squash embedded config 2024-02-27 03:18:02 -05:00
Derrick Hammer d5c0157fd8
dep: update libs5 2024-02-27 03:12:06 -05:00
Derrick Hammer 5ed22efa83
fix: wrong peers key 2024-02-27 03:06:32 -05:00
Derrick Hammer 795bc5f82a
dep: update libs5 2024-02-27 02:53:22 -05:00
Derrick Hammer fb03cd28b9
dep: update libs5 2024-02-27 02:50:21 -05:00
Derrick Hammer f89be1fef8
refactor: make mail init a lifecycle hook so it's called after initCheckRequiredConfig 2024-02-27 02:24:20 -05:00
Derrick Hammer 33af108d39
feat: add password reset endpoints 2024-02-26 11:04:05 -05:00
Derrick Hammer 25b4286011
feat: add password reset apis 2024-02-26 10:55:26 -05:00
Derrick Hammer 057db6a636
feat: add /api/auth/verify-email endpoint 2024-02-26 10:47:47 -05:00
Derrick Hammer e629618f25
fix: apply ProxyMiddleware to register 2024-02-26 10:43:55 -05:00
Derrick Hammer 92c33e0af5
refactor: delete all the users email verifications after a successful verification 2024-02-26 10:39:09 -05:00
Derrick Hammer 036520581f
refactor: handle both user verification, and changing email 2024-02-26 10:35:57 -05:00
Derrick Hammer 5c6224222f
fix: only update user if we aren't yet verified 2024-02-26 10:30:22 -05:00
Derrick Hammer b3d63007e1
fix: pass login ip 2024-02-26 08:30:17 -05:00
Derrick Hammer 355033634d
refactor: set login ip with LoginPubkey 2024-02-26 08:28:22 -05:00
Derrick Hammer 2573936000
refactor: add bypassSecurity to doLogin 2024-02-26 08:26:59 -05:00
Derrick Hammer b270d6f414
fix: CreateAccount needs a verify email argument 2024-02-26 08:23:53 -05:00
Derrick Hammer 88ef43acaa
fix: dnslink would not import 2024-02-26 08:18:55 -05:00
Derrick Hammer 90834601d7
refactor: modify CreateAccount to optionally send an email verification 2024-02-26 08:15:10 -05:00
Derrick Hammer 8965395fdf
feat: add apis for sending email verification, and verifying an email code 2024-02-26 08:14:30 -05:00
Derrick Hammer 25ebb00765
refactor: update const names 2024-02-26 08:03:30 -05:00
Derrick Hammer 1be4fb47fc
feat: add password reset, account verification models, and a verified field to users 2024-02-26 07:47:05 -05:00
Derrick Hammer 3da1ae3e5f
feat: add core.portal_name required config to be used for communication 2024-02-26 07:32:46 -05:00
Derrick Hammer 81e540c2ce
feat: initial mailer module with password reset and email verification templates 2024-02-26 07:30:53 -05:00
Derrick Hammer 39f8152e09
refactor: use just Read 2024-02-25 11:43:44 -05:00
Derrick Hammer 907de0b3af
fix: need to init apiDomain 2024-02-25 09:54:34 -05:00
Derrick Hammer 3a0c7bdea2
refactor: always preload all relations with exists 2024-02-25 09:48:26 -05:00
Derrick Hammer 38375d44d8
feat: initial DNS link support 2024-02-25 09:47:40 -05:00
Derrick Hammer 40b3504c1d
refactor: rewrite main api router to be a class, lazy init it, and introduce RoutableAPI which can dynamically handle incoming requests if the main routes aren't matched 2024-02-25 08:36:32 -05:00
Derrick Hammer 8cae7bba57
fix: syntax errors 2024-02-25 07:54:16 -05:00
Derrick Hammer 5b210de198
refactor: use job terminology for consistency 2024-02-25 07:52:28 -05:00
Derrick Hammer 8885fd37cc
refactor: use job terminology for consistency 2024-02-25 07:50:59 -05:00
Derrick Hammer d618f08275
refactor: rename RetryableTask to RetryableJob 2024-02-25 07:49:22 -05:00
Derrick Hammer 5c3d1144d4
feat: add ability for pinning to import a CID via cron task 2024-02-25 07:23:30 -05:00
Derrick Hammer 0010d6c5b9
feat: add verifier class that wraps a reader and proof 2024-02-25 07:22:17 -05:00
Derrick Hammer de3b226df5
feat: add support for verifying data in bao service 2024-02-25 07:21:20 -05:00
Derrick Hammer ea8c50edc7
refactor: make PostUploadLimit uint64 2024-02-25 02:10:41 -05:00
Derrick Hammer fc40563ae4
feat: add GetJobsByPrefix, GetJobByName, GetJobByID 2024-02-25 01:58:26 -05:00
Derrick Hammer f47552bf60
refactor: move s3 client creation to a factory method on storage for re-usability 2024-02-24 10:34:49 -05:00
Derrick Hammer 988a313f93
dep: update tusd 2024-02-24 09:37:53 -05:00
Derrick Hammer 3c1e60c3d2
feat: convert zap logger to slogger for tusd 2024-02-24 09:33:24 -05:00
Derrick Hammer 5a78750df1
fix: need to call the hook not pass it as a factory 2024-02-24 08:57:36 -05:00
Derrick Hammer fb8dfb2fa2
fix: dont log ErrRecordNotFound messages 2024-02-24 08:44:34 -05:00
Derrick Hammer f6f9a7f97a
feat: add custom logger wrapper to use zap logger for database 2024-02-24 08:41:40 -05:00
Derrick Hammer b5b0ed64b6
feat: add database cache support with both memory and redis modes 2024-02-24 08:19:27 -05:00
Derrick Hammer 995b227d7e
chore: unneeded config.go 2024-02-24 05:43:08 -05:00
Derrick Hammer 7987e597b0
fix: use snake_case 2024-02-23 08:26:27 -05:00
Derrick Hammer 8602d5ed97
fix: use snake_case 2024-02-23 08:21:45 -05:00
Derrick Hammer ee2f7331eb
fix: routes need http verbs 2024-02-23 08:20:03 -05:00
Derrick Hammer 1a9fa9c4be
fix: Config should embed a pointer to s5config.NodeConfig 2024-02-23 08:18:05 -05:00
Derrick Hammer daf63268eb
fix: need to use viper.Sub to scope on the protocol namespace and unmarshal 2024-02-23 07:54:45 -05:00
Derrick Hammer 6ddd10a4c9
dep: update libs5 2024-02-23 07:26:24 -05:00
Derrick Hammer 9932e6194e
fix: update error message 2024-02-22 03:44:27 -05:00
Derrick Hammer 7f5741a64b
fix: update config management in account pkg 2024-02-22 03:41:28 -05:00
Derrick Hammer 856b7fb627
fix: update config management in init functions 2024-02-22 03:39:29 -05:00
Derrick Hammer ef25887a87
feat: add save method that will re-unmarshal into the config struct 2024-02-22 03:38:05 -05:00
Derrick Hammer 10060f0245
fix: port in dsn is a number 2024-02-22 03:32:47 -05:00
Derrick Hammer 051cf59195
fix: need to update config management in db pkg 2024-02-22 03:30:40 -05:00
Derrick Hammer bbfea6fc9e
fix: need to update config management in storage pkg 2024-02-22 03:27:48 -05:00
Derrick Hammer a6218e9b7c
fix: need to update config management in s5 protocol 2024-02-22 03:27:40 -05:00
Derrick Hammer 939e329591
fix: need to update config management in renter pkg 2024-02-22 03:23:06 -05:00
Derrick Hammer 41a3b1faa6
fix: missing return in constructor 2024-02-22 03:19:46 -05:00
Derrick Hammer 4f094eab2c
fix: Unmarshal needs a pointer to config 2024-02-22 03:17:27 -05:00
Derrick Hammer 0125fb4d01
dep: move back to sia core v0.1.12 2024-02-22 02:56:19 -05:00
Derrick Hammer a42cda1ced
dep: need to update dependencies, downgrade sia core, downgrade kin-openapi, and switch to coreutils wallet 2024-02-22 02:46:37 -05:00
Derrick Hammer 7f12ee5b0d
refactor: implement new configuration management system 2024-02-22 02:13:59 -05:00
Derrick Hammer 99b97d9495
fix: need to use new_outboard 2024-02-18 22:19:43 -05:00
Derrick Hammer 901c68fdfc
refactor: have the forward slash always prefixed at the renter abstraction 2024-02-18 03:30:42 -05:00
Derrick Hammer ca289818a9
refactor: use errors.Is 2024-02-18 03:24:06 -05:00
Derrick Hammer a77981f0a6
fix: PinByHash does not query right 2024-02-18 03:21:52 -05:00
Derrick Hammer ae7d048e6c
fix: don't create subpath, as we use buckets 2024-02-18 03:07:14 -05:00
Derrick Hammer b4f8a1979b
fix: PROOF_EXTENSION already has file dot 2024-02-18 02:57:10 -05:00
Derrick Hammer 418cdf7836
fix: need to use a reader for mime detection 2024-02-18 02:44:57 -05:00
Derrick Hammer e435e6ded5
fix: need to return muReader early 2024-02-18 02:38:17 -05:00
Derrick Hammer 0e61a5c1fa
chore: dont log proof, too large 2024-02-18 02:29:33 -05:00
Derrick Hammer 938b6cdf9e
fix: store buf outside for loop 2024-02-18 02:29:15 -05:00
Derrick Hammer bff0981731
fix: use write_all 2024-02-18 02:25:13 -05:00
Derrick Hammer 0873dee1f3
fix: use a background ctx 2024-02-18 00:27:19 -05:00
Derrick Hammer 88a636ba9c
refactor: pass only the upload hash to the cron task 2024-02-18 00:23:25 -05:00
Derrick Hammer 15750acec0
fix: check for error 2024-02-18 00:04:28 -05:00
Derrick Hammer 8a32e69b06
fix: use storageProtocol 2024-02-18 00:01:01 -05:00
Derrick Hammer 517abe9193
fix: unneeded cast 2024-02-18 00:00:02 -05:00
Derrick Hammer 16ed748bfb
fix: cast to uint not uint64 2024-02-17 23:58:31 -05:00
Derrick Hammer 2ce26239da
fix: add missing tus background worker 2024-02-17 23:55:25 -05:00
Derrick Hammer 445084ca5b
fix: remove rows effected check 2024-02-17 23:42:32 -05:00
Derrick Hammer 9e35c614e3
refactor: change SaveUpload to use gorm create or selective Updates 2024-02-17 23:39:51 -05:00
Derrick Hammer 8f78f6fe87
fix: metadata service not passed in construction 2024-02-17 23:27:37 -05:00
Derrick Hammer fc042570ab
refactor: change to store the hash as a raw, but also make it unique with an index 2024-02-17 23:17:26 -05:00
Derrick Hammer a20b79ff90
fix: forgot to return UploadMetadata 2024-02-17 22:59:34 -05:00
Derrick Hammer cfb0abf81a
fix: try to fetch the upload by hash, and if no error, return the upload meta 2024-02-17 22:54:44 -05:00
Derrick Hammer 84a78b7a7e
fix: use realKey 2024-02-17 20:25:30 -05:00
Derrick Hammer 0cb1f96813
fix: bug fix exists 2024-02-17 20:19:00 -05:00
Derrick Hammer 484b9ac583
fix: missing returning the claim 2024-02-17 20:16:19 -05:00
Derrick Hammer 7ec03524ed
fix: need to pass the claim by ref 2024-02-17 20:12:49 -05:00
Derrick Hammer fc1dd491d6
fix: need to use the claim by ref 2024-02-17 20:07:43 -05:00
Derrick Hammer 2fd3368b5a
fix: trim bearer in lowercase 2024-02-17 19:48:28 -05:00
Derrick Hammer ca559eccc5
chore: remove AccountErrorNil 2024-02-17 09:02:17 -05:00
Derrick Hammer bbf8ec79d3
refactor: change info to be the model struct 2024-02-17 09:02:04 -05:00
Derrick Hammer 11f30700c3
fix: use tx.Statement.Changed to ensure email is being changed before validating it 2024-02-17 09:00:44 -05:00
Derrick Hammer c076d219d0
refactor: move checking of users name to api layer 2024-02-17 08:45:06 -05:00
Derrick Hammer a546089378
refactor: account api needs to use the error interface 2024-02-17 08:14:58 -05:00
Derrick Hammer 162af1e274
refactor: switch to using package init functions with build tags to selectively build protocol support 2024-02-17 07:40:22 -05:00
Derrick Hammer f73ad52864
feat: add support for resuming an existing upload 2024-02-17 06:37:58 -05:00
Derrick Hammer 3ded11d705
fix: HttpMiddlewareFunc needs its own case 2024-02-17 05:38:50 -05:00
Derrick Hammer 9949dae5e8
fix: add HttpMiddlewareFunc to switch case 2024-02-17 05:28:56 -05:00
Derrick Hammer d4be04eae9
fix: need to use a param struct 2024-02-17 05:23:33 -05:00
Derrick Hammer ac9a1a0b92
feat: implement StorageProtocol 2024-02-17 05:20:42 -05:00
Derrick Hammer 263473db41
refactor: add PreInit and Node setter to store s5 node 2024-02-17 05:16:52 -05:00
Derrick Hammer 997e362d90
refactor: add concept of a pre-init function that gets called before init 2024-02-17 05:16:24 -05:00
Derrick Hammer 0ac4d318b7
refactor: use a lazy setter approach for storageProtocol 2024-02-17 04:41:12 -05:00
Derrick Hammer 97297036c7
refactor: swap Protocol for to StorageProtocol 2024-02-17 04:06:25 -05:00
Derrick Hammer b52383b123
refactor: add StorageProtocol to S5ProtocolResult 2024-02-17 04:05:42 -05:00
Derrick Hammer 6f3f6015fe
fix: TusHandlerParams missing fx.In 2024-02-17 03:38:44 -05:00
Derrick Hammer 407114f527
fix: missing components in construction 2024-02-17 03:36:18 -05:00
Derrick Hammer 64492713f7
fix: Metadata needs to be exported 2024-02-17 03:35:51 -05:00
Derrick Hammer cdd458129b
fix: initParams missing fx.In 2024-02-17 03:33:55 -05:00
Derrick Hammer 76025f0a8a
fix: StorageServiceParams missing fx.In 2024-02-17 03:33:44 -05:00
Derrick Hammer 857ffe4fdd
refactor: need to use a param struct to get protocol value group 2024-02-17 03:28:05 -05:00
Derrick Hammer afc0b7a343
refactor: replicate what we did in api and remove the need for an InitFunc 2024-02-17 03:24:44 -05:00
Derrick Hammer 12a5f3f631
fix: missing fx.Annotate 2024-02-17 03:21:27 -05:00
Derrick Hammer 30b18a4ced
fix: update Routes signature 2024-02-17 03:14:17 -05:00
Derrick Hammer af0f3e19de
refactor: switch to using Supply 2024-02-17 03:12:44 -05:00
Derrick Hammer 24694ecbae
feat: create new swagger package 2024-02-17 03:05:22 -05:00
Derrick Hammer 3b9f4bbe3d
feat: add MergeRoutes 2024-02-17 03:04:27 -05:00
Derrick Hammer 7c330e308e
refactor: allow Routes to return an error 2024-02-17 03:04:15 -05:00
Derrick Hammer f34c041401
fix: UploadExists/GetUpload needs ctx 2024-02-17 02:39:42 -05:00
Derrick Hammer a5f0a4bfcc
fix: GetUploadReader needs ctx 2024-02-17 02:38:07 -05:00
Derrick Hammer c452b0c271
refactor: add context 2024-02-17 02:37:52 -05:00
Derrick Hammer b231f9d769
refactor: need to alias as interface 2024-02-17 02:36:27 -05:00
Derrick Hammer 6845dac609
refactor: add context to all tus apis 2024-02-16 22:08:34 -05:00
Derrick Hammer c468a81543
refactor: remove tus out of method names 2024-02-16 22:03:34 -05:00
Derrick Hammer 93e727ab3b
refactor: epic protocol and storage design refactor 2024-02-16 22:00:53 -05:00
Derrick Hammer c534162d6c
feat: add DeleteObject 2024-02-16 21:59:02 -05:00
Derrick Hammer d7da471b8b
refactor: update parameters and change MultipartUpload to UploadObjectMultipart 2024-02-16 21:58:44 -05:00
Derrick Hammer 1812b9cd38
refactor: change PinByHash to take hash in byte form 2024-02-16 21:57:20 -05:00
Derrick Hammer a76d13e75d
feat: add metadata service 2024-02-16 21:56:38 -05:00
Derrick Hammer 44c564761c
refactor: store length in result 2024-02-16 21:55:16 -05:00
Derrick Hammer ffbb7e371a
fix: pointer not needed as FileExists is by ref now 2024-02-16 08:53:53 -05:00
Derrick Hammer cd9cccc2a9
refactor: merge http handler back to account api struct 2024-02-16 08:52:30 -05:00
Derrick Hammer 7834471b84
refactor: merge http handler back to s5 api struct 2024-02-16 08:49:19 -05:00
Derrick Hammer 7f5847f7da
chore: remove commented code 2024-02-16 08:42:26 -05:00
Derrick Hammer f0d7a337db
refactor: change how init of protocols works and make router building part of the interface 2024-02-16 08:39:55 -05:00
Derrick Hammer 1b3934c793
refactor: update usage of errors 2024-02-15 21:06:30 -05:00
Derrick Hammer 16e8c84daa
refactor: OTPDisable needs to return *AccountError 2024-02-15 21:00:08 -05:00
Derrick Hammer 24d491ec4e
refactor: UpdateAccountName needs to return *AccountError 2024-02-15 20:56:48 -05:00
Derrick Hammer 1c3bfdc493
refactor: add ErrorCodeToHttpStatus 2024-02-15 20:56:08 -05:00
Derrick Hammer 3f90cbfe09
refactor: use new errors and optimize code 2024-02-15 20:55:38 -05:00
Derrick Hammer fc53bd3083
refactor: make PutFileSmall handle all tasks 2024-02-15 20:55:21 -05:00
Derrick Hammer c084743b47
refactor: apply auth and proxy middlewares 2024-02-14 00:41:02 -05:00
Derrick Hammer 829852c6c1
refactor: use new error struct and error messages 2024-02-14 00:24:22 -05:00
Derrick Hammer 593d8ea381
refactor: ensure key is unique 2024-02-14 00:13:28 -05:00
Derrick Hammer 41a6772c9f
chore: emailverifier no longer needed here 2024-02-13 23:32:23 -05:00
Derrick Hammer 143a563a51
refactor: param consistency changes 2024-02-13 23:31:08 -05:00
Derrick Hammer 8b9471aa04
refactor: switch to GetUserFromContext 2024-02-13 23:29:48 -05:00
Derrick Hammer 634a285ea8
refactor: use uint and param consistency changes 2024-02-13 23:29:36 -05:00
Derrick Hammer 96758a5559
feat: add opt support to account, refactor validation api 2024-02-13 23:23:45 -05:00
Derrick Hammer 431dec55f9
feat: otp api support 2024-02-13 23:23:01 -05:00
Derrick Hammer 16689f6c31
feat: add GetUserFromContext 2024-02-13 23:22:36 -05:00
Derrick Hammer 9d25690d72
feat: add OTP to user model 2024-02-13 23:21:53 -05:00
Derrick Hammer 93e7563bed
refactor: don't use pointer receiver 2024-02-13 22:38:13 -05:00
Derrick Hammer f645499c7f
refactor: prefix all jwt helpers 2024-02-13 22:31:44 -05:00
Derrick Hammer 50c4d8b945
refactor: clean up use of auth middleware in s5 2024-02-13 22:25:50 -05:00
Derrick Hammer 0b3d54e7c5
refactor: major middleware refactor 2024-02-13 22:17:34 -05:00
Derrick Hammer 9f6f2c9c87
feat: add purpose to jwt with consts 2024-02-13 20:58:17 -05:00
Derrick Hammer 764a7cbdaf
feat: add iat to jwt 2024-02-13 20:53:38 -05:00
Derrick Hammer 171b810504
fix: remove duplicate check 2024-02-13 20:01:45 -05:00
Derrick Hammer 8423578bdd
refactor: more re-organizing 2024-02-13 20:00:16 -05:00
Derrick Hammer f5bb0fa45f
chore: remove un-used function 2024-02-13 19:58:24 -05:00
Derrick Hammer 2d3b755cb2
refactor: update use of LoginPassword 2024-02-13 19:58:00 -05:00
Derrick Hammer 9b748e1f57
refactor: de-duplicate login logic and re-organize code 2024-02-13 19:57:41 -05:00
Derrick Hammer 40b830d669
refactor: update account login and ip address in LoginPassword 2024-02-13 19:52:18 -05:00
Derrick Hammer 23113d0f9c
refactor: create updateAccountInfo 2024-02-13 19:49:08 -05:00
Derrick Hammer 6f61f09ba4
refactor: move email validation to gorm 2024-02-13 19:41:00 -05:00
Derrick Hammer 302821d749
refactor: more refactoring of methods and move some validation to gorm 2024-02-13 19:36:23 -05:00
Derrick Hammer 3e629cf46e
refactor: update usage of account methods 2024-02-13 19:29:37 -05:00
Derrick Hammer bbb68aecb5
refactor: use EmailExists and add logging 2024-02-13 19:28:23 -05:00
Derrick Hammer 99c440ab88
refactor: unify account exists methods 2024-02-13 19:28:04 -05:00
Derrick Hammer 5598660176
refactor: un-export all s5 http handlers 2024-02-13 19:10:24 -05:00
Derrick Hammer 3c55ed2853
feat: add initial account services api 2024-02-13 19:07:24 -05:00
Derrick Hammer 75d9c7f46e
feat: add first and last name to user model 2024-02-13 19:06:04 -05:00
Derrick Hammer 78accd1f02
fix: add missing content responses to upload api's 2024-02-13 00:32:16 -05:00
Derrick Hammer 04948bde2c
fix: duplicate imports 2024-02-09 20:35:48 -05:00
Derrick Hammer 94fd1a6af0
fix: put buf generate command 1st 2024-02-09 16:06:07 -05:00
Derrick Hammer a43957b1db
chore: unneeded const 2024-02-09 15:58:44 -05:00
Derrick Hammer 227ac9b403
fix: wrong proof extension 2024-02-09 15:58:15 -05:00
Derrick Hammer a4afda0ecc
feat: add proof download s5 protocol file discovery 2024-02-09 15:55:04 -05:00
Derrick Hammer 0c00e2e7d9
feat: add proof download support to download endpoint 2024-02-09 15:49:34 -05:00
Derrick Hammer 850b575e1c
feat: add Proof method to fetch bao file 2024-02-09 15:43:38 -05:00
Derrick Hammer 28d966cbe2
refactor: switch to using a Params struct and store Renter 2024-02-09 15:42:53 -05:00
Derrick Hammer e7ac46de32
fix: update method calls and hash object usage 2024-02-09 15:28:50 -05:00
Derrick Hammer fc9724df2c
refactor: change PutFileSmall return value 2024-02-09 15:28:28 -05:00
Derrick Hammer c790c525ae
feat: implement adding bao hashing and uploading proofs 2024-02-09 15:23:33 -05:00
Derrick Hammer 2a1abb852b
refactor: have bao hash return totalReadSize 2024-02-09 15:22:46 -05:00
Derrick Hammer fc61da0d01
feat: add bao blake3 support via go-plugin grpc 2024-02-09 15:05:16 -05:00
Derrick Hammer b939ea109c
refactor: add tags to swagger 2024-02-07 20:31:42 -05:00
Derrick Hammer 7c945f0a2d
refactor: clean up and remove version check 2024-02-07 20:21:44 -05:00
Derrick Hammer 89cdd01698
fix: update swagger json url 2024-02-07 20:20:13 -05:00
Derrick Hammer 8e04de591e
fix: add content type header to byteHandler 2024-02-07 20:19:56 -05:00
Derrick Hammer 172b040365
refactor: more refactoring on swagger handlers 2024-02-07 20:19:22 -05:00
Derrick Hammer 279cc484fc
refactor: merge flowchartsman/swaggerui into our own code base to simplify routing 2024-02-07 19:18:11 -05:00
Derrick Hammer 69ae351d94
fix: schema fixes 2024-02-07 18:25:20 -05:00
Derrick Hammer 192ac364c5
fix: schema fixes 2024-02-07 18:14:43 -05:00
Derrick Hammer 0f3f92442a
fix: schema fixes 2024-02-07 17:56:16 -05:00
Derrick Hammer be92e036f3
feat: initial swagger support 2024-02-07 17:39:18 -05:00
Derrick Hammer d13c15212c
fix: need to create a special mock handler for tus cors, and make cores a dedicated middleware function 2024-02-03 20:11:47 -05:00
Derrick Hammer 6655abe61b
fix: need to allow Upload-Length through cors 2024-02-03 20:02:11 -05:00
Derrick Hammer 8a1e586b28
fix: need to allow Upload-Concat through cors 2024-02-03 20:00:40 -05:00
Derrick Hammer 39b4977d52
fix: need to allow Expires through cors 2024-02-03 19:59:43 -05:00
Derrick Hammer 130abe6098
fix: need to allow Authorization through cors 2024-02-03 19:58:17 -05:00
Derrick Hammer 5784afe064
fix: need to create custom cors middleware instance for tus 2024-02-03 19:55:32 -05:00
Derrick Hammer 6894cd7e54
refactor: put cors middleware 1st 2024-02-03 19:46:02 -05:00
Derrick Hammer 6323fbe166
dep: update jape fork with options support 2024-02-03 19:42:36 -05:00
Derrick Hammer d57a14d9a3
fix: need to add options routes 2024-02-03 19:34:55 -05:00
Derrick Hammer fd721077e5
fix: need to apply cors to tus upload 2024-02-03 19:25:13 -05:00
Derrick Hammer b82353cfa9
fix: fix need to add a root path on filename 2024-02-02 16:45:50 -05:00
Derrick Hammer a3846a8e07
fix: etag has to be provided to multipart complete 2024-02-01 21:14:44 -05:00
Derrick Hammer 185f5de87a
fix: ensure limit is greater than 0 2024-02-01 21:03:19 -05:00
Derrick Hammer 9063a80f8c
fix: need to use partNumber in MultipartCompletedPart 2024-02-01 19:35:05 -05:00
Derrick Hammer 5456773b81
fix: need to use a part number that is 1 indexed based 2024-02-01 19:32:06 -05:00
Derrick Hammer dc9b3b4fda
fix: add error handler if we max out our retries 2024-02-01 19:26:23 -05:00
Derrick Hammer c1915321e1
fix: if retry limit is reached pass the ErrRetryLimitReached error 2024-02-01 19:18:06 -05:00
Derrick Hammer 9825c904da
fix: remove defer chan close 2024-02-01 19:15:02 -05:00
Derrick Hammer d15ec4e81e
fix: need the reader to be created and closed inside the task 2024-02-01 19:02:24 -05:00
Derrick Hammer 9330bb36bf
fix: fix after and error functions 2024-02-01 18:51:05 -05:00
Derrick Hammer 0a6efaf0e3
fix: cron needs to be set on renter 2024-02-01 18:33:17 -05:00
Derrick Hammer 8abc41e46f
fix: pass by ref 2024-02-01 18:25:32 -05:00
Derrick Hammer 446e81ca19
fix: renter needs to be set on storage 2024-02-01 18:12:09 -05:00
Derrick Hammer 15b527933f
feat: implement initial version of multipart uploads 2024-02-01 02:03:04 -05:00
Derrick Hammer 95cfa393b4
fix: no need for singleton hack, use Replace not Decorate 2024-01-31 22:51:51 -05:00
Derrick Hammer 9a87004f31
fix: need to add lifecycle hook to start tusWorker 2024-01-31 21:28:31 -05:00
Derrick Hammer d88638dfc3
fix: need to invoke storage init to setup tus 2024-01-31 21:28:02 -05:00
Derrick Hammer aff6e8106c
refactor: split renter methods to a dedicated renter service 2024-01-31 21:27:38 -05:00
Derrick Hammer 6d34f5b683
refactor: change struct naming convention from "impl" to "default" 2024-01-31 20:29:27 -05:00
Derrick Hammer 8449b13a4a
dep: update libs5 2024-01-31 20:21:38 -05:00
Derrick Hammer 2de10a7401
feat: add cli switch fx-debug to disable our custom logger 2024-01-30 17:45:36 -05:00
Derrick Hammer 57c4a1ae7d
dep: update libs5 2024-01-30 17:26:52 -05:00
Derrick Hammer 7b24a2001c
dep: update libs5 2024-01-30 17:09:37 -05:00
Derrick Hammer e787efaa92
dep: update libs5 2024-01-30 16:54:47 -05:00
Derrick Hammer 8dec7fc75a
dep: update libs5 2024-01-30 16:07:58 -05:00
Derrick Hammer 56ddeef010
dep: update libs5 2024-01-30 16:01:31 -05:00
Derrick Hammer 791b291ce7
fic: add node init 2024-01-30 15:48:05 -05:00
Derrick Hammer 435fe8b754
dep: update libs5 2024-01-30 15:46:46 -05:00
Derrick Hammer 883504225e
fix: lifecycle setup needs to return nil 2024-01-30 14:53:50 -05:00
Derrick Hammer 64c3795876
fix: need to hack the condtructor and make a global var as the constructor is getting called twice? 2024-01-30 14:51:23 -05:00
Derrick Hammer 3a49375638
fix: need to not return logger, but decorate the one we do have and replace with the one in the config 2024-01-30 14:32:46 -05:00
Derrick Hammer e477d681d4
Revert "fix: use cfg.Logger"
This reverts commit b86b597cb8.
2024-01-30 14:28:50 -05:00
Derrick Hammer b86b597cb8
fix: use cfg.Logger 2024-01-30 14:21:55 -05:00
Derrick Hammer 9224dcd119
fix: db and logger need to be in S5ProtocolResult 2024-01-30 14:20:33 -05:00
Derrick Hammer a4737ab4b8
dep: update libs5 2024-01-30 14:17:48 -05:00
Derrick Hammer 45567fcda0
fix: update imports 2024-01-30 14:14:05 -05:00
Derrick Hammer 1721fbf832
fix: update imports 2024-01-30 00:33:57 -05:00
Derrick Hammer 73a1e1cffe
dep: update libs5 2024-01-30 00:32:01 -05:00
Derrick Hammer 2a745d6498
dep: update libs5 2024-01-29 22:39:21 -05:00
Derrick Hammer 741647c6dc
dep: update libs5 2024-01-29 22:36:28 -05:00
Derrick Hammer 27240f9f30
dep: update libs5 2024-01-29 22:31:29 -05:00
Derrick Hammer aaa39d810e
dep: update libs5 2024-01-29 22:26:31 -05:00
Derrick Hammer 81145ef404
dep: update libs5 2024-01-29 21:39:25 -05:00
Derrick Hammer 77ebdf9f4c
dep: update libs5 2024-01-29 21:05:01 -05:00
Derrick Hammer 34ba5b42f8
dep: update libs5 2024-01-29 20:53:07 -05:00
Derrick Hammer e034e1d54e
refactor: restructure s5 protocol/api to use new fx module and new library structure. Also move the proto/api to its own package for organization 2024-01-29 15:11:57 -05:00
Derrick Hammer 9b891f6ec7
dep: update libs5 2024-01-29 14:34:32 -05:00
Derrick Hammer 6b66250c08
dep: update libs5 2024-01-29 13:46:43 -05:00
Derrick Hammer a2ee46dbb3
refactor: change retry task to use the same job UUID, so we can track then 2024-01-28 16:39:04 -05:00
Derrick Hammer 4bb34315eb
feat: add support for tags in RetryableTask 2024-01-28 16:32:20 -05:00
Derrick Hammer 2a067102da
refactor: use new RetryableTask abstraction and move task function as a private method 2024-01-28 16:26:15 -05:00
Derrick Hammer 1af1ea9505
feat: create a cron job abstraction with a RetryableTask method, RetryableTaskParams struct, CronJob struct, and CreateJob method 2024-01-28 16:23:38 -05:00
Derrick Hammer b4e2e962e5
fix: we can use modules in the builders after all 2024-01-28 05:22:11 -05:00
Derrick Hammer d51c52b985
fix: bad address 2024-01-28 05:13:43 -05:00
Derrick Hammer 309cc50845
chore: cleanup imports 2024-01-28 05:09:14 -05:00
Derrick Hammer 46da3bdcee
fix: add invoke to ensure http server triggers 2024-01-28 05:08:19 -05:00
Derrick Hammer 47422524b8
refactor: move HTTP server to its own constructor 2024-01-28 05:07:13 -05:00
Derrick Hammer e6c1bab602
fix: set levels 2024-01-28 04:57:43 -05:00
Derrick Hammer 470bce2209
fix: fix fx logger 2024-01-28 04:53:44 -05:00
Derrick Hammer 18a54917a0
fix: add LifecyclesParams struct 2024-01-28 04:45:34 -05:00
Derrick Hammer 568ec2857a
fix: add LifecyclesParams struct 2024-01-28 04:44:33 -05:00
Derrick Hammer 610d5fe268
fix: add init to create an empty router 2024-01-28 04:44:16 -05:00
Derrick Hammer 6c31a0a79f
fix: result actually needs value group item without slice 2024-01-28 04:35:43 -05:00
Derrick Hammer 646a65f814
fix: need to use group tag 2024-01-28 04:23:58 -05:00
Derrick Hammer a410cc55f0
fix: dont return pointer 2024-01-28 04:22:49 -05:00
Derrick Hammer d212907f5d
fix: need pointer of logger 2024-01-28 04:19:29 -05:00
Derrick Hammer 4348ff6dfe
fix: HttpHandler needs a Result struct 2024-01-28 04:18:32 -05:00
Derrick Hammer 57516a2f4a
fix: wrong module name 2024-01-28 04:17:56 -05:00
Derrick Hammer 2aec82281d
fix: Protocol needs to ve a slice 2024-01-28 04:17:47 -05:00
Derrick Hammer c326d9b61b
fix: we cannot wrap the build into a module as it causes provide issues 2024-01-28 04:17:22 -05:00
Derrick Hammer 5fd601407f
chore: unused function 2024-01-28 04:02:40 -05:00
Derrick Hammer ceabb95f6d
fix: we need to pass out the api in both struct and interface form for fx to read it properly 2024-01-28 04:01:36 -05:00
Derrick Hammer 10ecaebbf2
fix: we need to pass out the protocol in both struct and interface form for fx to read it properly 2024-01-28 03:58:49 -05:00
Derrick Hammer 98c1784518
fix: S5ProviderStore cannot rely on S5Protocol, so we need to provide for it too 2024-01-28 03:57:59 -05:00
Derrick Hammer 2887a63a7a
fix: missing account module 2024-01-28 03:55:09 -05:00
Derrick Hammer faa7387106
fix: missing provide for gocron 2024-01-28 03:54:33 -05:00
Derrick Hammer ab3dd648e1
fix: missing SetupLifecycles 2024-01-28 03:29:52 -05:00
Derrick Hammer 9bb7a4cc83
fix: WithLogger is inside Decorate 2024-01-28 03:13:11 -05:00
Derrick Hammer 4a66be5b87
refactor: wrap in a module 2024-01-28 03:03:29 -05:00
Derrick Hammer 80484079d6
refactor: we dont need to register the router 2024-01-28 03:03:04 -05:00
Derrick Hammer 8c89796341
refactor: use a centralized list key for enabled protocols 2024-01-28 03:01:12 -05:00
Derrick Hammer f7057142eb
fix: add init func to s5 api 2024-01-28 02:51:35 -05:00
Derrick Hammer 55f515157d
refactor: move BuildS5TusApi and export middlewares to break import cycle 2024-01-28 02:48:02 -05:00
Derrick Hammer 92cddb40c3
dep: update aws dep 2024-01-28 02:40:44 -05:00
Derrick Hammer 2dc8fc56f5
refactor: epic refactor to use uber fx microframework/DI framework to manage dependency graph, remove the portal object, and remove the interfaces package 2024-01-28 02:20:59 -05:00
Derrick Hammer ad54cc70b3
fix: need to pass content type header before ServeContent 2024-01-25 19:18:35 -05:00
Derrick Hammer c051ef8e44
feat: add Mime to File 2024-01-25 19:17:48 -05:00
Derrick Hammer be27728b42
feat: need to support mime type in small uploads 2024-01-25 19:15:01 -05:00
Derrick Hammer f3be950ba7
feat: detect and add mime type to upload so we don't need to make extra requests on runtime 2024-01-25 19:05:52 -05:00
Derrick Hammer 00a58a3b98
feat: add mimetype column to tus_upload and upload 2024-01-25 18:58:56 -05:00
Derrick Hammer 541fcff779
feat: add cors support 2024-01-25 18:25:10 -05:00
Derrick Hammer 73bd74faeb
refactor: use storage NewFile 2024-01-25 16:34:31 -05:00
Derrick Hammer d21044baed
fix: exists is missing 2024-01-25 16:32:37 -05:00
Derrick Hammer e00922f49d
feat: add NewFile helper in storage 2024-01-25 16:31:05 -05:00
Derrick Hammer 15ba6e9695
feat: create interface for File 2024-01-25 16:30:45 -05:00
Derrick Hammer a90344daf0
refactor: use errors.Is 2024-01-25 10:39:45 -05:00
Derrick Hammer f4b981f97f
fix: remove return false 2024-01-25 09:58:53 -05:00
Derrick Hammer 46b407bd9a
feat: broadcast file if tus has the upload as well 2024-01-25 09:53:02 -05:00
Derrick Hammer e1556f2f68
feat: add support for ranges in tus s3 store 2024-01-25 09:50:17 -05:00
Derrick Hammer d91355796b
dep: use our tus fork again to have s3 range support 2024-01-25 09:44:37 -05:00
Derrick Hammer 8797460bf8
refactor: revert to using http.StripPrefix 2024-01-25 09:23:23 -05:00
Derrick Hammer 2c7300af6d
dep: switch back to mainline tus 2024-01-25 09:21:21 -05:00
Derrick Hammer fce08283a2
fix: both clients need specific api paths 2024-01-25 09:02:49 -05:00
Derrick Hammer 6d8beb0331
fix: need to set api path 2024-01-25 08:55:59 -05:00
Derrick Hammer 8f138a5df7
fix: wrong config keys 2024-01-25 08:48:05 -05:00
Derrick Hammer 263f34b89f
Revert "fix: need to pass protocol scheme"
This reverts commit 8528df5d96.
2024-01-25 08:47:38 -05:00
Derrick Hammer 8528df5d96
fix: need to pass protocol scheme 2024-01-25 08:44:54 -05:00
Derrick Hammer f544c30430
refactor: move to using renterd's bus and worker http clients 2024-01-25 08:37:15 -05:00
Derrick Hammer ce95437191
fix: reset read to false on init 2024-01-24 19:58:10 -05:00
Derrick Hammer 84bb08144b
fix: add a read state so we can noop a seek when we have not done anything yet 2024-01-24 19:47:25 -05:00
Derrick Hammer 14d8760c1f
fix: pass name to http.ServeContent 2024-01-24 19:23:42 -05:00
Derrick Hammer c3646fa4d4
refactor: have name return base58 cid 2024-01-24 19:23:17 -05:00
Derrick Hammer 7b7c705c0d
feat: add CID method 2024-01-24 19:22:31 -05:00
Derrick Hammer 6cac5c6a28
fix: actually implement Name 2024-01-24 19:16:50 -05:00
Derrick Hammer 6d998eeff4
fix: we need SeekEnd to spoof and just return the length of the file for http.ServeContent 2024-01-24 19:14:06 -05:00
Derrick Hammer bf15faf33f
feat: need else on hash check 2024-01-24 19:10:19 -05:00
Derrick Hammer bf25d7bfda
feat: use new file abstraction and use http.ServeContent 2024-01-24 19:08:13 -05:00
Derrick Hammer 2f9b684953
feat: add new File abstraction primarily to handle partial content/range requests 2024-01-24 19:07:35 -05:00
Derrick Hammer dcf05974e2
feat: add support for a range offset in GetFile 2024-01-24 19:05:54 -05:00
Derrick Hammer 95b57cffc0
fix: handle both cid and base64url hash 2024-01-24 17:15:21 -05:00
Derrick Hammer 562742fd8e
fix: disable auth for downloads temporarily 2024-01-24 17:07:50 -05:00
Derrick Hammer 47020fe738
fix: create StorageLocationTypeFull on StorageLocationTypeFull case 2024-01-24 16:43:17 -05:00
Derrick Hammer 5db53f6a21
dep: update libs5 2024-01-24 16:34:13 -05:00
Derrick Hammer f51c06bc24
fix: enable RespectForwardedHeaders 2024-01-24 14:46:46 -05:00
Derrick Hammer 4dd1f50dab
dep: update libs5 2024-01-24 13:56:24 -05:00
Derrick Hammer 4bb89fedf1
dep: update libs5 2024-01-24 13:53:48 -05:00
Derrick Hammer 523286df32
fix: use MaxUint32 2024-01-24 12:51:19 -05:00
Derrick Hammer 9b655b4a70
fix: preload uploads 2024-01-24 12:47:20 -05:00
Derrick Hammer 60c905181c
fix: manually encode list 2024-01-24 12:34:50 -05:00
Derrick Hammer c976ec31be
fix: create an AccountPinResponse with a custom msgpack encoder 2024-01-24 12:29:25 -05:00
Derrick Hammer e476ed4476
dep: update libs5 2024-01-24 11:59:15 -05:00
Derrick Hammer cd74127a6b
dep: update libs5 2024-01-24 11:51:40 -05:00
Derrick Hammer 4b9e362437
dep: update libs5 2024-01-24 11:23:41 -05:00
Derrick Hammer cc6df265d4
dep: update libs5 2024-01-24 11:11:18 -05:00
Derrick Hammer 7f2e38291e
dep: update libs5 2024-01-24 10:58:45 -05:00
Derrick Hammer fe2d1be764
fix: wrong download url 2024-01-24 10:32:44 -05:00
Derrick Hammer 77a4561f55
Revert "debug: revert tusd override"
This reverts commit 60a591f70b.
2024-01-24 09:51:13 -05:00
Derrick Hammer b3f8b483db
Revert "debug: revert prefix hack"
This reverts commit 9002064937.
2024-01-24 09:51:13 -05:00
Derrick Hammer 9002064937
debug: revert prefix hack 2024-01-24 09:34:05 -05:00
Derrick Hammer 60a591f70b
debug: revert tusd override 2024-01-24 09:26:38 -05:00
Derrick Hammer a93eca6a7c
fix: wrong download subdomain 2024-01-24 03:41:00 -05:00
Derrick Hammer fdfdf9b6a9
dep: update libs5 2024-01-24 03:38:09 -05:00
Derrick Hammer a4137102e6
fix: import cycle 2024-01-24 03:36:03 -05:00
Derrick Hammer 6a2b1b4a9b
feat: implement provider store 2024-01-24 03:28:47 -05:00
Derrick Hammer 35fa1c5e0d
dep: update libs5 2024-01-24 03:02:43 -05:00
Derrick Hammer 87fb81bf97
fix: wrong endpoint 2024-01-24 02:03:14 -05:00
Derrick Hammer 27cbe2d886
fix: wrong hash encoding 2024-01-24 02:00:53 -05:00
Derrick Hammer 5d715fcac4
fix: wrong endpoint 2024-01-24 01:59:09 -05:00
Derrick Hammer efcd5b0b8a
fix: set bucket to protocol name 2024-01-24 01:57:09 -05:00
Derrick Hammer 1948b9e332
dep: update libs5 2024-01-24 01:52:28 -05:00
Derrick Hammer 22eacc4af1
feat: implement /s5/download/:cid 2024-01-24 01:27:05 -05:00
Derrick Hammer 12093637ed
feat: add initial version of GetFile 2024-01-24 01:26:40 -05:00
Derrick Hammer fb1112f3a2
fix: we need to pin the file after creating the upload 2024-01-22 19:08:56 -05:00
Derrick Hammer 8df2ee9ee8
fix: we need to manually delete both the uploaded buffer file and the metafile 2024-01-22 19:06:28 -05:00
Derrick Hammer 7b96682ce0
feat: compute metadata file id and delete it 2024-01-22 18:54:19 -05:00
Derrick Hammer 1643dacdd4
refactor: store s3 client 2024-01-22 18:53:31 -05:00
Derrick Hammer 0eb67cd8da
refactor: add s3 import 2024-01-22 18:53:05 -05:00
Derrick Hammer 11e533577b
refactor: return bytes count from hashing to use for upload record 2024-01-22 18:52:37 -05:00
Derrick Hammer da298cc56f
refactor: return s3 client instance with BuildUploadBufferTus 2024-01-22 18:51:09 -05:00
Derrick Hammer bf36562fca
feat: create upload record after sending to renterd 2024-01-22 18:25:11 -05:00
Derrick Hammer 3fcb897e7a
fix: need to borrow the check from finishUploadIfComplete and invert it 2024-01-22 18:02:15 -05:00
Derrick Hammer 941ce27293
fix: only process completed upload if its final 2024-01-22 17:49:42 -05:00
Derrick Hammer e0c6c88e75
feat: add TusUploadCompleted method 2024-01-22 17:49:03 -05:00
Derrick Hammer aab4bb4d69
refactor: add completed column to tus_upload 2024-01-22 17:48:32 -05:00
Derrick Hammer 35cd041978
fix: can't use JapeMiddlewareFunc in type switch to cast 2024-01-22 17:06:39 -05:00
Derrick Hammer 2020a9f1d1
fix: change HttpMiddlewareFunc to not have a variable name 2024-01-22 17:03:54 -05:00
Derrick Hammer 6402410d75
fix: use HttpMiddlewareFunc 2024-01-22 17:00:57 -05:00
Derrick Hammer 2cc600b78b
fix: api routes need to use ApplyMiddlewares 2024-01-22 16:59:14 -05:00
Derrick Hammer 527334f829
refactor: create generic AdaptMiddleware factory and change ApplyMiddlewares to take interfaces and handle multiple situations 2024-01-22 16:50:03 -05:00
Derrick Hammer dd857650e0
fix: need create a compound index on hash and deleted at 2024-01-21 01:41:04 -05:00
Derrick Hammer d86e0e0105
fix: need to use where 2024-01-21 01:06:14 -05:00
Derrick Hammer e0de290cff
fix: pass model protocol property 2024-01-21 00:48:50 -05:00
Derrick Hammer 3e0246df28
fix: set client timeout to a high number for now 2024-01-21 00:19:04 -05:00
Derrick Hammer ed15133659
dep: upgrade to 1.21 2024-01-21 00:09:11 -05:00
Derrick Hammer ae4901757b
fix: log PutFile error 2024-01-21 00:08:49 -05:00
Derrick Hammer e73fa0a103
refactor: switch to github.com/imroc/req as go-resty can't handle streaming uploads 2024-01-20 23:36:55 -05:00
Derrick Hammer c2cccc4b84
fix: pass bucket as query arg, not as form data 2024-01-20 22:51:00 -05:00
Derrick Hammer 6bee380e75
fix: retain only the 1st 32 bytes 2024-01-20 12:30:18 -05:00
Derrick Hammer bcf2998faa
fix: need to get a new reader to stream the file 2024-01-20 12:26:31 -05:00
Derrick Hammer 7d80efb278
Revert "fix: start cron in new coroutine"
This reverts commit 08e034b1
2024-01-20 12:18:43 -05:00
Derrick Hammer 3a74e75a57
fix: portal missing in constructor 2024-01-20 12:05:41 -05:00
Derrick Hammer 521b37b642
debug: debug scheduler 2024-01-20 12:02:03 -05:00
Derrick Hammer 08e034b1cf
fix: start cron in new coroutine 2024-01-20 11:48:10 -05:00
Derrick Hammer b10798d71f
fix: update TusLock to use a compound unique index to work with soft deletes 2024-01-20 11:47:44 -05:00
Derrick Hammer 43c4590439
refactor: use time.Ticker 2024-01-20 11:16:54 -05:00
Derrick Hammer ea4a22c52d
fix: remove duplicate chan close 2024-01-20 11:07:06 -05:00
Derrick Hammer 6bbfac661a
fix: call released on ctx done 2024-01-20 11:06:50 -05:00
Derrick Hammer 112fbb4c51
refactor: moved anon func to private released method 2024-01-20 11:04:43 -05:00
Derrick Hammer 5465cf7a63
fix: RequestRelease needs to be inside for, but after we check for a non-busy error 2024-01-20 10:48:42 -05:00
Derrick Hammer 60e917120d
fix: only add slash if path is empty 2024-01-20 10:37:33 -05:00
Derrick Hammer c5b0865977
fix: need to create a custom version of strip prefix that appends a trailing slash for the router 2024-01-20 10:33:18 -05:00
Derrick Hammer 9f7f819369
dep: update tus 2024-01-20 10:15:44 -05:00
Derrick Hammer e201f97e0b
dep: replace tus with fork 2024-01-20 10:10:48 -05:00
Derrick Hammer 75e7ba00cd
fix: query needs to be manually built and re-encoded 2024-01-20 08:22:51 -05:00
Derrick Hammer 07c36109d8
refactor: move strip prefix to be last 2024-01-20 08:15:27 -05:00
Derrick Hammer 24e841ae97
fix: use new copy of request 2024-01-20 08:13:17 -05:00
Derrick Hammer a1e7cda659
refactor: move injectJwt to be processed after authMiddlewareFunc 2024-01-20 08:12:50 -05:00
Derrick Hammer 9bf10b19bf
refactor: add current request to tusJwtResponseWriter 2024-01-20 08:12:30 -05:00
Derrick Hammer 73fa265939
feat: implement tusJwtResponseWriter WriteHeader 2024-01-20 08:06:50 -05:00
Derrick Hammer a56fa20b6d
refactor: split findAuthToken to parseAuthTokenHeader 2024-01-20 08:03:26 -05:00
Derrick Hammer 1d1c552a0a
refactor: explicitly check the path and only for post 2024-01-20 07:57:09 -05:00
Derrick Hammer a2051acff1
feat: add initial tusJwtResponseWriter bones so we can append the auth_token to tus urls 2024-01-20 07:54:24 -05:00
Derrick Hammer 4378da70da
refactor: create and export GenerateTokenWithDuration and GenerateToken 2024-01-20 07:30:46 -05:00
Derrick Hammer 8c86ecc5b7
fix: TusUploadExists needs to operate on TusUpload not Upload 2024-01-20 07:05:27 -05:00
Derrick Hammer e8fbe46dfc
fix: uploaderID is uint64 2024-01-20 06:57:57 -05:00
Derrick Hammer 0ab70dcaa5
fix: if we have errors at the CreatedUploads hook, cancel the upload 2024-01-20 06:41:51 -05:00
Derrick Hammer af5b6241bf
fix: NotifyCreatedUploads needs to be enabled 2024-01-20 06:27:11 -05:00
Derrick Hammer b50c6c9f85
fix: dont put leading slash in prefix 2024-01-20 06:03:07 -05:00
Derrick Hammer 43e52e1ae1
fix: wrong prefix 2024-01-20 06:01:42 -05:00
Derrick Hammer fdef217078
fix: need to add a strip prefix middleware 2024-01-20 05:50:54 -05:00
Derrick Hammer fba3ee4213
fix: we don't need to wrap the tus middleware as NewHandler does it for us 2024-01-20 05:45:13 -05:00
Derrick Hammer eb063a8954
dep: replace jape with fork with HEAD support 2024-01-20 05:20:23 -05:00
Derrick Hammer eaa515345e
feat: add startCron to start list 2024-01-19 17:48:42 -05:00
Derrick Hammer 48e3c690ce
feat: register storage service with cron 2024-01-19 17:47:14 -05:00
Derrick Hammer 16a3b531ac
fix: add RegisterService 2024-01-19 17:45:52 -05:00
Derrick Hammer c397cc9fcb
fix: add initCron to init list 2024-01-19 17:44:08 -05:00
Derrick Hammer 60c7cc5c6c
refactor: fix import cycles 2024-01-19 17:12:26 -05:00
Derrick Hammer 1f7c05434a
refactor: fix import cycles 2024-01-19 17:11:16 -05:00
Derrick Hammer 72219eb59c
feat: add tus endpoints 2024-01-19 17:08:55 -05:00
Derrick Hammer 2e64b56115
refactor: need to use middleware package 2024-01-19 17:08:05 -05:00
Derrick Hammer 5b1838a63b
feat: create tus api builder 2024-01-19 17:06:41 -05:00
Derrick Hammer 26c28db1f2
feat: create ApplyMiddlewares helper 2024-01-19 17:05:18 -05:00
Derrick Hammer e9db71f3b8
refactor: move middleware to its own package to prevent import cycles 2024-01-19 17:04:25 -05:00
Derrick Hammer 4c92750dd0
feat: add tus getter 2024-01-19 16:51:41 -05:00
Derrick Hammer 2c30477465
refactor: update putfile and gethash call names 2024-01-19 15:52:26 -05:00
Derrick Hammer 6acf8a606a
feat: initial tus protocol and processing support 2024-01-19 15:51:31 -05:00
Derrick Hammer b50c16ff2a
feat: implement a tus locker based on mysql 2024-01-19 15:50:09 -05:00
Derrick Hammer 6a8936b9c9
feat: add initial tus db models 2024-01-19 15:49:41 -05:00
Derrick Hammer 1f8ad3ac1a
feat: s3 config key to required config 2024-01-19 15:47:18 -05:00
Derrick Hammer 0eb6a9a3a3
feat: added a cron service 2024-01-19 15:46:37 -05:00
Derrick Hammer 5323e43bdb
fix: initDatabase no longer needs to pass p to init 2024-01-19 15:45:02 -05:00
Derrick Hammer fbbb22145d
dep: add aws sdk, gocron, and tusd 2024-01-19 15:44:18 -05:00
Derrick Hammer 2693c892a4
refactor: use service interface 2024-01-19 12:49:06 -05:00
Derrick Hammer 2500b3f047
feat: add generic service interface 2024-01-19 12:48:37 -05:00
Derrick Hammer 5b6084986f
refactor: init doesn't need portal passed 2024-01-19 12:43:16 -05:00
Derrick Hammer 8044591697
feat: support auth in header, cookie, and query arg 2024-01-18 20:56:49 -05:00
Derrick Hammer 82f34726d6
debug: add logging 2024-01-18 14:38:15 -05:00
Derrick Hammer 12de0342f5
debug: add logging 2024-01-18 14:28:32 -05:00
Derrick Hammer 8161d36f0e
fix: bad response for existing file 2024-01-18 13:53:04 -05:00
Derrick Hammer 17fdad7d07
dep: update libs5 2024-01-18 13:52:46 -05:00
Derrick Hammer 31b6a70180
dep: update libs5 2024-01-18 12:28:46 -05:00
Derrick Hammer 677635aa08
refactor: remove auth from metadata endpoint 2024-01-18 12:18:45 -05:00
Derrick Hammer 1a62ab2855
dep: update libs5 2024-01-18 12:16:17 -05:00
Derrick Hammer ad0e6964eb
dep: update libs5 2024-01-18 12:12:20 -05:00
Derrick Hammer 4e72ddbde8
dep: update libs5 2024-01-18 10:18:06 -05:00
Derrick Hammer f957ef5d78
feat: implement /s5/metadata/:cid 2024-01-17 22:19:42 -05:00
Derrick Hammer 510a57162c
feat: implement /s5/debug/storage_locations/:hash 2024-01-17 22:02:36 -05:00
Derrick Hammer 0c88e80a66
feat: implement /s5/blob/:cid 2024-01-17 21:23:33 -05:00
Derrick Hammer dfd03673c9
fix: pin the file if it exists 2024-01-17 17:21:15 -05:00
Derrick Hammer 6545faad6a
refactor: have PinByHash use PinByID 2024-01-17 17:19:46 -05:00
Derrick Hammer 310c23b95e
refactor: have PinByID check for a pin before adding one 2024-01-17 17:18:58 -05:00
Derrick Hammer 7fde67aea5
fix: use PinByID and pin after creating the upload 2024-01-17 17:16:12 -05:00
Derrick Hammer b56a8ba5ac
feat: add PinByID 2024-01-17 17:14:45 -05:00
Derrick Hammer 8ff09b5f02
dep: update libs5 2024-01-17 17:03:39 -05:00
Derrick Hammer b3e1840fac
fix: missing http verb 2024-01-17 16:59:53 -05:00
Derrick Hammer 72c3167e5f
feat: implement POST /s5/registry/subscription 2024-01-17 16:46:13 -05:00
Derrick Hammer acb9604b02
feat: implement POST /s5/registry 2024-01-17 16:20:51 -05:00
Derrick Hammer cde3f90d2d
feat: implement GET /s5/registry 2024-01-17 16:05:31 -05:00
Derrick Hammer 1fcd7fdfdc
dep: update libs5 2024-01-17 16:04:35 -05:00
Derrick Hammer 7248570e6b
feat: implement /s5/debug/download_urls/:cid 2024-01-17 15:36:21 -05:00
Derrick Hammer 32be5fe6e1
dep: update libs5 2024-01-17 15:36:13 -05:00
Derrick Hammer c338a41efd
refactor: use CreateUpload 2024-01-17 14:49:35 -05:00
Derrick Hammer 5fec2f08ff
feat: implement /s5/upload/directory 2024-01-17 14:46:37 -05:00
Derrick Hammer 8c4687fd67
feat: add storage CreateUpload 2024-01-17 14:46:22 -05:00
Derrick Hammer d16731807c
dep: update libs5 2024-01-17 14:23:51 -05:00
Derrick Hammer 6a8a3c436a
feat: implement /s5/pin/:cid 2024-01-17 13:13:37 -05:00
Derrick Hammer 66dabf5150
feat: implement /s5/delete/:cid 2024-01-17 13:04:32 -05:00
Derrick Hammer 1a5aaa3927
feat: add DeletePinByHash 2024-01-17 13:03:52 -05:00
Derrick Hammer a5cbb4c4fb
fix: tier to use AccountTier struct 2024-01-17 12:38:52 -05:00
Derrick Hammer 1cf2d9880c
feat: add /s5/account/pins.bin endpoint 2024-01-17 12:33:05 -05:00
Derrick Hammer cc61a090b6
feat: add AccountPins 2024-01-17 12:32:50 -05:00
Derrick Hammer cf422aef0e
feat: add /s5/account/stats endpoint 2024-01-17 12:03:08 -05:00
Derrick Hammer ef872bf344
feat: add /s5/account endpoint 2024-01-17 11:52:54 -05:00
Derrick Hammer 897fec75ad
fix: update use of CIDFromHash 2024-01-17 11:31:48 -05:00
Derrick Hammer da86c96c87
dep: update libs5 2024-01-17 11:30:26 -05:00
Derrick Hammer 708bd82879
fix: need to seek to reset reader 2024-01-17 11:04:24 -05:00
Derrick Hammer bccd919872
dep: update libs5 2024-01-17 10:35:13 -05:00
Derrick Hammer 78f789b2c0
dep: update libs5 2024-01-17 10:18:06 -05:00
Derrick Hammer f779a61a76
dep: update libs5 2024-01-17 09:43:31 -05:00
Derrick Hammer 03f0d80ae5
fix: use AuthUserIDKey in context 2024-01-17 09:09:48 -05:00
Derrick Hammer a41cdbf52c
fix: use the userid context key 2024-01-17 09:05:39 -05:00
Derrick Hammer 1d019d905b
fix: use a type switch 2024-01-17 09:02:13 -05:00
Derrick Hammer af71f68ea9
fix: needs to be uint64 2024-01-17 08:58:58 -05:00
Derrick Hammer bf65e845f3
fix: access sub directly 2024-01-17 08:57:45 -05:00
Derrick Hammer e9aa676d94
fix: use public key, not private 2024-01-17 08:53:10 -05:00
Derrick Hammer 224d7a636d
fix: need to check the decoded public key in hex format 2024-01-17 08:47:58 -05:00
Derrick Hammer e31672aad0
feat: add support for checking the user account and storing in a new context 2024-01-17 08:43:32 -05:00
Derrick Hammer 4ae272205a
feat: add AccountExists method 2024-01-17 08:37:01 -05:00
Derrick Hammer ae0bddf3d1
feat: add jwt auth middleware 2024-01-17 08:16:03 -05:00
Derrick Hammer 1054c52e2f
fix: if email is empty set to a dummy address based on the pubkey 2024-01-16 16:16:37 -05:00
Derrick Hammer dd66f560ef
fix: make email optional 2024-01-16 16:15:50 -05:00
Derrick Hammer d237b42314
fix: if pubkey isnt registered, error 2024-01-16 16:05:28 -05:00
Derrick Hammer f8d0dc6787
fix: delete challenges after they have been used 2024-01-16 15:48:45 -05:00
Derrick Hammer 9cc05b3096
fix: wrong model 2024-01-16 15:42:42 -05:00
Derrick Hammer 6c34b383d7
fix: verify the response, not the challenge 2024-01-16 15:37:08 -05:00
Derrick Hammer bc5957f881
fix: need to pass the pubkey without the prefix and encoded to hex 2024-01-16 15:30:00 -05:00
Derrick Hammer accffade40
fix: update error handling 2024-01-16 15:26:57 -05:00
Derrick Hammer f34f009f17
fix: key needs to be 33 bytes 2024-01-16 15:26:36 -05:00
Derrick Hammer e1709a7910
fix: update error handling 2024-01-16 15:23:00 -05:00
Derrick Hammer d74d29e2c5
fix: check that key is 33 bytes and is a ed25519 2024-01-16 15:22:48 -05:00
Derrick Hammer 09216e2817
fix: challenge needs to be base64url encoded 2024-01-16 15:12:05 -05:00
Derrick Hammer db46fcd774
fix: check the challenge substring 2024-01-16 14:45:07 -05:00
Derrick Hammer 1b680dd399
fix: validate without key prefix 2024-01-16 14:43:26 -05:00
Derrick Hammer dd5c6332f3
fix: response must be 65 bytes 2024-01-16 14:32:53 -05:00
Derrick Hammer 5d393c3915
fix: pubkey missing from challenge record 2024-01-16 14:22:16 -05:00
Derrick Hammer 3be1042def
fix: add S5Challenge to AutoMigrate 2024-01-16 14:18:36 -05:00
Derrick Hammer 56b99f3c14
dep: update libs5 2024-01-16 14:07:22 -05:00
Derrick Hammer 40479d8bb3
fix: wrong account paths 2024-01-16 13:57:33 -05:00
Derrick Hammer ec9026c8b1
feat: implement AccountLogin 2024-01-16 13:56:25 -05:00
Derrick Hammer eee1faab18
feat: implement AccountLoginChallenge 2024-01-16 13:51:03 -05:00
Derrick Hammer 17441ff674
refactor: use setAuthCookie helper 2024-01-16 13:38:10 -05:00
Derrick Hammer 891ca20a72
feat: implement AccountRegister, and switch to using structs for request/response 2024-01-16 13:32:47 -05:00
Derrick Hammer 2512a6bdaf
dep: add jwt, email-verifier, and bcrypt 2024-01-16 13:31:29 -05:00
Derrick Hammer 9ad8d70f09
feat: implement EmailExists, PubkeyExists, CreateAccount,AddPubkeyToAccount, LoginPassword, LoginPubkey 2024-01-16 13:30:36 -05:00
Derrick Hammer 1bbedecad9
refactor: remove username from user 2024-01-16 13:28:33 -05:00
Derrick Hammer c4f0226d1a
refactor: re-implement s5 routes 2024-01-16 12:20:43 -05:00
Derrick Hammer 69b1938e87
feat: add AccountRegisterChallenge handler 2024-01-16 11:31:33 -05:00
Derrick Hammer a62c6daa4a
feat: add S5 challenge model 2024-01-16 11:31:25 -05:00
Derrick Hammer fb136234a9
dep: update libs5 2024-01-16 11:30:44 -05:00
Derrick Hammer 4b581d5879
dep: update libs5 2024-01-16 10:25:38 -05:00
Derrick Hammer 185269afea
feat: add upload create call 2024-01-16 02:01:18 -05:00
Derrick Hammer cc5fadeef3
fix: remove debug line 2024-01-16 01:54:46 -05:00
Derrick Hammer aa2ee9eee2
fix: handle both when IsError is true and when we have an error object 2024-01-16 01:36:19 -05:00
Derrick Hammer d017b0741c
fix: use DatabaseService 2024-01-16 01:30:06 -05:00
Derrick Hammer af4a2eed4a
fix: use Database not Db 2024-01-16 01:29:29 -05:00
Derrick Hammer d36bf67e85
fix: remove duplicate Db getter 2024-01-16 01:29:17 -05:00
Derrick Hammer c2075989fa
fix: need a Database and DatabaseService getter, and a getter on Database 2024-01-16 01:24:47 -05:00
Derrick Hammer 0c5827ce0b
Revert "fix: try to deref?"
This reverts commit a19af267cf.
2024-01-16 01:17:46 -05:00
Derrick Hammer a19af267cf
fix: try to deref? 2024-01-16 01:14:42 -05:00
Derrick Hammer 58f734d3b3
fix: use a 32 byte hash, not 64 2024-01-16 01:08:39 -05:00
Derrick Hammer 276719f47f
feat: handle existing files 2024-01-16 01:05:09 -05:00
Derrick Hammer a4e0e1fa58
refactor: have FileExists return the upload model if it exists 2024-01-16 01:01:57 -05:00
Derrick Hammer 866d105028
refactor: rename CIDExists to FileExists and have it work on hashes and check in hex format 2024-01-16 00:58:51 -05:00
Derrick Hammer 48f03c0f47
refactor: add Size field 2024-01-16 00:54:32 -05:00
Derrick Hammer 441e07f00e
refactor: rename ProtocolType 2024-01-16 00:53:39 -05:00
Derrick Hammer f986e3e483
refactor: uploads should not store the cid but just a hash 2024-01-16 00:52:55 -05:00
Derrick Hammer 61696f42b8
feat: add GetHash 2024-01-16 00:48:06 -05:00
Derrick Hammer 62e22d0d39
feat: add CIDExists 2024-01-16 00:40:50 -05:00
Derrick Hammer 47602854a0
feat: bare bones account service 2024-01-16 00:19:36 -05:00
Derrick Hammer 4a408a179d
dep: add jwt 2024-01-16 00:11:21 -05:00
Derrick Hammer 4976874453
fix: ensure blocklist is singular 2024-01-16 00:11:12 -05:00
Derrick Hammer 86c80aefaa
fix: fix user relationship 2024-01-16 00:05:25 -05:00
Derrick Hammer d30d2f34b5
fix: correct db name setting 2024-01-16 00:00:54 -05:00
Derrick Hammer 539d5ead65
fix: make core.db.name required 2024-01-15 23:59:58 -05:00
Derrick Hammer cd7790834b
fix: provide a default database name 2024-01-15 23:59:18 -05:00
Derrick Hammer 4b56de03a3
fix: fix policies 2024-01-15 23:56:44 -05:00
Derrick Hammer 1ccc5d1141
fix: fix policies 2024-01-15 23:46:54 -05:00
Derrick Hammer 46dbfe77bc
fix: update def's 2024-01-15 23:41:13 -05:00
Derrick Hammer e40111a276
fix: sec was not used 2024-01-15 23:15:57 -05:00
Derrick Hammer b2d863d466
Revert "fix: refactor AddPolicy"
This reverts commit 17f16c53af.
2024-01-15 23:08:44 -05:00
Derrick Hammer 17f16c53af
fix: refactor AddPolicy 2024-01-15 23:07:44 -05:00
Derrick Hammer f0b92aa2d6
fix: need to load policies 2024-01-15 22:53:51 -05:00
Derrick Hammer 87ebf5012b
fix: policies need to be added after 2024-01-15 22:53:12 -05:00
Derrick Hammer 212832eda1
refactor: since Portal.Logger can return a temporary logger, just use it 2024-01-15 22:44:28 -05:00
Derrick Hammer a8a39d523e
fix: if logger is not ready, use a temporary one 2024-01-15 22:42:55 -05:00
Derrick Hammer 27ad581026
fix: port default needs to be a number 2024-01-15 22:36:17 -05:00
Derrick Hammer 6076073dce
fix: logger needs to be inited before config check 2024-01-15 22:35:00 -05:00
Derrick Hammer 51a1f6051b
fix: implement database getter 2024-01-15 22:02:10 -05:00
Derrick Hammer 05c0aba484
feat: initial casbin support 2024-01-15 22:01:40 -05:00
Derrick Hammer b9fab1a4b5
fix: add db config to required config 2024-01-15 21:00:08 -05:00
Derrick Hammer 3a44305c44
refactor: rename to startProtocolRegistry 2024-01-15 20:58:53 -05:00
Derrick Hammer 82fe380b02
feat: initial database support and models 2024-01-15 20:58:20 -05:00
Derrick Hammer 2040d4edbc
fix: break import cycle 2024-01-15 20:10:15 -05:00
Derrick Hammer 66e1cba39b
refactor: move init and startup process to a functional approach for readability 2024-01-15 20:07:08 -05:00
Derrick Hammer 8ceb8c1eb9
refactor: logger does not need to be a singleton 2024-01-15 19:43:00 -05:00
Derrick Hammer eae3de419c
deps: update libs5 2024-01-15 19:35:03 -05:00
Derrick Hammer 6784cbf453
refactor: optimize 2024-01-15 15:16:48 -05:00
Derrick Hammer 387ec56bec
fix: disable resty warnings 2024-01-15 14:45:35 -05:00
Derrick Hammer 0cd60b7db0
fix: use CIDFromHash 2024-01-15 14:41:24 -05:00
Derrick Hammer 699869ca3e
fix: wrong payload to bucket create 2024-01-15 14:36:06 -05:00
Derrick Hammer 138b7a8e8c
fix: wrong bucket create endpoint 2024-01-15 14:33:51 -05:00
Derrick Hammer d4ed4eb9a2
fic: add createBucketIfNotExists helper 2024-01-15 14:25:12 -05:00
Derrick Hammer dc239b0cba
dep: update libs5 2024-01-15 14:14:04 -05:00
Derrick Hammer 61304ad3d2
fix: wrong api endpoint (again) 2024-01-15 14:08:41 -05:00
Derrick Hammer e4626fdbb8
fix: wrong api endpoint 2024-01-15 14:06:16 -05:00
Derrick Hammer d6d15cff73
debug: add debug line 2024-01-15 14:03:57 -05:00
Derrick Hammer b0a7566466
fix: ensure Error() is not nil 2024-01-15 14:03:05 -05:00
Derrick Hammer 01029430cb
dep: update libs5 2024-01-15 13:55:27 -05:00
Derrick Hammer f080dbc943
dep: update libs5 2024-01-15 13:41:14 -05:00
Derrick Hammer 2ef6df540f
dep: update libs5 2024-01-15 13:38:29 -05:00
Derrick Hammer a8f9dfdbd3
dep: update libs5 2024-01-15 13:20:15 -05:00
Derrick Hammer 0c49c70b98
dep: update libs5 2024-01-15 13:11:36 -05:00
Derrick Hammer a641e57656
dep: update libs5 2024-01-15 13:04:18 -05:00
Derrick Hammer df815c1ebf
dep: update libs5 2024-01-15 12:59:40 -05:00
Derrick Hammer ef9ecbf8e9
dep: update libs5 2024-01-15 12:14:44 -05:00
Derrick Hammer 73ab5b8214
refactor: add a means of saving the config if any default value is not set 2024-01-15 12:02:15 -05:00
Derrick Hammer 8ab200541e
fix: add default for protocol.s5.p2p.maxOutgoingPeerFailures 2024-01-15 11:50:31 -05:00
Derrick Hammer 4b1622511e
dep: update libs5 2024-01-15 11:46:14 -05:00
Derrick Hammer 41481fe89d
dep: update libs5 2024-01-15 11:39:20 -05:00
Derrick Hammer eb9f4513b5
dep: update libs5 2024-01-15 11:16:12 -05:00
Derrick Hammer 3f07a580ec
dep: update libs5 2024-01-15 10:55:02 -05:00
Derrick Hammer 0281936511
refactor: put logger as its own package with a custom config for the log level, allow it to be configurable with a default, add an init func for it, and have the config init use a temp logger 2024-01-15 10:02:58 -05:00
Derrick Hammer ba44b58897
fix: need to create init for storage service to ensure it configures the http client after we have read the config 2024-01-15 08:38:05 -05:00
Derrick Hammer 592b20c561
fix: check header for prefix/starts with 2024-01-15 08:29:09 -05:00
Derrick Hammer 4f50d645ad
fix: pass a buffer.Reader copying from Buffer 2024-01-15 08:20:28 -05:00
Derrick Hammer 35aa206687
fix: use s5 bucket 2024-01-15 08:09:37 -05:00
Derrick Hammer a16bcc788d
fix: make core.sia.url and core.sia.key required 2024-01-15 08:04:05 -05:00
Derrick Hammer 7ef2f819a8
fix: use GetHttpRouter 2024-01-15 07:56:37 -05:00
Derrick Hammer 908411f33f
feat: initial s5 basic upload 2024-01-15 07:54:13 -05:00
Derrick Hammer 9ebdeb74c6
refactor: set default options starting with core.post-upload-limi, and save the config when none exists 2024-01-14 23:54:01 -05:00
Derrick Hammer 492bd5b9de
dep: update libs5 2024-01-14 23:53:09 -05:00
Derrick Hammer 8afc157d4f
feat: initial storage service 2024-01-14 23:52:54 -05:00
Derrick Hammer 27773a7909
deps: update libs5 2024-01-13 11:23:24 -05:00
Derrick Hammer 27375a858f
deps: update 2024-01-12 15:11:23 -05:00
Derrick Hammer 7fd30e571c
refactor: make core.domain required 2024-01-12 10:40:39 -05:00
Derrick Hammer 89935f2f00
refactor: consolidate log lines and pass more protocol info 2024-01-12 10:38:57 -05:00
Derrick Hammer 59a3ac6b64
fix: pass domain as protocol subdomain 2024-01-12 10:32:19 -05:00
Derrick Hammer 866fa89cee
feat: add logging of S5 identity 2024-01-12 10:16:04 -05:00
Derrick Hammer ab6b71813d
fix: update error message 2024-01-12 09:30:55 -05:00
Derrick Hammer de407b2803
fix: fatal if protocol.s5 is not set 2024-01-12 09:30:23 -05:00
Derrick Hammer 4819bf45ac
refactor: move port check after identity init 2024-01-12 09:27:45 -05:00
Derrick Hammer 1e3addac8b
refactor: add check to ensure core.port is set 2024-01-12 09:20:43 -05:00
Derrick Hammer 2caaa07da8
refactor: try to use core.externalPort if set, else use core.port 2024-01-12 09:17:53 -05:00
Derrick Hammer fecc99b081
refactor: use core.port 2024-01-12 09:17:26 -05:00
Derrick Hammer 4d75f5659c
ci: use git provider 2024-01-12 08:27:48 -05:00
Derrick Hammer 862570bc57
ci: disable github provider 2024-01-12 08:24:12 -05:00
Derrick Hammer e6f3df2882
refactor: use a sub logger 2024-01-12 08:22:59 -05:00
Derrick Hammer 522ed11d50
refactor: store portal identity 2024-01-12 08:22:40 -05:00
Derrick Hammer 0bd089e046
refactor: update Initialize signature 2024-01-12 08:22:21 -05:00
Derrick Hammer b5c540ab8e
refactor: change Protocol Initialize to take a portal instance 2024-01-12 08:21:41 -05:00
Derrick Hammer 2fb08f35ed
feat: implement identity and seed generation 2024-01-12 08:15:36 -05:00
Derrick Hammer 7ccfaa57da
fix: use logger.Fatal 2024-01-12 06:37:11 -05:00
Derrick Hammer 5fbac2ae13
fix: use config for db 2024-01-12 06:37:01 -05:00
Derrick Hammer fa4a3e6f2a
fix: pass domain and port info to s5 http config 2024-01-11 23:33:11 -05:00
Derrick Hammer c7af6286ad
fix: namespace config under protocol 2024-01-11 23:32:31 -05:00
Derrick Hammer 8917be5077
fix: add init func for config 2024-01-11 23:32:02 -05:00
Derrick Hammer b8572ea712
fix: use logger and return errors 2024-01-11 23:23:22 -05:00
Derrick Hammer a54341dd68
fix: subdomain comes before domain 2024-01-11 23:22:56 -05:00
Derrick Hammer a57e575a82
fix: change config path 2024-01-11 23:22:33 -05:00
Derrick Hammer 519426ba7d
feat: bare bones s5 support, and more dynamic loading of subsystems 2024-01-11 23:13:10 -05:00
Derrick Hammer 82aded34ac
cleanup: remove random files 2024-01-11 21:32:47 -05:00
Derrick Hammer 959005f29c
ci: set defaultBranch 2024-01-11 21:09:21 -05:00
Derrick Hammer 444de35e31
feat: initial new portal bones 2024-01-11 19:11:53 -05:00
Derrick Hammer c80046e95f
Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	package.json
2024-01-11 14:59:16 -05:00
Derrick Hammer 8e8ea8ce86
ci: try switching to go-semantic-release 2024-01-11 14:58:44 -05:00
Derrick Hammer c7bce2ff23
refactor: Prune old code base to prepare for rewrite 2024-01-11 14:49:50 -05:00
semantic-release-bot 540457fb2f chore(release): 0.1.0-develop.3 [skip ci]
# [0.1.0-develop.3](https://git.lumeweb.com/LumeWeb/portal/compare/v0.1.0-develop.2...v0.1.0-develop.3) (2023-09-09)

### Bug Fixes

* handle failure on verifying token ([a06b79a](a06b79a537))
2023-09-09 18:02:27 +00:00
Derrick Hammer d1b0aa5139
Merge remote-tracking branch 'origin/develop' into develop 2023-09-09 14:00:22 -04:00
Derrick Hammer a06b79a537
fix: handle failure on verifying token 2023-09-09 14:00:16 -04:00
semantic-release-bot fdd6b08b71 chore(release): 0.1.0-develop.2 [skip ci]
# [0.1.0-develop.2](https://git.lumeweb.com/LumeWeb/portal/compare/v0.1.0-develop.1...v0.1.0-develop.2) (2023-08-15)

### Bug Fixes

* need to change dnslink route registration to use a path param based route ([ae071a3](ae071a30ec))
* need to string off forward slash at beginning to match manifest file paths ([2f64f18](2f64f18e24))
2023-08-15 13:24:38 +00:00
Derrick Hammer ae071a30ec
fix: need to change dnslink route registration to use a path param based route 2023-08-15 09:21:54 -04:00
Derrick Hammer 37cdbfbc0d
refactor: move cors handler inside api party 2023-08-15 09:20:54 -04:00
Derrick Hammer 2f64f18e24
fix: need to string off forward slash at beginning to match manifest file paths 2023-08-15 09:20:11 -04:00
semantic-release-bot 66b9cd2022 chore(release): 0.1.0-develop.1 [skip ci]
# [0.1.0-develop.1](https://git.lumeweb.com/LumeWeb/portal/compare/v0.0.1...v0.1.0-develop.1) (2023-08-15)

### Bug Fixes

* abort if we don't have a password for the account, assume its pubkey only ([c20dec0](c20dec0204))
* add a check for a 500 error ([df08fc9](df08fc980a))
* add missing request connection close ([dff3ca4](dff3ca4589))
* add shutdown signal and flag for renterd ([fb65690](fb65690abd))
* **auth:** eager load the account relation to return it ([a23d165](a23d165caa))
* change jwtKey to ed25519.PrivateKey ([bf576df](bf576dfaee))
* close db on shutdown ([78ee15c](78ee15cf4b))
* Ctx must be public ([a0d747f](a0d747fdf4))
* ctx needs to be public in AuthService ([a3cfeba](a3cfebab30))
* **db:** need to set charset, parseTime and loc in connection for mysql ([5d15ca3](5d15ca330a))
* disable client warnings ([9b8cb38](9b8cb38496))
* dont try to stream if we have an error ([b21a425](b21a425e24))
* encode size as uint64 to the end of the cid ([5aca66d](5aca66d919))
* ensure all models auto increment the id field ([934f8e6](934f8e6236))
* ensure we store the pubkey in lowercase ([def1b50](def1b50cfc))
* handle duplicate tus uploads by hash ([f3172b0](f3172b0d31))
* hasher needs the size set to 32 ([294370d](294370d88d))
* if upload status code isn't 200, make it an err based on the body ([039a4a3](039a4a3354))
* if uploading returns a 500 and its a slab error, treat as a 404 ([6ddef03](6ddef03790))
* if we have an existing upload, just return it as if successful ([90170e5](90170e5b81))
* iris context.User needs to be embedded in our User struct for type checking to properly work ([1cfc222](1cfc2223a6))
* just use the any route ([e100429](e100429b60))
* load config before db ([58165e0](58165e01af))
* make an attempt to look for the token before adding to db ([f11b285](f11b285d4e))
* missing setting SetTusComposer ([80561f8](80561f89e9))
* newer gorm version causes db rebuilds every boot ([72255eb](72255eb3c5))
* only panic if the error is other than a missing config file ([6e0ec8a](6e0ec8aaf9))
* output error info ([cfa7ceb](cfa7ceb2f4))
* PostPubkeyChallenge should be lowercasing the pubkey for consistency ([d680f06](d680f0660f))
* PostPubkeyChallenge should be using ChallengeRequest ([36745bb](36745bb55b))
* PostPubkeyChallenge should not be checking email, but pubkey ([db3ba1f](db3ba1f014))
* PostPubkeyLogin should be lowercasing the pubkey and signature ([09d53ff](09d53ffa76))
* PostPubkeyLogin should not preload any model ([27e7ea7](27e7ea7d7a))
* properly handle missing size bytes ([c0df04d](c0df04d7d5))
* public_key should be pubkey ([09b9f19](09b9f195f4))
* register LoginSession model ([48164ec](48164ec320))
* register request validation ([c197b14](c197b1425b))
* remove PrivateKey, rename PublicKey in Key model ([00f2b96](00f2b962a0))
* rewrite gorm query logic for tus uploads ([f8aaeff](f8aaeff6de))
* rewrite sql logic ([ce1b5e3](ce1b5e31d5))
* rewrite streaming logic and centralize in a helper function ([bb26cfc](bb26cfca5b))
* save upload info after every chunk ([038d2c4](038d2c440b))
* temp workaround on race condition ([e2db880](e2db880038))
* **tus:** switch to normal clone package, not generic ([faaec64](faaec649ea))
* update default flag values ([241db4d](241db4deb6))
* update model relationships ([628f1b4](628f1b4aca))
* **upload:** add account to upload record ([e018a4b](e018a4b743))
* uploading of main file ([7aea462](7aea462ab7))
* upstream renterd updates ([5ad91ad](5ad91ad263))
* use AccountID not Account ([f5e4377](f5e437777a))
* use bufio reader ([90e4ce6](90e4ce6408))
* use challengeObj ([9b82fa7](9b82fa7828))
* use database.path over database.name ([25c7d6d](25c7d6d4fb))
* use getWorkerObjectUrl ([4ff1334](4ff1334d8a))
* Use gorm save, and return nil if successful ([26042b6](26042b62ac))
* we can't use AddHandler inside BeginRequest ([f941ee4](f941ee46d4))
* wrap Register api in an atomic transaction to avoid dead locks ([e09e51b](e09e51bb52))
* wrong algo ([86380c7](86380c7b3a))

### Features

* add a status endpoint and move cid validation to a utility method ([38b7615](38b76155af))
* add a Status method for uploads ([1f195cf](1f195cf328))
* add auth status endpoint ([1dd4fa2](1dd4fa22cd))
* add bao package and rust bao wasm library ([4c649bf](4c649bfcb9))
* add cid package ([706f7a0](706f7a05b9))
* add ComputeFile bao RPC method ([687f26c](687f26cc77))
* add debug mode logging support ([99d7b83](99d7b8347a))
* add download endpoint ([79fd550](79fd550c54))
* add EncodeString function ([488f873](488f8737c0))
* add files service with upload endpoint ([b16beeb](b16beebabb))
* add files/upload/limit endpoint ([b77bebe](b77bebe3b1))
* add getCurrentUserId helper function ([29d6db2](29d6db2009))
* add global cors ([1f5a3d1](1f5a3d19e4))
* add jwt package ([ea99108](ea99108327))
* add more validation, and put account creation, with optional pubkey in a transaction ([699e424](699e4244e0))
* add new user service object that implements iris context User interface ([a14dad4](a14dad43ed))
* add newrelic support ([06b3ab8](06b3ab87f7))
* add pin model ([aaa2c17](aaa2c17212))
* add pin service method ([8692a02](8692a0225e))
* add PostPinBy controller endpoint for pinning a file ([be03a6c](be03a6c686))
* add pprof support ([ee17409](ee17409e12))
* add proof download ([3b1e860](3b1e860256))
* add StringHash ([118c679](118c679f76))
* add swagger support ([49c3844](49c3844406))
* add upload model ([f73a04b](f73a04bb2e))
* add Valid, and Decode methods, and create CID struct ([4e6c29f](4e6c29f1fd))
* add validation to account register ([7257b5d](7257b5d597))
* generate and/or load an ed25519 private key for jwt token generation ([85a0295](85a02952df))
* initial dnslink support ([cd2f63e](cd2f63eb72))
* pin file after basic upload ([892f093](892f093d93))
* pin file after tus upload ([5579ab8](5579ab85a3))
* tus support ([3005be6](3005be6fec))
* wip version ([9a4c3d5](9a4c3d5d13))
2023-08-15 06:18:56 +00:00
Derrick Hammer 9879662d5b
ci: add semantic-release pkgs 2023-08-15 02:16:22 -04:00
Derrick Hammer cd2f63eb72
feat: initial dnslink support 2023-08-15 02:11:55 -04:00
Derrick Hammer 3e80bb43fa
reactor: revert
Revert "feat: add pprof support"

This reverts commit ee17409e12.

Revert "fix: just use the any route"

This reverts commit e100429b60.
2023-08-14 23:17:25 -04:00
Derrick Hammer e100429b60
fix: just use the any route 2023-08-09 03:28:42 -04:00
Derrick Hammer ee17409e12
feat: add pprof support 2023-08-09 03:03:12 -04:00
Derrick Hammer 18529f2cd1
refactor: Revert "feat: add newrelic support"
This reverts commit 06b3ab87f7.
2023-08-09 02:36:24 -04:00
Derrick Hammer 06b3ab87f7
feat: add newrelic support 2023-08-05 17:19:03 -04:00
Derrick Hammer 18e102cc8a
refactor: always ensure the db connection closes by using a defer 2023-08-05 17:17:26 -04:00
Derrick Hammer f11b285d4e
fix: make an attempt to look for the token before adding to db 2023-08-04 12:54:45 -04:00
Derrick Hammer a7ac5a5b72
refactor: change generateToken to set audience based on a type to separate auth and challenge tokens 2023-08-04 12:54:13 -04:00
Derrick Hammer e2db880038
fix: temp workaround on race condition 2023-08-04 12:53:13 -04:00
Derrick Hammer e09e51bb52
fix: wrap Register api in an atomic transaction to avoid dead locks 2023-08-04 11:51:18 -04:00
Derrick Hammer dff3ca4589
fix: add missing request connection close 2023-08-04 11:46:25 -04:00
Derrick Hammer 8d3f490c01
Merge remote-tracking branch 'origin/develop' into develop 2023-08-03 08:49:20 -04:00
Derrick Hammer 78ee15cf4b
fix: close db on shutdown 2023-08-03 08:48:49 -04:00
Derrick Hammer 1cfc2223a6
fix: iris context.User needs to be embedded in our User struct for type checking to properly work 2023-06-29 07:05:46 -04:00
Derrick Hammer a23d165caa
fix(auth): eager load the account relation to return it 2023-06-29 07:04:24 -04:00
Derrick Hammer 934f8e6236
fix: ensure all models auto increment the id field 2023-06-29 06:19:50 -04:00
Derrick Hammer 504dcefb35
ci: allow both "deps" and "dep" to be a patch 2023-06-29 06:01:38 -04:00
Derrick Hammer 76d3043dda
deps: update 2023-06-29 06:01:05 -04:00
Derrick Hammer faaec649ea
fix(tus): switch to normal clone package, not generic 2023-06-29 06:00:45 -04:00
Derrick Hammer ceb729f11d
refactor(tus): add auth requirement on TUS and add support for tracking and storing the uploader throughout the upload lifecycle 2023-06-29 05:48:56 -04:00
Derrick Hammer 0bc862e35d
dep: used forked tusd 2023-06-29 05:46:51 -04:00
Derrick Hammer 53f29c99bc
dep: update package deps 2023-06-29 05:46:37 -04:00
Derrick Hammer e018a4b743
fix(upload): add account to upload record 2023-06-29 05:42:59 -04:00
Derrick Hammer 637b656d36
refactor(auth): move getCurrentUserId to auth package and make public 2023-06-29 05:41:26 -04:00
Derrick Hammer 5d15ca330a
fix(db): need to set charset, parseTime and loc in connection for mysql 2023-06-29 02:54:31 -04:00
Derrick Hammer 993b9e8208
ci: add .releaserc.json 2023-06-29 00:38:11 -04:00
Derrick Hammer 66f2545781
ci: add dummy index.html 2023-06-28 01:56:03 -04:00
Derrick Hammer 2062562f6b
ci: ensure app dir exists 2023-06-28 01:53:22 -04:00
Derrick Hammer b122626e97
ci: fix swag command path 2023-06-28 01:49:19 -04:00
Derrick Hammer 976394b29d
ci: setup swagger build 2023-06-28 01:38:18 -04:00
Derrick Hammer 914313a585
ci: setup and add semantic-release 2023-06-28 01:33:42 -04:00
Derrick Hammer 1f5a3d19e4
feat: add global cors 2023-06-28 01:31:55 -04:00
127 changed files with 13097 additions and 4190 deletions

35
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,35 @@
name: Build/Publish
on:
workflow_dispatch:
inputs:
debug_enabled:
description: Debug
type: boolean
default: false
push:
branches:
- master
- develop
- develop-*
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install SSH key
uses: shimataro/ssh-key-action@v2
with:
key: ${{ secrets.GITEA_SSH_KEY }}
known_hosts: ${{ secrets.GITEA_KNOWN_HOST }}
- name: Publish
uses: go-semantic-release/action@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
prerelease: true
- name: Setup tmate session
uses: mxschmitt/action-tmate@v3
if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled && failure() }}
with:
limit-access-to-actor: true

4
.gitmodules vendored Normal file
View File

@ -0,0 +1,4 @@
[submodule "api/account/app"]
path = api/account/app
url = https://git.lumeweb.com/LumeWeb/portal-dashboard.git
branch = develop

27
.semrelrc Normal file
View File

@ -0,0 +1,27 @@
{
"plugins": {
"commit-analyzer": {
"name": "default@^1.0.0"
},
"ci-condition": {
"name": "github",
"options": {
"defaultBranch": "*"
}
},
"changelog-generator": {
"name": "default",
"options": {
"emojis": "true"
}
},
"provider": {
"name": "git",
"options": {
"default_branch": "develop",
"tagger_email": "gitea@git.lumeweb.com",
"auth": "ssh"
}
}
}
}

115
CHANGELOG.md Normal file
View File

@ -0,0 +1,115 @@
# [0.1.0-develop.3](https://git.lumeweb.com/LumeWeb/portal/compare/v0.1.0-develop.2...v0.1.0-develop.3) (2023-09-09)
### Bug Fixes
* handle failure on verifying token ([a06b79a](https://git.lumeweb.com/LumeWeb/portal/commit/a06b79a537f08d741faeb8319d558c9e64977c4b))
# [0.1.0-develop.2](https://git.lumeweb.com/LumeWeb/portal/compare/v0.1.0-develop.1...v0.1.0-develop.2) (2023-08-15)
### Bug Fixes
* need to change dnslink route registration to use a path param based route ([ae071a3](https://git.lumeweb.com/LumeWeb/portal/commit/ae071a30ecaa62ff431878c71a54059e3d3ce8b7))
* need to string off forward slash at beginning to match manifest file paths ([2f64f18](https://git.lumeweb.com/LumeWeb/portal/commit/2f64f18e24fa1e4ddd74ed6a8d2d44e483fff1dc))
# [0.1.0-develop.1](https://git.lumeweb.com/LumeWeb/portal/compare/v0.0.1...v0.1.0-develop.1) (2023-08-15)
### Bug Fixes
* abort if we don't have a password for the account, assume its pubkey only ([c20dec0](https://git.lumeweb.com/LumeWeb/portal/commit/c20dec020437d91cf2728852b8bed5c4a0c481e9))
* add a check for a 500 error ([df08fc9](https://git.lumeweb.com/LumeWeb/portal/commit/df08fc980ac3f710a67bd692b8126eb978699d5b))
* add missing request connection close ([dff3ca4](https://git.lumeweb.com/LumeWeb/portal/commit/dff3ca45895095b82ba2e76b2e61487e28151b7d))
* add shutdown signal and flag for renterd ([fb65690](https://git.lumeweb.com/LumeWeb/portal/commit/fb65690abd5c190dce30d3cfe0d079b27040a309))
* **auth:** eager load the account relation to return it ([a23d165](https://git.lumeweb.com/LumeWeb/portal/commit/a23d165caa3ba4832c9d37a0b833b9b58df60732))
* change jwtKey to ed25519.PrivateKey ([bf576df](https://git.lumeweb.com/LumeWeb/portal/commit/bf576dfaeef51078d7bdae885550fc235d49c1eb))
* close db on shutdown ([78ee15c](https://git.lumeweb.com/LumeWeb/portal/commit/78ee15cf4b5d3a55209a9c7559700a2c5b227f87))
* Ctx must be public ([a0d747f](https://git.lumeweb.com/LumeWeb/portal/commit/a0d747fdf4e6ee3fa6a3b4dca180e4f14af30ed9))
* ctx needs to be public in AuthService ([a3cfeba](https://git.lumeweb.com/LumeWeb/portal/commit/a3cfebab307a87bc895d7b1c1f0e6632a708562c))
* **db:** need to set charset, parseTime and loc in connection for mysql ([5d15ca3](https://git.lumeweb.com/LumeWeb/portal/commit/5d15ca330abd26576ef9865c110975aeb27c3ab3))
* disable client warnings ([9b8cb38](https://git.lumeweb.com/LumeWeb/portal/commit/9b8cb38496541b0ab50d28eef63658f9723c5802))
* dont try to stream if we have an error ([b21a425](https://git.lumeweb.com/LumeWeb/portal/commit/b21a425e24f5543802e7267369f37967d4805697))
* encode size as uint64 to the end of the cid ([5aca66d](https://git.lumeweb.com/LumeWeb/portal/commit/5aca66d91981d8fae88194df6b03c239dbd179a8))
* ensure all models auto increment the id field ([934f8e6](https://git.lumeweb.com/LumeWeb/portal/commit/934f8e6236ef1eef8db1d06a1d7a7fded8afe694))
* ensure we store the pubkey in lowercase ([def1b50](https://git.lumeweb.com/LumeWeb/portal/commit/def1b50cfcba8c68f3b95209790418638374fad9))
* handle duplicate tus uploads by hash ([f3172b0](https://git.lumeweb.com/LumeWeb/portal/commit/f3172b0d31f844b95a0e64b3a5d821f71b0fbe07))
* hasher needs the size set to 32 ([294370d](https://git.lumeweb.com/LumeWeb/portal/commit/294370d88dd159ae173a6a955a417a1547de60ed))
* if upload status code isn't 200, make it an err based on the body ([039a4a3](https://git.lumeweb.com/LumeWeb/portal/commit/039a4a33547a59b4f3ec86199664b5bb94d258a6))
* if uploading returns a 500 and its a slab error, treat as a 404 ([6ddef03](https://git.lumeweb.com/LumeWeb/portal/commit/6ddef03790971e346fa0a7d33a462f39348bc6cc))
* if we have an existing upload, just return it as if successful ([90170e5](https://git.lumeweb.com/LumeWeb/portal/commit/90170e5b81831f3d768291fd37c7c13e32d522fe))
* iris context.User needs to be embedded in our User struct for type checking to properly work ([1cfc222](https://git.lumeweb.com/LumeWeb/portal/commit/1cfc2223a6df614f26fd0337ced68d92e774589f))
* just use the any route ([e100429](https://git.lumeweb.com/LumeWeb/portal/commit/e100429b60e783f6c7c3ddecab7bb9b4dd599726))
* load awsConfig before db ([58165e0](https://git.lumeweb.com/LumeWeb/portal/commit/58165e01af9f2b183d654d3d8809cbd1eda0a9bb))
* make an attempt to look for the token before adding to db ([f11b285](https://git.lumeweb.com/LumeWeb/portal/commit/f11b285d4e255c1c4c95f6ac15aa904d7a5730e4))
* missing setting SetTusComposer ([80561f8](https://git.lumeweb.com/LumeWeb/portal/commit/80561f89e92dfa86887ada8361e0046ee6288234))
* newer gorm version causes db rebuilds every boot ([72255eb](https://git.lumeweb.com/LumeWeb/portal/commit/72255eb3c50892aa5f2cfdc4cb1daa5883f0affc))
* only panic if the error is other than a missing awsConfig file ([6e0ec8a](https://git.lumeweb.com/LumeWeb/portal/commit/6e0ec8aaf90e86bcb7cb6c8c53f6569e6885e0aa))
* output error info ([cfa7ceb](https://git.lumeweb.com/LumeWeb/portal/commit/cfa7ceb2f422a6e594a424315c8eaeffc6572926))
* PostPubkeyChallenge should be lowercasing the pubkey for consistency ([d680f06](https://git.lumeweb.com/LumeWeb/portal/commit/d680f0660f910e323356a1169ee13ef2e647a015))
* PostPubkeyChallenge should be using ChallengeRequest ([36745bb](https://git.lumeweb.com/LumeWeb/portal/commit/36745bb55b1d7cd464b085e410333089504591c1))
* PostPubkeyChallenge should not be checking email, but pubkey ([db3ba1f](https://git.lumeweb.com/LumeWeb/portal/commit/db3ba1f0148b6abc34b4606f9b8103963a3c6850))
* PostPubkeyLogin should be lowercasing the pubkey and signature ([09d53ff](https://git.lumeweb.com/LumeWeb/portal/commit/09d53ffa7645b64aed4170e698b8eb62d2c3590e))
* PostPubkeyLogin should not preload any model ([27e7ea7](https://git.lumeweb.com/LumeWeb/portal/commit/27e7ea7d7a0bbf6c147ff625591acf6376c6c62d))
* properly handle missing size bytes ([c0df04d](https://git.lumeweb.com/LumeWeb/portal/commit/c0df04d7d5309e32348ceecc68eecd64c5e5cba4))
* public_key should be pubkey ([09b9f19](https://git.lumeweb.com/LumeWeb/portal/commit/09b9f195f47ea9ae47069a517a77609c74ea3ca5))
* register LoginSession model ([48164ec](https://git.lumeweb.com/LumeWeb/portal/commit/48164ec320c693937ead352246ec1e94bede3684))
* register request validation ([c197b14](https://git.lumeweb.com/LumeWeb/portal/commit/c197b1425bbd689e8f662846de0478aff8d38f35))
* remove PrivateKey, rename PublicKey in Key model ([00f2b96](https://git.lumeweb.com/LumeWeb/portal/commit/00f2b962a0da956f971dc94d75726c1bab693232))
* rewrite gorm query logic for tus uploads ([f8aaeff](https://git.lumeweb.com/LumeWeb/portal/commit/f8aaeff6de2dc5e5321840460d55d79ad1b5ab1a))
* rewrite sql logic ([ce1b5e3](https://git.lumeweb.com/LumeWeb/portal/commit/ce1b5e31d5d6a69dc91d88a6fd2f1317e07dc1ea))
* rewrite streaming logic and centralize in a helper function ([bb26cfc](https://git.lumeweb.com/LumeWeb/portal/commit/bb26cfca5b4017bbbbf5aeee9bd3577c724f83ca))
* save upload info after every chunk ([038d2c4](https://git.lumeweb.com/LumeWeb/portal/commit/038d2c440b24b7c0f1ea72e0bfeda369f766c691))
* temp workaround on race condition ([e2db880](https://git.lumeweb.com/LumeWeb/portal/commit/e2db880038f51e0e16ce270fe29fce7785cce878))
* **tus:** switch to normal clone package, not generic ([faaec64](https://git.lumeweb.com/LumeWeb/portal/commit/faaec649ead00567ced56edfa9db11eb34655178))
* update default flag values ([241db4d](https://git.lumeweb.com/LumeWeb/portal/commit/241db4deb6808d950d55efa38e11d60469cc6778))
* update model relationships ([628f1b4](https://git.lumeweb.com/LumeWeb/portal/commit/628f1b4acaac1d2bf373b7008f2e0c070fd64ae5))
* **upload:** add account to upload record ([e018a4b](https://git.lumeweb.com/LumeWeb/portal/commit/e018a4b7430bc375ff3b72537e71295cdf67ef93))
* uploading of main file ([7aea462](https://git.lumeweb.com/LumeWeb/portal/commit/7aea462ab752e999030837d13733508369524cf3))
* upstream renterd updates ([5ad91ad](https://git.lumeweb.com/LumeWeb/portal/commit/5ad91ad263f01830623958141a7e7c8523bee85f))
* use AccountID not Account ([f5e4377](https://git.lumeweb.com/LumeWeb/portal/commit/f5e437777a52e2a9bbf55903cea17ec073fbb406))
* use bufio reader ([90e4ce6](https://git.lumeweb.com/LumeWeb/portal/commit/90e4ce6408391dc270ca4405a7c5282c2d4766b2))
* use challengeObj ([9b82fa7](https://git.lumeweb.com/LumeWeb/portal/commit/9b82fa7828946803289add03fc84be1dc4f86d8b))
* use database.path over database.name ([25c7d6d](https://git.lumeweb.com/LumeWeb/portal/commit/25c7d6d4fb48b69239eba131232a78e90a576e2f))
* use getWorkerObjectUrl ([4ff1334](https://git.lumeweb.com/LumeWeb/portal/commit/4ff1334d8afd9379db687fc6b764f5b0f1bcc08c))
* Use gorm save, and return nil if successful ([26042b6](https://git.lumeweb.com/LumeWeb/portal/commit/26042b62acd7f7346f1a99a0ac37b3f2f99e3f75))
* we can't use AddHandler inside BeginRequest ([f941ee4](https://git.lumeweb.com/LumeWeb/portal/commit/f941ee46d469a3f0a6302b188f566029fdec4e70))
* wrap Register api in an atomic transaction to avoid dead locks ([e09e51b](https://git.lumeweb.com/LumeWeb/portal/commit/e09e51bb52d513abcbbf53352a5d8ff68eb5364a))
* wrong algo ([86380c7](https://git.lumeweb.com/LumeWeb/portal/commit/86380c7b3a97e785b99af456305c01d18f776ddf))
### Features
* add a status endpoint and move cid validation to a utility method ([38b7615](https://git.lumeweb.com/LumeWeb/portal/commit/38b76155af954dc3602a5035cb7b53a7f625fbfd))
* add a Status method for uploads ([1f195cf](https://git.lumeweb.com/LumeWeb/portal/commit/1f195cf328ee176be9283ab0cc40e65bb6c40948))
* add auth status endpoint ([1dd4fa2](https://git.lumeweb.com/LumeWeb/portal/commit/1dd4fa22cdfc749c5474f94108bca0aec34aea81))
* add bao package and rust bao wasm library ([4c649bf](https://git.lumeweb.com/LumeWeb/portal/commit/4c649bfcb92e8632e45cf10b27fa062ff1680c32))
* add cid package ([706f7a0](https://git.lumeweb.com/LumeWeb/portal/commit/706f7a05b9a4ed464f693941235aa7e9ca14145a))
* add ComputeFile bao RPC method ([687f26c](https://git.lumeweb.com/LumeWeb/portal/commit/687f26cc779f4f50166108d6e78fe1456cfa128d))
* add debug mode logging support ([99d7b83](https://git.lumeweb.com/LumeWeb/portal/commit/99d7b8347af25fe65a1f1aecc9960424a101c279))
* add download endpoint ([79fd550](https://git.lumeweb.com/LumeWeb/portal/commit/79fd550c54bf74e84d012805f60c036c19fbbef2))
* add EncodeString function ([488f873](https://git.lumeweb.com/LumeWeb/portal/commit/488f8737c09b7757c5649b3d8a3568e3c1d5fe45))
* add files service with upload endpoint ([b16beeb](https://git.lumeweb.com/LumeWeb/portal/commit/b16beebabb254488897edde870e9588b7be5293e))
* add files/upload/limit endpoint ([b77bebe](https://git.lumeweb.com/LumeWeb/portal/commit/b77bebe3b1a03cecdd7e80f575452d5ce91ccfac))
* add getCurrentUserId helper function ([29d6db2](https://git.lumeweb.com/LumeWeb/portal/commit/29d6db20096e61efa9a792ef837ef93ca14107ae))
* add global cors ([1f5a3d1](https://git.lumeweb.com/LumeWeb/portal/commit/1f5a3d19e44f1db2f8587623e868fa48b23d1a74))
* add jwt package ([ea99108](https://git.lumeweb.com/LumeWeb/portal/commit/ea991083276a576003eb3633bd1bde98e13dfe84))
* add more validation, and put account creation, with optional pubkey in a transaction ([699e424](https://git.lumeweb.com/LumeWeb/portal/commit/699e4244e0d877d8d9df9d3d4894351785fe7f4d))
* add new user service object that implements iris context User interface ([a14dad4](https://git.lumeweb.com/LumeWeb/portal/commit/a14dad43ed3140f73d817ef2438aacbc0939de69))
* add newrelic support ([06b3ab8](https://git.lumeweb.com/LumeWeb/portal/commit/06b3ab87f7e1b982d3fb42a3e06897a2fd1387ed))
* add pin model ([aaa2c17](https://git.lumeweb.com/LumeWeb/portal/commit/aaa2c17212bd5e646036252a0e1f8d8bdb68f5a7))
* add pin service method ([8692a02](https://git.lumeweb.com/LumeWeb/portal/commit/8692a0225ebb71502811cba063e32dd11cdd10c9))
* add PostPinBy controller endpoint for pinning a file ([be03a6c](https://git.lumeweb.com/LumeWeb/portal/commit/be03a6c6867f305529af90e6206a0597bb84f015))
* add pprof support ([ee17409](https://git.lumeweb.com/LumeWeb/portal/commit/ee17409e1252e9cbae0b17ccbb1949c9a81dff82))
* add proof download ([3b1e860](https://git.lumeweb.com/LumeWeb/portal/commit/3b1e860256297d3515f0fcd58dd28292c316d79f))
* add StringHash ([118c679](https://git.lumeweb.com/LumeWeb/portal/commit/118c679f769bec2971e4e4b00ec41841a02b8a1c))
* add swagger support ([49c3844](https://git.lumeweb.com/LumeWeb/portal/commit/49c38444066c89d7258fd85d114d9d74babb8d55))
* add upload model ([f73a04b](https://git.lumeweb.com/LumeWeb/portal/commit/f73a04bb2e48b78e22b531a9121fe4baa011deaf))
* add Valid, and Decode methods, and create CID struct ([4e6c29f](https://git.lumeweb.com/LumeWeb/portal/commit/4e6c29f1fd7c33ce442fe741e08b32c8e3e9f393))
* add validation to account register ([7257b5d](https://git.lumeweb.com/LumeWeb/portal/commit/7257b5d597a28069c87437cabd71f51c187eb80c))
* generate and/or load an ed25519 private key for jwt token generation ([85a0295](https://git.lumeweb.com/LumeWeb/portal/commit/85a02952dffb1873c557f30483606d678e46749d))
* initial dnslink support ([cd2f63e](https://git.lumeweb.com/LumeWeb/portal/commit/cd2f63eb72c2bfc404d8d1b5a6fdb53f61a31d1b))
* pin file after basic upload ([892f093](https://git.lumeweb.com/LumeWeb/portal/commit/892f093d93348459d113041104d773fdd5124a8d))
* pin file after tus upload ([5579ab8](https://git.lumeweb.com/LumeWeb/portal/commit/5579ab85a374be457163d06caf1ac6e260082cca))
* tus support ([3005be6](https://git.lumeweb.com/LumeWeb/portal/commit/3005be6fec8136214c1e9480c788f62564a2c5f9))
* wip version ([9a4c3d5](https://git.lumeweb.com/LumeWeb/portal/commit/9a4c3d5d13a3e76fe91eb5d78a6f2f0f8e238f80))

55
Dockerfile Normal file
View File

@ -0,0 +1,55 @@
# Use the official Node.js image as the base image for building the api/account/portal
FROM node:20-alpine as nodejs-builder
# Set the working directory
WORKDIR /portal
# Clone the repository with submodules
RUN apk add --no-cache git \
&& git clone --recurse-submodules https://git.lumeweb.com/LumeWeb/portal.git -b develop .
# Set the working directory
WORKDIR /portal/api/account/app
# Build the dashboard
RUN npm ci && npm run build
# Use the official Go image as the base image for the final Go build
FROM golang:1.21.6-alpine as go-builder
# Set the working directory
WORKDIR /portal
# Build the Go application with configurable tags
ARG BUILD_TAGS
RUN apk add --no-cache git && git clone --recurse-submodules https://git.lumeweb.com/LumeWeb/portal.git -b develop .
# Copy the built dashboard from the nodejs-builder stage
COPY --from=nodejs-builder /portal/api/account/app/build/client /portal/api/account/app/build/client
# Install the necessary dependencies
RUN apk add bash gcc curl musl-dev
## Build the Go application
RUN go mod download
## Build the Go application
RUN go generate ./...
## Build the Go application
RUN go build -tags "${BUILD_TAGS}" -gcflags="all=-N -l" -o portal ./cmd/portal
# Use a lightweight base image for the final stage
FROM alpine:latest
# Set the working directory
WORKDIR /portal
# Copy the built binary from the go-builder stage
COPY --from=go-builder /portal/portal .
# Expose the necessary port(s)
EXPOSE 8080
# Run the application
CMD ["./portal"]

731
account/account.go Normal file
View File

@ -0,0 +1,731 @@
package account
import (
"context"
"crypto/ed25519"
"crypto/rand"
"errors"
"fmt"
"time"
"github.com/go-sql-driver/mysql"
"git.lumeweb.com/LumeWeb/portal/metadata"
"git.lumeweb.com/LumeWeb/portal/mailer"
"gorm.io/gorm/clause"
"git.lumeweb.com/LumeWeb/portal/config"
"git.lumeweb.com/LumeWeb/portal/db/models"
"go.uber.org/fx"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
var (
ErrInvalidOTPCode = errors.New("Invalid OTP code")
)
const ACCOUNT_SUBDOMAIN = "account"
type AccountServiceParams struct {
fx.In
Db *gorm.DB
Config *config.Manager
Identity ed25519.PrivateKey
Mailer *mailer.Mailer
Metadata metadata.MetadataService
}
var Module = fx.Module("account",
fx.Options(
fx.Provide(NewAccountService),
),
)
type AccountServiceDefault struct {
db *gorm.DB
config *config.Manager
identity ed25519.PrivateKey
mailer *mailer.Mailer
metadata metadata.MetadataService
}
func NewAccountService(params AccountServiceParams) *AccountServiceDefault {
return &AccountServiceDefault{db: params.Db, config: params.Config, identity: params.Identity, mailer: params.Mailer, metadata: params.Metadata}
}
func (s *AccountServiceDefault) EmailExists(email string) (bool, *models.User, error) {
user := &models.User{}
exists, model, err := s.exists(user, map[string]interface{}{"email": email})
if !exists || err != nil {
return false, nil, err
}
return true, model.(*models.User), nil // Type assertion since `exists` returns interface{}
}
func (s *AccountServiceDefault) PubkeyExists(pubkey string) (bool, *models.PublicKey, error) {
publicKey := &models.PublicKey{}
exists, model, err := s.exists(publicKey, map[string]interface{}{"key": pubkey})
if !exists || err != nil {
return false, nil, err
}
return true, model.(*models.PublicKey), nil // Type assertion is necessary
}
func (s *AccountServiceDefault) AccountExists(id uint) (bool, *models.User, error) {
user := &models.User{}
exists, model, err := s.exists(user, map[string]interface{}{"id": id})
if !exists || err != nil {
return false, nil, err
}
return true, model.(*models.User), nil // Ensure to assert the type correctly
}
func (s *AccountServiceDefault) HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", NewAccountError(ErrKeyHashingFailed, err)
}
return string(bytes), nil
}
func (s *AccountServiceDefault) CreateAccount(email string, password string, verifyEmail bool) (*models.User, error) {
passwordHash, err := s.HashPassword(password)
if err != nil {
return nil, err
}
user := models.User{
Email: email,
PasswordHash: passwordHash,
}
result := s.db.Create(&user)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrDuplicatedKey) {
return nil, NewAccountError(ErrKeyEmailAlreadyExists, nil)
}
if err, ok := result.Error.(*mysql.MySQLError); ok {
if err.Number == 1062 {
return nil, NewAccountError(ErrKeyEmailAlreadyExists, nil)
}
}
return nil, NewAccountError(ErrKeyAccountCreationFailed, result.Error)
}
if verifyEmail {
err = s.SendEmailVerification(user.ID)
if err != nil {
return nil, err
}
}
return &user, nil
}
func (s AccountServiceDefault) SendEmailVerification(userId uint) error {
exists, user, err := s.AccountExists(userId)
if !exists || err != nil {
return err
}
if user.Verified {
return NewAccountError(ErrKeyAccountAlreadyVerified, nil)
}
token := GenerateSecurityToken()
var verification models.EmailVerification
verification.UserID = user.ID
verification.Token = token
verification.ExpiresAt = time.Now().Add(time.Hour)
err = s.db.Create(&verification).Error
if err != nil {
return NewAccountError(ErrKeyDatabaseOperationFailed, err)
}
verifyUrl := fmt.Sprintf("%s/account/verify?token=%s", fmt.Sprintf("https://%s.%s", ACCOUNT_SUBDOMAIN, s.config.Config().Core.Domain), token)
vars := map[string]interface{}{
"FirstName": user.FirstName,
"Email": user.Email,
"VerificationLink": verifyUrl,
"ExpireTime": verification.ExpiresAt.Sub(time.Now()).Round(time.Second * 2),
"PortalName": s.config.Config().Core.PortalName,
}
return s.mailer.TemplateSend(mailer.TPL_VERIFY_EMAIL, vars, vars, user.Email)
}
func (s AccountServiceDefault) SendPasswordReset(user *models.User) error {
token := GenerateSecurityToken()
var reset models.PasswordReset
reset.UserID = user.ID
reset.Token = token
reset.ExpiresAt = time.Now().Add(time.Hour)
err := s.db.Create(&reset).Error
if err != nil {
return NewAccountError(ErrKeyDatabaseOperationFailed, err)
}
vars := map[string]interface{}{
"FirstName": user.FirstName,
"Email": user.Email,
"ResetCode": token,
"ExpireTime": reset.ExpiresAt,
"PortalName": s.config.Config().Core.PortalName,
"PortalDomain": s.config.Config().Core.Domain,
}
return s.mailer.TemplateSend(mailer.TPL_PASSWORD_RESET, vars, vars, user.Email)
}
func (s AccountServiceDefault) VerifyEmail(email string, token string) error {
var verification models.EmailVerification
verification.Token = token
result := s.db.Model(&verification).
Preload("User").
Where(&verification).
First(&verification)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return NewAccountError(ErrKeySecurityInvalidToken, nil)
}
return NewAccountError(ErrKeyDatabaseOperationFailed, nil)
}
if verification.ExpiresAt.Before(time.Now()) {
return NewAccountError(ErrKeySecurityTokenExpired, nil)
}
if len(verification.NewEmail) > 0 && verification.NewEmail != email {
return NewAccountError(ErrKeySecurityInvalidToken, nil)
} else if verification.User.Email != email {
return NewAccountError(ErrKeySecurityInvalidToken, nil)
}
var update models.User
doUpdate := false
if !verification.User.Verified {
update.Verified = true
doUpdate = true
}
if len(verification.NewEmail) > 0 {
update.Email = verification.NewEmail
doUpdate = true
}
if doUpdate {
err := s.updateAccountInfo(verification.UserID, update)
if err != nil {
return err
}
}
verification = models.EmailVerification{
UserID: verification.UserID,
}
if result := s.db.Where(&verification).Delete(&verification); result.Error != nil {
return NewAccountError(ErrKeyDatabaseOperationFailed, result.Error)
}
return nil
}
func (s AccountServiceDefault) ResetPassword(email string, token string, password string) error {
var reset models.PasswordReset
reset.Token = token
result := s.db.Model(&reset).
Preload("User").
Where(&reset).
First(&reset)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return NewAccountError(ErrKeyUserNotFound, result.Error)
}
return NewAccountError(ErrKeyDatabaseOperationFailed, result.Error)
}
if reset.ExpiresAt.Before(time.Now()) {
return NewAccountError(ErrKeySecurityTokenExpired, nil)
}
if reset.User.Email != email {
return NewAccountError(ErrKeySecurityInvalidToken, nil)
}
passwordHash, err := s.HashPassword(password)
if err != nil {
return err
}
err = s.updateAccountInfo(reset.UserID, models.User{PasswordHash: passwordHash})
if err != nil {
return err
}
reset = models.PasswordReset{
UserID: reset.UserID,
}
if result := s.db.Where(&reset).Delete(&reset); result.Error != nil {
return NewAccountError(ErrKeyDatabaseOperationFailed, result.Error)
}
return nil
}
func (s AccountServiceDefault) UpdateAccountName(userId uint, firstName string, lastName string) error {
return s.updateAccountInfo(userId, models.User{FirstName: firstName, LastName: lastName})
}
func (s AccountServiceDefault) UpdateAccountEmail(userId uint, email string, password string) error {
exists, euser, err := s.EmailExists(email)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) || (exists && euser.ID != userId) {
return NewAccountError(ErrKeyEmailAlreadyExists, nil)
}
valid, user, err := s.ValidLoginByUserID(userId, password)
if err != nil {
return err
}
if !valid {
return NewAccountError(ErrKeyInvalidLogin, nil)
}
if user.Email == email {
return NewAccountError(ErrKeyUpdatingSameEmail, nil)
}
var update models.User
update.Email = email
return s.updateAccountInfo(userId, update)
}
func (s AccountServiceDefault) UpdateAccountPassword(userId uint, password string, newPassword string) error {
valid, _, err := s.ValidLoginByUserID(userId, password)
if err != nil {
return err
}
if !valid {
return NewAccountError(ErrKeyInvalidPassword, nil)
}
passwordHash, err := s.HashPassword(newPassword)
if err != nil {
return err
}
return s.updateAccountInfo(userId, models.User{PasswordHash: passwordHash})
}
func (s AccountServiceDefault) AddPubkeyToAccount(user models.User, pubkey string) error {
var model models.PublicKey
model.Key = pubkey
model.UserID = user.ID
result := s.db.Create(&model)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrDuplicatedKey) {
return NewAccountError(ErrKeyPublicKeyExists, result.Error)
}
return NewAccountError(ErrKeyDatabaseOperationFailed, result.Error)
}
return nil
}
func (s AccountServiceDefault) LoginPassword(email string, password string, ip string) (string, *models.User, error) {
valid, user, err := s.ValidLoginByEmail(email, password)
if err != nil {
return "", nil, err
}
if !valid {
return "", nil, nil
}
token, err := s.doLogin(user, ip, false)
if err != nil {
return "", nil, err
}
return token, user, nil
}
func (s AccountServiceDefault) LoginOTP(userId uint, code string) (string, error) {
valid, err := s.OTPVerify(userId, code)
if err != nil {
return "", err
}
if !valid {
return "", NewAccountError(ErrKeyInvalidOTPCode, nil)
}
var user models.User
user.ID = userId
token, tokenErr := JWTGenerateToken(s.config.Config().Core.Domain, s.identity, user.ID, JWTPurposeLogin)
if tokenErr != nil {
return "", err
}
return token, nil
}
func (s AccountServiceDefault) ValidLoginByUserObj(user *models.User, password string) bool {
return s.validPassword(user, password)
}
func (s AccountServiceDefault) ValidLoginByEmail(email string, password string) (bool, *models.User, error) {
var user models.User
result := s.db.Model(&models.User{}).Where(&models.User{Email: email}).First(&user)
if result.RowsAffected == 0 || result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return false, nil, NewAccountError(ErrKeyInvalidLogin, result.Error)
}
return false, nil, NewAccountError(ErrKeyDatabaseOperationFailed, result.Error)
}
valid := s.ValidLoginByUserObj(&user, password)
if !valid {
return false, nil, nil
}
return true, &user, nil
}
func (s AccountServiceDefault) ValidLoginByUserID(id uint, password string) (bool, *models.User, error) {
var user models.User
user.ID = id
result := s.db.Model(&user).Where(&user).First(&user)
if result.RowsAffected == 0 || result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return false, nil, NewAccountError(ErrKeyInvalidLogin, result.Error)
}
return false, nil, NewAccountError(ErrKeyDatabaseOperationFailed, result.Error)
}
valid := s.ValidLoginByUserObj(&user, password)
if !valid {
return false, nil, nil
}
return true, &user, nil
}
func (s AccountServiceDefault) LoginPubkey(pubkey string, ip string) (string, error) {
var model models.PublicKey
result := s.db.Model(&models.PublicKey{}).Preload("User").Where(&models.PublicKey{Key: pubkey}).First(&model)
if result.RowsAffected == 0 || result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return "", NewAccountError(ErrKeyInvalidLogin, result.Error)
}
return "", NewAccountError(ErrKeyDatabaseOperationFailed, result.Error)
}
user := model.User
token, err := s.doLogin(&user, ip, true)
if err != nil {
return "", err
}
return token, nil
}
func (s AccountServiceDefault) AccountPins(id uint, createdAfter uint64) ([]models.Pin, error) {
var pins []models.Pin
result := s.db.Model(&models.Pin{}).
Preload("Upload"). // Preload the related Upload for each Pin
Where(&models.Pin{UserID: id}).
Where("created_at > ?", createdAfter).
Order("created_at desc").
Find(&pins)
if result.Error != nil {
return nil, NewAccountError(ErrKeyPinsRetrievalFailed, result.Error)
}
return pins, nil
}
func (s AccountServiceDefault) DeletePinByHash(hash []byte, userId uint) error {
// Define a struct for the query condition
uploadQuery := models.Upload{Hash: hash}
// Retrieve the upload ID for the given hash
var uploadID uint
result := s.db.
Model(&models.Upload{}).
Where(&uploadQuery).
Select("id").
First(&uploadID)
if result.Error != nil {
if result.Error == gorm.ErrRecordNotFound {
// No record found, nothing to delete
return nil
}
return result.Error
}
// Delete pins with the retrieved upload ID and matching account ID
pinQuery := models.Pin{UploadID: uploadID, UserID: userId}
result = s.db.
Where(&pinQuery).
Delete(&models.Pin{})
if result.Error != nil {
return result.Error
}
return nil
}
func (s AccountServiceDefault) PinByHash(hash []byte, userId uint) error {
// Define a struct for the query condition
uploadQuery := models.Upload{Hash: hash}
result := s.db.
Model(&uploadQuery).
Where(&uploadQuery).
First(&uploadQuery)
if result.Error != nil {
return result.Error
}
return s.PinByID(uploadQuery.ID, userId)
}
func (s AccountServiceDefault) PinByID(uploadId uint, userId uint) error {
result := s.db.Model(&models.Pin{}).Where(&models.Pin{UploadID: uploadId, UserID: userId}).First(&models.Pin{})
if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) {
return result.Error
}
if result.RowsAffected > 0 {
return nil
}
// Create a pin with the retrieved upload ID and matching account ID
pinQuery := models.Pin{UploadID: uploadId, UserID: userId}
result = s.db.Create(&pinQuery)
if result.Error != nil {
return result.Error
}
return nil
}
func (s AccountServiceDefault) OTPGenerate(userId uint) (string, error) {
exists, user, err := s.AccountExists(userId)
if !exists || err != nil {
return "", err
}
otp, otpErr := TOTPGenerate(user.Email, s.config.Config().Core.Domain)
if otpErr != nil {
return "", NewAccountError(ErrKeyOTPGenerationFailed, otpErr)
}
err = s.updateAccountInfo(user.ID, models.User{OTPSecret: otp})
return otp, nil
}
func (s AccountServiceDefault) OTPVerify(userId uint, code string) (bool, error) {
exists, user, err := s.AccountExists(userId)
if !exists || err != nil {
return false, err
}
valid := TOTPValidate(user.OTPSecret, code)
if !valid {
return false, nil
}
return true, nil
}
func (s AccountServiceDefault) OTPEnable(userId uint, code string) error {
verify, err := s.OTPVerify(userId, code)
if err != nil {
return err
}
if !verify {
return ErrInvalidOTPCode
}
return s.updateAccountInfo(userId, models.User{OTPEnabled: true})
}
func (s AccountServiceDefault) OTPDisable(userId uint) error {
return s.updateAccountInfo(userId, models.User{OTPEnabled: false, OTPSecret: ""})
}
func (s AccountServiceDefault) DNSLinkExists(hash []byte) (bool, *models.DNSLink, error) {
upload, err := s.metadata.GetUpload(context.Background(), hash)
if err != nil {
return false, nil, err
}
exists, model, err := s.exists(&models.DNSLink{}, map[string]interface{}{"upload_id": upload.ID})
if !exists || err != nil {
return false, nil, err
}
pinned, err := s.UploadPinned(hash)
if err != nil {
return false, nil, err
}
if !pinned {
return false, nil, nil
}
return true, model.(*models.DNSLink), nil
}
func (s AccountServiceDefault) UploadPinned(hash []byte) (bool, error) {
upload, err := s.metadata.GetUpload(context.Background(), hash)
if err != nil {
return false, err
}
var pin models.Pin
result := s.db.Model(&models.Pin{}).Where(&models.Pin{UploadID: upload.ID}).First(&pin)
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return false, nil
}
return false, result.Error
}
return true, nil
}
func GenerateSecurityToken() string {
const charset = "abcdefghijklmnopqrstuvwxyz0123456789"
b := make([]byte, 6)
_, err := rand.Read(b)
if err != nil {
panic(err)
}
for i := 0; i < 6; i++ {
b[i] = charset[b[i]%byte(len(charset))]
}
return string(b)
}
func (s AccountServiceDefault) doLogin(user *models.User, ip string, bypassSecurity bool) (string, error) {
purpose := JWTPurposeLogin
if user.OTPEnabled && !bypassSecurity {
purpose = JWTPurpose2FA
}
token, jwtErr := JWTGenerateToken(s.config.Config().Core.Domain, s.identity, user.ID, purpose)
if jwtErr != nil {
return "", NewAccountError(ErrKeyJWTGenerationFailed, jwtErr)
}
now := time.Now()
err := s.updateAccountInfo(user.ID, models.User{LastLoginIP: ip, LastLogin: &now})
if err != nil {
return "", err
}
return token, nil
}
func (s AccountServiceDefault) updateAccountInfo(userId uint, info models.User) error {
var user models.User
user.ID = userId
result := s.db.Model(&models.User{}).Where(&user).Updates(info)
if result.Error != nil {
return NewAccountError(ErrKeyDatabaseOperationFailed, result.Error)
}
return nil
}
func (s AccountServiceDefault) exists(model interface{}, conditions map[string]interface{}) (bool, interface{}, error) {
// Conduct a query with the provided model and conditions
result := s.db.Preload(clause.Associations).Model(model).Where(conditions).First(model)
// Check if any rows were found
exists := result.RowsAffected > 0
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return false, nil, nil
}
if exists {
return true, model, nil
}
return false, model, NewAccountError(ErrKeyDatabaseOperationFailed, result.Error)
}
func (s AccountServiceDefault) validPassword(user *models.User, password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(password))
return err == nil
}

179
account/errors.go Normal file
View File

@ -0,0 +1,179 @@
package account
import (
"fmt"
"net/http"
)
const (
// Account creation errors
ErrKeyAccountCreationFailed = "ErrAccountCreationFailed"
ErrKeyEmailAlreadyExists = "ErrEmailAlreadyExists"
ErrKeyUpdatingSameEmail = "ErrUpdatingSameEmail"
ErrKeyPasswordHashingFailed = "ErrPasswordHashingFailed"
// Account lookup and existence verification errors
ErrKeyUserNotFound = "ErrUserNotFound"
ErrKeyPublicKeyNotFound = "ErrPublicKeyNotFound"
// Authentication and login errors
ErrKeyInvalidLogin = "ErrInvalidLogin"
ErrKeyInvalidPassword = "ErrInvalidPassword"
ErrKeyInvalidOTPCode = "ErrInvalidOTPCode"
ErrKeyOTPVerificationFailed = "ErrOTPVerificationFailed"
ErrKeyLoginFailed = "ErrLoginFailed"
ErrKeyHashingFailed = "ErrHashingFailed"
// Account update errors
ErrKeyAccountUpdateFailed = "ErrAccountUpdateFailed"
ErrKeyAccountAlreadyVerified = "ErrAccountAlreadyVerified"
// JWT generation errors
ErrKeyJWTGenerationFailed = "ErrJWTGenerationFailed"
// OTP management errors
ErrKeyOTPGenerationFailed = "ErrOTPGenerationFailed"
ErrKeyOTPEnableFailed = "ErrOTPEnableFailed"
ErrKeyOTPDisableFailed = "ErrOTPDisableFailed"
// Public key management errors
ErrKeyAddPublicKeyFailed = "ErrAddPublicKeyFailed"
ErrKeyPublicKeyExists = "ErrPublicKeyExists"
// Pin management errors
ErrKeyPinAddFailed = "ErrPinAddFailed"
ErrKeyPinDeleteFailed = "ErrPinDeleteFailed"
ErrKeyPinsRetrievalFailed = "ErrPinsRetrievalFailed"
// General errors
ErrKeyDatabaseOperationFailed = "ErrDatabaseOperationFailed"
// Security token errors
ErrKeySecurityTokenExpired = "ErrSecurityTokenExpired"
ErrKeySecurityInvalidToken = "ErrSecurityInvalidToken"
)
var defaultErrorMessages = map[string]string{
// Account creation errors
ErrKeyAccountCreationFailed: "Account creation failed due to an internal error.",
ErrKeyEmailAlreadyExists: "The email address provided is already in use.",
ErrKeyPasswordHashingFailed: "Failed to secure the password, please try again later.",
ErrKeyUpdatingSameEmail: "The email address provided is the same as your current one.",
// Account lookup and existence verification errors
ErrKeyUserNotFound: "The requested user was not found.",
ErrKeyPublicKeyNotFound: "The specified public key was not found.",
ErrKeyHashingFailed: "Failed to hash the password.",
// Authentication and login errors
ErrKeyInvalidLogin: "The login credentials provided are invalid.",
ErrKeyInvalidPassword: "The password provided is incorrect.",
ErrKeyInvalidOTPCode: "The OTP code provided is invalid or expired.",
ErrKeyOTPVerificationFailed: "OTP verification failed, please try again.",
ErrKeyLoginFailed: "Login failed due to an internal error.",
// Account update errors
ErrKeyAccountUpdateFailed: "Failed to update account information.",
ErrKeyAccountAlreadyVerified: "Account is already verified.",
// JWT generation errors
ErrKeyJWTGenerationFailed: "Failed to generate a new JWT token.",
// OTP management errors
ErrKeyOTPGenerationFailed: "Failed to generate a new OTP secret.",
ErrKeyOTPEnableFailed: "Enabling OTP authentication failed.",
ErrKeyOTPDisableFailed: "Disabling OTP authentication failed.",
// Public key management errors
ErrKeyAddPublicKeyFailed: "Adding the public key to the account failed.",
ErrKeyPublicKeyExists: "The public key already exists for this account.",
// Pin management errors
ErrKeyPinAddFailed: "Failed to add the pin.",
ErrKeyPinDeleteFailed: "Failed to delete the pin.",
ErrKeyPinsRetrievalFailed: "Failed to retrieve pins.",
// General errors
ErrKeyDatabaseOperationFailed: "A database operation failed.",
// Security token errors
ErrKeySecurityTokenExpired: "The security token has expired.",
ErrKeySecurityInvalidToken: "The security token is invalid.",
}
var (
ErrorCodeToHttpStatus = map[string]int{
// Account creation errors
ErrKeyAccountCreationFailed: http.StatusInternalServerError,
ErrKeyEmailAlreadyExists: http.StatusConflict,
ErrKeyPasswordHashingFailed: http.StatusInternalServerError,
// Account lookup and existence verification errors
ErrKeyUserNotFound: http.StatusNotFound,
ErrKeyPublicKeyNotFound: http.StatusNotFound,
// Authentication and login errors
ErrKeyInvalidLogin: http.StatusUnauthorized,
ErrKeyInvalidPassword: http.StatusUnauthorized,
ErrKeyInvalidOTPCode: http.StatusBadRequest,
ErrKeyOTPVerificationFailed: http.StatusBadRequest,
ErrKeyLoginFailed: http.StatusInternalServerError,
// Account update errors
ErrKeyAccountUpdateFailed: http.StatusInternalServerError,
ErrKeyAccountAlreadyVerified: http.StatusConflict,
// JWT generation errors
ErrKeyJWTGenerationFailed: http.StatusInternalServerError,
// OTP management errors
ErrKeyOTPGenerationFailed: http.StatusInternalServerError,
ErrKeyOTPEnableFailed: http.StatusInternalServerError,
ErrKeyOTPDisableFailed: http.StatusInternalServerError,
// Public key management errors
ErrKeyAddPublicKeyFailed: http.StatusInternalServerError,
ErrKeyPublicKeyExists: http.StatusConflict,
// Pin management errors
ErrKeyPinAddFailed: http.StatusInternalServerError,
ErrKeyPinDeleteFailed: http.StatusInternalServerError,
ErrKeyPinsRetrievalFailed: http.StatusInternalServerError,
// General errors
ErrKeyDatabaseOperationFailed: http.StatusInternalServerError,
ErrKeyHashingFailed: http.StatusInternalServerError,
// Security token errors
ErrKeySecurityTokenExpired: http.StatusUnauthorized,
ErrKeySecurityInvalidToken: http.StatusUnauthorized,
}
)
type AccountError struct {
Key string // A unique identifier for the error type
Message string // Human-readable error message
Err error // Underlying error, if any
}
func (e *AccountError) Error() string {
if e.Err != nil {
return fmt.Sprintf("%s: %v", e.Message, e.Err)
}
return e.Message
}
func NewAccountError(key string, err error, customMessage ...string) *AccountError {
message, exists := defaultErrorMessages[key]
if !exists {
message = "An unknown error occurred"
}
if len(customMessage) > 0 {
message = customMessage[0]
}
return &AccountError{
Key: key,
Message: message,
Err: err,
}
}

187
account/jwt.go Normal file
View File

@ -0,0 +1,187 @@
package account
import (
"crypto/ed25519"
"errors"
"fmt"
"net/http"
"strconv"
"time"
"git.lumeweb.com/LumeWeb/portal/config"
"github.com/samber/lo"
"go.sia.tech/jape"
"git.lumeweb.com/LumeWeb/portal/api/router"
apiRegistry "git.lumeweb.com/LumeWeb/portal/api/registry"
"github.com/golang-jwt/jwt/v5"
)
const AUTH_COOKIE_NAME = "auth_token"
type JWTPurpose string
type VerifyTokenFunc func(claim *jwt.RegisteredClaims) error
var (
nopVerifyFunc VerifyTokenFunc = func(claim *jwt.RegisteredClaims) error {
return nil
}
ErrJWTUnexpectedClaimsType = errors.New("unexpected claims type")
ErrJWTUnexpectedIssuer = errors.New("unexpected issuer")
ErrJWTInvalid = errors.New("invalid JWT")
)
const (
JWTPurposeLogin JWTPurpose = "login"
JWTPurpose2FA JWTPurpose = "2fa"
JWTPurposeNone JWTPurpose = ""
)
func JWTGenerateToken(domain string, privateKey ed25519.PrivateKey, userID uint, purpose JWTPurpose) (string, error) {
return JWTGenerateTokenWithDuration(domain, privateKey, userID, time.Hour*24, purpose)
}
func JWTGenerateTokenWithDuration(domain string, privateKey ed25519.PrivateKey, userID uint, duration time.Duration, purpose JWTPurpose) (string, error) {
// Define the claims
claims := jwt.RegisteredClaims{
Issuer: domain,
Subject: strconv.Itoa(int(userID)),
ExpiresAt: jwt.NewNumericDate(time.Now().Add(duration)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Audience: []string{string(purpose)},
}
// Create the token
token := jwt.NewWithClaims(jwt.SigningMethodEdDSA, claims)
// Sign the token with the Ed25519 private key
tokenString, err := token.SignedString(privateKey)
if err != nil {
return "", err
}
return tokenString, nil
}
func JWTVerifyToken(token string, domain string, privateKey ed25519.PrivateKey, verifyFunc VerifyTokenFunc) (*jwt.RegisteredClaims, error) {
validatedToken, err := jwt.ParseWithClaims(token, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodEd25519); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
publicKey := privateKey.Public()
return publicKey, nil
})
if err != nil {
return nil, err
}
if verifyFunc == nil {
verifyFunc = nopVerifyFunc
}
claim, ok := validatedToken.Claims.(*jwt.RegisteredClaims)
if !ok {
return nil, fmt.Errorf("%w: %s", ErrJWTUnexpectedClaimsType, validatedToken.Claims)
}
if domain != claim.Issuer {
return nil, fmt.Errorf("%w: %s", ErrJWTUnexpectedIssuer, claim.Issuer)
}
err = verifyFunc(claim)
return claim, err
}
func SetAuthCookie(jc jape.Context, c *config.Manager, jwt string) {
for _, api := range apiRegistry.GetAllAPIs() {
routeableApi, ok := api.(router.RoutableAPI)
if !ok {
continue
}
http.SetCookie(jc.ResponseWriter, &http.Cookie{
Name: routeableApi.AuthTokenName(),
Value: jwt,
MaxAge: int((24 * time.Hour).Seconds()),
Secure: true,
HttpOnly: true,
Path: "/",
Domain: c.Config().Core.Domain,
})
}
}
func EchoAuthCookie(jc jape.Context, config *config.Manager) {
for _, api := range apiRegistry.GetAllAPIs() {
routeableApi, ok := api.(router.RoutableAPI)
if !ok {
continue
}
cookies := lo.Filter(jc.Request.Cookies(), func(item *http.Cookie, _ int) bool {
return item.Name == routeableApi.AuthTokenName()
})
if len(cookies) == 0 {
continue
}
unverified, _, err := jwt.NewParser().ParseUnverified(cookies[0].Value, &jwt.RegisteredClaims{})
if err != nil {
http.Error(jc.ResponseWriter, err.Error(), http.StatusInternalServerError)
return
}
exp, err := unverified.Claims.GetExpirationTime()
if err != nil {
http.Error(jc.ResponseWriter, err.Error(), http.StatusInternalServerError)
return
}
http.SetCookie(jc.ResponseWriter, &http.Cookie{
Name: cookies[0].Name,
Value: cookies[0].Value,
MaxAge: int(exp.Time.Sub(time.Now()).Seconds()),
Secure: true,
HttpOnly: true,
Path: "/",
Domain: config.Config().Core.Domain,
})
}
}
func ClearAuthCookie(jc jape.Context, config *config.Manager) {
for _, api := range apiRegistry.GetAllAPIs() {
routeableApi, ok := api.(router.RoutableAPI)
if !ok {
continue
}
jc.ResponseWriter.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
jc.ResponseWriter.Header().Set("Pragma", "no-cache")
jc.ResponseWriter.Header().Set("Expires", "0")
http.SetCookie(jc.ResponseWriter, &http.Cookie{
Name: routeableApi.AuthTokenName(),
Value: "",
Expires: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC),
MaxAge: -1,
Secure: true,
HttpOnly: true,
Path: "/",
Domain: config.Config().Core.Domain,
})
}
}

19
account/totp.go Normal file
View File

@ -0,0 +1,19 @@
package account
import "github.com/pquerna/otp/totp"
func TOTPGenerate(domain string, email string) (string, error) {
key, err := totp.Generate(totp.GenerateOpts{
Issuer: domain,
AccountName: email,
})
if err != nil {
return "", err
}
return key.Secret(), nil
}
func TOTPValidate(secret string, code string) bool {
return totp.Validate(code, secret)
}

7
account/web.go Normal file
View File

@ -0,0 +1,7 @@
package account
import "go.sia.tech/jape"
func SendJWT(jc jape.Context, jwt string) {
jc.ResponseWriter.Header().Set("Authorization", "Bearer "+jwt)
}

13
api/account.go Normal file
View File

@ -0,0 +1,13 @@
package api
import (
"git.lumeweb.com/LumeWeb/portal/api/account"
"git.lumeweb.com/LumeWeb/portal/api/registry"
)
func init() {
registry.RegisterEntry(registry.APIEntry{
Key: "account",
Module: account.Module,
})
}

509
api/account/account.go Normal file
View File

@ -0,0 +1,509 @@
package account
import (
"context"
"crypto/ed25519"
"embed"
_ "embed"
"errors"
"io/fs"
"net/http"
"strings"
"github.com/rs/cors"
"git.lumeweb.com/LumeWeb/portal/api/swagger"
"git.lumeweb.com/LumeWeb/portal/api/router"
"git.lumeweb.com/LumeWeb/portal/config"
"go.uber.org/zap"
"github.com/julienschmidt/httprouter"
"git.lumeweb.com/LumeWeb/portal/account"
"git.lumeweb.com/LumeWeb/portal/api/middleware"
"git.lumeweb.com/LumeWeb/portal/api/registry"
"go.sia.tech/jape"
"go.uber.org/fx"
)
//go:embed swagger.yaml
var swagSpec []byte
//go:embed all:app/build/client
var appFs embed.FS
var (
_ registry.API = (*AccountAPI)(nil)
_ router.RoutableAPI = (*AccountAPI)(nil)
)
type AccountAPI struct {
config *config.Manager
accounts *account.AccountServiceDefault
identity ed25519.PrivateKey
logger *zap.Logger
}
type AccountAPIParams struct {
fx.In
Config *config.Manager
Accounts *account.AccountServiceDefault
Identity ed25519.PrivateKey
Logger *zap.Logger
}
func NewS5(params AccountAPIParams) AccountApiResult {
api := &AccountAPI{
config: params.Config,
accounts: params.Accounts,
identity: params.Identity,
logger: params.Logger,
}
return AccountApiResult{
API: api,
AccountAPI: api,
}
}
var Module = fx.Module("s5_api",
fx.Provide(NewS5),
)
type AccountApiResult struct {
fx.Out
API registry.API `group:"api"`
AccountAPI *AccountAPI
}
func (a AccountAPI) Name() string {
return "account"
}
func (a *AccountAPI) Init() error {
return nil
}
func (a AccountAPI) Start(ctx context.Context) error {
return nil
}
func (a AccountAPI) Stop(ctx context.Context) error {
return nil
}
func (a AccountAPI) login(jc jape.Context) {
var request LoginRequest
if jc.Decode(&request) != nil {
return
}
exists, _, err := a.accounts.EmailExists(request.Email)
if !exists {
_ = jc.Error(account.NewAccountError(account.ErrKeyInvalidLogin, nil), http.StatusUnauthorized)
if err != nil {
a.logger.Error("failed to check if email exists", zap.Error(err))
}
return
}
jwt, user, err := a.accounts.LoginPassword(request.Email, request.Password, jc.Request.RemoteAddr)
if err != nil || user == nil {
_ = jc.Error(account.NewAccountError(account.ErrKeyInvalidLogin, err), http.StatusUnauthorized)
if err != nil {
a.logger.Error("failed to login", zap.Error(err))
}
return
}
account.SetAuthCookie(jc, a.config, jwt)
account.SendJWT(jc, jwt)
jc.Encode(&LoginResponse{
Token: jwt,
Otp: user.OTPEnabled && user.OTPVerified,
})
}
func (a AccountAPI) register(jc jape.Context) {
var request RegisterRequest
if jc.Decode(&request) != nil {
return
}
if len(request.FirstName) == 0 || len(request.LastName) == 0 {
_ = jc.Error(account.NewAccountError(account.ErrKeyAccountCreationFailed, nil), http.StatusBadRequest)
return
}
user, err := a.accounts.CreateAccount(request.Email, request.Password, true)
if err != nil {
_ = jc.Error(err, http.StatusUnauthorized)
a.logger.Error("failed to update account name", zap.Error(err))
return
}
err = a.accounts.UpdateAccountName(user.ID, request.FirstName, request.LastName)
if err != nil {
_ = jc.Error(account.NewAccountError(account.ErrKeyAccountCreationFailed, err), http.StatusBadRequest)
a.logger.Error("failed to update account name", zap.Error(err))
return
}
}
func (a AccountAPI) verifyEmail(jc jape.Context) {
var request VerifyEmailRequest
if jc.Decode(&request) != nil {
return
}
if request.Email == "" || request.Token == "" {
_ = jc.Error(errors.New("invalid request"), http.StatusBadRequest)
return
}
err := a.accounts.VerifyEmail(request.Email, request.Token)
if jc.Check("Failed to verify email", err) != nil {
return
}
}
func (a AccountAPI) resendVerifyEmail(jc jape.Context) {
user := middleware.GetUserFromContext(jc.Request.Context())
err := a.accounts.SendEmailVerification(user)
if jc.Check("failed to resend email verification", err) != nil {
return
}
}
func (a AccountAPI) otpGenerate(jc jape.Context) {
user := middleware.GetUserFromContext(jc.Request.Context())
otp, err := a.accounts.OTPGenerate(user)
if jc.Check("failed to generate otp", err) != nil {
return
}
jc.Encode(&OTPGenerateResponse{
OTP: otp,
})
}
func (a AccountAPI) otpVerify(jc jape.Context) {
user := middleware.GetUserFromContext(jc.Request.Context())
var request OTPVerifyRequest
if jc.Decode(&request) != nil {
return
}
err := a.accounts.OTPEnable(user, request.OTP)
if jc.Check("failed to verify otp", err) != nil {
return
}
}
func (a AccountAPI) otpValidate(jc jape.Context) {
user := middleware.GetUserFromContext(jc.Request.Context())
var request OTPValidateRequest
if jc.Decode(&request) != nil {
return
}
jwt, err := a.accounts.LoginOTP(user, request.OTP)
if jc.Check("failed to validate otp", err) != nil {
return
}
account.SetAuthCookie(jc, a.config, jwt)
account.SendJWT(jc, jwt)
jc.Encode(&LoginResponse{
Token: jwt,
Otp: false,
})
}
func (a AccountAPI) otpDisable(jc jape.Context) {
user := middleware.GetUserFromContext(jc.Request.Context())
var request OTPDisableRequest
if jc.Decode(&request) != nil {
return
}
valid, _, err := a.accounts.ValidLoginByUserID(user, request.Password)
if !valid {
_ = jc.Error(account.NewAccountError(account.ErrKeyInvalidLogin, nil), http.StatusUnauthorized)
return
}
err = a.accounts.OTPDisable(user)
if jc.Check("failed to disable otp", err) != nil {
return
}
}
func (a AccountAPI) passwordResetRequest(jc jape.Context) {
var request PasswordResetRequest
if jc.Decode(&request) != nil {
return
}
exists, user, err := a.accounts.EmailExists(request.Email)
if jc.Check("invalid request", err) != nil || !exists {
return
}
err = a.accounts.SendPasswordReset(user)
if jc.Check("failed to request password reset", err) != nil {
return
}
jc.ResponseWriter.WriteHeader(http.StatusOK)
}
func (a AccountAPI) passwordResetConfirm(jc jape.Context) {
var request PasswordResetVerifyRequest
if jc.Decode(&request) != nil {
return
}
exists, _, err := a.accounts.EmailExists(request.Email)
if jc.Check("invalid request", err) != nil || !exists {
return
}
err = a.accounts.ResetPassword(request.Email, request.Password, request.Token)
if jc.Check("failed to reset password", err) != nil {
return
}
jc.ResponseWriter.WriteHeader(http.StatusOK)
}
func (a AccountAPI) ping(jc jape.Context) {
token := middleware.GetAuthTokenFromContext(jc.Request.Context())
account.EchoAuthCookie(jc, a.config)
jc.Encode(&PongResponse{
Ping: "pong",
Token: token,
})
}
func (a AccountAPI) accountInfo(jc jape.Context) {
user := middleware.GetUserFromContext(jc.Request.Context())
_, acct, _ := a.accounts.AccountExists(user)
jc.Encode(&AccountInfoResponse{
ID: acct.ID,
Email: acct.Email,
FirstName: acct.FirstName,
LastName: acct.LastName,
Verified: acct.Verified,
})
}
func (a AccountAPI) logout(c jape.Context) {
account.ClearAuthCookie(c, a.config)
}
func (a AccountAPI) uploadLimit(c jape.Context) {
c.Encode(&UploadLimitResponse{
Limit: a.config.Config().Core.PostUploadLimit,
})
}
func (a AccountAPI) updateEmail(c jape.Context) {
user := middleware.GetUserFromContext(c.Request.Context())
var request UpdateEmailRequest
if c.Decode(&request) != nil {
return
}
err := a.accounts.UpdateAccountEmail(user, request.Email, request.Password)
if c.Check("failed to update email", err) != nil {
return
}
}
func (a AccountAPI) updatePassword(c jape.Context) {
user := middleware.GetUserFromContext(c.Request.Context())
var request UpdatePasswordRequest
if c.Decode(&request) != nil {
return
}
err := a.accounts.UpdateAccountPassword(user, request.CurrentPassword, request.NewPassword)
if c.Check("failed to update password", err) != nil {
return
}
}
func (a AccountAPI) meta(c jape.Context) {
c.Encode(&MetaResponse{
Domain: a.config.Config().Core.Domain,
})
}
func (a *AccountAPI) Routes() (*httprouter.Router, error) {
loginAuthMw2fa := authMiddleware(middleware.AuthMiddlewareOptions{
Identity: a.identity,
Accounts: a.accounts,
Config: a.config,
Purpose: account.JWTPurpose2FA,
EmptyAllowed: true,
ExpiredAllowed: true,
})
authMw := authMiddleware(middleware.AuthMiddlewareOptions{
Identity: a.identity,
Accounts: a.accounts,
Config: a.config,
Purpose: account.JWTPurposeNone,
})
pingAuthMw := authMiddleware(middleware.AuthMiddlewareOptions{
Identity: a.identity,
Accounts: a.accounts,
Config: a.config,
Purpose: account.JWTPurposeLogin,
})
appFiles, _ := fs.Sub(appFs, "app/build/client")
appServ := http.FileServer(http.FS(appFiles))
appHandler := func(c jape.Context) {
appServ.ServeHTTP(c.ResponseWriter, c.Request)
}
appServer := middleware.ApplyMiddlewares(appHandler, middleware.ProxyMiddleware)
swaggerRoutes, err := swagger.Swagger(swagSpec, map[string]jape.Handler{})
if err != nil {
return nil, err
}
swaggerJape := jape.Mux(swaggerRoutes)
getApiJape := jape.Mux(map[string]jape.Handler{
"GET /api/auth/otp/generate": middleware.ApplyMiddlewares(a.otpGenerate, authMw, middleware.ProxyMiddleware),
"GET /api/account": middleware.ApplyMiddlewares(a.accountInfo, authMw, middleware.ProxyMiddleware),
"GET /api/upload-limit": middleware.ApplyMiddlewares(a.uploadLimit, middleware.ProxyMiddleware),
"GET /api/meta": middleware.ApplyMiddlewares(a.meta, middleware.ProxyMiddleware),
})
getHandler := func(c jape.Context) {
if strings.HasPrefix(c.Request.URL.Path, "/api") {
getApiJape.ServeHTTP(c.ResponseWriter, c.Request)
return
}
if strings.HasPrefix(c.Request.URL.Path, "/swagger") {
swaggerJape.ServeHTTP(c.ResponseWriter, c.Request)
return
}
if !strings.HasPrefix(c.Request.URL.Path, "/assets") && c.Request.URL.Path != "favicon.ico" && c.Request.URL.Path != "/" && !strings.HasSuffix(c.Request.URL.Path, ".html") {
c.Request.URL.Path = "/"
}
appServer(c)
}
corsMw := cors.New(cors.Options{
AllowOriginFunc: func(origin string) bool {
return true
},
AllowedMethods: []string{"GET", "POST", "DELETE"},
AllowedHeaders: []string{"Authorization", "Content-Type"},
})
corsOptionsHandler := func(c jape.Context) {
c.ResponseWriter.WriteHeader(http.StatusOK)
}
routes := map[string]jape.Handler{
// Auth
"POST /api/auth/ping": middleware.ApplyMiddlewares(a.ping, corsMw.Handler, pingAuthMw, middleware.ProxyMiddleware),
"POST /api/auth/login": middleware.ApplyMiddlewares(a.login, corsMw.Handler, loginAuthMw2fa, middleware.ProxyMiddleware),
"POST /api/auth/register": middleware.ApplyMiddlewares(a.register, corsMw.Handler, middleware.ProxyMiddleware),
"POST /api/auth/otp/validate": middleware.ApplyMiddlewares(a.otpValidate, corsMw.Handler, authMw, middleware.ProxyMiddleware),
"POST /api/auth/logout": middleware.ApplyMiddlewares(a.logout, corsMw.Handler, authMw, middleware.ProxyMiddleware),
// Account
"POST /api/account/verify-email": middleware.ApplyMiddlewares(a.verifyEmail, corsMw.Handler, authMw, middleware.ProxyMiddleware),
"POST /api/account/verify-email/resend": middleware.ApplyMiddlewares(a.resendVerifyEmail, corsMw.Handler, authMw, middleware.ProxyMiddleware),
"POST /api/account/otp/verify": middleware.ApplyMiddlewares(a.otpVerify, corsMw.Handler, authMw, middleware.ProxyMiddleware),
"POST /api/account/otp/disable": middleware.ApplyMiddlewares(a.otpDisable, corsMw.Handler, authMw, middleware.ProxyMiddleware),
"POST /api/account/password-reset/request": middleware.ApplyMiddlewares(a.passwordResetRequest, corsMw.Handler, middleware.ProxyMiddleware),
"POST /api/account/password-reset/confirm": middleware.ApplyMiddlewares(a.passwordResetConfirm, corsMw.Handler, middleware.ProxyMiddleware),
"POST /api/account/update-email": middleware.ApplyMiddlewares(a.updateEmail, corsMw.Handler, authMw, middleware.ProxyMiddleware),
"POST /api/account/update-password": middleware.ApplyMiddlewares(a.updatePassword, corsMw.Handler, authMw, middleware.ProxyMiddleware),
// CORS
"OPTIONS /api/auth/ping": middleware.ApplyMiddlewares(corsOptionsHandler, corsMw.Handler, authMw, middleware.ProxyMiddleware),
"OPTIONS /api/auth/login": middleware.ApplyMiddlewares(corsOptionsHandler, corsMw.Handler, loginAuthMw2fa, middleware.ProxyMiddleware),
"OPTIONS /api/auth/register": middleware.ApplyMiddlewares(corsOptionsHandler, corsMw.Handler, middleware.ProxyMiddleware),
"OPTIONS /api/auth/otp/validate": middleware.ApplyMiddlewares(corsOptionsHandler, corsMw.Handler, middleware.ProxyMiddleware),
"OPTIONS /api/auth/logout": middleware.ApplyMiddlewares(corsOptionsHandler, corsMw.Handler, authMw, middleware.ProxyMiddleware),
"OPTIONS /api/account/verify-email": middleware.ApplyMiddlewares(corsOptionsHandler, corsMw.Handler, middleware.ProxyMiddleware),
"OPTIONS /api/account/verify-email/resend": middleware.ApplyMiddlewares(corsOptionsHandler, corsMw.Handler, authMw, middleware.ProxyMiddleware),
"OPTIONS /api/account/otp/verify": middleware.ApplyMiddlewares(corsOptionsHandler, corsMw.Handler, authMw, middleware.ProxyMiddleware),
"OPTIONS /api/account/otp/disable": middleware.ApplyMiddlewares(corsOptionsHandler, corsMw.Handler, authMw, middleware.ProxyMiddleware),
"OPTIONS /api/account/password-reset/request": middleware.ApplyMiddlewares(corsOptionsHandler, corsMw.Handler, middleware.ProxyMiddleware),
"OPTIONS /api/account/password-reset/confirm": middleware.ApplyMiddlewares(corsOptionsHandler, corsMw.Handler, middleware.ProxyMiddleware),
"OPTIONS /api/account/update-email": middleware.ApplyMiddlewares(corsOptionsHandler, corsMw.Handler, authMw, middleware.ProxyMiddleware),
"OPTIONS /api/account/update-password": middleware.ApplyMiddlewares(corsOptionsHandler, corsMw.Handler, authMw, middleware.ProxyMiddleware),
// Get Routes
"OPTIONS /api/upload-limit": middleware.ApplyMiddlewares(corsOptionsHandler, corsMw.Handler, middleware.ProxyMiddleware),
"OPTIONS /api/account": middleware.ApplyMiddlewares(corsOptionsHandler, corsMw.Handler, authMw, middleware.ProxyMiddleware),
"OPTIONS /api/auth/otp/generate": middleware.ApplyMiddlewares(corsOptionsHandler, corsMw.Handler, authMw, middleware.ProxyMiddleware),
"GET /*path": middleware.ApplyMiddlewares(getHandler, corsMw.Handler),
}
return jape.Mux(routes), nil
}
func (a AccountAPI) Can(w http.ResponseWriter, r *http.Request) bool {
return false
}
func (a AccountAPI) Handle(w http.ResponseWriter, r *http.Request) {
}
func (a *AccountAPI) Domain() string {
return router.BuildSubdomain(a, a.config)
}
func (a AccountAPI) AuthTokenName() string {
return account.AUTH_COOKIE_NAME
}

1
api/account/app Submodule

@ -0,0 +1 @@
Subproject commit d7f0154fb89dc9dfafc2bc7fd22e24c6ebaaafc7

73
api/account/messages.go Normal file
View File

@ -0,0 +1,73 @@
package account
type LoginRequest struct {
Email string `json:"email"`
Password string `json:"password"`
}
type LoginResponse struct {
Token string `json:"token"`
Otp bool `json:"otp"`
}
type RegisterRequest struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Email string `json:"email"`
Password string `json:"password"`
}
type OTPGenerateResponse struct {
OTP string `json:"otp"`
}
type OTPVerifyRequest struct {
OTP string `json:"otp"`
}
type OTPValidateRequest struct {
OTP string `json:"otp"`
}
type OTPDisableRequest struct {
Password string `json:"password"`
}
type VerifyEmailRequest struct {
Email string `json:"email"`
Token string `json:"token"`
}
type PasswordResetRequest struct {
Email string `json:"email"`
}
type PasswordResetVerifyRequest struct {
Email string `json:"email"`
Token string `json:"token"`
Password string `json:"password"`
}
type PongResponse struct {
Ping string `json:"ping"`
Token string `json:"token"`
}
type AccountInfoResponse struct {
ID uint `json:"id"`
Email string `json:"email"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Verified bool `json:"verified"`
}
type UploadLimitResponse struct {
Limit uint64 `json:"limit"`
}
type UpdateEmailRequest struct {
Email string `json:"email"`
Password string `json:"password"`
}
type UpdatePasswordRequest struct {
CurrentPassword string `json:"current_password"`
NewPassword string `json:"new_password"`
}
type MetaResponse struct {
Domain string `json:"domain"`
}

23
api/account/middleware.go Normal file
View File

@ -0,0 +1,23 @@
package account
import (
"net/http"
"git.lumeweb.com/LumeWeb/portal/account"
"git.lumeweb.com/LumeWeb/portal/api/middleware"
)
const (
authCookieName = account.AUTH_COOKIE_NAME
authQueryParam = "auth_token"
)
func findToken(r *http.Request) string {
return middleware.FindAuthToken(r, authCookieName, authQueryParam)
}
func authMiddleware(options middleware.AuthMiddlewareOptions) middleware.HttpMiddlewareFunc {
options.FindToken = findToken
return middleware.AuthMiddleware(options)
}

351
api/account/swagger.yaml Normal file
View File

@ -0,0 +1,351 @@
openapi: 3.0.0
info:
title: Account Management API
version: "1.0"
description: API for managing user accounts, including login, registration, OTP operations, and password resets.
paths:
/api/auth/login:
post:
summary: Login to the system
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LoginRequest'
responses:
'200':
description: Successfully logged in
content:
application/json:
schema:
$ref: '#/components/schemas/LoginResponse'
'401':
description: Unauthorized
/api/auth/logout:
post:
summary: Logout of account service
responses:
'200':
description: Successfully logged out
/api/auth/register:
post:
summary: Register a new account
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RegisterRequest'
responses:
'200':
description: Successfully registered
'400':
description: Bad Request
/api/account/verify-email:
post:
summary: Verify email address
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/VerifyEmailRequest'
responses:
'200':
description: Email verified successfully
/api/account/verify-email/resend:
post:
summary: Resend email verification
responses:
'200':
description: Email verification resent successfully
/api/auth/otp/generate:
get:
summary: Generate OTP for two-factor authentication
responses:
'200':
description: OTP generated successfully
content:
application/json:
schema:
$ref: '#/components/schemas/OTPGenerateResponse'
/api/account/otp/verify:
post:
summary: Verify OTP for enabling two-factor authentication
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/OTPVerifyRequest'
responses:
'200':
description: OTP verified successfully
/api/account/otp/validate:
post:
summary: Validate OTP for two-factor authentication login
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/OTPValidateRequest'
responses:
'200':
description: OTP validated successfully
/api/auth/otp/disable:
post:
summary: Disable OTP for two-factor authentication
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/OTPDisableRequest'
responses:
'200':
description: OTP disabled successfully
/api/account/password-reset/request:
post:
summary: Request a password reset
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PasswordResetRequest'
responses:
'200':
description: Password reset requested successfully
/api/account/password-reset/confirm:
post:
summary: Confirm a password reset
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PasswordResetVerifyRequest'
responses:
'200':
description: Password reset successfully
/api/auth/ping:
post:
summary: Auth check endpoint
responses:
'200':
description: Pong
content:
application/json:
schema:
$ref: '#/components/schemas/PingResponse'
'401':
description: Unauthorized
/api/account:
get:
summary: Get account information
responses:
'200':
description: Account information retrieved successfully
content:
application/json:
schema:
$ref: '#/components/schemas/AccountInfoResponse'
'401':
description: Unauthorized
/api/account/update-email:
post:
summary: Update email address
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdateEmailRequest'
responses:
'200':
description: Email updated successfully
/api/account/update-password:
post:
summary: Update password
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UpdatePasswordRequest'
responses:
'200':
description: Password updated successfully
/api/upload-limit:
get:
summary: Get the basic file upload (POST) upload limit set by the portal
responses:
'200':
description: Upload limit retrieved successfully
content:
application/json:
schema:
$ref: '#/components/schemas/UploadLimitResponse'
/api/meta:
get:
summary: Get metadata about the portal
responses:
'200':
description: Metadata retrieved successfully
content:
application/json:
schema:
$ref: '#/components/schemas/MetaResponse'
components:
schemas:
LoginRequest:
type: object
required:
- email
- password
properties:
email:
type: string
password:
type: string
LoginResponse:
type: object
properties:
token:
type: string
RegisterRequest:
type: object
required:
- first_name
- last_name
- email
- password
properties:
first_name:
type: string
last_name:
type: string
email:
type: string
password:
type: string
VerifyEmailRequest:
type: object
required:
- email
- token
properties:
email:
type: string
token:
type: string
OTPGenerateResponse:
type: object
properties:
OTP:
type: string
OTPVerifyRequest:
type: object
required:
- OTP
properties:
OTP:
type: string
OTPValidateRequest:
type: object
required:
- OTP
properties:
OTP:
type: string
OTPDisableRequest:
type: object
required:
- password
properties:
password:
type: string
PasswordResetRequest:
type: object
required:
- email
properties:
email:
type: string
PasswordResetVerifyRequest:
type: object
required:
- email
- token
- password
properties:
email:
type: string
token:
type: string
password:
type: string
UpdateEmailRequest:
type: object
required:
- email
- password
properties:
email:
type: string
password:
type: string
UpdatePasswordRequest:
type: object
required:
- current_password
- new_password
properties:
current_password:
type: string
new_password:
type: string
PingResponse:
type: object
properties:
ping:
type: string
token:
type: string
AccountInfoResponse:
type: object
required:
- id
- first_name
- last_name
- email
- verified
properties:
id:
type: number
first_name:
type: string
last_name:
type: string
email:
type: string
verified:
type: boolean
UploadLimitResponse:
type: object
properties:
limit:
type: number
required:
- limit
MetaResponse:
type: object
required:
- domain
properties:
domain:
type: string

68
api/api.go Normal file
View File

@ -0,0 +1,68 @@
package api
import (
"context"
"slices"
"git.lumeweb.com/LumeWeb/portal/config"
"git.lumeweb.com/LumeWeb/portal/api/registry"
"go.uber.org/fx"
)
var alwaysEnabled = []string{"account"}
func BuildApis(cm *config.Manager) fx.Option {
var options []fx.Option
enabledProtocols := cm.Viper().GetStringSlice("core.protocols")
for _, entry := range registry.GetEntryRegistry() {
if slices.Contains(enabledProtocols, entry.Key) || slices.Contains(alwaysEnabled, entry.Key) {
options = append(options, entry.Module)
}
}
type initParams struct {
fx.In
Apis []registry.API `group:"api"`
}
options = append(options, fx.Invoke(func(params initParams) error {
for _, protocol := range params.Apis {
err := protocol.Init()
if err != nil {
return err
}
registry.RegisterAPI(protocol)
}
return nil
}))
return fx.Module("api", fx.Options(options...))
}
type LifecyclesParams struct {
fx.In
Protocols []registry.API `group:"protocol"`
}
func SetupLifecycles(lifecycle fx.Lifecycle, params LifecyclesParams) error {
for _, entry := range registry.GetEntryRegistry() {
for _, protocol := range params.Protocols {
if protocol.Name() == entry.Key {
lifecycle.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
return protocol.Start(ctx)
},
OnStop: func(ctx context.Context) error {
return protocol.Stop(ctx)
},
})
}
}
}
return nil
}

100
api/casbin.go Normal file
View File

@ -0,0 +1,100 @@
package api
import (
"github.com/casbin/casbin/v2"
"github.com/casbin/casbin/v2/model"
"github.com/casbin/casbin/v2/persist"
"go.uber.org/zap"
"strings"
"sync"
)
func NewCasbin(logger *zap.Logger) *casbin.Enforcer {
m := model.NewModel()
m.AddDef("r", "r", "sub, obj, act")
m.AddDef("p", "p", "sub, obj, act")
m.AddDef("g", "g", "_, _")
m.AddDef("e", "e", "some(where (p.eft == allow))")
m.AddDef("m", "m", "g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act")
a := NewPolicyAdapter(logger)
e, err := casbin.NewEnforcer(m, a)
if err != nil {
logger.Fatal("Failed to create casbin enforcer", zap.Error(err))
}
// Add policies after creating the enforcer
_ = a.AddPolicy("p", "p", []string{"admin", "/admin*"})
err = e.LoadPolicy()
if err != nil {
logger.Fatal("Failed to load policies into Casbin model", zap.Error(err))
}
return e
}
type PolicyAdapter struct {
policy []string
lock sync.RWMutex
logger *zap.Logger
}
// NewPolicyAdapter creates a new PolicyAdapter instance.
func NewPolicyAdapter(logger *zap.Logger) *PolicyAdapter {
return &PolicyAdapter{
policy: make([]string, 0),
logger: logger,
}
}
// LoadPolicy loads all policy rules from the storage.
func (a *PolicyAdapter) LoadPolicy(model model.Model) error {
a.lock.RLock()
defer a.lock.RUnlock()
for _, line := range a.policy {
err := persist.LoadPolicyLine(line, model)
if err != nil {
a.logger.Fatal("Failed to load policy line", zap.Error(err))
}
}
return nil
}
// SavePolicy saves all policy rules to the storage.
func (a *PolicyAdapter) SavePolicy(model model.Model) error {
return nil
}
// AddPolicy adds a policy rule to the storage.
// AddPolicy adds a policy rule to the storage.
func (a *PolicyAdapter) AddPolicy(sec string, ptype string, rule []string) error {
a.lock.Lock()
defer a.lock.Unlock()
// Create a line representing the policy rule with the section
line := sec + ", " + ptype + ", " + strings.Join(rule, ", ")
// Check if the policy rule already exists
for _, existingLine := range a.policy {
if line == existingLine {
return nil // Policy rule already exists, no need to add it again
}
}
// Add the policy rule to the storage
a.policy = append(a.policy, line)
return nil
}
// RemovePolicy removes a policy rule from the storage.
func (a *PolicyAdapter) RemovePolicy(sec string, ptype string, rule []string) error {
return nil
}
// RemoveFilteredPolicy removes policy rules that match the filter from the storage.
func (a *PolicyAdapter) RemoveFilteredPolicy(sec string, ptype string, fieldIndex int, fieldValues ...string) error {
return nil
}

View File

@ -0,0 +1,260 @@
package middleware
import (
"context"
"crypto/ed25519"
"errors"
"net/http"
"slices"
"strconv"
"strings"
"git.lumeweb.com/LumeWeb/portal/config"
"git.lumeweb.com/LumeWeb/portal/account"
"github.com/golang-jwt/jwt/v5"
"go.sia.tech/jape"
)
const DEFAULT_AUTH_CONTEXT_KEY = "user_id"
const AUTH_TOKEN_CONTEXT_KEY = "auth_token"
type JapeMiddlewareFunc func(jape.Handler) jape.Handler
type HttpMiddlewareFunc func(http.Handler) http.Handler
type FindAuthTokenFunc func(r *http.Request) string
func AdaptMiddleware(mid func(http.Handler) http.Handler) JapeMiddlewareFunc {
return jape.Adapt(func(h http.Handler) http.Handler {
handler := mid(h)
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
handler.ServeHTTP(w, r)
})
})
}
// ProxyMiddleware creates a new HTTP middleware for handling X-Forwarded-For headers.
func ProxyMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
ips := strings.Split(xff, ", ")
if len(ips) > 0 {
r.RemoteAddr = ips[0]
}
}
next.ServeHTTP(w, r)
})
}
func ApplyMiddlewares(handler jape.Handler, middlewares ...interface{}) jape.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
switch middlewares[i].(type) {
case JapeMiddlewareFunc:
mid := middlewares[i].(JapeMiddlewareFunc)
handler = mid(handler)
case func(http.Handler) http.Handler:
mid := middlewares[i].(func(http.Handler) http.Handler)
handler = AdaptMiddleware(mid)(handler)
case HttpMiddlewareFunc:
mid := middlewares[i].(HttpMiddlewareFunc)
handler = AdaptMiddleware(mid)(handler)
default:
panic("Invalid middleware type")
}
}
return handler
}
func FindAuthToken(r *http.Request, cookieName string, queryParam string) string {
authHeader := ParseAuthTokenHeader(r.Header)
if authHeader != "" {
return authHeader
}
if cookie, err := r.Cookie(cookieName); cookie != nil && err == nil {
return cookie.Value
}
if cookie, err := r.Cookie(account.AUTH_COOKIE_NAME); cookie != nil && err == nil {
return cookie.Value
}
return r.FormValue(queryParam)
}
func ParseAuthTokenHeader(headers http.Header) string {
authHeader := headers.Get("Authorization")
if authHeader == "" {
return ""
}
authHeader = strings.TrimPrefix(authHeader, "Bearer ")
authHeader = strings.TrimPrefix(authHeader, "bearer ")
return authHeader
}
type AuthMiddlewareOptions struct {
Identity ed25519.PrivateKey
Accounts *account.AccountServiceDefault
FindToken FindAuthTokenFunc
Purpose account.JWTPurpose
AuthContextKey string
Config *config.Manager
EmptyAllowed bool
ExpiredAllowed bool
}
func AuthMiddleware(options AuthMiddlewareOptions) func(http.Handler) http.Handler {
if options.AuthContextKey == "" {
options.AuthContextKey = DEFAULT_AUTH_CONTEXT_KEY
}
domain := options.Config.Config().Core.Domain
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authToken := options.FindToken(r)
if authToken == "" {
if !options.EmptyAllowed {
http.Error(w, "Invalid JWT", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
return
}
var audList *jwt.ClaimStrings
claim, err := account.JWTVerifyToken(authToken, domain, options.Identity, func(claim *jwt.RegisteredClaims) error {
aud, _ := claim.GetAudience()
audList = &aud
if options.Purpose != account.JWTPurposeNone && jwtPurposeEqual(aud, options.Purpose) == false {
return account.ErrJWTInvalid
}
return nil
})
if err != nil {
unauthorized := true
if errors.Is(err, jwt.ErrTokenExpired) && options.ExpiredAllowed {
unauthorized = false
}
if !unauthorized && audList == nil {
if audList == nil {
var claim jwt.RegisteredClaims
unverified, _, err := jwt.NewParser().ParseUnverified(authToken, &claim)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
audList, err := unverified.Claims.GetAudience()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if jwtPurposeEqual(audList, options.Purpose) == true {
unauthorized = true
}
}
}
if unauthorized {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
}
if claim == nil && options.ExpiredAllowed {
next.ServeHTTP(w, r)
return
}
userId, err := strconv.ParseUint(claim.Subject, 10, 64)
if err != nil {
http.Error(w, account.ErrJWTInvalid.Error(), http.StatusBadRequest)
return
}
exists, _, err := options.Accounts.AccountExists(uint(userId))
if !exists || err != nil {
http.Error(w, account.ErrJWTInvalid.Error(), http.StatusBadRequest)
return
}
ctx := context.WithValue(r.Context(), options.AuthContextKey, uint(userId))
ctx = context.WithValue(ctx, AUTH_TOKEN_CONTEXT_KEY, authToken)
r = r.WithContext(ctx)
next.ServeHTTP(w, r)
})
}
}
func MergeRoutes(routes ...map[string]jape.Handler) map[string]jape.Handler {
merged := make(map[string]jape.Handler)
for _, route := range routes {
for k, v := range route {
merged[k] = v
}
}
return merged
}
func GetUserFromContext(ctx context.Context, key ...string) uint {
realKey := ""
if len(key) > 0 {
realKey = key[0]
}
if realKey == "" {
realKey = DEFAULT_AUTH_CONTEXT_KEY
}
userId, ok := ctx.Value(realKey).(uint)
if !ok {
panic("user id stored in context is not of type uint")
}
return userId
}
func GetAuthTokenFromContext(ctx context.Context) string {
authToken, ok := ctx.Value(AUTH_TOKEN_CONTEXT_KEY).(string)
if !ok {
panic("auth token stored in context is not of type string")
}
return authToken
}
func CtxAborted(ctx context.Context) bool {
select {
case <-ctx.Done():
return true
default:
return false
}
}
func jwtPurposeEqual(aud jwt.ClaimStrings, purpose account.JWTPurpose) bool {
return slices.Contains[jwt.ClaimStrings, string](aud, string(purpose))
}

57
api/registry/registry.go Normal file
View File

@ -0,0 +1,57 @@
package registry
import (
"context"
router2 "git.lumeweb.com/LumeWeb/portal/api/router"
"go.uber.org/fx"
)
type API interface {
Name() string
Init() error
Start(ctx context.Context) error
Stop(ctx context.Context) error
}
type APIEntry struct {
Key string
Module fx.Option
}
var apiEntryRegistry []APIEntry
var apiRegistry map[string]API
var router *router2.APIRouter
func init() {
router = router2.NewAPIRouter()
apiRegistry = make(map[string]API)
}
func RegisterEntry(entry APIEntry) {
apiEntryRegistry = append(apiEntryRegistry, entry)
}
func RegisterAPI(api API) {
apiRegistry[api.Name()] = api
}
func GetEntryRegistry() []APIEntry {
return apiEntryRegistry
}
func GetAPI(name string) API {
if _, ok := apiRegistry[name]; !ok {
panic("API not found: " + name)
}
return apiRegistry[name]
}
func GetAllAPIs() map[string]API {
return apiRegistry
}
func GetRouter() *router2.APIRouter {
return router
}

115
api/router/router.go Normal file
View File

@ -0,0 +1,115 @@
package router
import (
"net/http"
"sync"
"git.lumeweb.com/LumeWeb/portal/config"
"go.uber.org/zap"
"github.com/julienschmidt/httprouter"
)
type RoutableAPI interface {
Name() string
Domain() string
AuthTokenName() string
Can(w http.ResponseWriter, r *http.Request) bool
Handle(w http.ResponseWriter, r *http.Request)
Routes() (*httprouter.Router, error)
}
type APIRouter struct {
apis map[string]RoutableAPI
apiDomain map[string]string
apiHandlers map[string]http.Handler
logger *zap.Logger
config *config.Manager
mutex *sync.RWMutex
}
// Implement the ServeHTTP method on our new type
func (hs APIRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if handler := hs.getHandlerByDomain(r.Host); handler != nil {
handler.ServeHTTP(w, r)
return
}
for _, api := range hs.apis {
if api.Can(w, r) {
api.Handle(w, r)
return
}
}
http.NotFound(w, r)
}
func (hs *APIRouter) RegisterAPI(impl RoutableAPI) {
name := impl.Name()
hs.apis[name] = impl
hs.apiDomain[name+"."+hs.config.Config().Core.Domain] = name
}
func (hs *APIRouter) getHandlerByDomain(domain string) http.Handler {
if apiName := hs.apiDomain[domain]; apiName != "" {
return hs.getHandler(apiName)
}
return nil
}
func (hs *APIRouter) getHandler(protocol string) http.Handler {
hs.mutex.RLock()
handler, ok := hs.apiHandlers[protocol]
hs.mutex.RUnlock()
if ok {
return handler
}
hs.mutex.Lock()
defer hs.mutex.Unlock()
// Double-check if the handler was created while acquiring the write lock
if handler, ok := hs.apiHandlers[protocol]; ok {
return handler
}
proto, ok := hs.apis[protocol]
if !ok {
hs.logger.Fatal("Protocol not found", zap.String("protocol", protocol))
return nil
}
routes, err := proto.Routes()
if err != nil {
hs.logger.Fatal("Error getting routes", zap.Error(err))
return nil
}
hs.apiHandlers[protocol] = routes
return routes
}
func NewAPIRouter() *APIRouter {
return &APIRouter{
apis: make(map[string]RoutableAPI),
apiHandlers: make(map[string]http.Handler),
apiDomain: make(map[string]string),
mutex: &sync.RWMutex{},
}
}
func (hs *APIRouter) SetLogger(logger *zap.Logger) {
hs.logger = logger
}
func (hs *APIRouter) SetConfig(config *config.Manager) {
hs.config = config
}
func BuildSubdomain(api RoutableAPI, cfg *config.Manager) string {
return api.Name() + "." + cfg.Config().Core.Domain
}

15
api/s5.go Normal file
View File

@ -0,0 +1,15 @@
//go:build s5
package api
import (
"git.lumeweb.com/LumeWeb/portal/api/registry"
"git.lumeweb.com/LumeWeb/portal/api/s5"
)
func init() {
registry.RegisterEntry(registry.APIEntry{
Key: "s5",
Module: s5.Module,
})
}

122
api/s5/errors.go Normal file
View File

@ -0,0 +1,122 @@
package s5
import (
"fmt"
"net/http"
)
// S5-specific error keys
const (
// File-related errors
ErrKeyFileUploadFailed = "ErrFileUploadFailed"
ErrKeyFileDownloadFailed = "ErrFileDownloadFailed"
ErrKeyMetadataFetchFailed = "ErrMetadataFetchFailed"
ErrKeyInvalidFileFormat = "ErrInvalidFileFormat"
ErrKeyUnsupportedFileType = "ErrUnsupportedFileType"
ErrKeyFileProcessingFailed = "ErrFileProcessingFailed"
// Storage and data handling errors
ErrKeyStorageOperationFailed = "ErrStorageOperationFailed"
ErrKeyResourceNotFound = "ErrResourceNotFound"
ErrKeyResourceLimitExceeded = "ErrResourceLimitExceeded"
ErrKeyDataIntegrityError = "ErrDataIntegrityError"
// User and permission errors
ErrKeyPermissionDenied = "ErrPermissionDenied"
ErrKeyInvalidOperation = "ErrInvalidOperation"
ErrKeyAuthenticationFailed = "ErrAuthenticationFailed"
ErrKeyAuthorizationFailed = "ErrAuthorizationFailed"
// Network and communication errors
ErrKeyNetworkError = "ErrNetworkError"
ErrKeyServiceUnavailable = "ErrServiceUnavailable"
// General errors
ErrKeyInternalError = "ErrInternalError"
ErrKeyConfigurationError = "ErrConfigurationError"
ErrKeyOperationTimeout = "ErrOperationTimeout"
)
// Default error messages for S5-specific errors
var defaultErrorMessages = map[string]string{
ErrKeyFileUploadFailed: "File upload failed due to an internal error.",
ErrKeyFileDownloadFailed: "File download failed.",
ErrKeyMetadataFetchFailed: "Failed to fetch metadata for the resource.",
ErrKeyInvalidFileFormat: "Invalid file format provided.",
ErrKeyUnsupportedFileType: "Unsupported file type.",
ErrKeyFileProcessingFailed: "Failed to process the file.",
ErrKeyStorageOperationFailed: "Storage operation failed unexpectedly.",
ErrKeyResourceNotFound: "The specified resource was not found.",
ErrKeyResourceLimitExceeded: "The operation exceeded the resource limit.",
ErrKeyDataIntegrityError: "Data integrity check failed.",
ErrKeyPermissionDenied: "Permission denied for the requested operation.",
ErrKeyInvalidOperation: "Invalid or unsupported operation requested.",
ErrKeyAuthenticationFailed: "Authentication failed.",
ErrKeyAuthorizationFailed: "Authorization failed or insufficient permissions.",
ErrKeyNetworkError: "Network error or connectivity issue.",
ErrKeyServiceUnavailable: "The requested service is temporarily unavailable.",
ErrKeyInternalError: "An internal server error occurred.",
ErrKeyConfigurationError: "Configuration error or misconfiguration detected.",
ErrKeyOperationTimeout: "The operation timed out.",
}
// Mapping of S5-specific error keys to HTTP status codes
var errorCodeToHttpStatus = map[string]int{
ErrKeyFileUploadFailed: http.StatusInternalServerError,
ErrKeyFileDownloadFailed: http.StatusInternalServerError,
ErrKeyMetadataFetchFailed: http.StatusInternalServerError,
ErrKeyInvalidFileFormat: http.StatusBadRequest,
ErrKeyUnsupportedFileType: http.StatusBadRequest,
ErrKeyFileProcessingFailed: http.StatusInternalServerError,
ErrKeyStorageOperationFailed: http.StatusInternalServerError,
ErrKeyResourceNotFound: http.StatusNotFound,
ErrKeyResourceLimitExceeded: http.StatusForbidden,
ErrKeyDataIntegrityError: http.StatusInternalServerError,
ErrKeyPermissionDenied: http.StatusForbidden,
ErrKeyInvalidOperation: http.StatusBadRequest,
ErrKeyAuthenticationFailed: http.StatusUnauthorized,
ErrKeyAuthorizationFailed: http.StatusUnauthorized,
ErrKeyNetworkError: http.StatusBadGateway,
ErrKeyServiceUnavailable: http.StatusServiceUnavailable,
ErrKeyInternalError: http.StatusInternalServerError,
ErrKeyConfigurationError: http.StatusInternalServerError,
ErrKeyOperationTimeout: http.StatusRequestTimeout,
}
// S5Error struct for representing S5-specific errors
type S5Error struct {
Key string
Message string
Err error
}
// Error method to implement the error interface
func (e *S5Error) Error() string {
if e.Err != nil {
return fmt.Sprintf("%s: %v", e.Message, e.Err)
}
return e.Message
}
func (e *S5Error) HttpStatus() int {
if code, exists := errorCodeToHttpStatus[e.Key]; exists {
return code
}
return http.StatusInternalServerError
}
func NewS5Error(key string, err error, customMessage ...string) *S5Error {
message, exists := defaultErrorMessages[key]
if !exists {
message = "An unknown error occurred"
}
if len(customMessage) > 0 {
message = customMessage[0]
}
return &S5Error{
Key: key,
Message: message,
Err: err,
}
}

547
api/s5/file.go Normal file
View File

@ -0,0 +1,547 @@
package s5
import (
"context"
"encoding/hex"
"errors"
"io"
"io/fs"
"path"
"slices"
"sort"
"strings"
"time"
s5libmetadata "git.lumeweb.com/LumeWeb/libs5-go/metadata"
"git.lumeweb.com/LumeWeb/portal/protocols/s5"
"git.lumeweb.com/LumeWeb/portal/metadata"
"git.lumeweb.com/LumeWeb/portal/storage"
"git.lumeweb.com/LumeWeb/libs5-go/encoding"
"git.lumeweb.com/LumeWeb/libs5-go/types"
)
var _ io.ReadSeekCloser = (*S5File)(nil)
var _ fs.File = (*S5File)(nil)
var _ fs.ReadDirFile = (*S5File)(nil)
var _ fs.DirEntry = (*S5File)(nil)
var _ fs.FileInfo = (*S5FileInfo)(nil)
type S5File struct {
reader io.ReadCloser
hash []byte
storage storage.StorageService
metadata metadata.MetadataService
record *metadata.UploadMetadata
protocol *s5.S5Protocol
cid *encoding.CID
typ types.CIDType
read bool
tus *s5.TusHandler
ctx context.Context
name string
root []byte
rootType types.CIDType
rootCid *encoding.CID
}
func (f *S5File) IsDir() bool {
return f.typ == types.CIDTypeDirectory
}
func (f *S5File) Type() fs.FileMode {
if f.typ == types.CIDTypeDirectory {
return fs.ModeDir
}
return 0
}
func (f *S5File) Info() (fs.FileInfo, error) {
return f.Stat()
}
type FileParams struct {
Storage storage.StorageService
Metadata metadata.MetadataService
Hash []byte
Type types.CIDType
Protocol *s5.S5Protocol
Tus *s5.TusHandler
Name string
Root []byte
RootType types.CIDType
}
func NewFile(params FileParams) *S5File {
return &S5File{
storage: params.Storage,
metadata: params.Metadata,
hash: params.Hash,
typ: params.Type,
protocol: params.Protocol,
tus: params.Tus,
ctx: context.Background(),
name: params.Name,
root: params.Root,
rootType: params.RootType,
}
}
func (f *S5File) Exists() bool {
ctx := context.Background()
exists, _ := f.tus.UploadExists(ctx, f.hash)
if exists {
return true
}
_, err := f.metadata.GetUpload(context.Background(), f.hash)
if err != nil {
return false
}
return true
}
func (f *S5File) Read(p []byte) (n int, err error) {
err = f.init(0)
if err != nil {
return 0, err
}
f.read = true
return f.reader.Read(p)
}
func (f *S5File) Seek(offset int64, whence int) (int64, error) {
switch whence {
case io.SeekStart:
if !f.read && offset == 0 {
return 0, nil
}
if f.reader != nil {
err := f.reader.Close()
if err != nil {
return 0, err
}
f.reader = nil
}
err := f.init(offset)
if err != nil {
return 0, err
}
case io.SeekCurrent:
return 0, errors.New("not supported")
case io.SeekEnd:
return int64(f.Size()), nil
default:
return 0, errors.New("invalid whence")
}
return 0, nil
}
func (f *S5File) Close() error {
if f.reader != nil {
r := f.reader
f.reader = nil
return r.Close()
}
return nil
}
func (f *S5File) init(offset int64) error {
if f.reader == nil {
reader, err := f.tus.GetUploadReader(f.ctx, f.hash, offset)
if err == nil {
f.reader = reader
f.read = false
return nil
}
reader, err = f.storage.DownloadObject(context.Background(), f.StorageProtocol(), f.hash, offset)
if err != nil {
return err
}
f.reader = reader
f.read = false
}
return nil
}
func (f *S5File) Record() (*metadata.UploadMetadata, error) {
if f.record == nil {
exists, tusRecord := f.tus.UploadExists(context.Background(), f.hash)
if exists {
size, err := f.tus.GetUploadSize(context.Background(), f.hash)
if err != nil {
return nil, err
}
return &metadata.UploadMetadata{
Hash: f.hash,
Size: uint64(size),
MimeType: tusRecord.MimeType,
Created: tusRecord.CreatedAt,
Protocol: f.protocol.Name(),
UploaderIP: tusRecord.UploaderIP,
UserID: tusRecord.UploaderID,
}, nil
}
record, err := f.metadata.GetUpload(context.Background(), f.hash)
if err != nil {
return nil, errors.New("file does not exist")
}
f.record = &record
}
return f.record, nil
}
func (f *S5File) Hash() []byte {
hashStr := f.HashString()
if hashStr == "" {
return nil
}
str, err := hex.DecodeString(hashStr)
if err != nil {
return nil
}
return str
}
func (f *S5File) HashString() string {
record, err := f.Record()
if err != nil {
return ""
}
return hex.EncodeToString(record.Hash)
}
func (f *S5File) Name() string {
if f.name != "" {
return f.name
}
cid, _ := f.CID().ToString()
return cid
}
func (f *S5File) Modtime() time.Time {
record, err := f.Record()
if err != nil {
return time.Unix(0, 0)
}
return record.Created
}
func (f *S5File) Size() uint64 {
record, err := f.Record()
if err != nil {
return 0
}
return record.Size
}
func (f *S5File) CID() *encoding.CID {
if f.cid == nil {
multihash := encoding.MultihashFromBytes(f.Hash(), types.HashTypeBlake3)
typ := f.typ
if typ == 0 {
typ = types.CIDTypeRaw
}
cid := encoding.NewCID(typ, *multihash, f.Size())
f.cid = cid
}
return f.cid
}
func (f *S5File) RootCID() *encoding.CID {
if f.rootCid == nil {
if f.root == nil {
return nil
}
multihash := encoding.MultihashFromBytes(f.root, types.HashTypeBlake3)
typ := f.rootType
if typ == 0 {
typ = types.CIDTypeRaw
}
cid := encoding.NewCID(typ, *multihash, f.Size())
f.rootCid = cid
}
return f.rootCid
}
func (f *S5File) Mime() string {
record, err := f.Record()
if err != nil {
return ""
}
return record.MimeType
}
func (f *S5File) StorageProtocol() storage.StorageProtocol {
return s5.GetStorageProtocol(f.protocol)
}
func (f *S5File) Proof() ([]byte, error) {
object, err := f.storage.DownloadObjectProof(context.Background(), f.StorageProtocol(), f.hash)
if err != nil {
return nil, err
}
proof, err := io.ReadAll(object)
if err != nil {
return nil, err
}
err = object.Close()
if err != nil {
return nil, err
}
return proof, nil
}
func (f *S5File) Manifest() (s5libmetadata.Metadata, error) {
cid := f.RootCID()
if cid == nil {
cid = f.CID()
}
if f.Exists() {
data, err := io.ReadAll(f)
if err != nil {
return nil, err
}
_, err = f.Seek(0, io.SeekStart)
if err != nil {
return nil, err
}
md, err := f.protocol.Node().Services().Storage().ParseMetadata(data, cid)
if err != nil {
return nil, err
}
return md, nil
}
meta, err := f.protocol.Node().Services().Storage().GetMetadataByCID(cid)
if err != nil {
return nil, err
}
return meta, nil
}
func (f *S5File) Stat() (fs.FileInfo, error) {
return newS5FileInfo(f), nil
}
type S5FileInfo struct {
file *S5File
}
func (s S5FileInfo) Name() string {
return s.file.Name()
}
func (s S5FileInfo) Size() int64 {
return int64(s.file.Size())
}
func (s S5FileInfo) Mode() fs.FileMode {
return 0
}
func (s S5FileInfo) ModTime() time.Time {
return s.file.Modtime()
}
func (s S5FileInfo) IsDir() bool {
if s.file.name == "." {
return true
}
manifest, err := s.file.Manifest()
if err == nil && s.file.root != nil {
webApp, ok := manifest.(*s5libmetadata.WebAppMetadata)
if ok {
if slices.Contains(webApp.TryFiles, path.Base(s.file.name)) {
return true
}
}
}
return s.file.typ == types.CIDTypeDirectory
}
func (s S5FileInfo) Sys() any {
return nil
}
func (f *S5File) ReadDir(n int) ([]fs.DirEntry, error) {
manifest, err := f.Manifest()
if err != nil {
return nil, err
}
switch f.CID().Type {
case types.CIDTypeDirectory:
dir, ok := manifest.(*s5libmetadata.DirectoryMetadata)
if !ok {
return nil, errors.New("manifest is not a directory")
}
var entries []fs.DirEntry
for _, file := range dir.Files.Items() {
entries = append(entries, NewFile(FileParams{
Storage: f.storage,
Metadata: f.metadata,
Hash: file.File.CID().Hash.HashBytes(),
Type: file.File.CID().Type,
Tus: f.tus,
Name: file.Name,
}))
}
for _, subDir := range dir.Directories.Items() {
cid, err := resolveDirCid(subDir, f.protocol.Node())
if err != nil {
return nil, err
}
entries = append(entries, NewFile(FileParams{
Storage: f.storage,
Metadata: f.metadata,
Hash: cid.Hash.HashBytes(),
Type: cid.Type,
Name: subDir.Name,
}))
}
return entries, nil
case types.CIDTypeMetadataWebapp:
webApp, ok := manifest.(*s5libmetadata.WebAppMetadata)
if !ok {
return nil, errors.New("manifest is not a web app")
}
var entries []fs.DirEntry
dirMap := make(map[string]bool)
webApp.Paths.Keys()
for _, path := range webApp.Paths.Keys() {
pathSegments := strings.Split(path, "/")
// Check if the path is an immediate child (either a file or a direct subdirectory)
if len(pathSegments) == 1 {
// It's a file directly within `dirPath`
entries = append(entries, newWebAppEntry(pathSegments[0], false))
} else if len(pathSegments) > 1 {
// It's a subdirectory, but ensure to add each unique subdirectory only once
subDirName := pathSegments[0] // The immediate subdirectory name
if _, exists := dirMap[subDirName]; !exists {
entries = append(entries, newWebAppEntry(subDirName, true))
dirMap[subDirName] = true
}
}
}
sort.Slice(entries, func(i, j int) bool {
return entries[i].Name() < entries[j].Name()
})
return entries, nil
}
return nil, errors.New("unsupported CID type")
}
func newS5FileInfo(file *S5File) *S5FileInfo {
return &S5FileInfo{
file: file,
}
}
type webAppEntry struct {
name string
isDir bool
}
func newWebAppEntry(name string, isDir bool) *webAppEntry {
return &webAppEntry{name: name, isDir: isDir}
}
func (d *webAppEntry) Name() string {
return d.name
}
func (d *webAppEntry) IsDir() bool {
return d.isDir
}
func (d *webAppEntry) Type() fs.FileMode {
if d.isDir {
return fs.ModeDir
}
return 0
}
func (d *webAppEntry) Info() (fs.FileInfo, error) {
return &webAppFileInfo{name: d.name, isDir: true}, nil
}
type webAppFileInfo struct {
name string
isDir bool
}
func (fi *webAppFileInfo) Name() string { return fi.name }
func (fi *webAppFileInfo) Size() int64 { return 0 }
func (fi *webAppFileInfo) Mode() fs.FileMode {
if fi.isDir {
return fs.ModeDir
}
return 0
}
func (fi *webAppFileInfo) ModTime() time.Time {
return time.Time{}
}
func (fi *webAppFileInfo) IsDir() bool {
return fi.isDir
}
func (fi *webAppFileInfo) Sys() interface{} {
return nil
}

132
api/s5/fs_dir.go Normal file
View File

@ -0,0 +1,132 @@
package s5
import (
"errors"
"io/fs"
"strings"
"git.lumeweb.com/LumeWeb/libs5-go/node"
"git.lumeweb.com/LumeWeb/libs5-go/encoding"
"git.lumeweb.com/LumeWeb/libs5-go/metadata"
)
var _ fs.FS = (*dirFs)(nil)
type dirFs struct {
root *encoding.CID
s5 *S5API
}
func (w *dirFs) Open(name string) (fs.File, error) {
file := w.s5.newFile(FileParams{
Hash: w.root.Hash.HashBytes(),
Type: w.root.Type,
})
manifest, err := file.Manifest()
if err != nil {
return nil, err
}
dir, ok := manifest.(*metadata.DirectoryMetadata)
if !ok {
return nil, errors.New("manifest is not a directory")
}
segments := strings.Split(name, "/")
if len(segments) == 1 {
return w.openDirectly(name, dir)
}
nextDirName := segments[0]
remainingPath := strings.Join(segments[1:], "/")
return w.openNestedDir(nextDirName, remainingPath, dir)
}
func (w *dirFs) openDirectly(name string, dir *metadata.DirectoryMetadata) (fs.File, error) {
file := dir.Files.Get(name)
subDir := dir.Directories.Get(name)
if file != nil {
return w.s5.newFile(FileParams{
Hash: file.File.CID().Hash.HashBytes(),
Type: file.File.CID().Type,
Name: file.Name,
}), nil
}
if subDir != nil {
cid, err := w.resolveDirCid(subDir)
if err != nil {
return nil, err
}
return w.s5.newFile(FileParams{
Hash: cid.Hash.HashBytes(),
Type: cid.Type,
Name: name,
}), nil
}
if name == "." {
return w.s5.newFile(FileParams{
Hash: w.root.Hash.HashBytes(),
Type: w.root.Type,
Name: name,
}), nil
}
return nil, fs.ErrNotExist
}
func (w dirFs) openNestedDir(name string, remainingPath string, dir *metadata.DirectoryMetadata) (fs.File, error) {
subDir := dir.Directories.Get(name)
if subDir == nil {
return nil, fs.ErrNotExist
}
cid, err := w.resolveDirCid(subDir)
if err != nil {
return nil, err
}
nestedFs := newDirFs(cid, w.s5)
return nestedFs.Open(remainingPath)
}
func (w *dirFs) resolveDirCid(dir *metadata.DirectoryReference) (*encoding.CID, error) {
return resolveDirCid(dir, w.s5.getNode())
}
func newDirFs(root *encoding.CID, s5 *S5API) *dirFs {
return &dirFs{
root: root,
s5: s5,
}
}
func resolveDirCid(dir *metadata.DirectoryReference, node *node.Node) (*encoding.CID, error) {
if len(dir.PublicKey) == 0 {
return nil, errors.New("missing public key")
}
entry, err := node.Services().Registry().Get(dir.PublicKey)
if err != nil {
return nil, err
}
cid, err := encoding.CIDFromRegistry(entry.Data())
if err != nil {
return nil, err
}
return cid, nil
}

73
api/s5/fs_webapp.go Normal file
View File

@ -0,0 +1,73 @@
package s5
import (
"errors"
"io/fs"
"path"
"git.lumeweb.com/LumeWeb/libs5-go/encoding"
"git.lumeweb.com/LumeWeb/libs5-go/metadata"
)
var _ fs.FS = (*webAppFs)(nil)
type webAppFs struct {
root *encoding.CID
s5 *S5API
}
func (w webAppFs) Open(name string) (fs.File, error) {
file := w.s5.newFile(FileParams{
Hash: w.root.Hash.HashBytes(),
Type: w.root.Type,
})
manifest, err := file.Manifest()
if err != nil {
return nil, err
}
webApp, ok := manifest.(*metadata.WebAppMetadata)
if !ok {
return nil, errors.New("manifest is not a web app")
}
if name == "." {
return w.s5.newFile(FileParams{
Hash: w.root.Hash.HashBytes(),
Type: w.root.Type,
Name: name,
}), nil
}
item, ok := webApp.Paths.Get(name)
if !ok {
name = path.Join(name, "index.html")
item, ok = webApp.Paths.Get(name)
if !ok {
return nil, fs.ErrNotExist
}
return w.s5.newFile(FileParams{
Hash: item.Cid.Hash.HashBytes(),
Type: item.Cid.Type,
Name: name,
Root: w.root.Hash.HashBytes(),
RootType: w.root.Type,
}), nil
}
return w.s5.newFile(FileParams{
Hash: item.Cid.Hash.HashBytes(),
Type: item.Cid.Type,
Name: name,
}), nil
}
func newWebAppFs(root *encoding.CID, s5 *S5API) *webAppFs {
return &webAppFs{
root: root,
s5: s5,
}
}

140
api/s5/messages.go Normal file
View File

@ -0,0 +1,140 @@
package s5
import (
"time"
"git.lumeweb.com/LumeWeb/libs5-go/encoding"
"git.lumeweb.com/LumeWeb/libs5-go/types"
"git.lumeweb.com/LumeWeb/portal/db/models"
"github.com/vmihailenco/msgpack/v5"
)
var (
_ msgpack.CustomEncoder = (*AccountPinBinaryResponse)(nil)
)
type AccountRegisterRequest struct {
Pubkey string `json:"pubkey"`
Response string `json:"response"`
Signature string `json:"signature"`
Email string `json:"email,omitempty"`
}
type SmallUploadResponse struct {
CID string `json:"cid"`
}
type AccountRegisterChallengeResponse struct {
Challenge string `json:"challenge"`
}
type AccountLoginRequest struct {
Pubkey string `json:"pubkey"`
Response string `json:"response"`
Signature string `json:"signature"`
}
type AccountLoginChallengeResponse struct {
Challenge string `json:"challenge"`
}
type AccountInfoResponse struct {
Email string `json:"email"`
QuotaExceeded bool `json:"quotaExceeded"`
EmailConfirmed bool `json:"emailConfirmed"`
IsRestricted bool `json:"isRestricted"`
Tier AccountTier `json:"tier"`
}
type AccountStatsResponse struct {
AccountInfoResponse
Stats AccountStats `json:"stats"`
}
type AccountTier struct {
Id uint64 `json:"id"`
Name string `json:"name"`
UploadBandwidth uint64 `json:"uploadBandwidth"`
StorageLimit uint64 `json:"storageLimit"`
Scopes []interface{} `json:"scopes"`
}
type AccountStats struct {
Total AccountStatsTotal `json:"total"`
}
type AccountStatsTotal struct {
UsedStorage uint64 `json:"usedStorage"`
}
type AppUploadResponse struct {
CID string `json:"cid"`
}
type RegistryQueryResponse struct {
Pk string `json:"pk"`
Revision uint64 `json:"revision"`
Data string `json:"data"`
Signature string `json:"signature"`
}
type RegistrySetRequest struct {
Pk string `json:"pk"`
Revision uint64 `json:"revision"`
Data string `json:"data"`
Signature string `json:"signature"`
}
type DebugStorageLocation struct {
Type int `json:"type"`
Parts []string `json:"parts"`
Expiry int64 `json:"expiry"`
NodeId string `json:"nodeId"`
Score float64 `json:"score"`
}
type DebugStorageLocationsResponse struct {
Locations []DebugStorageLocation `json:"locations"`
}
type AccountPinBinaryResponse struct {
Pins []models.Pin
Cursor uint64
}
func (a AccountPinBinaryResponse) EncodeMsgpack(enc *msgpack.Encoder) error {
err := enc.EncodeInt(0)
if err != nil {
return err
}
err = enc.EncodeInt(int64(a.Cursor))
if err != nil {
return err
}
err = enc.EncodeArrayLen(len(a.Pins))
if err != nil {
return err
}
for _, pin := range a.Pins {
err = enc.EncodeBytes(encoding.MultihashFromBytes(pin.Upload.Hash, types.HashTypeBlake3).FullBytes())
if err != nil {
return err
}
}
return nil
}
type AccountPinResponse struct {
Pins []AccountPin `json:"pins"`
}
type AccountPin struct {
Hash string `json:"hash"`
Size uint64 `json:"size"`
PinnedAt time.Time `json:"pinned_at"`
MimeType string `json:"mime_type"`
}
type AccountPinStatusResponse struct {
Status models.ImportStatus `json:"status"`
Progress float64 `json:"progress"`
}

20
api/s5/middleware.go Normal file
View File

@ -0,0 +1,20 @@
package s5
import (
"git.lumeweb.com/LumeWeb/portal/api/middleware"
"net/http"
)
const (
authCookieName = "s5-auth-token"
authQueryParam = "auth_token"
)
func findToken(r *http.Request) string {
return middleware.FindAuthToken(r, authCookieName, authQueryParam)
}
func authMiddleware(options middleware.AuthMiddlewareOptions) middleware.HttpMiddlewareFunc {
options.FindToken = findToken
return middleware.AuthMiddleware(options)
}

2336
api/s5/s5.go Normal file

File diff suppressed because it is too large Load Diff

1074
api/s5/swagger.yaml Normal file

File diff suppressed because it is too large Load Diff

111
api/swagger/generate.go Normal file
View File

@ -0,0 +1,111 @@
// This program downloads the dist assets for the current swagger-ui version and places them into the embed directory
// TODO: Compress?
//go:build ignore
// +build ignore
package main
import (
"archive/tar"
"compress/gzip"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"regexp"
"strings"
)
type releaseResp []struct {
// TagName is a release tag name
TagName string `json:"tag_name"`
}
func main() {
log.SetFlags(0)
releases := releaseResp{}
// get the releases so we can download the latest one
req, _ := http.NewRequest("GET", "https://api.github.com/repos/swagger-api/swagger-ui/releases", nil)
req.Header.Set("Accept", "application/vnd.github.v3+json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatalf("error getting release list: %v", err)
}
if resp.StatusCode != http.StatusOK {
log.Fatalf("got status [%s] on release list download", resp.Status)
}
if err := json.NewDecoder(resp.Body).Decode(&releases); err != nil {
log.Fatalf("error decoding response: %v", err)
}
resp.Body.Close()
if len(releases) == 0 {
log.Fatal("somehow got no releases, nothing to do")
}
tag := releases[0].TagName
log.Printf("downloading release %s...", tag)
resp, err = http.Get(fmt.Sprintf("https://github.com/swagger-api/swagger-ui/archive/%s.tar.gz", tag))
if err != nil {
log.Fatalf("error downloading release archive: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Fatalf("got status [%s] on release archive download", resp.Status)
}
zr, err := gzip.NewReader(resp.Body)
if err != nil {
log.Fatalf("error opening file as gzip: %v", err)
}
if err := os.RemoveAll("embed"); err != nil {
log.Fatalf("error removing old embed directory")
}
if err := os.Mkdir("embed", 0o700); err != nil {
log.Fatalf("error recreating embed directory")
}
tr := tar.NewReader(zr)
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("tar parsing error: %v", err)
}
if header.Typeflag == tar.TypeReg {
// got a file, remove version directory
fname := header.Name[strings.Index(header.Name, `/`):]
if strings.HasPrefix(fname, `/dist`) {
fname = strings.TrimPrefix(fname, `/dist`)
out, err := os.Create(filepath.Join("embed", fname))
if err != nil {
log.Fatalf("error create output file: %v", err)
}
if _, err := io.Copy(out, tr); err != nil {
log.Fatalf("error writing output file: %v", err)
}
}
}
}
// replace the hard-coded JSON file with a generic file and disable the topbar
initFile, err := os.ReadFile(filepath.Join("embed", "swagger-initializer.js"))
if err != nil {
log.Fatalf("error opening swagger-initializer.js for templating :%v", err)
}
newInit := regexp.MustCompile(`url:\s+"[^"]*"`).ReplaceAllLiteral(initFile, []byte(`url: "/swagger.json"`))
newInit = regexp.MustCompile(`,?\s+SwaggerUIStandalonePreset.*\n`).ReplaceAllLiteral(newInit, []byte("\n"))
newInit = regexp.MustCompile(`(?s),\s+plugins: \[.*],\n`).ReplaceAllLiteral(newInit, []byte("\n"))
newInit = regexp.MustCompile(`\n\s*layout:.*\n`).ReplaceAllLiteral(newInit, []byte("\n"))
newinitFile, err := os.Create(filepath.Join("embed", "swagger-initializer.js"))
if err != nil {
log.Fatalf("error re-creating swagger-initializer.js file: %v", err)
}
defer newinitFile.Close()
if _, err := newinitFile.Write(newInit); err != nil {
log.Fatalf("unable to write to swagger-initializer.js: %v", err)
}
}

65
api/swagger/swagger.go Normal file
View File

@ -0,0 +1,65 @@
package swagger
import (
"embed"
"io/fs"
"net/http"
"git.lumeweb.com/LumeWeb/portal/api/middleware"
"go.sia.tech/jape"
"github.com/getkin/kin-openapi/openapi3"
)
//go:generate go run generate.go
//go:embed embed
var swagfs embed.FS
func byteHandler(b []byte) jape.Handler {
return func(c jape.Context) {
c.ResponseWriter.Header().Set("Content-Type", "application/json")
c.ResponseWriter.Write(b)
}
}
func Swagger(spec []byte, routes map[string]jape.Handler) (map[string]jape.Handler, error) {
loader := openapi3.NewLoader()
doc, err := loader.LoadFromData(spec)
if err != nil {
return nil, err
}
if err = doc.Validate(loader.Context); err != nil {
return nil, err
}
jsonDoc, err := doc.MarshalJSON()
if err != nil {
return nil, err
}
swaggerFiles, _ := fs.Sub(swagfs, "embed")
swaggerServ := http.FileServer(http.FS(swaggerFiles))
handler := func(c jape.Context) {
swaggerServ.ServeHTTP(c.ResponseWriter, c.Request)
}
strip := func(next http.Handler) http.Handler {
return http.StripPrefix("/swagger", next)
}
redirect := func(jc jape.Context) {
http.Redirect(jc.ResponseWriter, jc.Request, "/swagger/", http.StatusMovedPermanently)
}
swagRoutes := map[string]jape.Handler{
"GET /swagger.json": byteHandler(jsonDoc),
"GET /swagger": redirect,
"GET /swagger/*path": middleware.ApplyMiddlewares(handler, strip),
}
return middleware.MergeRoutes(routes, swagRoutes), nil
}

View File

@ -1,31 +1,161 @@
package bao
import (
"bufio"
"bytes"
_ "embed"
"errors"
"io"
"lukechampine.com/blake3"
"time"
"github.com/samber/lo"
"go.uber.org/zap"
"lukechampine.com/blake3/bao"
)
func ComputeTree(reader io.Reader, size int64) ([]byte, [32]byte, error) {
bufSize := blake3.BaoEncodedSize(int(size), true)
buf := bufferAt{buf: make([]byte, bufSize)}
var _ io.ReadCloser = (*Verifier)(nil)
var _ io.WriterAt = (*proofWriter)(nil)
hash, err := blake3.BaoEncode(&buf, bufio.NewReader(reader), size, true)
if err != nil {
return nil, [32]byte{}, err
}
var ErrVerifyFailed = errors.New("verification failed")
return buf.buf, hash, nil
const groupLog = 8
const groupChunks = 1 << groupLog
type Verifier struct {
r io.ReadCloser
proof Result
read uint64
buffer *bytes.Buffer
logger *zap.Logger
readTime []time.Duration
verifyTime time.Duration
}
type bufferAt struct {
type Result struct {
Hash []byte
Proof []byte
Length uint
}
func (v *Verifier) Read(p []byte) (int, error) {
// Initial attempt to read from the buffer
n, err := v.buffer.Read(p)
if n == len(p) {
// If the buffer already had enough data to fulfill the request, return immediately
return n, nil
} else if err != nil && err != io.EOF {
// For errors other than EOF, return the error immediately
return n, err
}
buf := make([]byte, groupChunks)
// Continue reading from the source and verifying until we have enough data or hit an error
for v.buffer.Len() < len(p)-n {
readStart := time.Now()
bytesRead, err := io.ReadFull(v.r, buf)
if err != nil && err != io.EOF && err != io.ErrUnexpectedEOF {
return n, err // Return any read error immediately
}
readEnd := time.Now()
v.readTime = append(v.readTime, readEnd.Sub(readStart))
timeStart := time.Now()
if bytesRead > 0 {
if status := bao.VerifyChunk(buf[:bytesRead], v.proof.Proof, groupChunks, v.read, [32]byte(v.proof.Hash)); !status {
return n, errors.Join(ErrVerifyFailed, err)
}
v.read += uint64(bytesRead)
v.buffer.Write(buf[:bytesRead]) // Append new data to the buffer
}
timeEnd := time.Now()
v.verifyTime += timeEnd.Sub(timeStart)
if err == io.EOF {
// If EOF, break the loop as no more data can be read
break
}
}
if len(v.readTime) > 0 {
averageReadTime := lo.Reduce(v.readTime, func(acc time.Duration, cur time.Duration, _ int) time.Duration {
return acc + cur
}, time.Duration(0)) / time.Duration(len(v.readTime))
v.logger.Debug("Read time", zap.Duration("average", averageReadTime))
}
averageVerifyTime := v.verifyTime / time.Duration(v.read/groupChunks)
v.logger.Debug("Verification time", zap.Duration("average", averageVerifyTime))
// Attempt to read the remainder of the data from the buffer
additionalBytes, _ := v.buffer.Read(p[n:])
return n + additionalBytes, nil
}
func (v *Verifier) Close() error {
return v.r.Close()
}
func Hash(r io.Reader, size uint64) (*Result, error) {
reader := newSizeReader(r)
writer := newProofWriter(int(size))
hash, err := bao.Encode(writer, reader, int64(size), groupLog, true)
if err != nil {
return nil, err
}
return &Result{
Hash: hash[:],
Proof: writer.buf,
Length: uint(size),
}, nil
}
func NewVerifier(r io.ReadCloser, proof Result, logger *zap.Logger) *Verifier {
return &Verifier{
r: r,
proof: proof,
buffer: new(bytes.Buffer),
logger: logger,
}
}
type proofWriter struct {
buf []byte
}
func (b *bufferAt) WriteAt(p []byte, off int64) (int, error) {
if copy(b.buf[off:], p) != len(p) {
func (p proofWriter) WriteAt(b []byte, off int64) (n int, err error) {
if copy(p.buf[off:], b) != len(b) {
panic("bad buffer size")
}
return len(p), nil
return len(b), nil
}
func newProofWriter(size int) *proofWriter {
return &proofWriter{
buf: make([]byte, bao.EncodedSize(size, groupLog, true)),
}
}
type sizeReader struct {
reader io.Reader
read int64
}
func (s sizeReader) Read(p []byte) (int, error) {
n, err := s.reader.Read(p)
s.read += int64(n)
return n, err
}
func newSizeReader(r io.Reader) *sizeReader {
return &sizeReader{
reader: r,
read: 0,
}
}

View File

@ -1,100 +0,0 @@
package cid
import (
"bytes"
"encoding/binary"
"encoding/hex"
"errors"
"github.com/multiformats/go-multibase"
)
var MAGIC_BYTES = []byte{0x26, 0x1f}
var (
ErrMissingEmptySize = errors.New("Missing or empty size")
ErrInvalidCIDMagic = errors.New("CID magic bytes missing or invalid")
)
type CID struct {
Hash [32]byte
Size uint64
}
func (c CID) StringHash() string {
return hex.EncodeToString(c.Hash[:])
}
func Encode(hash []byte, size uint64) (string, error) {
var hashBytes [32]byte
copy(hashBytes[:], hash)
return EncodeFixed(hashBytes, size)
}
func EncodeFixed(hash [32]byte, size uint64) (string, error) {
sizeBytes := make([]byte, 8)
binary.LittleEndian.PutUint64(sizeBytes, size)
prefixedHash := append(MAGIC_BYTES, hash[:]...)
prefixedHash = append(prefixedHash, sizeBytes...)
return multibase.Encode(multibase.Base58BTC, prefixedHash)
}
func EncodeString(hash string, size uint64) (string, error) {
hashBytes, err := hex.DecodeString(hash)
if err != nil {
return "", err
}
return Encode(hashBytes, size)
}
func Valid(cid string) (bool, error) {
_, err := maybeDecode(cid)
if err != nil {
return false, err
}
return true, nil
}
func Decode(cid string) (*CID, error) {
data, err := maybeDecode(cid)
if err != nil {
return &CID{}, err
}
data = data[len(MAGIC_BYTES):]
var hash [32]byte
copy(hash[:], data[:])
size := binary.LittleEndian.Uint64(data[32:])
return &CID{Hash: hash, Size: size}, nil
}
func maybeDecode(cid string) ([]byte, error) {
_, data, err := multibase.Decode(cid)
if err != nil {
return nil, err
}
if bytes.Compare(data[0:len(MAGIC_BYTES)], MAGIC_BYTES) != 0 {
return nil, ErrInvalidCIDMagic
}
sizeBytes := data[len(MAGIC_BYTES)+32:]
if len(sizeBytes) == 0 {
return nil, ErrMissingEmptySize
}
size := binary.LittleEndian.Uint64(sizeBytes)
if size == 0 {
return nil, ErrMissingEmptySize
}
return data, nil
}

90
cmd/portal/init.go Normal file
View File

@ -0,0 +1,90 @@
package main
import (
"context"
"crypto/ed25519"
"net"
"net/http"
"strconv"
"git.lumeweb.com/LumeWeb/portal/api/router"
"git.lumeweb.com/LumeWeb/portal/config"
"git.lumeweb.com/LumeWeb/portal/api/registry"
"go.sia.tech/core/wallet"
"go.uber.org/fx"
"go.uber.org/zap"
)
func NewIdentity(config *config.Manager, logger *zap.Logger) (ed25519.PrivateKey, error) {
var seed [32]byte
identitySeed := config.Config().Core.Identity
if identitySeed == "" {
logger.Info("Generating new identity seed")
identitySeed = wallet.NewSeedPhrase()
config.Viper().Set("core.identity", identitySeed)
err := config.Save()
if err != nil {
return nil, err
}
}
err := wallet.SeedFromPhrase(&seed, identitySeed)
if err != nil {
return nil, err
}
return ed25519.PrivateKey(wallet.KeyFromSeed(&seed, 0)), nil
}
type NewServerParams struct {
fx.In
Config *config.Manager
Logger *zap.Logger
APIs []registry.API `group:"api"`
}
func NewServer(lc fx.Lifecycle, params NewServerParams) (*http.Server, error) {
r := registry.GetRouter()
r.SetConfig(params.Config)
r.SetLogger(params.Logger)
for _, api := range params.APIs {
routableAPI, ok := interface{}(api).(router.RoutableAPI)
if !ok {
params.Logger.Fatal("API does not implement RoutableAPI", zap.String("api", api.Name()))
}
r.RegisterAPI(routableAPI)
}
srv := &http.Server{
Addr: ":" + strconv.FormatUint(uint64(params.Config.Config().Core.Port), 10),
Handler: r,
}
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
ln, err := net.Listen("tcp", srv.Addr)
if err != nil {
return err
}
go func() {
err := srv.Serve(ln)
if err != nil {
params.Logger.Fatal("Failed to serve", zap.Error(err))
}
}()
return nil
},
OnStop: func(ctx context.Context) error {
return srv.Shutdown(ctx)
},
})
return srv, nil
}

76
cmd/portal/main.go Normal file
View File

@ -0,0 +1,76 @@
package main
import (
"flag"
"net/http"
_import "git.lumeweb.com/LumeWeb/portal/import"
"git.lumeweb.com/LumeWeb/portal/mailer"
"git.lumeweb.com/LumeWeb/portal/config"
"git.lumeweb.com/LumeWeb/portal/account"
"git.lumeweb.com/LumeWeb/portal/api"
"git.lumeweb.com/LumeWeb/portal/cron"
"git.lumeweb.com/LumeWeb/portal/db"
_logger "git.lumeweb.com/LumeWeb/portal/logger"
"git.lumeweb.com/LumeWeb/portal/metadata"
"git.lumeweb.com/LumeWeb/portal/protocols"
"git.lumeweb.com/LumeWeb/portal/renter"
"git.lumeweb.com/LumeWeb/portal/storage"
"go.uber.org/fx"
"go.uber.org/fx/fxevent"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func main() {
cfg, err := config.NewManager()
logger, logLevel := _logger.NewLogger(cfg)
if err != nil {
logger.Fatal("Failed to load config", zap.Error(err))
}
var fxDebug bool
flag.BoolVar(&fxDebug, "fx-debug", false, "Enable fx framework debug logging")
flag.Parse()
var fxLogger fx.Option
fxLogger = fx.WithLogger(func(logger *zap.Logger) fxevent.Logger {
log := &fxevent.ZapLogger{Logger: logger}
log.UseLogLevel(zapcore.InfoLevel)
log.UseErrorLevel(zapcore.ErrorLevel)
return log
})
if fxDebug {
fxLogger = fx.Options()
}
fx.New(
fx.Supply(cfg),
fx.Supply(logger, logLevel),
fxLogger,
fx.Provide(NewIdentity),
db.Module,
renter.Module,
storage.Module,
cron.Module,
account.Module,
metadata.Module,
_import.Module,
mailer.Module,
protocols.BuildProtocols(cfg),
api.BuildApis(cfg),
fx.Provide(api.NewCasbin),
fx.Invoke(protocols.SetupLifecycles),
fx.Invoke(api.SetupLifecycles),
fx.Provide(NewServer),
fx.Invoke(func(*http.Server) {}),
).Run()
}

56
config/cluster.go Normal file
View File

@ -0,0 +1,56 @@
package config
import (
"reflect"
"github.com/mitchellh/mapstructure"
)
type ClusterConfig struct {
Enabled bool `mapstructure:"enabled"`
Redis *RedisConfig `mapstructure:"redis"`
Etcd *EtcdConfig `mapstructure:"etcd"`
}
func clusterConfigHook() mapstructure.DecodeHookFuncType {
return func(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) {
if f.Kind() != reflect.Map || t != reflect.TypeOf(&ClusterConfig{}) {
return data, nil
}
var clusterConfig ClusterConfig
if err := mapstructure.Decode(data, &clusterConfig); err != nil {
return nil, err
}
// Check if the input data map includes "redis" configuration
if opts, ok := data.(map[string]interface{})["redis"].(map[string]interface{}); ok && opts != nil {
var redisOptions RedisConfig
if err := mapstructure.Decode(opts, &redisOptions); err != nil {
return nil, err
}
if err := redisOptions.Validate(); err != nil {
return nil, err
}
clusterConfig.Redis = &redisOptions
}
// Check if the input data map includes "etcd" configuration
if opts, ok := data.(map[string]interface{})["etcd"].(map[string]interface{}); ok && opts != nil {
var etcdOptions EtcdConfig
if err := mapstructure.Decode(opts, &etcdOptions); err != nil {
return nil, err
}
if err := etcdOptions.Validate(); err != nil {
return nil, err
}
clusterConfig.Etcd = &etcdOptions
}
return &clusterConfig, nil
}
}

View File

@ -3,9 +3,10 @@ package config
import (
"errors"
"fmt"
"github.com/spf13/pflag"
"reflect"
"github.com/spf13/viper"
"log"
"go.uber.org/zap"
)
var (
@ -16,9 +17,252 @@ var (
}
)
func Init() {
type Defaults interface {
Defaults() map[string]interface{}
}
type Validator interface {
Validate() error
}
type Config struct {
Core CoreConfig `mapstructure:"core"`
Protocol map[string]interface{} `mapstructure:"protocol"`
}
type Manager struct {
viper *viper.Viper
root *Config
changes bool
}
func NewManager() (*Manager, error) {
v, err := newConfig()
if err != nil {
return nil, err
}
var config Config
m := &Manager{
viper: v,
root: &config,
}
m.setDefaultsForObject(m.root.Core, "core")
err = m.maybeSave()
if err != nil {
return nil, err
}
err = v.Unmarshal(&config, viper.DecodeHook(clusterConfigHook()), viper.DecodeHook(cacheConfigHook()))
if err != nil {
return nil, err
}
err = m.validateObject(m.root)
if err != nil {
return nil, err
}
err = m.maybeConfigureCluster()
if err != nil {
return m, err
}
return m, nil
}
func (m *Manager) ConfigureProtocol(name string, cfg ProtocolConfig) error {
protocolPrefix := fmt.Sprintf("protocol.%s", name)
m.setDefaultsForObject(cfg, protocolPrefix)
err := m.maybeSave()
if err != nil {
return err
}
err = m.viper.Sub(protocolPrefix).Unmarshal(cfg)
if err != nil {
return err
}
err = m.validateObject(cfg)
if err != nil {
return err
}
if m.root.Protocol == nil {
m.root.Protocol = make(map[string]interface{})
}
m.root.Protocol[name] = cfg
return nil
}
func (m *Manager) setDefaultsForObject(obj interface{}, prefix string) {
// Reflect on the object to traverse its fields
objValue := reflect.ValueOf(obj)
objType := reflect.TypeOf(obj)
// If the object is a pointer, we need to work with its element
if objValue.Kind() == reflect.Ptr {
objValue = objValue.Elem()
objType = objType.Elem()
}
// Check if the object itself implements Defaults
if setter, ok := obj.(Defaults); ok {
m.applyDefaults(setter, prefix)
}
// Recursively handle struct fields
for i := 0; i < objValue.NumField(); i++ {
field := objValue.Field(i)
fieldType := objType.Field(i)
// Check if the field is exported and can be interfaced
if !field.CanInterface() {
continue
}
mapstructureTag := fieldType.Tag.Get("mapstructure")
// Construct new prefix based on the mapstructure tag, if available
newPrefix := prefix
if mapstructureTag != "" && mapstructureTag != "-" {
if newPrefix != "" {
newPrefix += "."
}
newPrefix += mapstructureTag
}
// If field is a struct or pointer to a struct, recurse
if field.Kind() == reflect.Struct || (field.Kind() == reflect.Ptr && field.Elem().Kind() == reflect.Struct) {
if field.Kind() == reflect.Ptr && field.IsNil() {
// Initialize nil pointer to struct
field.Set(reflect.New(fieldType.Type.Elem()))
}
m.setDefaultsForObject(field.Interface(), newPrefix)
}
}
}
func (m *Manager) validateObject(obj interface{}) error {
// Reflect on the object to traverse its fields
objValue := reflect.ValueOf(obj)
objType := reflect.TypeOf(obj)
// If the object is a pointer, we need to work with its element
if objValue.Kind() == reflect.Ptr {
objValue = objValue.Elem()
objType = objType.Elem()
}
// Check if the object itself implements Defaults
if validator, ok := obj.(Validator); ok {
err := validator.Validate()
if err != nil {
return err
}
}
// Recursively handle struct fields
for i := 0; i < objValue.NumField(); i++ {
field := objValue.Field(i)
fieldType := objType.Field(i)
if !field.CanInterface() {
continue
}
// If field is a struct or pointer to a struct, recurse
if field.Kind() == reflect.Struct || (field.Kind() == reflect.Ptr && field.Elem().Kind() == reflect.Struct) {
if field.Kind() == reflect.Ptr && field.IsNil() {
// Initialize nil pointer to struct
field.Set(reflect.New(fieldType.Type.Elem()))
}
err := m.validateObject(field.Interface())
if err != nil {
return err
}
}
}
return nil
}
func (m *Manager) applyDefaults(setter Defaults, prefix string) {
defaults := setter.Defaults()
for key, value := range defaults {
fullKey := key
if prefix != "" {
fullKey = fmt.Sprintf("%s.%s", prefix, key)
}
if m.setDefault(fullKey, value) {
m.changes = true
}
}
}
func (m *Manager) setDefault(key string, value interface{}) bool {
if !m.viper.IsSet(key) {
m.viper.SetDefault(key, value)
return true
}
return false
}
func (m *Manager) maybeSave() error {
if m.changes {
ret := m.viper.WriteConfig()
if ret != nil {
return ret
}
m.changes = false
}
return nil
}
func (m *Manager) maybeConfigureCluster() error {
if m.root.Core.Clustered != nil && m.root.Core.Clustered.Enabled {
m.root.Core.DB.Cache.Mode = "redis"
m.root.Core.DB.Cache.Options = m.root.Core.Clustered.Redis
}
return nil
}
func (m *Manager) Config() *Config {
return m.root
}
func (m *Manager) Viper() *viper.Viper {
return m.viper
}
func (m *Manager) Save() error {
err := m.viper.WriteConfig()
if err != nil {
return err
}
err = m.viper.Unmarshal(&m.root)
if err != nil {
return err
}
return nil
}
func newConfig() (*viper.Viper, error) {
logger := newFallbackLogger()
viper.SetConfigName("config")
viper.SetConfigType("json")
viper.SetConfigType("yaml")
for _, path := range ConfigFilePaths {
viper.AddConfigPath(path)
@ -27,33 +271,26 @@ func Init() {
viper.SetEnvPrefix("LUME_WEB_PORTAL")
viper.AutomaticEnv()
pflag.String("database.type", "sqlite", "Database type")
pflag.String("database.host", "localhost", "Database host")
pflag.Int("database.port", 3306, "Database port")
pflag.String("database.user", "root", "Database user")
pflag.String("database.password", "", "Database password")
pflag.String("database.name", "lumeweb_portal", "Database name")
pflag.String("database.path", "./db.sqlite", "Database path for SQLite")
pflag.String("renterd-api-password", ".", "admin password for renterd")
pflag.Bool("debug", false, "enable debug mode")
pflag.Parse()
err := viper.BindPFlags(pflag.CommandLine)
err := viper.ReadInConfig()
if err != nil {
log.Fatalf("Fatal error arguments: %s \n", err)
return
}
err = viper.ReadInConfig()
if err != nil {
if errors.As(err, &viper.ConfigFileNotFoundError{}) {
// Config file not found, this is not an error.
fmt.Println("Config file not found, using default settings.")
} else {
// Other error, panic.
panic(fmt.Errorf("Fatal error config file: %s \n", err))
if !errors.As(err, &viper.ConfigFileNotFoundError{}) {
return nil, err
}
logger.Info("Config file not found, using default settings.")
err := viper.SafeWriteConfig()
if err != nil {
return nil, err
}
return viper.GetViper(), nil
}
return viper.GetViper(), nil
}
func newFallbackLogger() *zap.Logger {
l, _ := zap.NewDevelopment()
return l
}

45
config/core.go Normal file
View File

@ -0,0 +1,45 @@
package config
import (
"errors"
"github.com/docker/go-units"
)
var _ Defaults = (*CoreConfig)(nil)
var _ Validator = (*CoreConfig)(nil)
type CoreConfig struct {
DB DatabaseConfig `mapstructure:"db"`
Domain string `mapstructure:"domain"`
PortalName string `mapstructure:"portal_name"`
ExternalPort uint `mapstructure:"external_port"`
Identity string `mapstructure:"identity"`
Log LogConfig `mapstructure:"log"`
Port uint `mapstructure:"port"`
PostUploadLimit uint64 `mapstructure:"post_upload_limit"`
Storage StorageConfig `mapstructure:"storage"`
Protocols []string `mapstructure:"protocols"`
Mail MailConfig `mapstructure:"mail"`
Clustered *ClusterConfig `mapstructure:"clustered"`
}
func (c CoreConfig) Validate() error {
if c.Domain == "" {
return errors.New("core.domain is required")
}
if c.PortalName == "" {
return errors.New("core.portal_name is required")
}
if c.Port == 0 {
return errors.New("core.port is required")
}
return nil
}
func (c CoreConfig) Defaults() map[string]interface{} {
return map[string]interface{}{
"post_upload_limit": units.MiB * 100,
}
}

95
config/database.go Normal file
View File

@ -0,0 +1,95 @@
package config
import (
"errors"
"reflect"
"github.com/mitchellh/mapstructure"
)
var _ Defaults = (*DatabaseConfig)(nil)
var _ Validator = (*DatabaseConfig)(nil)
type DatabaseConfig struct {
Charset string `mapstructure:"charset"`
Host string `mapstructure:"host"`
Name string `mapstructure:"name"`
Password string `mapstructure:"password"`
Port int `mapstructure:"port"`
Username string `mapstructure:"username"`
Cache *CacheConfig `mapstructure:"cache"`
}
func (d DatabaseConfig) Validate() error {
if d.Host == "" {
return errors.New("core.db.host is required")
}
if d.Port == 0 {
return errors.New("core.db.port is required")
}
if d.Username == "" {
return errors.New("core.db.username is required")
}
if d.Password == "" {
return errors.New("core.db.password is required")
}
if d.Name == "" {
return errors.New("core.db.name is required")
}
return nil
}
func (d DatabaseConfig) Defaults() map[string]interface{} {
return map[string]interface{}{
"host": "localhost",
"charset": "utf8mb4",
"port": 3306,
"name": "portal",
}
}
type CacheConfig struct {
Mode string `mapstructure:"mode"`
Options interface{} `mapstructure:"options"`
}
type MemoryConfig struct {
}
func cacheConfigHook() mapstructure.DecodeHookFuncType {
return func(f reflect.Type, t reflect.Type, data interface{}) (interface{}, error) {
// This hook is designed to operate on the options field within the CacheConfig
if f.Kind() != reflect.Map || t != reflect.TypeOf(&CacheConfig{}) {
return data, nil
}
var cacheConfig CacheConfig
if err := mapstructure.Decode(data, &cacheConfig); err != nil {
return nil, err
}
// Assuming the input data map includes "mode" and "options"
switch cacheConfig.Mode {
case "redis":
var redisOptions RedisConfig
if opts, ok := cacheConfig.Options.(map[string]interface{}); ok && opts != nil {
if err := mapstructure.Decode(opts, &redisOptions); err != nil {
return nil, err
}
cacheConfig.Options = redisOptions
}
case "memory":
// For "memory", you might simply use an empty MemoryConfig,
// or decode options similarly if there are any specific to memory caching.
cacheConfig.Options = MemoryConfig{}
case "false":
// If "false", ensure no options are set, or set to a nil or similar neutral value.
cacheConfig.Options = nil
default:
cacheConfig.Options = nil
}
return cacheConfig, nil
}
}

23
config/etcd.go Normal file
View File

@ -0,0 +1,23 @@
package config
import "errors"
var _ Defaults = (*EtcdConfig)(nil)
type EtcdConfig struct {
Endpoints []string `mapstructure:"endpoints"`
DialTimeout int `mapstructure:"dial_timeout"`
}
func (r *EtcdConfig) Validate() error {
if len(r.Endpoints) == 0 {
return errors.New("endpoints is required")
}
return nil
}
func (r *EtcdConfig) Defaults() map[string]interface{} {
return map[string]interface{}{
"dial_timeout": 5,
}
}

13
config/log.go Normal file
View File

@ -0,0 +1,13 @@
package config
var _ Defaults = (*LogConfig)(nil)
type LogConfig struct {
Level string `mapstructure:"level"`
}
func (l LogConfig) Defaults() map[string]interface{} {
return map[string]interface{}{
"level": "info",
}
}

39
config/mail.go Normal file
View File

@ -0,0 +1,39 @@
package config
import (
"errors"
)
var _ Validator = (*MailConfig)(nil)
var _ Defaults = (*MailConfig)(nil)
type MailConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
SSL bool `mapstructure:"ssl"`
AuthType string `mapstructure:"auth_type"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
From string `mapstructure:"from"`
}
func (m MailConfig) Validate() error {
if m.Host == "" {
return errors.New("core.mail.host is required")
}
if m.Username == "" {
return errors.New("core.mail.username is required")
}
if m.Password == "" {
return errors.New("core.mail.password is required")
}
if m.From == "" {
return errors.New("core.mail.from is required")
}
return nil
}
func (c MailConfig) Defaults() map[string]interface{} {
return map[string]interface{}{
"auth_type": "plain",
}
}

5
config/protocol.go Normal file
View File

@ -0,0 +1,5 @@
package config
type ProtocolConfig interface {
Defaults
}

26
config/redis.go Normal file
View File

@ -0,0 +1,26 @@
package config
import "errors"
var _ Validator = (*RedisConfig)(nil)
var _ Defaults = (*RedisConfig)(nil)
type RedisConfig struct {
Address string `mapstructure:"address"`
Password string `mapstructure:"password"`
DB int `mapstructure:"db"`
}
func (r *RedisConfig) Defaults() map[string]interface{} {
return map[string]interface{}{
"address": "localhost:6379",
"db": 0,
}
}
func (r *RedisConfig) Validate() error {
if r.Address == "" {
return errors.New("address is required")
}
return nil
}

32
config/s3.go Normal file
View File

@ -0,0 +1,32 @@
package config
import "errors"
var _ Validator = (*DatabaseConfig)(nil)
type S3Config struct {
BufferBucket string `mapstructure:"buffer_bucket"`
Endpoint string `mapstructure:"endpoint"`
Region string `mapstructure:"region"`
AccessKey string `mapstructure:"access_key"`
SecretKey string `mapstructure:"secret_key"`
}
func (s S3Config) Validate() error {
if s.BufferBucket == "" {
return errors.New("core.storage.s3.buffer_bucket is required")
}
if s.Endpoint == "" {
return errors.New("core.storage.s3.endpoint is required")
}
if s.Region == "" {
return errors.New("core.storage.s3.region is required")
}
if s.AccessKey == "" {
return errors.New("core.storage.s3.access_key is required")
}
if s.SecretKey == "" {
return errors.New("core.storage.s3.secret_key is required")
}
return nil
}

75
config/sia.go Normal file
View File

@ -0,0 +1,75 @@
package config
import (
"errors"
"math/big"
)
var _ Validator = (*SiaConfig)(nil)
var _ Defaults = (*SiaConfig)(nil)
type SiaConfig struct {
Key string `mapstructure:"key"`
URL string `mapstructure:"url"`
PriceHistoryDays uint64 `mapstructure:"price_history_days"`
MaxUploadPrice string `mapstructure:"max_upload_price"`
MaxDownloadPrice string `mapstructure:"max_download_price"`
MaxStoragePrice string `mapstructure:"max_storage_price"`
MaxContractSCPrice string `mapstructure:"max_contract_sc_price"`
MaxRPCSCPrice string `mapstructure:"max_rpc_sc_price"`
}
func (s SiaConfig) Defaults() map[string]interface{} {
return map[string]interface{}{
"max_rpc_sc_price": 0.1,
"max_contract_sc_price": 1,
"price_history_days": 90,
}
}
func (s SiaConfig) Validate() error {
if s.Key == "" {
return errors.New("core.storage.sia.key is required")
}
if s.URL == "" {
return errors.New("core.storage.sia.url is required")
}
if err := validateStringNumber(s.MaxUploadPrice, "core.storage.sia.max_upload_price"); err != nil {
return err
}
if err := validateStringNumber(s.MaxDownloadPrice, "core.storage.sia.max_download_price"); err != nil {
return err
}
if err := validateStringNumber(s.MaxStoragePrice, "core.storage.sia.max_storage_price"); err != nil {
return err
}
if err := validateStringNumber(s.MaxContractSCPrice, "core.storage.sia.max_contract_sc_price"); err != nil {
return err
}
if err := validateStringNumber(s.MaxRPCSCPrice, "core.storage.sia.max_rpc_sc_price"); err != nil {
return err
}
return nil
}
func validateStringNumber(s string, name string) error {
if s == "" {
return errors.New(name + " is required")
}
rat, ok := new(big.Rat).SetString(s)
if !ok {
return errors.New("failed to parse " + name)
}
if rat.Cmp(new(big.Rat).SetUint64(0)) <= 0 {
return errors.New(name + " must be greater than 0")
}
return nil
}

6
config/storage.go Normal file
View File

@ -0,0 +1,6 @@
package config
type StorageConfig struct {
S3 S3Config `mapstructure:"s3"`
Sia SiaConfig `mapstructure:"sia"`
}

View File

@ -1,35 +0,0 @@
package controller
import (
"git.lumeweb.com/LumeWeb/portal/controller/request"
"git.lumeweb.com/LumeWeb/portal/service/account"
"github.com/kataras/iris/v12"
)
type AccountController struct {
Controller
}
func (a *AccountController) PostRegister() {
ri, success := tryParseRequest(request.RegisterRequest{}, a.Ctx)
if !success {
return
}
r, _ := ri.(*request.RegisterRequest)
err := account.Register(r.Email, r.Password, r.Pubkey)
if err != nil {
if err == account.ErrQueryingAcct || err == account.ErrFailedCreateAccount {
a.Ctx.StopWithError(iris.StatusInternalServerError, err)
} else {
a.Ctx.StopWithError(iris.StatusBadRequest, err)
}
return
}
// Return a success response to the client.
a.Ctx.StatusCode(iris.StatusCreated)
}

View File

@ -1,112 +0,0 @@
package controller
import (
"git.lumeweb.com/LumeWeb/portal/controller/request"
"git.lumeweb.com/LumeWeb/portal/controller/response"
"git.lumeweb.com/LumeWeb/portal/middleware"
"git.lumeweb.com/LumeWeb/portal/service/auth"
"github.com/kataras/iris/v12"
)
type AuthController struct {
Controller
}
// PostLogin handles the POST /api/auth/login request to authenticate a user and return a JWT token.
func (a *AuthController) PostLogin() {
ri, success := tryParseRequest(request.LoginRequest{}, a.Ctx)
if !success {
return
}
r, _ := ri.(*request.LoginRequest)
token, err := auth.LoginWithPassword(r.Email, r.Password)
if err != nil {
if err == auth.ErrFailedGenerateToken {
a.Ctx.StopWithError(iris.StatusInternalServerError, err)
} else {
a.Ctx.StopWithError(iris.StatusUnauthorized, err)
}
return
}
a.respondJSON(&response.LoginResponse{Token: token})
}
// PostChallenge handles the POST /api/auth/pubkey/challenge request to generate a challenge for a user's public key.
func (a *AuthController) PostPubkeyChallenge() {
ri, success := tryParseRequest(request.PubkeyChallengeRequest{}, a.Ctx)
if !success {
return
}
r, _ := (ri).(*request.PubkeyChallengeRequest)
challenge, err := auth.GeneratePubkeyChallenge(r.Pubkey)
if err != nil {
if err == auth.ErrFailedGenerateKeyChallenge {
a.Ctx.StopWithError(iris.StatusInternalServerError, err)
} else {
a.Ctx.StopWithError(iris.StatusUnauthorized, err)
}
return
}
a.respondJSON(&response.ChallengeResponse{Challenge: challenge})
}
// PostKeyLogin handles the POST /api/auth/pubkey/login request to authenticate a user using a public key challenge and return a JWT token.
func (a *AuthController) PostPubkeyLogin() {
ri, success := tryParseRequest(request.PubkeyLoginRequest{}, a.Ctx)
if !success {
return
}
r, _ := ri.(*request.PubkeyLoginRequest)
token, err := auth.LoginWithPubkey(r.Pubkey, r.Challenge, r.Signature)
if err != nil {
if err == auth.ErrFailedGenerateKeyChallenge || err == auth.ErrFailedGenerateToken || err == auth.ErrFailedSaveToken {
a.Ctx.StopWithError(iris.StatusInternalServerError, err)
} else {
a.Ctx.StopWithError(iris.StatusUnauthorized, err)
}
return
}
a.respondJSON(&response.LoginResponse{Token: token})
}
// PostLogout handles the POST /api/auth/logout request to invalidate a JWT token.
func (a *AuthController) PostLogout() {
ri, success := tryParseRequest(request.LogoutRequest{}, a.Ctx)
if !success {
return
}
r, _ := ri.(*request.LogoutRequest)
err := auth.Logout(r.Token)
if err != nil {
a.Ctx.StopWithError(iris.StatusBadRequest, err)
return
}
// Return a success response to the client.
a.Ctx.StatusCode(iris.StatusNoContent)
}
func (a *AuthController) GetStatus() {
middleware.VerifyJwt(a.Ctx)
if a.Ctx.IsStopped() {
return
}
a.respondJSON(&response.AuthStatusResponse{Status: true})
}

View File

@ -1,86 +0,0 @@
package controller
import (
"git.lumeweb.com/LumeWeb/portal/controller/validators"
"git.lumeweb.com/LumeWeb/portal/logger"
"github.com/kataras/iris/v12"
"go.uber.org/zap"
"strconv"
)
func tryParseRequest(r interface{}, ctx iris.Context) (interface{}, bool) {
v, ok := r.(validators.Validatable)
if !ok {
return r, true
}
var d map[string]interface{}
// Read the logout request from the client.
if err := ctx.ReadJSON(&d); err != nil {
logger.Get().Debug("failed to parse request", zap.Error(err))
ctx.StopWithError(iris.StatusBadRequest, err)
return nil, false
}
data, err := v.Import(d)
if err != nil {
logger.Get().Debug("failed to parse request", zap.Error(err))
ctx.StopWithError(iris.StatusBadRequest, err)
return nil, false
}
if err := data.Validate(); err != nil {
logger.Get().Debug("failed to parse request", zap.Error(err))
ctx.StopWithError(iris.StatusBadRequest, err)
return nil, false
}
return data, true
}
func sendErrorCustom(ctx iris.Context, err error, customError error, irisError int) bool {
if err != nil {
if customError != nil {
err = customError
}
ctx.StopWithError(irisError, err)
return true
}
return false
}
func internalError(ctx iris.Context, err error) bool {
return sendErrorCustom(ctx, err, nil, iris.StatusInternalServerError)
}
func internalErrorCustom(ctx iris.Context, err error, customError error) bool {
return sendErrorCustom(ctx, err, customError, iris.StatusInternalServerError)
}
func sendError(ctx iris.Context, err error, irisError int) bool {
return sendErrorCustom(ctx, err, nil, irisError)
}
type Controller struct {
Ctx iris.Context
}
func (c Controller) respondJSON(data interface{}) {
err := c.Ctx.JSON(data)
if err != nil {
logger.Get().Error("failed to generate response", zap.Error(err))
}
}
func getCurrentUserId(ctx iris.Context) uint {
usr := ctx.User()
if usr == nil {
return 0
}
sid, _ := usr.GetID()
userID, _ := strconv.Atoi(sid)
return uint(userID)
}

View File

@ -1,212 +0,0 @@
package controller
import (
"errors"
"git.lumeweb.com/LumeWeb/portal/cid"
"git.lumeweb.com/LumeWeb/portal/controller/response"
"git.lumeweb.com/LumeWeb/portal/logger"
"git.lumeweb.com/LumeWeb/portal/middleware"
"git.lumeweb.com/LumeWeb/portal/service/files"
"github.com/kataras/iris/v12"
"go.uber.org/zap"
"io"
)
var errStreamDone = errors.New("done")
type FilesController struct {
Controller
}
func (f *FilesController) BeginRequest(ctx iris.Context) {
middleware.VerifyJwt(ctx)
}
func (f *FilesController) EndRequest(ctx iris.Context) {
}
func (f *FilesController) PostUpload() {
ctx := f.Ctx
file, meta, err := f.Ctx.FormFile("file")
if internalErrorCustom(ctx, err, errors.New("invalid file data")) {
logger.Get().Debug("invalid file data", zap.Error(err))
return
}
upload, err := files.Upload(file, meta.Size, nil)
if internalError(ctx, err) {
logger.Get().Debug("failed uploading file", zap.Error(err))
return
}
err = files.Pin(upload.Hash, upload.AccountID)
if internalError(ctx, err) {
logger.Get().Debug("failed pinning file", zap.Error(err))
return
}
cidString, err := cid.EncodeString(upload.Hash, uint64(meta.Size))
if internalError(ctx, err) {
logger.Get().Debug("failed creating cid", zap.Error(err))
return
}
err = ctx.JSON(&response.UploadResponse{Cid: cidString})
if err != nil {
logger.Get().Error("failed to create response", zap.Error(err))
}
}
func (f *FilesController) GetDownloadBy(cidString string) {
ctx := f.Ctx
hashHex, valid := validateCid(cidString, true, ctx)
if !valid {
return
}
download, err := files.Download(hashHex)
if internalError(ctx, err) {
logger.Get().Debug("failed fetching file", zap.Error(err))
return
}
err = passThroughStream(download, ctx)
if err != errStreamDone && internalError(ctx, err) {
logger.Get().Debug("failed streaming file", zap.Error(err))
}
}
func (f *FilesController) GetProofBy(cidString string) {
ctx := f.Ctx
hashHex, valid := validateCid(cidString, true, ctx)
if !valid {
return
}
proof, err := files.DownloadProof(hashHex)
if internalError(ctx, err) {
logger.Get().Debug("failed fetching file proof", zap.Error(err))
return
}
err = passThroughStream(proof, ctx)
if internalError(ctx, err) {
logger.Get().Debug("failed streaming file proof", zap.Error(err))
}
}
func (f *FilesController) GetStatusBy(cidString string) {
ctx := f.Ctx
hashHex, valid := validateCid(cidString, false, ctx)
if !valid {
return
}
status := files.Status(hashHex)
var statusCode string
switch status {
case files.STATUS_UPLOADED:
statusCode = "uploaded"
break
case files.STATUS_UPLOADING:
statusCode = "uploading"
break
case files.STATUS_NOT_FOUND:
statusCode = "not_found"
break
}
f.respondJSON(&response.FileStatusResponse{Status: statusCode})
}
func (f *FilesController) PostPinBy(cidString string) {
ctx := f.Ctx
hashHex, valid := validateCid(cidString, true, ctx)
if !valid {
return
}
err := files.Pin(hashHex, getCurrentUserId(ctx))
if internalError(ctx, err) {
logger.Get().Error(err.Error())
return
}
f.Ctx.StatusCode(iris.StatusCreated)
}
func (f *FilesController) GetUploadLimit() {
f.respondJSON(&response.UploadLimit{Limit: f.Ctx.Application().ConfigurationReadOnly().GetPostMaxMemory()})
}
func validateCid(cidString string, validateStatus bool, ctx iris.Context) (string, bool) {
_, err := cid.Valid(cidString)
if sendError(ctx, err, iris.StatusBadRequest) {
logger.Get().Debug("invalid cid", zap.Error(err))
return "", false
}
cidObject, _ := cid.Decode(cidString)
hashHex := cidObject.StringHash()
if validateStatus {
status := files.Status(hashHex)
if status == files.STATUS_NOT_FOUND {
err := errors.New("cid not found")
sendError(ctx, errors.New("cid not found"), iris.StatusNotFound)
logger.Get().Debug("cid not found", zap.Error(err))
return "", false
}
}
return hashHex, true
}
func passThroughStream(stream io.Reader, ctx iris.Context) error {
closed := false
err := ctx.StreamWriter(func(w io.Writer) error {
if closed {
return errStreamDone
}
count, err := io.CopyN(w, stream, 1024)
if count == 0 || err == io.EOF {
err = stream.(io.Closer).Close()
if err != nil {
logger.Get().Error("failed closing stream", zap.Error(err))
return err
}
closed = true
return nil
}
if err != nil {
return err
}
return nil
})
if err == errStreamDone {
err = nil
}
return err
}

View File

@ -1,23 +0,0 @@
package request
import (
"git.lumeweb.com/LumeWeb/portal/controller/validators"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/go-ozzo/ozzo-validation/v4/is"
)
type LoginRequest struct {
validatable validators.ValidatableImpl
Email string `json:"email"`
Password string `json:"password"`
}
func (r LoginRequest) Validate() error {
return validation.ValidateStruct(&r,
validation.Field(&r.Email, is.EmailFormat, validation.Required),
validation.Field(&r.Password, validation.Required),
)
}
func (r LoginRequest) Import(d map[string]interface{}) (validators.Validatable, error) {
return r.validatable.Import(d, r)
}

View File

@ -1,19 +0,0 @@
package request
import (
"git.lumeweb.com/LumeWeb/portal/controller/validators"
validation "github.com/go-ozzo/ozzo-validation/v4"
)
type LogoutRequest struct {
validatable validators.ValidatableImpl
Token string `json:"token"`
}
func (r LogoutRequest) Validate() error {
return validation.ValidateStruct(&r, validation.Field(&r.Token, validation.Required))
}
func (r LogoutRequest) Import(d map[string]interface{}) (validators.Validatable, error) {
return r.validatable.Import(d, r)
}

View File

@ -1,21 +0,0 @@
package request
import (
"git.lumeweb.com/LumeWeb/portal/controller/validators"
validation "github.com/go-ozzo/ozzo-validation/v4"
)
type PubkeyChallengeRequest struct {
validatable validators.ValidatableImpl
Pubkey string `json:"pubkey"`
}
func (r PubkeyChallengeRequest) Validate() error {
return validation.ValidateStruct(&r,
validation.Field(&r.Pubkey, validation.Required, validation.By(validators.CheckPubkeyValidator)),
)
}
func (r PubkeyChallengeRequest) Import(d map[string]interface{}) (validators.Validatable, error) {
return r.validatable.Import(d, r)
}

View File

@ -1,25 +0,0 @@
package request
import (
"git.lumeweb.com/LumeWeb/portal/controller/validators"
validation "github.com/go-ozzo/ozzo-validation/v4"
)
type PubkeyLoginRequest struct {
validatable validators.ValidatableImpl
Pubkey string `json:"pubkey"`
Challenge string `json:"challenge"`
Signature string `json:"signature"`
}
func (r PubkeyLoginRequest) Validate() error {
return validation.ValidateStruct(&r,
validation.Field(&r.Pubkey, validation.Required, validation.By(validators.CheckPubkeyValidator)),
validation.Field(&r.Challenge, validation.Required),
validation.Field(&r.Signature, validation.Required, validation.Length(128, 128)),
)
}
func (r PubkeyLoginRequest) Import(d map[string]interface{}) (validators.Validatable, error) {
return r.validatable.Import(d, r)
}

View File

@ -1,25 +0,0 @@
package request
import (
"git.lumeweb.com/LumeWeb/portal/controller/validators"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/go-ozzo/ozzo-validation/v4/is"
)
type RegisterRequest struct {
validatable validators.ValidatableImpl
Email string `json:"email"`
Password string `json:"password"`
Pubkey string `json:"pubkey"`
}
func (r RegisterRequest) Validate() error {
return validation.ValidateStruct(&r,
validation.Field(&r.Email, validation.Required, is.EmailFormat),
validation.Field(&r.Pubkey, validation.When(len(r.Password) == 0, validation.Required, validation.By(validators.CheckPubkeyValidator))),
validation.Field(&r.Password, validation.When(len(r.Pubkey) == 0, validation.Required)),
)
}
func (r RegisterRequest) Import(d map[string]interface{}) (validators.Validatable, error) {
return r.validatable.Import(d, r)
}

View File

@ -1,5 +0,0 @@
package response
type AuthStatusResponse struct {
Status bool `json:"status"`
}

View File

@ -1,5 +0,0 @@
package response
type ChallengeResponse struct {
Challenge string `json:"challenge"`
}

View File

@ -1,5 +0,0 @@
package response
type FileStatusResponse struct {
Status string `json:"status"`
}

View File

@ -1,5 +0,0 @@
package response
type LoginResponse struct {
Token string `json:"token"`
}

View File

@ -1,5 +0,0 @@
package response
type UploadResponse struct {
Cid string `json:"cid"`
}

View File

@ -1,5 +0,0 @@
package response
type UploadLimit struct {
Limit int64 `json:"limit"`
}

View File

@ -1,43 +0,0 @@
package validators
import (
"crypto/ed25519"
"encoding/hex"
"errors"
"fmt"
validation "github.com/go-ozzo/ozzo-validation/v4"
"github.com/imdario/mergo"
"reflect"
)
func CheckPubkeyValidator(value interface{}) error {
p, _ := value.(string)
pubkeyBytes, err := hex.DecodeString(p)
if err != nil {
return err
}
if len(pubkeyBytes) != ed25519.PublicKeySize {
return errors.New(fmt.Sprintf("pubkey must be %d bytes in hexadecimal format", ed25519.PublicKeySize))
}
return nil
}
type Validatable interface {
validation.Validatable
Import(d map[string]interface{}) (Validatable, error)
}
type ValidatableImpl struct {
}
func (v ValidatableImpl) Import(d map[string]interface{}, destType Validatable) (Validatable, error) {
instance := reflect.New(reflect.TypeOf(destType)).Interface().(Validatable)
// Perform the import logic
if err := mergo.Map(instance, d, mergo.WithOverride); err != nil {
return nil, err
}
return instance, nil
}

208
cron/cron.go Normal file
View File

@ -0,0 +1,208 @@
package cron
import (
"context"
"errors"
"strings"
"time"
"github.com/google/uuid"
"go.uber.org/fx"
"go.uber.org/zap"
"github.com/go-co-op/gocron/v2"
)
var (
ErrRetryLimitReached = errors.New("Retry limit reached")
)
type CronService interface {
Scheduler() gocron.Scheduler
RegisterService(service CronableService)
}
type CronableService interface {
LoadInitialTasks(cron CronService) error
}
type CronServiceParams struct {
fx.In
Logger *zap.Logger
Scheduler gocron.Scheduler
}
var Module = fx.Module("cron",
fx.Options(
fx.Provide(NewCronService),
fx.Provide(gocron.NewScheduler),
),
)
type CronServiceDefault struct {
scheduler gocron.Scheduler
services []CronableService
logger *zap.Logger
}
type RetryableJobParams struct {
Name string
Tags []string
Function any
Args []any
Attempt uint
Limit uint
After func(jobID uuid.UUID, jobName string)
Error func(jobID uuid.UUID, jobName string, err error)
}
type CronJob struct {
JobId uuid.UUID
Job gocron.JobDefinition
Task gocron.Task
Options []gocron.JobOption
}
func (c *CronServiceDefault) Scheduler() gocron.Scheduler {
return c.scheduler
}
func NewCronService(lc fx.Lifecycle, params CronServiceParams) *CronServiceDefault {
sc := &CronServiceDefault{
logger: params.Logger,
scheduler: params.Scheduler,
}
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
return sc.start()
},
})
return sc
}
func (c *CronServiceDefault) start() error {
for _, service := range c.services {
err := service.LoadInitialTasks(c)
if err != nil {
c.logger.Fatal("Failed to load initial tasks for service", zap.Error(err))
}
}
go c.scheduler.Start()
return nil
}
func (c *CronServiceDefault) RegisterService(service CronableService) {
c.services = append(c.services, service)
}
func (c *CronServiceDefault) RetryableJob(params RetryableJobParams) CronJob {
job := gocron.OneTimeJob(gocron.OneTimeJobStartImmediately())
if params.Attempt > 0 {
job = gocron.OneTimeJob(gocron.OneTimeJobStartDateTime(time.Now().Add(time.Duration(params.Attempt) * time.Minute)))
}
task := gocron.NewTask(params.Function, params.Args...)
if params.After == nil {
params.After = func(jobID uuid.UUID, jobName string) {}
}
if params.Error == nil {
params.Error = func(jobID uuid.UUID, jobName string, err error) {}
}
listeners := gocron.WithEventListeners(gocron.AfterJobRunsWithError(func(jobID uuid.UUID, jobName string, err error) {
params.Error(jobID, jobName, err)
if params.Attempt >= params.Limit && params.Limit > 0 {
c.logger.Error("Retryable task limit reached", zap.String("jobName", jobName), zap.String("jobID", jobID.String()))
params.Error(jobID, jobName, ErrRetryLimitReached)
return
}
taskRetry := params
taskRetry.Attempt++
retryTask := c.RetryableJob(taskRetry)
retryTask.JobId = jobID
_, err = c.RerunJob(retryTask)
if err != nil {
c.logger.Error("Failed to create retry job", zap.Error(err))
}
}), gocron.AfterJobRuns(params.After))
name := gocron.WithName(params.Name)
options := []gocron.JobOption{listeners, name}
if len(params.Tags) > 0 {
options = append(options, gocron.WithTags(params.Tags...))
}
return CronJob{
Job: job,
Task: task,
Options: options,
}
}
func (c *CronServiceDefault) CreateJob(job CronJob) (gocron.Job, error) {
ret, err := c.Scheduler().NewJob(job.Job, job.Task, job.Options...)
if err != nil {
return nil, err
}
return ret, nil
}
func (c *CronServiceDefault) RerunJob(job CronJob) (gocron.Job, error) {
ret, err := c.Scheduler().Update(job.JobId, job.Job, job.Task, job.Options...)
if err != nil {
return nil, err
}
return ret, nil
}
func (c *CronServiceDefault) GetJobsByPrefix(prefix string) []gocron.Job {
jobs := c.Scheduler().Jobs()
var ret []gocron.Job
for _, job := range jobs {
if strings.HasPrefix(job.Name(), prefix) {
ret = append(ret, job)
}
}
return ret
}
func (c *CronServiceDefault) GetJobByName(name string) gocron.Job {
jobs := c.Scheduler().Jobs()
for _, job := range jobs {
if job.Name() == name {
return job
}
}
return nil
}
func (c *CronServiceDefault) GetJobByID(id uuid.UUID) gocron.Job {
jobs := c.Scheduler().Jobs()
for _, job := range jobs {
if job.ID() == id {
return job
}
}
return nil
}

48
db/cache_memory.go Normal file
View File

@ -0,0 +1,48 @@
package db
import (
"context"
"sync"
"github.com/go-gorm/caches/v4"
)
type memoryCacher struct {
store *sync.Map
}
func (c *memoryCacher) init() {
if c.store == nil {
c.store = &sync.Map{}
}
}
func (c *memoryCacher) Get(ctx context.Context, key string, q *caches.Query[any]) (*caches.Query[any], error) {
c.init()
val, ok := c.store.Load(key)
if !ok {
return nil, nil
}
if err := q.Unmarshal(val.([]byte)); err != nil {
return nil, err
}
return q, nil
}
func (c *memoryCacher) Store(ctx context.Context, key string, val *caches.Query[any]) error {
c.init()
res, err := val.Marshal()
if err != nil {
return err
}
c.store.Store(key, res)
return nil
}
func (c *memoryCacher) Invalidate(ctx context.Context) error {
c.store = &sync.Map{}
return nil
}

70
db/cache_redis.go Normal file
View File

@ -0,0 +1,70 @@
package db
import (
"context"
"errors"
"fmt"
"time"
"github.com/go-gorm/caches/v4"
"github.com/redis/go-redis/v9"
)
type redisCacher struct {
rdb *redis.Client
}
func (c *redisCacher) Get(ctx context.Context, key string, q *caches.Query[any]) (*caches.Query[any], error) {
res, err := c.rdb.Get(ctx, key).Result()
if errors.Is(err, redis.Nil) {
return nil, nil
}
if err != nil {
return nil, err
}
if err := q.Unmarshal([]byte(res)); err != nil {
return nil, err
}
return q, nil
}
func (c *redisCacher) Store(ctx context.Context, key string, val *caches.Query[any]) error {
res, err := val.Marshal()
if err != nil {
return err
}
c.rdb.Set(ctx, key, res, 300*time.Second) // Set proper cache time
return nil
}
func (c *redisCacher) Invalidate(ctx context.Context) error {
var (
cursor uint64
keys []string
)
for {
var (
k []string
err error
)
k, cursor, err = c.rdb.Scan(ctx, cursor, fmt.Sprintf("%s*", caches.IdentifierPrefix), 0).Result()
if err != nil {
return err
}
keys = append(keys, k...)
if cursor == 0 {
break
}
}
if len(keys) > 0 {
if _, err := c.rdb.Del(ctx, keys...).Result(); err != nil {
return err
}
}
return nil
}

157
db/db.go
View File

@ -1,65 +1,116 @@
package db
import (
"context"
"fmt"
"git.lumeweb.com/LumeWeb/portal/model"
"github.com/spf13/viper"
"git.lumeweb.com/LumeWeb/portal/db/models"
"github.com/redis/go-redis/v9"
"go.uber.org/zap"
"git.lumeweb.com/LumeWeb/portal/config"
"github.com/go-gorm/caches/v4"
"go.uber.org/fx"
"gorm.io/driver/mysql"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
// Declare a global variable to hold the database connection.
var db *gorm.DB
// Init initializes the database connection based on the app's configuration settings.
func Init() {
// If the database connection has already been initialized, panic.
if db != nil {
panic("DB already initialized")
}
// Retrieve database connection settings from the app's configuration using the viper library.
dbType := viper.GetString("database.type")
dbHost := viper.GetString("database.host")
dbPort := viper.GetInt("database.port")
dbSocket := viper.GetString("database.socket")
dbUser := viper.GetString("database.user")
dbPassword := viper.GetString("database.password")
dbName := viper.GetString("database.name")
dbPath := viper.GetString("database.path")
var err error
var dsn string
switch dbType {
// Connect to a MySQL database.
case "mysql":
if dbSocket != "" {
dsn = fmt.Sprintf("%s:%s@unix(%s)/%s", dbUser, dbPassword, dbSocket, dbName)
} else {
dsn = fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", dbUser, dbPassword, dbHost, dbPort, dbName)
}
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
// Connect to a SQLite database.
case "sqlite":
db, err = gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
// If the database type is unsupported, panic.
default:
panic(fmt.Errorf("Unsupported database type: %s \n", dbType))
}
// If there was an error connecting to the database, panic.
if err != nil {
panic(fmt.Errorf("Failed to connect to database: %s \n", err))
}
// Automatically migrate the database schema based on the model definitions.
err = db.Migrator().AutoMigrate(&model.Account{}, &model.Key{}, &model.KeyChallenge{}, &model.LoginSession{}, &model.Upload{}, &model.Pin{}, &model.Tus{})
if err != nil {
panic(fmt.Errorf("Database setup failed database type: %s \n", err))
}
type DatabaseParams struct {
fx.In
Config *config.Manager
Logger *zap.Logger
LoggerLevel *zap.AtomicLevel
}
// Get returns the database connection instance.
func Get() *gorm.DB {
var Module = fx.Module("db",
fx.Options(
fx.Provide(NewDatabase),
),
)
func NewDatabase(lc fx.Lifecycle, params DatabaseParams) *gorm.DB {
username := params.Config.Config().Core.DB.Username
password := params.Config.Config().Core.DB.Password
host := params.Config.Config().Core.DB.Host
port := params.Config.Config().Core.DB.Port
dbname := params.Config.Config().Core.DB.Name
charset := params.Config.Config().Core.DB.Charset
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=%s&parseTime=True&loc=Local", username, password, host, port, dbname, charset)
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
Logger: newLogger(params.Logger, params.LoggerLevel),
})
if err != nil {
panic(err)
}
cacher := getCacher(params.Config, params.Logger)
if cacher != nil {
cache := &caches.Caches{Conf: &caches.Config{
Cacher: cacher,
}}
err := db.Use(cache)
if err != nil {
return nil
}
}
lc.Append(fx.Hook{
OnStart: func(ctx context.Context) error {
return db.AutoMigrate(models.GetModels()...)
},
})
return db
}
func getCacheMode(cm *config.Manager, logger *zap.Logger) string {
if cm.Config().Core.DB.Cache == nil {
return "none"
}
switch cm.Config().Core.DB.Cache.Mode {
case "", "none":
return "none"
case "memory":
return "memory"
case "redis":
return "redis"
default:
logger.Fatal("invalid cache mode", zap.String("mode", cm.Config().Core.DB.Cache.Mode))
}
return "none"
}
func getCacher(cm *config.Manager, logger *zap.Logger) caches.Cacher {
mode := getCacheMode(cm, logger)
switch mode {
case "none":
return nil
case "memory":
return &memoryCacher{}
case "redis":
rcfg, ok := cm.Config().Core.DB.Cache.Options.(config.RedisConfig)
if !ok {
logger.Fatal("invalid redis config")
return nil
}
return &redisCacher{
redis.NewClient(&redis.Options{
Addr: rcfg.Address,
Password: rcfg.Password,
DB: rcfg.DB,
}),
}
}
return nil
}

82
db/logger.go Normal file
View File

@ -0,0 +1,82 @@
package db
import (
"context"
"errors"
"strconv"
"time"
"gorm.io/gorm"
"go.uber.org/zap"
dbLogger "gorm.io/gorm/logger"
)
var _ dbLogger.Interface = (*logger)(nil)
var (
levels = map[dbLogger.LogLevel]zap.AtomicLevel{
dbLogger.Silent: zap.NewAtomicLevelAt(zap.InfoLevel),
dbLogger.Error: zap.NewAtomicLevelAt(zap.ErrorLevel),
dbLogger.Warn: zap.NewAtomicLevelAt(zap.WarnLevel),
dbLogger.Info: zap.NewAtomicLevelAt(zap.InfoLevel),
}
)
type logger struct {
logger *zap.Logger
level *zap.AtomicLevel
}
func (l logger) LogMode(level dbLogger.LogLevel) dbLogger.Interface {
if atomicLevel, ok := levels[level]; ok {
l.level.SetLevel(atomicLevel.Level())
return l
}
l.logger.Fatal("invalid log level", zap.Int("level", int(level)))
return nil
}
func (l logger) Info(ctx context.Context, s string, i ...interface{}) {
l.logger.Info(s, interfacesToFields(i...)...)
}
func (l logger) Warn(ctx context.Context, s string, i ...interface{}) {
l.logger.Warn(s, interfacesToFields(i...)...)
}
func (l logger) Error(ctx context.Context, s string, i ...interface{}) {
l.logger.Error(s, interfacesToFields(i...)...)
}
func (l logger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {
if l.level.Level() <= zap.DebugLevel {
if errors.Is(err, gorm.ErrRecordNotFound) {
return
}
sql, rowsAffected := fc()
fields := []zap.Field{
zap.String("sql", sql),
zap.Int64("rows_affected", rowsAffected),
zap.Duration("elapsed", time.Since(begin)),
}
if err != nil {
fields = append(fields, zap.Error(err))
}
l.logger.Debug("trace", fields...)
}
}
func newLogger(zlog *zap.Logger, zlogLevel *zap.AtomicLevel) *logger {
return &logger{logger: zlog, level: zlogLevel}
}
func interfacesToFields(i ...interface{}) []zap.Field {
fields := make([]zap.Field, 0)
for idx, v := range i {
fields = append(fields, zap.Any(strconv.Itoa(idx), v))
}
return fields
}

14
db/models/api_key.go Normal file
View File

@ -0,0 +1,14 @@
package models
import "gorm.io/gorm"
func init() {
registerModel(&APIKey{})
}
type APIKey struct {
gorm.Model
UserID uint
Key string
User User
}

22
db/models/blocklist.go Normal file
View File

@ -0,0 +1,22 @@
package models
import (
"time"
"gorm.io/gorm"
)
func init() {
registerModel(&Blocklist{})
}
type Blocklist struct {
gorm.Model
IP string
Reason string
BlockedAt time.Time
}
func (Blocklist) TableName() string {
return "blocklist"
}

15
db/models/dnslink.go Normal file
View File

@ -0,0 +1,15 @@
package models
import "gorm.io/gorm"
func init() {
registerModel(&DNSLink{})
}
type DNSLink struct {
gorm.Model
UserID uint `gorm:"uniqueIndex:idx_user_id_upload"`
User User
UploadID uint `gorm:"uniqueIndex:idx_user_id_upload"`
Upload Upload
}

21
db/models/download.go Normal file
View File

@ -0,0 +1,21 @@
package models
import (
"time"
"gorm.io/gorm"
)
func init() {
registerModel(&Download{})
}
type Download struct {
gorm.Model
UserID uint
User User
UploadID uint
Upload Upload
DownloadedAt time.Time
IP string
}

View File

@ -0,0 +1,21 @@
package models
import (
"time"
"gorm.io/gorm"
)
func init() {
registerModel(&EmailVerification{})
}
type EmailVerification struct {
gorm.Model
UserID uint
User User
NewEmail string
Token string
ExpiresAt time.Time
}

26
db/models/import.go Normal file
View File

@ -0,0 +1,26 @@
package models
import "gorm.io/gorm"
type ImportStatus string
const (
ImportStatusQueued ImportStatus = "queued"
ImportStatusProcessing ImportStatus = "processing"
ImportStatusCompleted ImportStatus = "completed"
)
func init() {
registerModel(&Import{})
}
type Import struct {
gorm.Model
UserID uint
Hash []byte `gorm:"type:binary(32);"`
Protocol string
User User
ImporterIP string
Status ImportStatus
Progress float64
}

11
db/models/models.go Normal file
View File

@ -0,0 +1,11 @@
package models
var registered []interface{}
func registerModel(model interface{}) {
registered = append(registered, model)
}
func GetModels() []interface{} {
return registered
}

View File

@ -0,0 +1,20 @@
package models
import (
"time"
"gorm.io/gorm"
)
func init() {
registerModel(&PasswordReset{})
}
type PasswordReset struct {
gorm.Model
UserID uint
User User
Token string
ExpiresAt time.Time
}

15
db/models/pin.go Normal file
View File

@ -0,0 +1,15 @@
package models
import "gorm.io/gorm"
func init() {
registerModel(&Pin{})
}
type Pin struct {
gorm.Model
UploadID uint
Upload Upload
UserID uint
User User
}

14
db/models/public_key.go Normal file
View File

@ -0,0 +1,14 @@
package models
import "gorm.io/gorm"
func init() {
registerModel(&PublicKey{})
}
type PublicKey struct {
gorm.Model
UserID uint
Key string `gorm:"unique;not null"`
User User
}

14
db/models/s3_upload.go Normal file
View File

@ -0,0 +1,14 @@
package models
import "gorm.io/gorm"
func init() {
registerModel(&S3Upload{})
}
type S3Upload struct {
gorm.Model
UploadID string `gorm:"unique;not null"`
Bucket string `gorm:"not null;index:idx_bucket_key"`
Key string `gorm:"not null;index:idx_bucket_key"`
}

16
db/models/s5_challenge.go Normal file
View File

@ -0,0 +1,16 @@
//go:build s5
package models
import "gorm.io/gorm"
func init() {
registerModel(&S5Challenge{})
}
type S5Challenge struct {
gorm.Model
Challenge string
Pubkey string
Type string
}

View File

@ -0,0 +1,26 @@
package models
import (
"time"
"github.com/shopspring/decimal"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)
var _ schema.Tabler = (*SCPriceHistory)(nil)
func init() {
registerModel(&SCPriceHistory{})
}
type SCPriceHistory struct {
gorm.Model
CreatedAt time.Time `gorm:"index:idx_rate"`
Rate decimal.Decimal `gorm:"type:DECIMAL(30,20);index:idx_rate"`
}
func (SCPriceHistory) TableName() string {
return "sc_price_history"
}

14
db/models/sia_upload.go Normal file
View File

@ -0,0 +1,14 @@
package models
import "gorm.io/gorm"
func init() {
registerModel(&SiaUpload{})
}
type SiaUpload struct {
gorm.Model
UploadID string `gorm:"unique;not null"`
Bucket string `gorm:"not null;index:idx_bucket_key"`
Key string `gorm:"not null;index:idx_bucket_key"`
}

94
db/models/tus_lock.go Normal file
View File

@ -0,0 +1,94 @@
package models
import (
"context"
"errors"
"time"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
func init() {
registerModel(&TusLock{})
}
var (
ErrTusLockBusy = errors.New("lock is currently held by another process")
)
type TusLock struct {
gorm.Model
LockId string `gorm:"index:idx_lock_id,unique"`
HolderPID int `gorm:"index"`
AcquiredAt time.Time
ExpiresAt time.Time
ReleaseRequested bool
DeletedAt gorm.DeletedAt `gorm:"index:idx_lock_id,unique"`
}
func (t *TusLock) TryLock(db *gorm.DB, ctx context.Context) error {
return db.Transaction(func(tx *gorm.DB) error {
var existingLock TusLock
if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Where("lock_id = ?", t.LockId).First(&existingLock).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// Insert new lock record
err := tx.WithContext(ctx).Create(t).Error
if err != nil {
return err
}
t = &existingLock
return nil
}
return err
}
// Check if existing lock is expired
if existingLock.ExpiresAt.Before(time.Now()) || existingLock.ReleaseRequested {
err := tx.Model(&existingLock).Updates(t).Error
if err != nil {
return err
}
t = &existingLock
return nil
}
// Lock is currently held by another process
return ErrTusLockBusy
})
}
func (t *TusLock) RequestRelease(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
// Update the ReleaseRequested flag in the database for the specific lock
return tx.Model(t).Where("lock_id = ?", t.LockId).Update("release_requested", true).Error
})
}
func (t *TusLock) Released(db *gorm.DB) error {
return db.Transaction(func(tx *gorm.DB) error {
// Update the ReleaseRequested flag in the database for the specific lock
return tx.Model(t).Where("lock_id = ?", t.LockId).Update("release_requested", false).Error
})
}
func (t *TusLock) IsReleaseRequested(db *gorm.DB) (bool, error) {
var count int64
err := db.Model(&TusLock{}).Where(&TusLock{LockId: t.LockId, ReleaseRequested: true}).Count(&count).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return true, nil
}
return false, err
}
return count > 0, nil
}
func (t *TusLock) Delete(db *gorm.DB) error {
return db.Where("lock_id = ?", t.LockId).Delete(&TusLock{}).Error
}

21
db/models/tus_upload.go Normal file
View File

@ -0,0 +1,21 @@
package models
import "gorm.io/gorm"
func init() {
registerModel(&TusUpload{})
}
type TusUpload struct {
gorm.Model
Hash []byte `gorm:"type:binary(32);uniqueIndex:idx_hash_deleted"`
MimeType string
UploadID string `gorm:"uniqueIndex"`
UploaderID uint
UploaderIP string
Uploader User `gorm:"foreignKey:UploaderID"`
Protocol string
Completed bool
DeletedAt gorm.DeletedAt `gorm:"uniqueIndex:idx_hash_deleted"`
}

18
db/models/upload.go Normal file
View File

@ -0,0 +1,18 @@
package models
import "gorm.io/gorm"
func init() {
registerModel(&Upload{})
}
type Upload struct {
gorm.Model
UserID uint
Hash []byte `gorm:"type:binary(32);uniqueIndex"`
MimeType string
Protocol string
User User
UploaderIP string
Size uint64
}

61
db/models/user.go Normal file
View File

@ -0,0 +1,61 @@
package models
import (
"errors"
"time"
emailverifier "github.com/AfterShip/email-verifier"
"gorm.io/gorm"
)
func init() {
registerModel(&User{})
}
type User struct {
gorm.Model
FirstName string
LastName string
Email string `gorm:"unique"`
PasswordHash string
Role string
PublicKeys []PublicKey
APIKeys []APIKey
Uploads []Upload
LastLogin *time.Time
LastLoginIP string
OTPEnabled bool `gorm:"default:false;"`
OTPVerified bool `gorm:"default:false;"`
OTPSecret string
OTPAuthUrl string
Verified bool `gorm:"default:false;"`
EmailVerifications []EmailVerification
PasswordResets []PasswordReset
}
func (u *User) BeforeUpdate(tx *gorm.DB) error {
dest := tx.Statement.Dest.(User)
if tx.Statement.Changed("Email") {
verify, err := getEmailVerfier().Verify(dest.Email)
if err != nil {
return err
}
if !verify.Syntax.Valid {
return errors.New("email is invalid")
}
}
return nil
}
func getEmailVerfier() *emailverifier.Verifier {
verifier := emailverifier.NewVerifier()
verifier.DisableSMTPCheck()
verifier.DisableGravatarCheck()
verifier.DisableDomainSuggest()
verifier.DisableAutoUpdateDisposable()
return verifier
}

241
go.mod
View File

@ -1,127 +1,152 @@
module git.lumeweb.com/LumeWeb/portal
go 1.18
go 1.21.6
require (
github.com/go-ozzo/ozzo-validation v3.6.0+incompatible
github.com/go-ozzo/ozzo-validation/v4 v4.3.0
github.com/go-resty/resty/v2 v2.7.0
github.com/golang-queue/queue v0.1.3
github.com/iris-contrib/swagger v0.0.0-20230311205341-32127a753a68
github.com/joomcode/errorx v1.1.0
github.com/kataras/iris/v12 v12.2.0
github.com/kataras/jwt v0.1.8
github.com/multiformats/go-multibase v0.2.0
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.15.0
github.com/swaggo/swag v1.16.1
github.com/tus/tusd v1.11.0
go.uber.org/zap v1.24.0
golang.org/x/crypto v0.8.0
golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1
gorm.io/driver/mysql v1.4.6
gorm.io/driver/sqlite v1.4.3
gorm.io/gorm v1.24.3
lukechampine.com/blake3 v1.2.1
git.lumeweb.com/LumeWeb/libs5-go v0.0.0-20240314105331-6510beddf2cf
github.com/AfterShip/email-verifier v1.4.0
github.com/LumeWeb/siacentral-api v0.0.0-20240311114304-4ff40c07bce5
github.com/aws/aws-sdk-go-v2 v1.25.1
github.com/aws/aws-sdk-go-v2/config v1.27.2
github.com/aws/aws-sdk-go-v2/credentials v1.17.2
github.com/aws/aws-sdk-go-v2/service/s3 v1.50.3
github.com/casbin/casbin/v2 v2.82.0
github.com/ddo/rq v0.0.0-20190828174524-b3daa55fcaba
github.com/dnslink-std/go v0.6.0
github.com/docker/go-units v0.5.0
github.com/gabriel-vasile/mimetype v1.4.3
github.com/getkin/kin-openapi v0.118.0
github.com/go-co-op/gocron/v2 v2.2.4
github.com/go-gorm/caches/v4 v4.0.0
github.com/go-resty/resty/v2 v2.11.0
github.com/go-sql-driver/mysql v1.7.1
github.com/golang-jwt/jwt/v5 v5.2.0
github.com/golang-queue/queue v0.2.0
github.com/google/uuid v1.6.0
github.com/hashicorp/go-plugin v1.6.0
github.com/julienschmidt/httprouter v1.3.0
github.com/mitchellh/mapstructure v1.5.0
github.com/pquerna/otp v1.4.0
github.com/redis/go-redis/v9 v9.5.1
github.com/rs/cors v1.10.1
github.com/samber/lo v1.39.0
github.com/shopspring/decimal v1.3.1
github.com/spf13/viper v1.18.2
github.com/tus/tusd/v2 v2.2.3-0.20240125123123-9080d351525d
github.com/vmihailenco/msgpack/v5 v5.4.1
github.com/wneessen/go-mail v0.4.1
go.sia.tech/core v0.1.12
go.sia.tech/jape v0.11.2-0.20240228204811-29a0f056d231
go.sia.tech/renterd v1.0.5
go.uber.org/fx v1.20.1
go.uber.org/zap v1.26.0
go.uber.org/zap/exp v0.2.0
golang.org/x/crypto v0.21.0
gorm.io/driver/mysql v1.5.4
gorm.io/gorm v1.25.7
lukechampine.com/blake3 v1.2.2-0.20240329192137-af604d0fbc33
nhooyr.io/websocket v1.8.10
)
require (
github.com/BurntSushi/toml v1.2.1 // indirect
github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect
github.com/CloudyKit/jet/v6 v6.2.0 // indirect
github.com/Joker/jade v1.1.3 // indirect
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/Shopify/goreferrer v0.0.0-20220729165902-8cddb4f5de06 // indirect
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40 // indirect
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/flosch/pongo2/v4 v4.0.2 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/spec v0.20.9 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/go-sql-driver/mysql v1.7.1 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.2.0 // indirect
github.com/goccy/go-json v0.9.11 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.1 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.1 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.1 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.1 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.19.2 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.22.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.27.2 // indirect
github.com/aws/smithy-go v1.20.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
github.com/casbin/govaluate v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/dchest/threefish v0.0.0-20120919164726-3ecf4c494abf // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/fatih/color v1.14.1 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/go-openapi/jsonpointer v0.20.2 // indirect
github.com/go-openapi/swag v0.22.8 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/gorilla/websocket v1.5.1 // indirect
github.com/hashicorp/go-hclog v1.6.2 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/imdario/mergo v0.3.16 // indirect
github.com/iris-contrib/go.uuid v2.0.0+incompatible // indirect
github.com/iris-contrib/schema v0.0.6 // indirect
github.com/hashicorp/yamux v0.1.1 // indirect
github.com/hbollon/go-edlib v1.6.0 // indirect
github.com/invopop/yaml v0.2.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/jonboulle/clockwork v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/kataras/blocks v0.0.7 // indirect
github.com/kataras/golog v0.1.8 // indirect
github.com/kataras/neffos v0.0.21 // indirect
github.com/kataras/pio v0.0.11 // indirect
github.com/kataras/sitemap v0.0.6 // indirect
github.com/kataras/tunnel v0.0.4 // indirect
github.com/klauspost/compress v1.16.5 // indirect
github.com/klauspost/cpuid/v2 v2.2.4 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/klauspost/reedsolomon v1.12.1 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mailgun/raymond/v2 v2.0.48 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-sqlite3 v1.14.16 // indirect
github.com/mediocregopher/radix/v3 v3.8.1 // indirect
github.com/microcosm-cc/bluemonday v1.0.23 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/multiformats/go-base32 v0.1.0 // indirect
github.com/multiformats/go-base36 v0.2.0 // indirect
github.com/nats-io/nats.go v1.25.0 // indirect
github.com/nats-io/nkeys v0.4.4 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/pelletier/go-toml/v2 v2.0.7 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/schollz/closestmatch v2.1.0+incompatible // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/spf13/afero v1.9.5 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/subosito/gotenv v1.4.2 // indirect
github.com/tdewolff/minify/v2 v2.12.5 // indirect
github.com/tdewolff/parse/v2 v2.6.5 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
github.com/miekg/dns v1.1.58 // indirect
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77 // indirect
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/mr-tron/base58 v1.1.0 // indirect
github.com/multiformats/go-base32 v0.0.3 // indirect
github.com/multiformats/go-base36 v0.1.0 // indirect
github.com/multiformats/go-multibase v0.2.0 // indirect
github.com/oklog/run v1.0.0 // indirect
github.com/olebedev/emitter v0.0.0-20230411050614-349169dec2ba // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/prometheus/client_golang v1.18.0 // indirect
github.com/prometheus/client_model v0.5.0 // indirect
github.com/prometheus/common v0.45.0 // indirect
github.com/prometheus/procfs v0.12.0 // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/yosssi/ace v0.0.5 // indirect
go.uber.org/atomic v1.11.0 // indirect
gitlab.com/NebulousLabs/bolt v1.4.4 // indirect
gitlab.com/NebulousLabs/encoding v0.0.0-20200604091946-456c3dc907fe // indirect
gitlab.com/NebulousLabs/entropy-mnemonics v0.0.0-20181018051301-7532f67e3500 // indirect
gitlab.com/NebulousLabs/errors v0.0.0-20200929122200-06c536cf6975 // indirect
gitlab.com/NebulousLabs/fastrand v0.0.0-20181126182046-603482d69e40 // indirect
gitlab.com/NebulousLabs/go-upnp v0.0.0-20211002182029-11da932010b6 // indirect
gitlab.com/NebulousLabs/log v0.0.0-20210609172545-77f6775350e2 // indirect
gitlab.com/NebulousLabs/merkletree v0.0.0-20200118113624-07fbf710afc4 // indirect
gitlab.com/NebulousLabs/persist v0.0.0-20200605115618-007e5e23d877 // indirect
gitlab.com/NebulousLabs/ratelimit v0.0.0-20200811080431-99b8f0768b2e // indirect
gitlab.com/NebulousLabs/siamux v0.0.2-0.20220630142132-142a1443a259 // indirect
gitlab.com/NebulousLabs/threadgroup v0.0.0-20200608151952-38921fbef213 // indirect
go.etcd.io/bbolt v1.3.8 // indirect
go.sia.tech/mux v1.2.0 // indirect
go.sia.tech/siad v1.5.10-0.20230228235644-3059c0b930ca // indirect
go.uber.org/dig v1.17.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/net v0.9.0 // indirect
golang.org/x/sys v0.8.0 // indirect
golang.org/x/text v0.9.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.8.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/protobuf v1.30.0 // indirect
golang.org/x/exp v0.0.0-20231219180239-dc181d75b848 // indirect
golang.org/x/mod v0.16.0 // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.19.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect
google.golang.org/grpc v1.62.0 // indirect
google.golang.org/protobuf v1.32.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/frand v1.4.2 // indirect
)
replace go.uber.org/multierr => go.uber.org/multierr v1.9.0
replace (
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp => go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.39.0
go.opentelemetry.io/otel => go.opentelemetry.io/otel v1.14.0
go.opentelemetry.io/otel/exporters/otlp/internal/retry => go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.12.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace => go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.12.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp => go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.12.0
go.opentelemetry.io/otel/metric => go.opentelemetry.io/otel/metric v0.37.0
go.opentelemetry.io/otel/sdk => go.opentelemetry.io/otel/sdk v1.12.0
go.opentelemetry.io/otel/trace => go.opentelemetry.io/otel/trace v1.14.0
go.opentelemetry.io/proto/otlp => go.opentelemetry.io/proto/otlp v0.19.0
)
replace github.com/tus/tusd/v2 => github.com/LumeWeb/tusd/v2 v2.2.3-0.20240224143554-96925dd43120

2035
go.sum

File diff suppressed because it is too large Load Diff

217
import/import.go Normal file
View File

@ -0,0 +1,217 @@
package _import
import (
"context"
"errors"
"time"
"git.lumeweb.com/LumeWeb/portal/db/models"
"go.uber.org/fx"
"gorm.io/gorm"
)
var ErrNotFound = gorm.ErrRecordNotFound
var _ ImportService = (*ImportServiceDefault)(nil)
type ImportMetadata struct {
ID uint
UserID uint
Hash []byte
Status models.ImportStatus
Progress float64
Protocol string
ImporterIP string
Created time.Time
}
type ImportService interface {
SaveImport(ctx context.Context, metadata ImportMetadata, skipExisting bool) error
GetImport(ctx context.Context, objectHash []byte) (ImportMetadata, error)
DeleteImport(ctx context.Context, objectHash []byte) error
UpdateProgress(ctx context.Context, objectHash []byte, stage int, totalStages int) error
UpdateStatus(ctx context.Context, objectHash []byte, status models.ImportStatus) error
}
func (u ImportMetadata) IsEmpty() bool {
if u.UserID != 0 || u.Protocol != "" || u.ImporterIP != "" || u.Status != "" {
return false
}
if !u.Created.IsZero() {
return false
}
if len(u.Hash) != 0 {
return false
}
return true
}
var Module = fx.Module("import",
fx.Provide(
fx.Annotate(
NewImportService,
fx.As(new(ImportService)),
),
),
)
type ImportServiceDefault struct {
db *gorm.DB
}
func (i ImportServiceDefault) UpdateProgress(ctx context.Context, objectHash []byte, stage int, totalStages int) error {
_import, err := i.GetImport(ctx, objectHash)
if err != nil {
return err
}
if _import.IsEmpty() {
return ErrNotFound
}
_import.Progress = float64(stage) / float64(totalStages) * 100.0
return i.SaveImport(ctx, _import, false)
}
func (i ImportServiceDefault) UpdateStatus(ctx context.Context, objectHash []byte, status models.ImportStatus) error {
_import, err := i.GetImport(ctx, objectHash)
if err != nil {
return err
}
if _import.IsEmpty() {
return ErrNotFound
}
_import.Status = status
return i.SaveImport(ctx, _import, false)
}
func (i ImportServiceDefault) SaveImport(ctx context.Context, metadata ImportMetadata, skipExisting bool) error {
var __import models.Import
__import.Hash = metadata.Hash
ret := i.db.WithContext(ctx).Model(&models.Import{}).Where(&__import).First(&__import)
if ret.Error != nil {
if errors.Is(ret.Error, gorm.ErrRecordNotFound) {
return i.createImport(ctx, metadata)
}
return ret.Error
}
if skipExisting {
return nil
}
changed := false
if __import.UserID != metadata.UserID {
__import.UserID = metadata.UserID
changed = true
}
if __import.Status != metadata.Status {
__import.Status = metadata.Status
changed = true
}
if __import.Progress != metadata.Progress {
__import.Progress = metadata.Progress
changed = true
}
if __import.Protocol != metadata.Protocol {
__import.Protocol = metadata.Protocol
changed = true
}
if __import.ImporterIP != metadata.ImporterIP {
__import.ImporterIP = metadata.ImporterIP
changed = true
}
if changed {
return i.db.Updates(&__import).Error
}
return nil
}
func (m *ImportServiceDefault) createImport(ctx context.Context, metadata ImportMetadata) error {
__import := models.Import{
UserID: metadata.UserID,
Hash: metadata.Hash,
Status: metadata.Status,
Progress: metadata.Progress,
Protocol: metadata.Protocol,
ImporterIP: metadata.ImporterIP,
}
if __import.Status == "" {
__import.Status = models.ImportStatusQueued
}
return m.db.WithContext(ctx).Create(&__import).Error
}
func (i ImportServiceDefault) GetImport(ctx context.Context, objectHash []byte) (ImportMetadata, error) {
var _import models.Import
_import.Hash = objectHash
ret := i.db.WithContext(ctx).Model(&models.Import{}).Where(&_import).First(&_import)
if ret.Error != nil {
if errors.Is(ret.Error, gorm.ErrRecordNotFound) {
return ImportMetadata{}, ErrNotFound
}
return ImportMetadata{}, ret.Error
}
return ImportMetadata{
ID: _import.ID,
UserID: _import.UserID,
Hash: _import.Hash,
Protocol: _import.Protocol,
Status: _import.Status,
Progress: _import.Progress,
ImporterIP: _import.ImporterIP,
Created: _import.CreatedAt,
}, nil
}
func (i ImportServiceDefault) DeleteImport(ctx context.Context, objectHash []byte) error {
var _import models.Import
_import.Hash = objectHash
ret := i.db.WithContext(ctx).Model(&models.Import{}).Where(&_import).Delete(&_import)
if ret.Error != nil {
if errors.Is(ret.Error, gorm.ErrRecordNotFound) {
return ErrNotFound
}
return ret.Error
}
return nil
}
type ImportServiceParams struct {
fx.In
Db *gorm.DB
}
func NewImportService(params ImportServiceParams) *ImportServiceDefault {
return &ImportServiceDefault{
db: params.Db,
}
}

View File

@ -1,30 +1,45 @@
package logger
import (
"github.com/spf13/viper"
"os"
"git.lumeweb.com/LumeWeb/portal/config"
"go.uber.org/zap"
"log"
"go.uber.org/zap/zapcore"
)
var logger *zap.Logger
func NewLogger(cm *config.Manager) (*zap.Logger, *zap.AtomicLevel) {
func Init() {
var newLogger *zap.Logger
var err error
// Create a new atomic level
atomicLevel := zap.NewAtomicLevel()
if viper.GetBool("debug") {
newLogger, err = zap.NewDevelopment()
if cm != nil {
// Set initial log level, for example, info level
atomicLevel.SetLevel(mapLogLevel(cm.Config().Core.Log.Level))
} else {
newLogger, err = zap.NewProduction()
atomicLevel.SetLevel(mapLogLevel("debug"))
}
if err != nil {
log.Fatal(err)
// Create the logger with the atomic level
logger := zap.New(zapcore.NewCore(
zapcore.NewConsoleEncoder(zap.NewDevelopmentEncoderConfig()),
zapcore.Lock(os.Stdout),
atomicLevel,
), zap.AddCaller())
return logger, &atomicLevel
}
func mapLogLevel(level string) zapcore.Level {
switch level {
case "debug":
return zapcore.DebugLevel
case "info":
return zapcore.InfoLevel
case "warn":
return zapcore.WarnLevel
default:
return zapcore.ErrorLevel
}
logger = newLogger
}
func Get() *zap.Logger {
return logger
}

69
mailer/email.go Normal file
View File

@ -0,0 +1,69 @@
package mailer
import "github.com/wneessen/go-mail"
type Email struct {
to string
from string
subject string
body string
}
func (e *Email) To() string {
return e.to
}
func (e *Email) SetTo(to string) {
e.to = to
}
func (e *Email) From() string {
return e.from
}
func (e *Email) SetFrom(from string) {
e.from = from
}
func (e *Email) Subject() string {
return e.subject
}
func (e *Email) SetSubject(subject string) {
e.subject = subject
}
func (e *Email) Body() string {
return e.body
}
func (e *Email) SetBody(body string) {
e.body = body
}
func (e *Email) ToMessage() (*mail.Msg, error) {
msg :=
mail.NewMsg()
err := msg.From(e.from)
if err != nil {
return nil, err
}
err = msg.To(e.to)
if err != nil {
return nil, err
}
msg.SetBodyString("text/plain", e.body)
return msg, nil
}
func NewEmail(subject, body string) *Email {
return &Email{
subject: subject,
body: body,
}
}

83
mailer/mailer.go Normal file
View File

@ -0,0 +1,83 @@
package mailer
import (
"context"
"strings"
"git.lumeweb.com/LumeWeb/portal/config"
"github.com/wneessen/go-mail"
"go.uber.org/fx"
"go.uber.org/zap"
)
type TemplateData = map[string]interface{}
var Module = fx.Module("mailer",
fx.Options(
fx.Provide(NewMailer),
fx.Provide(NewTemplateRegistry),
fx.Invoke(func(registry *TemplateRegistry) error {
return registry.loadTemplates()
}),
),
)
type Mailer struct {
config *config.Manager
logger *zap.Logger
client *mail.Client
templateRegistry *TemplateRegistry
}
func (m *Mailer) TemplateSend(template string, subjectVars TemplateData, bodyVars TemplateData, to string) error {
email, err := m.templateRegistry.RenderTemplate(template, subjectVars, bodyVars)
if err != nil {
return err
}
email.SetFrom(m.config.Config().Core.Mail.From)
email.SetTo(to)
msg, err := email.ToMessage()
if err != nil {
return err
}
return m.client.DialAndSend(msg)
}
func NewMailer(lc fx.Lifecycle, config *config.Manager, logger *zap.Logger, templateRegistry *TemplateRegistry) (*Mailer, error) {
m := &Mailer{config: config, logger: logger, templateRegistry: templateRegistry}
lc.Append(fx.Hook{
OnStart: func(context.Context) error {
var options []mail.Option
if config.Config().Core.Mail.Port != 0 {
options = append(options, mail.WithPort(config.Config().Core.Mail.Port))
}
if config.Config().Core.Mail.AuthType != "" {
options = append(options, mail.WithSMTPAuth(mail.SMTPAuthType(strings.ToUpper(config.Config().Core.Mail.AuthType))))
}
if config.Config().Core.Mail.SSL {
options = append(options, mail.WithSSLPort(true))
}
options = append(options, mail.WithUsername(config.Config().Core.Mail.Username))
options = append(options, mail.WithPassword(config.Config().Core.Mail.Password))
client, err := mail.NewClient(config.Config().Core.Mail.Host, options...)
if err != nil {
return err
}
m.client = client
return nil
},
})
return m, nil
}

97
mailer/templates.go Normal file
View File

@ -0,0 +1,97 @@
package mailer
import (
"embed"
"errors"
"io/fs"
"strings"
"text/template"
)
const EMAIL_FS_PREFIX = "templates/"
const TPL_PASSWORD_RESET = "password_reset"
const TPL_VERIFY_EMAIL = "verify_email"
type EmailTemplate struct {
Subject *template.Template
Body *template.Template
}
//go:embed templates/*
var templateFS embed.FS
var ErrTemplateNotFound = errors.New("template not found")
type TemplateRegistry struct {
templates map[string]EmailTemplate
}
func NewTemplateRegistry() *TemplateRegistry {
return &TemplateRegistry{
templates: make(map[string]EmailTemplate),
}
}
func (tr *TemplateRegistry) loadTemplates() error {
subjectTemplates, err := fs.Glob(templateFS, EMAIL_FS_PREFIX+"*_subject*")
if err != nil {
return err
}
for _, subjectTemplate := range subjectTemplates {
templateName := strings.TrimPrefix(subjectTemplate, EMAIL_FS_PREFIX)
templateName = strings.TrimSuffix(templateName, "_subject.tpl")
bodyTemplate := strings.TrimSuffix(subjectTemplate, "_subject.tpl") + "_body.tpl"
bodyTemplate = strings.TrimPrefix(bodyTemplate, EMAIL_FS_PREFIX)
subjectContent, err := fs.ReadFile(templateFS, EMAIL_FS_PREFIX+templateName+"_subject.tpl")
if err != nil {
return err
}
subjectTmpl, err := template.New(templateName).Parse(string(subjectContent))
if err != nil {
return err
}
bodyContent, err := fs.ReadFile(templateFS, EMAIL_FS_PREFIX+bodyTemplate)
if err != nil {
return err
}
bodyTmpl, err := template.New(templateName).Parse(string(bodyContent))
if err != nil {
return err
}
tr.templates[templateName] = EmailTemplate{
Subject: subjectTmpl,
Body: bodyTmpl,
}
}
return nil
}
func (tr *TemplateRegistry) RenderTemplate(templateName string, subjectVars TemplateData, bodyVars TemplateData) (*Email, error) {
tmpl, ok := tr.templates[templateName]
if !ok {
return nil, ErrTemplateNotFound
}
var subjectBuilder strings.Builder
err := tmpl.Subject.Execute(&subjectBuilder, subjectVars)
if err != nil {
return nil, err
}
var bodyBuilder strings.Builder
err = tmpl.Body.Execute(&bodyBuilder, bodyVars)
if err != nil {
return nil, err
}
return NewEmail(subjectBuilder.String(), bodyBuilder.String()), nil
}

View File

@ -0,0 +1,16 @@
Dear {{if .FirstName}}{{.FirstName}}{{else}}{{.Email}}{{end}},
You are receiving this email because we received a password reset request for your account. If you did not request a password reset, please ignore this email.
To reset your password, please click the link below:
{{.ResetURL}}
This link will expire in {{.ExpireTime}} hours. If you did not request a password reset, no further action is required.
If you're having trouble clicking the reset link, copy and paste the URL below into your web browser:
{{.ResetURL}}
Thank you for using {{.PortalName}}.
Best regards,
The {{.PortalName}} Team

View File

@ -0,0 +1 @@
Reset Your Password for {{.PortalName}}

View File

@ -0,0 +1,10 @@
Dear {{if .FirstName}}{{.FirstName}}{{else}}{{.Email}}{{end}},
Thank you for registering with {{.PortalName}}. To complete your registration and verify your email address, please go to the following link:
{{.VerificationLink}}
Please note, this link will expire in {{.ExpireTime}}. If you did not initiate this request, please ignore this email or contact our support team for assistance.
Best regards,
The {{.PortalName}} Team

View File

@ -0,0 +1 @@
Verify Your Email for {{.PortalName}}

106
main.go
View File

@ -1,106 +0,0 @@
package main
import (
"embed"
"git.lumeweb.com/LumeWeb/portal/config"
"git.lumeweb.com/LumeWeb/portal/controller"
"git.lumeweb.com/LumeWeb/portal/db"
_ "git.lumeweb.com/LumeWeb/portal/docs"
"git.lumeweb.com/LumeWeb/portal/logger"
"git.lumeweb.com/LumeWeb/portal/service/auth"
"git.lumeweb.com/LumeWeb/portal/service/files"
"git.lumeweb.com/LumeWeb/portal/tus"
"github.com/iris-contrib/swagger"
"github.com/iris-contrib/swagger/swaggerFiles"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/mvc"
"go.uber.org/zap"
"log"
"net/http"
)
// Embed a directory of static files for serving from the app's root path
//
//go:embed app/*
var embedFrontend embed.FS
// @title Lume Web Portal
// @version 1.0
// @description A decentralized data storage portal for the open web
// @contact.name Lume Web Project
// @contact.url https://lumeweb.com
// @contact.email contact@lumeweb.com
// @license.name MIT
// @license.url https://opensource.org/license/mit/
// @externalDocs.description OpenAPI
// @externalDocs.url https://swagger.io/resources/open-api/
func main() {
// Initialize the configuration settings
config.Init()
// Initialize the database connection
db.Init()
logger.Init()
files.Init()
auth.Init()
// Create a new Iris app instance
app := iris.New()
// Enable Gzip compression for responses
app.Use(iris.Compression)
// Serve static files from the embedded directory at the app's root path
app.HandleDir("/", embedFrontend)
api := app.Party("/api")
v1 := api.Party("/v1")
// Register the AccountController with the MVC framework and attach it to the "/api/account" path
mvc.Configure(v1.Party("/account"), func(app *mvc.Application) {
app.Handle(new(controller.AccountController))
})
mvc.Configure(v1.Party("/auth"), func(app *mvc.Application) {
app.Handle(new(controller.AuthController))
})
mvc.Configure(v1.Party("/files"), func(app *mvc.Application) {
app.Handle(new(controller.FilesController))
app.Router.Use()
})
tusHandler := tus.Init()
v1.Any(tus.TUS_API_PATH+"/{fileparam:path}", iris.FromStd(http.StripPrefix(v1.GetRelPath()+tus.TUS_API_PATH+"/", tusHandler)))
v1.Post(tus.TUS_API_PATH, iris.FromStd(http.StripPrefix(v1.GetRelPath()+tus.TUS_API_PATH, tusHandler)))
swaggerConfig := swagger.Config{
// The url pointing to API definition.
URL: "http://localhost:8080/swagger/doc.json",
DeepLinking: true,
DocExpansion: "list",
DomID: "#swagger-ui",
// The UI prefix URL (see route).
Prefix: "/swagger",
}
swaggerUI := swagger.Handler(swaggerFiles.Handler, swaggerConfig)
app.Get("/swagger", swaggerUI)
// And the wildcard one for index.html, *.js, *.css and e.t.c.
app.Get("/swagger/{any:path}", swaggerUI)
// Start the Iris app and listen for incoming requests on port 80
err := app.Listen(":8080", func(app *iris.Application) {
routes := app.GetRoutes()
for _, route := range routes {
log.Println(route)
}
})
if err != nil {
logger.Get().Error("Failed starting webserver proof", zap.Error(err))
}
}

174
metadata/metadata.go Normal file
View File

@ -0,0 +1,174 @@
package metadata
import (
"context"
"errors"
"time"
"git.lumeweb.com/LumeWeb/portal/db/models"
"go.uber.org/fx"
"gorm.io/gorm"
)
var ErrNotFound = gorm.ErrRecordNotFound
var _ MetadataService = (*MetadataServiceDefault)(nil)
type UploadMetadata struct {
ID uint `json:"upload_id"`
UserID uint `json:"user_id"`
Hash []byte `json:"hash"`
MimeType string `json:"mime_type"`
Protocol string `json:"protocol"`
UploaderIP string `json:"uploader_ip"`
Size uint64 `json:"size"`
Created time.Time `json:"created"`
}
func (u UploadMetadata) IsEmpty() bool {
if u.UserID != 0 || u.MimeType != "" || u.Protocol != "" || u.UploaderIP != "" || u.Size != 0 {
return false
}
if !u.Created.IsZero() {
return false
}
if len(u.Hash) != 0 {
return false
}
return true
}
var Module = fx.Module("metadata",
fx.Provide(
fx.Annotate(
NewMetadataService,
fx.As(new(MetadataService)),
),
),
)
type MetadataService interface {
SaveUpload(ctx context.Context, metadata UploadMetadata, skipExisting bool) error
GetUpload(ctx context.Context, objectHash []byte) (UploadMetadata, error)
DeleteUpload(ctx context.Context, objectHash []byte) error
}
type MetadataServiceDefault struct {
db *gorm.DB
}
type MetadataServiceParams struct {
fx.In
Db *gorm.DB
}
func NewMetadataService(params MetadataServiceParams) *MetadataServiceDefault {
return &MetadataServiceDefault{
db: params.Db,
}
}
func (m *MetadataServiceDefault) SaveUpload(ctx context.Context, metadata UploadMetadata, skipExisting bool) error {
var upload models.Upload
upload.Hash = metadata.Hash
ret := m.db.WithContext(ctx).Model(&models.Upload{}).Where(&upload).First(&upload)
if ret.Error != nil {
if errors.Is(ret.Error, gorm.ErrRecordNotFound) {
return m.createUpload(ctx, metadata)
}
return ret.Error
}
if skipExisting {
return nil
}
changed := false
if upload.UserID != metadata.UserID {
upload.UserID = metadata.UserID
changed = true
}
if upload.MimeType != metadata.MimeType {
upload.MimeType = metadata.MimeType
changed = true
}
if upload.Protocol != metadata.Protocol {
upload.Protocol = metadata.Protocol
changed = true
}
if upload.UploaderIP != metadata.UploaderIP {
upload.UploaderIP = metadata.UploaderIP
changed = true
}
if upload.Size != metadata.Size {
upload.Size = metadata.Size
changed = true
}
if changed {
return m.db.Updates(&upload).Error
}
return nil
}
func (m *MetadataServiceDefault) createUpload(ctx context.Context, metadata UploadMetadata) error {
upload := models.Upload{
UserID: metadata.UserID,
Hash: metadata.Hash,
MimeType: metadata.MimeType,
Protocol: metadata.Protocol,
UploaderIP: metadata.UploaderIP,
Size: metadata.Size,
}
return m.db.WithContext(ctx).Create(&upload).Error
}
func (m *MetadataServiceDefault) GetUpload(ctx context.Context, objectHash []byte) (UploadMetadata, error) {
var upload models.Upload
upload.Hash = objectHash
ret := m.db.WithContext(ctx).Model(&models.Upload{}).Where(&upload).First(&upload)
if ret.Error != nil {
return UploadMetadata{}, ret.Error
}
return UploadMetadata{
ID: upload.ID,
UserID: upload.UserID,
Hash: upload.Hash,
MimeType: upload.MimeType,
Protocol: upload.Protocol,
UploaderIP: upload.UploaderIP,
Size: upload.Size,
}, nil
}
func (m *MetadataServiceDefault) DeleteUpload(ctx context.Context, objectHash []byte) error {
var upload models.Upload
upload.Hash = objectHash
ret := m.db.WithContext(ctx).Model(&models.Upload{}).Where(&upload).First(&upload)
if ret.Error != nil {
return ret.Error
}
return m.db.Delete(&upload).Error
}

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