From cd3cb01791d28950905cee5e8c3c0100c1ebd0b5 Mon Sep 17 00:00:00 2001 From: Arnab Chakraborty <11457760+Rocky43007@users.noreply.github.com> Date: Fri, 30 Aug 2024 10:01:37 -0400 Subject: [PATCH 01/34] Update deps --- pnpm-lock.yaml | Bin 1049984 -> 1180425 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 04cae94f1d1eaf099472b28cbc76a7ecafd1da23..16b9cdcefec9c94389f9392ef6647752aa556a06 100644 GIT binary patch delta 94268 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 T!)+0+q?(#CQ&^k4NLTLPqZCrx delta 48785 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 From d9e988a00d635c0b20a5b1e9d643978deece36c2 Mon Sep 17 00:00:00 2001 From: lynx <141365347+iLynxcat@users.noreply.github.com> Date: Fri, 30 Aug 2024 17:18:53 -0500 Subject: [PATCH 02/34] pnpm install lockfile update --- pnpm-lock.yaml | Bin 1180425 -> 1180418 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 16b9cdcefec9c94389f9392ef6647752aa556a06..8069f72f2cdc07d9f2af2a02417ba0dbe1b316fc 100644 GIT binary patch delta 187 zcmeBd^Jr@G*sxJ+^4guU&8M~6Pirv(F%u9o12GE_vjQ<25VHd@$M(}&oHduG@B724 zzCG_RXASdoxqqDU?VOBUK+FxqJV4C5os*GoEBkbX8{BNu<2m`br%(UEEIK`tolk6f zzH62X`M?s}WjXmGnL!Fgw)gPxiSQul*(||Vjo`LBO7a0QKM)ISca#)7@e%;r CSv~jw delta 235 zcmZo_^XP2z*sxKHCC9y@ z)%u^i^*>MRf8MSC`3|wO8|WDt>KR#1@B6{5INgJrPiA@mCm#n)c)I{6Un(< Date: Fri, 30 Aug 2024 18:51:27 -0400 Subject: [PATCH 03/34] New Navbar & Banner --- packages/ui/style/tailwind.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/style/tailwind.js b/packages/ui/style/tailwind.js index 2cb264f14..7398981cf 100644 --- a/packages/ui/style/tailwind.js +++ b/packages/ui/style/tailwind.js @@ -126,7 +126,7 @@ module.exports = function (app, options) { 850: '#08090D', 900: '#060609', 950: '#030303' - } + }, }, extend: { transitionTimingFunction: { From 6eb21b14886514b7b3a1250579005a3aece825fd Mon Sep 17 00:00:00 2001 From: Arnab Chakraborty <11457760+Rocky43007@users.noreply.github.com> Date: Fri, 30 Aug 2024 18:55:13 -0400 Subject: [PATCH 04/34] Update to latest Next & React versions --- pnpm-lock.yaml | Bin 1180418 -> 1185138 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8069f72f2cdc07d9f2af2a02417ba0dbe1b316fc..543eb911fbf9963a7bec74d751ac9e082afce7a3 100644 GIT binary patch delta 7328 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 delta 4919 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 k%5ov?9KY3LmciDl^7_0rsoZ2_d| Date: Fri, 30 Aug 2024 19:13:32 -0500 Subject: [PATCH 05/34] fix node_modules watch in tailwind config --- packages/ui/style/tailwind.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/style/tailwind.js b/packages/ui/style/tailwind.js index 7398981cf..c313f5270 100644 --- a/packages/ui/style/tailwind.js +++ b/packages/ui/style/tailwind.js @@ -11,7 +11,7 @@ module.exports = function (app, options) { content: [ `../../apps/${app}/src/**/*.{ts,tsx,html,stories.tsx}`, '../../packages/*/src/**/*.{ts,tsx,html,stories.tsx}', - '../../interface/**/*.{ts,tsx,html,stories.tsx}' + '../../interface/{app,components}/*.{ts,tsx,html,stories.tsx}' ], darkMode: 'class', theme: { From 96cd24d19e330093dbcce13f859deaa9c7a6b182 Mon Sep 17 00:00:00 2001 From: lynx <141365347+iLynxcat@users.noreply.github.com> Date: Fri, 30 Aug 2024 19:13:57 -0500 Subject: [PATCH 06/34] add Inter and Plex to font family options --- packages/ui/style/tailwind.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/ui/style/tailwind.js b/packages/ui/style/tailwind.js index c313f5270..e6ab4e142 100644 --- a/packages/ui/style/tailwind.js +++ b/packages/ui/style/tailwind.js @@ -38,6 +38,10 @@ module.exports = function (app, options) { '7xl': '5rem' }, extend: { + fontFamily: { + plex: ['var(--font-plex-sans)', ...defaultTheme.fontFamily.sans], + sans: ['var(--font-inter)', ...defaultTheme.fontFamily.sans] + }, colors: { accent: { DEFAULT: alpha('--color-accent'), @@ -126,7 +130,7 @@ module.exports = function (app, options) { 850: '#08090D', 900: '#060609', 950: '#030303' - }, + } }, extend: { transitionTimingFunction: { From a4770aacc4c782c0bcc8398332ea5d7cca5820f0 Mon Sep 17 00:00:00 2001 From: Arnab Chakraborty <11457760+Rocky43007@users.noreply.github.com> Date: Sat, 31 Aug 2024 09:31:06 -0400 Subject: [PATCH 07/34] Change React Version so we can compile Using React 18.3 was breaking some of our deps it seems in prod. --- pnpm-lock.yaml | Bin 1185138 -> 1180421 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 543eb911fbf9963a7bec74d751ac9e082afce7a3..c37cc0c1c6d15513d582d09aa914ce8364fe4092 100644 GIT binary patch delta 3187 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

RCoaYb9nbOYo1}MgmCxrJOf>xXDppJ z+F#pkgNc#FZeiwBV<>jLY>dI#%|?Mo#&COXVk@>AZHt*%&+INWpgY=6kAJ#sjO4Tp zTsC%Q8Kzk?hkQlwO!k#Qj9LgaWLa&>GY*>qBIw`M!b^MQ5-eTB72vn8(mQa@S(O&| z*60P){YpR?3$8Qf5bU4p*~WaAOl0kSD&DQtSK`)IVTX(!ztbytu4@z4$*3r}vhEi) zM6-O1_}G$`+Em-rG{%s;uB%j zFm-z86QNp-f4C~7DCqf;GR}F7#@! zwO;_QwfFi3&n~_Eh7gAL`-NSutKq>TrIbluS7B2@V2FNH+0r)BG zImu!;hNX&TF8q12Xz^YU_}RC^Dn{Vm`oK{7QL6Zr8oz7^3>(eSTaCsjbY2p)SQ7(U zsZfXBYtvUZVOa|a!ACB=4*McSfBLRPELVF8n_?BSxpy8zk5znCJ^1A)PA$e@)FmO9 zX4%EAszHT=EuyDH*u^H{;6(_dH7#P8bT#H#@sdN|+$UP2y`y)Ji@DqhTZaz1_Jnvp z(kt>QF#pH2p3xYrx+Hq`CpKD+DVG^dUtAJvM=5Q*EY^DZbL$pkg72ddPEt?K_fAf^ b0VXo6XZbD`(%s%4uc#+`PFVNUlSBRwgkxFM From 8264ea2899f774a86f810620886739716f0697ec Mon Sep 17 00:00:00 2001 From: myung03 Date: Tue, 3 Sep 2024 22:32:36 -0700 Subject: [PATCH 08/34] parallax effect on hero --- pnpm-lock.yaml | Bin 1180421 -> 1180992 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c37cc0c1c6d15513d582d09aa914ce8364fe4092..5304c53c1f8be4b6eebe6b7dff60261e0fb5a9b2 100644 GIT binary patch delta 330 zcmZo|^ElAuvB5=EAh|@hAh9ShCnvE&wCW!1{=QXtCc!zA4L>VRFK}e$oc_d`Lw&kH1_$@_f+@@#(|Npr zTy7f<&UVd24j=~6TtLhX#5_RE3&eat%)ebTQNa12C^mPSPoLPp?=gMC0|CD24VFA2 Z)BPIxMW-)#ExP`H*V+Ayq~oW&&bnAZ7t#Rv=~rV)pHaR5_knx1UPl0Afxc<^p1F fAm#yLULfWJV*c%?k_4O&P6w(I+`dCau;T#$V_7I5 From 436ad95b583865b3015bc25f0d70f5cec3e53e6d Mon Sep 17 00:00:00 2001 From: lynx <141365347+iLynxcat@users.noreply.github.com> Date: Wed, 4 Sep 2024 20:49:06 -0500 Subject: [PATCH 09/34] remove default 2x screen --- packages/ui/style/tailwind.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/ui/style/tailwind.js b/packages/ui/style/tailwind.js index e6ab4e142..d5e1246c3 100644 --- a/packages/ui/style/tailwind.js +++ b/packages/ui/style/tailwind.js @@ -20,8 +20,7 @@ module.exports = function (app, options) { sm: '650px', md: '868px', lg: '1024px', - xl: '1280px', - ...defaultTheme.screens + xl: '1280px' }, fontSize: { 'tiny': '.65rem', From 4f977a1537c81e3165304bff2aff743b15e244bd Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Wed, 18 Sep 2024 15:28:06 +0300 Subject: [PATCH 10/34] videos in explorer section, update search video, and spacing --- .../app/$libraryId/Explorer/FilePath/LayeredFileIcon.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/interface/app/$libraryId/Explorer/FilePath/LayeredFileIcon.tsx b/interface/app/$libraryId/Explorer/FilePath/LayeredFileIcon.tsx index 1d0ebf8f9..bf5ef1931 100644 --- a/interface/app/$libraryId/Explorer/FilePath/LayeredFileIcon.tsx +++ b/interface/app/$libraryId/Explorer/FilePath/LayeredFileIcon.tsx @@ -12,8 +12,8 @@ const SUPPORTED_ICONS = ['Document', 'Code', 'Text', 'Config']; const positionConfig: Record = { Text: 'flex h-full w-full items-center justify-center', - Code: 'flex h-full w-full items-center justify-center', - Config: 'flex h-full w-full items-center justify-center' + Code: 'flex h-full w-full items-center justify-center pt-[18px]', + Config: 'flex h-full w-full items-center justify-center pt-[18px]' }; const LayeredFileIcon = forwardRef( @@ -38,7 +38,7 @@ const LayeredFileIcon = forwardRef( className={clsx('pointer-events-none absolute bottom-0 right-0', positionClass)} > - + From 078efd7918dd9d7724120b1dfa92b6fbcdf96b22 Mon Sep 17 00:00:00 2001 From: ameer2468 <33054370+ameer2468@users.noreply.github.com> Date: Fri, 1 Nov 2024 16:27:59 +0300 Subject: [PATCH 11/34] fix footer and button --- packages/client/src/core.ts | 2 +- pnpm-lock.yaml | Bin 1140637 -> 1144198 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/core.ts b/packages/client/src/core.ts index ebe660c03..9640f5d6e 100644 --- a/packages/client/src/core.ts +++ b/packages/client/src/core.ts @@ -557,7 +557,7 @@ name: string; identity: RemoteIdentity; p2p: NodeConfigP2P; features: BackendFea export type NonCriticalError = { indexer: NonCriticalIndexerError } | { file_identifier: NonCriticalFileIdentifierError } | { media_processor: NonCriticalMediaProcessorError } -export type NonCriticalFileIdentifierError = { failed_to_extract_file_metadata: string } | { failed_to_extract_isolated_file_path_data: { file_path_pub_id: string; error: string } } | { file_path_without_is_dir_field: number } +export type NonCriticalFileIdentifierError = { failed_to_extract_file_metadata: string } | { failed_to_extract_metadata_from_on_demand_file: string } | { failed_to_extract_isolated_file_path_data: { file_path_pub_id: string; error: string } } | { file_path_without_is_dir_field: number } export type NonCriticalIndexerError = { failed_directory_entry: string } | { metadata: string } | { indexer_rule: string } | { file_path_metadata: string } | { fetch_already_existing_file_path_ids: string } | { fetch_file_paths_to_remove: string } | { iso_file_path: string } | { dispatch_keep_walking: string } | { missing_file_path_data: string } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0f810152c8779fd32faaa6c9b61463757970970d..fedce2313f9fbadee6498c05630bebb743573d64 100644 GIT binary patch delta 1512 zcmZvbTWl0{6vor(0^82C-QE=l6iTh>7It=LcV<_EFuSv}vzOWK{T5%Qd!60xZT8wt z45HB{JQ(7NM~x)tBO$#<@P=&OYPKnb#pPtjlUb4?5{m}|{<@bweE2zdeifaO2R}tF zZre}_<8C3?Bx`NTOzCoNBF&lzaiJgvJuTX?#PV&^Qn@Zw8ABzMx9ZLA#pOkJ+3w61 zyor$8M&fb1jq!)vj!+=USe>3$B2taGo3QUZvJa;2ZR-QwPsjuWE+b=5{stY9uUtg% z9gzA2nGHwtt`0ASqJf2ShpBq3cq0&vH}l4DBUa;e29MriFZv0(p>?{V)mF1rwOCsz zt+Ly>-*nb_JlmGb&>7RtttEd`E zcYF4RGu&cPtSv<&g@oWtrCHLEXtOx2^Kv@EnwK2TcwS4V$#kd~vPqQPiU<5O9kGkm zTFgyW2%Re#s78WTe>s^>NaZ|%yYfWr;c<^Z0Kq1}*Mseldw)i_e#qSInGFlgMmkur)r(eyg}&O)svMRF-AXfmXUMw$0pJBge(Ks)Vt z-dYMT$D-b1Fz?`UHZ}?0G3vQ39CSnExC%&C|H~! zV?rA?b{K0WMOsB6$aJ*&9O+|Hwqn3;^(?d)UN8`%)E0SKh-cb%ea6>Fr}Ud^h|p>Q zJVtdN$fpJdS5Cy$3VjOw@`*S)nuTfqzyYYNq61JlGo+G_uc8msaA^*m1AGk~Uw`Ze zGp8Ab@MUCJR_^5%)>R$T!Kw9FE-!3^!@}K9mhsM^EEmI)5ZZayfcf9 zy?zZH*{DtZpmbCW8dS|bHKm8+^TVUvlLt`cG_WB!`if>6X0D^Na`Z>^+=wDVcGun; zmXH08-rTv~Y^&zA@6p|G^Slyi%_Y>hCEU-du<}kIdI#oV>MhMA94lzl(0>EnuY6z^ z_@2i4;DZa;c8Fa@24Ov!aBVL(2`8_hJX9W_!!UFeJp#Vp(8NYnR;u{;BmFpK!IWXv zW7ccJ?)UTD m>@i~s0}6u*L-HQ8=7MYdQPIKSmm`{&Hr6Wt@q*^VU;hR0!VQD~ delta 241 zcmZpB?K=0o>jq!d=F_U}r&SrZpH^ksIc56NKqmF=9=n+qux>wjkjapJJLf4TF7fSB zkC>dfwwt|W65-gc|BY!1Q~Ro)Aj}NJEI`Z(#BAGF{bcVND5CE(BUbV-r&Jg0#e4Z{kb-ef#~+>jyyjY f+x=a5fS4DE`GA;zyT6OT9dEE>wqKqgu=xo9T?Aup From bc8d6a1993722e606a8a73fac4f583fca27a72ca Mon Sep 17 00:00:00 2001 From: James Pine Date: Mon, 18 Nov 2024 00:15:21 -0800 Subject: [PATCH 12/34] feat: add FontAwesome icons and repo stats to landing page, enhance pricing plans UI with add-ons dialog --- pnpm-lock.yaml | Bin 1144198 -> 1146146 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fedce2313f9fbadee6498c05630bebb743573d64..54856320fb0bd45b72be466618cd888323a94fee 100644 GIT binary patch delta 3211 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 D9~TuH From 73632625cd630f8448abbe57d8e698fa8b931d7c Mon Sep 17 00:00:00 2001 From: Lynx <141365347+iLynxcat@users.noreply.github.com> Date: Mon, 18 Nov 2024 05:11:27 -0800 Subject: [PATCH 13/34] somebody forgot to update codegen --- packages/client/src/core.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/client/src/core.ts b/packages/client/src/core.ts index 9640f5d6e..ebe660c03 100644 --- a/packages/client/src/core.ts +++ b/packages/client/src/core.ts @@ -557,7 +557,7 @@ name: string; identity: RemoteIdentity; p2p: NodeConfigP2P; features: BackendFea export type NonCriticalError = { indexer: NonCriticalIndexerError } | { file_identifier: NonCriticalFileIdentifierError } | { media_processor: NonCriticalMediaProcessorError } -export type NonCriticalFileIdentifierError = { failed_to_extract_file_metadata: string } | { failed_to_extract_metadata_from_on_demand_file: string } | { failed_to_extract_isolated_file_path_data: { file_path_pub_id: string; error: string } } | { file_path_without_is_dir_field: number } +export type NonCriticalFileIdentifierError = { failed_to_extract_file_metadata: string } | { failed_to_extract_isolated_file_path_data: { file_path_pub_id: string; error: string } } | { file_path_without_is_dir_field: number } export type NonCriticalIndexerError = { failed_directory_entry: string } | { metadata: string } | { indexer_rule: string } | { file_path_metadata: string } | { fetch_already_existing_file_path_ids: string } | { fetch_file_paths_to_remove: string } | { iso_file_path: string } | { dispatch_keep_walking: string } | { missing_file_path_data: string } From b58e4415bc47fece6b27ac45c613d573bd9745ba Mon Sep 17 00:00:00 2001 From: James Pine Date: Mon, 25 Nov 2024 01:35:24 -0800 Subject: [PATCH 14/34] peer --- core/src/search/mod.rs | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 core/src/search/mod.rs diff --git a/core/src/search/mod.rs b/core/src/search/mod.rs new file mode 100644 index 000000000..61dca91e9 --- /dev/null +++ b/core/src/search/mod.rs @@ -0,0 +1,32 @@ +pub enum SpacedrivePath { + Location(u64, PathBuf), + Virtual(PathBuf), + NonIndexed(PathBuf), +} + +pub struct ExplorerItem { + pub id: u64, + pub pub_id: Bytes, + pub inode: Option, + // the unique Object for this item + pub object_id: Option, + // the path of this item in Spacedrive + pub path: SpacedrivePath, + + // metadata about this item + pub name: String, + pub extension: Option, + pub kind: ObjectKind, + pub size: Option, + pub date_created: Option>, + pub date_modified: Option>, + pub date_indexed: Option>, + pub is_dir: bool, + pub is_hidden: bool, + pub key_id: Option, + + // computed properties + pub thumbnail: Option, + pub has_created_thumbnail: bool, + pub duplicate_paths: Vec, +} From 8b3e9a1408b1768b96c880e47947bdf4ec8f7972 Mon Sep 17 00:00:00 2001 From: Ericson Soares Date: Tue, 10 Dec 2024 12:22:46 -0300 Subject: [PATCH 15/34] Update deps --- Cargo.lock | Bin 337573 -> 341075 bytes Cargo.toml | 2 +- core/crates/cloud-services/Cargo.toml | 6 +++--- core/crates/cloud-services/src/client.rs | 2 +- core/crates/cloud-services/src/p2p/mod.rs | 5 ++--- .../src/p2p/new_sync_messages_notifier.rs | 2 +- core/crates/cloud-services/src/p2p/runner.rs | 2 +- packages/client/src/core.ts | 18 ++++++++++-------- 8 files changed, 19 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f10466234c1281c4d8a592a3f8cff4e8568cdef1..1a71c55c898bc3182bd4831a1935dd115b976e36 100644 GIT binary patch delta 3671 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 Wyxws^, result: null } | { key: "tags.update", input: LibraryArgs, result: null } | { key: "toggleFeatureFlag", input: BackendFeature, result: null } | - { key: "volumes.track", input: LibraryArgs, result: null } | + { key: "volumes.track", input: LibraryArgs, result: null } | { key: "volumes.unmount", input: LibraryArgs, result: null }, subscriptions: { key: "cloud.listenCloudServicesNotifications", input: never, result: CloudP2PNotifyUser } | @@ -790,8 +790,6 @@ export type TextMatch = { contains: string } | { startsWith: string } | { endsWi */ export type ThumbKey = { shard_hex: string; cas_id: CasId; base_directory_str: string } -export type TrackVolumeInput = { volume_id: VolumeFingerprint } - export type UpdateThumbnailerPreferences = Record export type VideoProps = { pixel_format: string | null; color_range: string | null; bits_per_channel: number | null; color_space: string | null; color_primaries: string | null; color_transfer: string | null; field_order: string | null; chroma_location: string | null; width: number; height: number; aspect_ratio_num: number | null; aspect_ratio_den: number | null; properties: string[] } @@ -800,6 +798,11 @@ export type VideoProps = { pixel_format: string | null; color_range: string | nu * Represents a physical or virtual storage volume in the system */ export type Volume = { +/** + * Fingerprint of the volume as a hash of its properties, not persisted to the database + * Used as the unique identifier for a volume in this module + */ +fingerprint: VolumeFingerprint | null; /** * Database ID (None if not yet committed to database) */ @@ -863,11 +866,7 @@ total_bytes_capacity: string; /** * Available storage space in bytes */ -total_bytes_available: string; -/** - * Fingerprint of the volume, not persisted to the database - */ -fingerprint: string } +total_bytes_available: string } /** * Events emitted by the Volume Manager when volume state changes @@ -898,4 +897,7 @@ export type VolumeEvent = */ { VolumeError: { fingerprint: VolumeFingerprint; error: string } } +/** + * A fingerprint of a volume, used to identify it when it is not persisted in the database + */ export type VolumeFingerprint = number[] From dd56820898001c0260e556abaf30d4cf7a985f10 Mon Sep 17 00:00:00 2001 From: Ericson Soares Date: Tue, 10 Dec 2024 12:37:14 -0300 Subject: [PATCH 16/34] Rustfmt --- crates/ffmpeg/src/filter_graph.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/crates/ffmpeg/src/filter_graph.rs b/crates/ffmpeg/src/filter_graph.rs index f0ec54264..e9547f9ab 100644 --- a/crates/ffmpeg/src/filter_graph.rs +++ b/crates/ffmpeg/src/filter_graph.rs @@ -232,16 +232,16 @@ fn thumb_scale_filter_args( // if the pixel aspect ratio is defined and is not 1, we have an anamorphic stream if pixel_aspect_ratio.num != 0 && pixel_aspect_ratio.num != pixel_aspect_ratio.den { match std::panic::catch_unwind(|| { - width - .checked_mul(pixel_aspect_ratio.num.unsigned_abs()) - .and_then(|v| v.checked_div(pixel_aspect_ratio.den.unsigned_abs())) - }) { - Ok(Some(w)) => width = w, - Ok(None) | Err(_) => { - eprintln!("Warning: Failed to calculate width with pixel aspect ratio"); - // Keep the original width as fallback - } - }; + width + .checked_mul(pixel_aspect_ratio.num.unsigned_abs()) + .and_then(|v| v.checked_div(pixel_aspect_ratio.den.unsigned_abs())) + }) { + Ok(Some(w)) => width = w, + Ok(None) | Err(_) => { + eprintln!("Warning: Failed to calculate width with pixel aspect ratio"); + // Keep the original width as fallback + } + }; if size != 0 { if height > width { width = (width * size) / height; From 77fcb7dbff58ecf8ad056e5499584563e37b514e Mon Sep 17 00:00:00 2001 From: Ericson Soares Date: Tue, 10 Dec 2024 13:03:29 -0300 Subject: [PATCH 17/34] pnpm typecheck fix --- .../Layout/Sidebar/sections/Local/index.tsx | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/interface/app/$libraryId/Layout/Sidebar/sections/Local/index.tsx b/interface/app/$libraryId/Layout/Sidebar/sections/Local/index.tsx index 31225bc70..3e0d5e34b 100644 --- a/interface/app/$libraryId/Layout/Sidebar/sections/Local/index.tsx +++ b/interface/app/$libraryId/Layout/Sidebar/sections/Local/index.tsx @@ -1,14 +1,8 @@ -import { ArrowRight, EjectSimple } from '@phosphor-icons/react'; +import { EjectSimple } from '@phosphor-icons/react'; import { useQueryClient } from '@tanstack/react-query'; import clsx from 'clsx'; import { MouseEvent, PropsWithChildren, useMemo } from 'react'; -import { - useBridgeQuery, - useLibraryMutation, - useLibraryQuery, - useLibrarySubscription, - Volume -} from '@sd/client'; +import { useLibraryMutation, useLibraryQuery, useLibrarySubscription, Volume } from '@sd/client'; import { Button, toast, tw } from '@sd/ui'; import { Icon, IconName } from '~/components'; import { useLocale } from '~/hooks'; @@ -156,9 +150,7 @@ export default function LocalSection() { onTrack={async () => { if (!isTracked && volume.pub_id) { try { - await trackVolumeMutation.mutateAsync({ - volume_id: Array.from(volume.pub_id) // Convert Uint8Array to number[] - }); + await trackVolumeMutation.mutateAsync(volume.pub_id); toast.success('Volume tracked successfully'); } catch (error) { toast.error('Failed to track volume'); From 058e54e2264a8d65cf6d97737e348e09d610aef9 Mon Sep 17 00:00:00 2001 From: Ericson Soares Date: Tue, 10 Dec 2024 13:15:10 -0300 Subject: [PATCH 18/34] pnpm autoformat fix --- interface/app/$libraryId/settings/client/general.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interface/app/$libraryId/settings/client/general.tsx b/interface/app/$libraryId/settings/client/general.tsx index cfe278458..32872cd97 100644 --- a/interface/app/$libraryId/settings/client/general.tsx +++ b/interface/app/$libraryId/settings/client/general.tsx @@ -191,7 +191,7 @@ export const Component = () => {

From 6f50f438e192b53a50cd0c5182acfe73ce80e892 Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Thu, 12 Dec 2024 15:55:57 -0800 Subject: [PATCH 19/34] yes --- pnpm-lock.yaml | Bin 1157841 -> 1159850 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 692ad3bdff057b6a57f380ee0d2df7bdb6e127ed..0b19456cae2326158cb1a0ac02a43dc8eaeaccc3 100644 GIT binary patch delta 2273 zcmah~dr(wm6ra8Og1ygAVPRNaw)g-H_o1+gqrk$lEGo-_vNDqj#$$P|AR_CFGF_8$ zs5^O>%myn%ObtT)f|@9LkTd1vRHIWiI+fKZ1!|eSyZ5fFQijjXpTYBpm4(V0` z@nI2eDS-Ax4)`Oc63~5>N{5S=DLrIeq4N2;N28$gIvK(nEbM?+N6GF6;JrE)_S~RC z`TLJ&^WAmSz)&pb3N-htcV@_)&AsCD#=z8FD4stEe*WX$#DSWweW6m{1F6p)QAi#? z*b~Oj>?xaEY?^eY@*59z++O7Nwn_`(#M`g;;_JOK3S?>gfits~SCwb`%*Cy)raWh{ z*<)CnUD;Ao-CW(^E|~8#nDdH_%ky1%D?D>n&26Z&H@oUf94%F~73K3y&6$Sk z(g$nZj_MX)G8j8lQWci((S|89Qd0s)YH+yHQY+Rueb$DhZPn$T+{#q9p=@nkwx_AI z$y4lft+mw~SK2nTw0PSb1-Y$t%WMXdsnxtPYh6onTa(>Y-H`9{`fToGORFP)ohPrR zq_h^w+)Ak!;}^z5Fu7m~!IXlj1XBwZB3P(k8k}w=N9=GegP5rnrfcSc_)TyogNTEn zOoEi9L4T+w4koNbBur!wlt#!4&8HYO8h)>Kx^$9%K|&@WkZ2_mWwYQwCXpb{CzKJ? zY>`SY7bt-JfHQz8n~25Q$H*6>#9L2;J=Zu2yf0BIi0UD`#F$PHf)b8o(JB7$LnIDA zM43aOeUT;xoO2KjACYnf+Na7j_*6etD+9|o%fR7rb_%>&L?^$B-u%Vwi!UfdvY%?-&br{6~T$e*9S#J(~z-z`GMA;MFzsx9wtV z0qoyRtKpd|wDq45zh#WZmNCYxg02bXe)wx6tAp z#^l1qFBpY*zf*9L^x06a(w4IEG@EeR-WKcrqIgR=D78tp~{0{Hm zU`%&bQ%a<8@PZQIQ$I6=3=UsVQXsg~?gXnNDjOTtSC*}@J;Voou@ zTdT+j{N7FGJOM|4{r@_6|8Go>68d78`{3d*8-<5utVi~rcQP(#&4GkR!CE8_epZTD<85_njOsd5c4X(jda>9mt4> zeaI%0Mh)g|MmfPu6ui3`y&g;;7}6?{1_OvxUJ5Dk$hc3nd(WtpjP}{s!d^a7N|FoHR0Sj>Mec}971B31AcAa;v+C4a= z^LgqAHw=^skcP3v!rbZYEarZzd#Ss+Z$)dRxz=FR z`n`@Gzhz^0K-Z+wdA!xehN`-k?CO9)Gia@?aySAt722M<8m+0u*Q=>A)HL+8u5W2} zIV`R2K)-i+qtl?ZaOZCQXE(m9vqk#qvgr*qjaEaAr?II`wa&J|yRNdi#i^+`nFC() zs{UGw-R@}jH1)SRb<3KyE$-GHLuq}zt;1Jub5?rGI~t5b>wAnYXNAjTZr2z(>#KZS z9jY!TygMl_8Rn11n}|0tZ)vi7gHW9m>(;~l=p%QlW0?_0VakQ ze*O;>5vAyAfmp2&Vft@$DIV$)I4jPS!hSLH06HHc>l4Z6?lonE1e63{dx*q(Qjvi# zayk6^4K+XHrpn;W1ynk0enUh-nUh)w>K2NEd!5t*Xwy)+xO6dfxrCsk;DU$Jac&ni zM@V2OVQQ3p4hOrbpMwO7BHpA*IDCt;5E#=U!>Zd9R^6s6BzR+h5=lUoyWH;4?91 zt`sZ&Vg#Q*?rZ+hNr$2XOdi}lER$mDg8ck+-lL~b4%|2`&w{;Ye9oHn=pxsyQw^o4|^v z;3oo>P&h^ar!#LIWu#CzFS8^x#*&`U=~jm|6Lc2b9H3^vjWM>Eqzd`t>~UxxXAnHt zB4c2`Ue3bxx7b-QG0x^E^V$(I(Fov?Tn=Z(S; Date: Sat, 14 Dec 2024 19:49:50 -0500 Subject: [PATCH 20/34] Move Auth Tokens storage from Local Storage to custom encrypted `.sdks` file (#2831) * Initial encryption functions * Storage of encrypted tokens & working login * Add customized `tauri-plugin-cors-fetch` We need to have it in our crates folder due to the dependency of `sd-crypto` for the handling of cookies. * Lint --- Cargo.lock | Bin 341075 -> 341015 bytes .../crates/macos/src-swift/window.swift | 74 +-- apps/desktop/package.json | 2 +- apps/desktop/src-tauri/Cargo.toml | 2 +- apps/desktop/src/App.tsx | 10 +- core/src/api/keys.rs | 186 ++++++++ core/src/api/mod.rs | 2 + core/src/node/config.rs | 45 +- core/src/volume/volumes.rs | 2 +- crates/crypto/Cargo.toml | 9 +- crates/crypto/src/cookie.rs | 306 ++++++++++++ crates/crypto/src/lib.rs | 2 + crates/tauri-plugin-cors-fetch/CHANGELOG.md | 11 + crates/tauri-plugin-cors-fetch/Cargo.toml | 33 ++ crates/tauri-plugin-cors-fetch/LICENSE | 21 + crates/tauri-plugin-cors-fetch/README.md | 88 ++++ crates/tauri-plugin-cors-fetch/api-iife.js | 118 +++++ crates/tauri-plugin-cors-fetch/banner.png | Bin 0 -> 23635 bytes crates/tauri-plugin-cors-fetch/build.rs | 7 + .../commands/cancel_cors_request.toml | 13 + .../autogenerated/commands/cors_request.toml | 13 + .../permissions/autogenerated/reference.md | 68 +++ .../permissions/default.toml | 4 + .../permissions/schemas/schema.json | 325 +++++++++++++ .../tauri-plugin-cors-fetch/src/commands.rs | 434 ++++++++++++++++++ crates/tauri-plugin-cors-fetch/src/error.rs | 49 ++ crates/tauri-plugin-cors-fetch/src/lib.rs | 41 ++ .../settings/client/account/Profile.tsx | 11 +- .../client/account/handlers/cookieHandler.ts | 92 +++- .../client/account/handlers/windowHandler.ts | 7 +- .../settings/client/account/index.tsx | 9 +- interface/components/Login.tsx | 3 +- interface/util/index.tsx | 20 +- packages/client/src/core.ts | 2 + pnpm-lock.yaml | Bin 1159850 -> 1159852 bytes 35 files changed, 1923 insertions(+), 86 deletions(-) create mode 100644 core/src/api/keys.rs create mode 100644 crates/crypto/src/cookie.rs create mode 100644 crates/tauri-plugin-cors-fetch/CHANGELOG.md create mode 100644 crates/tauri-plugin-cors-fetch/Cargo.toml create mode 100644 crates/tauri-plugin-cors-fetch/LICENSE create mode 100644 crates/tauri-plugin-cors-fetch/README.md create mode 100644 crates/tauri-plugin-cors-fetch/api-iife.js create mode 100644 crates/tauri-plugin-cors-fetch/banner.png create mode 100644 crates/tauri-plugin-cors-fetch/build.rs create mode 100644 crates/tauri-plugin-cors-fetch/permissions/autogenerated/commands/cancel_cors_request.toml create mode 100644 crates/tauri-plugin-cors-fetch/permissions/autogenerated/commands/cors_request.toml create mode 100644 crates/tauri-plugin-cors-fetch/permissions/autogenerated/reference.md create mode 100644 crates/tauri-plugin-cors-fetch/permissions/default.toml create mode 100644 crates/tauri-plugin-cors-fetch/permissions/schemas/schema.json create mode 100644 crates/tauri-plugin-cors-fetch/src/commands.rs create mode 100644 crates/tauri-plugin-cors-fetch/src/error.rs create mode 100644 crates/tauri-plugin-cors-fetch/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 1a71c55c898bc3182bd4831a1935dd115b976e36..1904d0aeb90c58db344606c9e996f93f79f9b6f2 100644 GIT binary patch delta 116 zcmV-)0E_?A=oFXe6o7;QgaWh!HK3P@mIEG_BH#lOms6kv`j>AT0tuIXhyw})bY*jN zw@0A^K8y-2b7U=Jb#ruYZI>}s0T!1kE(9_NWG!QId2n=Zm$5GdI+u_x1Rj@grUML@ WzkLB6mtZgi4u{z;1h?5P1$qU?jwsv! delta 139 zcmV;60CfMC=oHiF6o7;QgaWh!HK4agpaU2Iw|t=kIE Bool { - // Check if App Nap is already disabled + activityLock.lock() + defer { activityLock.unlock() } + guard activity == nil else { return false } @@ -26,37 +30,55 @@ public func disableAppNap(reason: SRString) -> Bool { @_cdecl("enable_app_nap") public func enableAppNap() -> Bool { - // Check if App Nap is already enabled - guard let pinfo = activity else { + activityLock.lock() + defer { activityLock.unlock() } + + guard let currentActivity = activity else { return false } - ProcessInfo.processInfo.endActivity(pinfo) + ProcessInfo.processInfo.endActivity(currentActivity) activity = nil return true } @_cdecl("lock_app_theme") public func lockAppTheme(themeType: AppThemeType) { - var theme: NSAppearance? - switch themeType { - case .auto: - theme = nil - case .dark: - theme = NSAppearance(named: .darkAqua)! - case .light: - theme = NSAppearance(named: .aqua)! - } - - DispatchQueue.main.async { - NSApp.appearance = theme - - // Trigger a repaint of the window - if let window = NSApplication.shared.mainWindow { - window.invalidateShadow() - window.displayIfNeeded() + // Prevent concurrent theme updates + guard !isThemeUpdating else { + return + } + + isThemeUpdating = true + + let theme: NSAppearance? + switch themeType { + case .auto: + theme = nil + case .dark: + theme = NSAppearance(named: .darkAqua) + case .light: + theme = NSAppearance(named: .aqua) + } + + // Use sync to ensure completion before return + DispatchQueue.main.sync { + autoreleasepool { + NSApp.appearance = theme + + if let window = NSApplication.shared.mainWindow { + NSAnimationContext.runAnimationGroup({ context in + context.duration = 0 + window.invalidateShadow() + window.displayIfNeeded() + }, completionHandler: { + isThemeUpdating = false + }) + } else { + isThemeUpdating = false + } + } } - } } @_cdecl("set_titlebar_style") diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 31b5cdee0..7dc516d43 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -31,7 +31,7 @@ "react-dom": "^18.2.0", "react-router-dom": "=6.20.1", "sonner": "^1.0.3", - "supertokens-web-js": "^0.13.0" + "supertokens-web-js": "=0.13.0" }, "devDependencies": { "@sd/config": "workspace:*", diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 0f2a3acb2..a01f07dee 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -38,6 +38,7 @@ uuid = { workspace = true, features = ["serde"] } opener = { version = "0.7.1", features = ["reveal"], default-features = false } specta-typescript = "=0.0.7" tauri-plugin-clipboard-manager = "=2.0.1" +tauri-plugin-cors-fetch = { path = "../../../crates/tauri-plugin-cors-fetch" } tauri-plugin-deep-link = "=2.0.1" tauri-plugin-dialog = "=2.0.3" tauri-plugin-http = "=2.0.3" @@ -47,7 +48,6 @@ tauri-plugin-updater = "=2.0.2" # memory allocator mimalloc = { workspace = true } -tauri-plugin-cors-fetch = "2.1.1" [dependencies.tauri] features = ["linux-libxdo", "macos-private-api", "native-tls-vendored", "unstable"] diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index c3d9e12af..5ad32744b 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -126,13 +126,15 @@ type RedirectPath = { pathname: string; search: string | undefined }; function AppInner() { const [tabs, setTabs] = useState(() => [createTab()]); const [selectedTabIndex, setSelectedTabIndex] = useState(0); - const tokens = getTokens(); const cloudBootstrap = useBridgeMutation('cloud.bootstrap'); useEffect(() => { - // If the access token and/or refresh token are missing, we need to skip the cloud bootstrap - if (tokens.accessToken.length === 0 || tokens.refreshToken.length === 0) return; - cloudBootstrap.mutate([tokens.accessToken, tokens.refreshToken]); + (async () => { + const tokens = await getTokens(); + // If the access token and/or refresh token are missing, we need to skip the cloud bootstrap + if (tokens.accessToken.length === 0 || tokens.refreshToken.length === 0) return; + cloudBootstrap.mutate([tokens.accessToken, tokens.refreshToken]); + })(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/core/src/api/keys.rs b/core/src/api/keys.rs new file mode 100644 index 000000000..82c671308 --- /dev/null +++ b/core/src/api/keys.rs @@ -0,0 +1,186 @@ +use super::{Ctx, SanitizedNodeConfig, R}; +use rspc::{alpha::AlphaRouter, ErrorCode}; +use sd_crypto::cookie::CookieCipher; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use tokio::io::AsyncWriteExt; +use tokio::sync::RwLock; +use tracing::{debug, error}; + +#[derive(Clone)] +struct CipherCache { + uuid: String, + cipher: CookieCipher, +} + +async fn get_cipher( + node: &Ctx, + cache: Arc>>, +) -> Result { + let config = SanitizedNodeConfig::from(node.config.get().await); + let uuid = config.id.to_string(); + + { + let cache_read = cache.read().await; + if let Some(ref cache) = *cache_read { + if cache.uuid == uuid { + return Ok(cache.cipher.clone()); + } + } + } + + let uuid_key = CookieCipher::generate_key_from_string(&uuid).map_err(|e| { + error!("Failed to generate key: {:?}", e.to_string()); + rspc::Error::new( + ErrorCode::InternalServerError, + "Failed to generate key".to_string(), + ) + })?; + + let cipher = CookieCipher::new(&uuid_key).map_err(|e| { + error!("Failed to create cipher: {:?}", e.to_string()); + rspc::Error::new( + ErrorCode::InternalServerError, + "Failed to create cipher".to_string(), + ) + })?; + + { + let mut cache_write = cache.write().await; + *cache_write = Some(CipherCache { + uuid, + cipher: cipher.clone(), + }); + } + + Ok(cipher) +} + +async fn read_file(path: &Path) -> Result, rspc::Error> { + tokio::fs::read(path).await.map_err(|e| { + error!("Failed to read file: {:?}", e.to_string()); + rspc::Error::new( + ErrorCode::InternalServerError, + format!("Failed to read file {:?}", path), + ) + }) +} + +async fn write_file(path: &Path, data: &[u8]) -> Result<(), rspc::Error> { + let mut file = tokio::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(path) + .await + .map_err(|e| { + error!("Failed to open file: {:?}", e.to_string()); + rspc::Error::new( + ErrorCode::InternalServerError, + format!("Failed to open file {:?}", path), + ) + })?; + file.write_all(data).await.map_err(|e| { + error!("Failed to write to file: {:?}", e.to_string()); + rspc::Error::new( + ErrorCode::InternalServerError, + format!("Failed to write to file {:?}", path), + ) + }) +} + +fn sanitize_path(base_dir: &Path, path: &Path) -> Result { + let abs_base = base_dir.canonicalize().map_err(|e| { + error!("Failed to canonicalize base directory: {:?}", e.to_string()); + rspc::Error::new( + ErrorCode::InternalServerError, + "Failed to canonicalize base directory".to_string(), + ) + })?; + let abs_path = abs_base.join(path).canonicalize().map_err(|e| { + error!("Failed to canonicalize path: {:?}", e.to_string()); + rspc::Error::new( + ErrorCode::InternalServerError, + "Failed to canonicalize path".to_string(), + ) + })?; + if abs_path.starts_with(&abs_base) { + Ok(abs_path) + } else { + error!("Path injection attempt detected: {:?}", abs_path); + Err(rspc::Error::new( + ErrorCode::InternalServerError, + "Invalid path".to_string(), + )) + } +} + +pub(crate) fn mount() -> AlphaRouter { + let cipher_cache = Arc::new(RwLock::new(None)); + + R.router() + .procedure("get", { + let cipher_cache = cipher_cache.clone(); + R.query(move |node, _: ()| { + let cipher_cache = cipher_cache.clone(); + async move { + let base_dir = node.config.data_directory(); + let path = sanitize_path(&base_dir, Path::new(".sdks"))?; + let data = read_file(&path).await?; + let cipher = get_cipher(&node, cipher_cache).await?; + + let data_str = String::from_utf8(data).map_err(|e| { + error!("Failed to convert data to string: {:?}", e.to_string()); + rspc::Error::new( + ErrorCode::InternalServerError, + "Failed to convert data to string".to_string(), + ) + })?; + let data = CookieCipher::base64_decode(&data_str).map_err(|e| { + error!("Failed to decode data: {:?}", e.to_string()); + rspc::Error::new( + ErrorCode::InternalServerError, + "Failed to decode data".to_string(), + ) + })?; + let de_data = cipher.decrypt(&data).map_err(|e| { + error!("Failed to decrypt data: {:?}", e.to_string()); + rspc::Error::new( + ErrorCode::InternalServerError, + "Failed to decrypt data".to_string(), + ) + })?; + let de_data = String::from_utf8(de_data).map_err(|e| { + error!("Failed to convert data to string: {:?}", e.to_string()); + rspc::Error::new( + ErrorCode::InternalServerError, + "Failed to convert data to string".to_string(), + ) + })?; + Ok(de_data) + } + }) + }) + .procedure("save", { + let cipher_cache = cipher_cache.clone(); + R.mutation(move |node, args: String| { + let cipher_cache = cipher_cache.clone(); + async move { + let cipher = get_cipher(&node, cipher_cache).await?; + let en_data = cipher.encrypt(args.as_bytes()).map_err(|e| { + error!("Failed to encrypt data: {:?}", e.to_string()); + rspc::Error::new( + ErrorCode::InternalServerError, + "Failed to encrypt data".to_string(), + ) + })?; + let en_data = CookieCipher::base64_encode(&en_data); + let base_dir = node.config.data_directory(); + let path = sanitize_path(&base_dir, Path::new(".sdks"))?; + write_file(&path, en_data.as_bytes()).await?; + debug!("Saved data to {:?}", path); + Ok(()) + } + }) + }) +} diff --git a/core/src/api/mod.rs b/core/src/api/mod.rs index 7a1dd1597..bd87916f5 100644 --- a/core/src/api/mod.rs +++ b/core/src/api/mod.rs @@ -29,6 +29,7 @@ mod cloud; mod ephemeral_files; mod files; mod jobs; +mod keys; mod labels; mod libraries; pub mod locations; @@ -210,6 +211,7 @@ pub(crate) fn mount() -> Arc { .merge("preferences.", preferences::mount()) .merge("notifications.", notifications::mount()) .merge("backups.", backups::mount()) + .merge("keys.", keys::mount()) .merge("invalidation.", utils::mount_invalidate()) .sd_patch_types_dangerously(|type_map| { let def = diff --git a/core/src/node/config.rs b/core/src/node/config.rs index 28ddb7a4a..dff363dad 100644 --- a/core/src/node/config.rs +++ b/core/src/node/config.rs @@ -401,13 +401,46 @@ impl NodeConfig { config.remove("sd_api_origin"); config.remove("image_labeler_version"); - config.remove("id"); - config.insert( - String::from("id"), - serde_json::to_value(DevicePubId::from(Uuid::now_v7())) - .map_err(VersionManagerError::SerdeJson)?, - ); + // Verify that the ID isn't already set to a UUID v7. If it is, we don't want to overwrite it. + // Get the current ID, if it's a string, parse it as a UUID and check if it's a UUID v7. + // If it's not a UUID v7, set it to a UUID v7. + let id = config + .get("id") + .and_then(|v| v.as_str()) + .and_then(|s| Uuid::parse_str(s).ok()); + if let Some(id) = id { + if id.get_version() != Some(uuid::Version::Md5) { + config.remove("id"); + config.insert( + String::from("id"), + serde_json::to_value(DevicePubId::from(Uuid::now_v7())) + .map_err(VersionManagerError::SerdeJson)?, + ); + } + } + // config.remove("id"); + // config.insert( + // String::from("id"), + // serde_json::to_value(DevicePubId::from(Uuid::now_v7())) + // .map_err(VersionManagerError::SerdeJson)?, + // ); + // Create a .sdks file in the data directory if it doesn't exist + let data_directory = path + .parent() + .expect("Config path must have a parent directory"); + let sdks_file = data_directory.join(".sdks"); + if !sdks_file.exists() { + fs::write(&sdks_file, b"").await.map_err(|e| { + FileIOError::from(( + sdks_file.clone(), + e, + "Failed to create .sdks file", + )) + })?; + } + + // Write the updated config back to disk fs::write( path, serde_json::to_vec(&config).map_err(VersionManagerError::SerdeJson)?, diff --git a/core/src/volume/volumes.rs b/core/src/volume/volumes.rs index 53a831225..20d9438b8 100644 --- a/core/src/volume/volumes.rs +++ b/core/src/volume/volumes.rs @@ -122,7 +122,7 @@ impl Volumes { .await .map_err(|_| VolumeError::Cancelled)?; - rx.await.map_err(|_| VolumeError::Cancelled)?; + let _ = rx.await.map_err(|_| VolumeError::Cancelled)?; Ok(()) } diff --git a/crates/crypto/Cargo.toml b/crates/crypto/Cargo.toml index 376769023..6f989a25b 100644 --- a/crates/crypto/Cargo.toml +++ b/crates/crypto/Cargo.toml @@ -2,7 +2,11 @@ name = "sd-crypto" version = "0.0.1" -authors = ["Ericson Soares ", "Jake Robinson "] +authors = [ + "Arnab Chakraborty ", + "Ericson Soares ", + "Jake Robinson " +] description = """ A cryptographic library that provides safe and high-level encryption, hashing, and encoding interfaces. @@ -18,12 +22,15 @@ rust-version.workspace = true [dependencies] # Workspace dependencies async-stream = { workspace = true } +base64 = { workspace = true } blake3 = { workspace = true } futures = { workspace = true } rand = { workspace = true } serde = { workspace = true, features = ["derive"] } thiserror = { workspace = true } tokio = { workspace = true, features = ["io-util", "macros", "rt-multi-thread", "sync"] } +tracing = { workspace = true } +tracing-test = { workspace = true } zeroize = { workspace = true, features = ["derive"] } # External dependencies diff --git a/crates/crypto/src/cookie.rs b/crates/crypto/src/cookie.rs new file mode 100644 index 000000000..dafd0df2b --- /dev/null +++ b/crates/crypto/src/cookie.rs @@ -0,0 +1,306 @@ +//! Encryption and decryption functionality for cookie strings using the `ChaCha20Poly1305` AEAD cipher. +//! +//! This module provides a secure way to encrypt and decrypt cookie data using the +//! `ChaCha20Poly1305` authenticated encryption algorithm. It includes functionality for: +//! - Key generation from UUIDs +//! - Encryption with random nonces +//! - Decryption with authentication +//! - Base64 encoding/decoding utilities + +use base64::Engine; +use blake3; +use chacha20poly1305::{ + aead::{Aead, AeadCore, KeyInit}, + ChaCha20Poly1305, Key, +}; +use std::convert::TryFrom; +use tracing::{debug, error}; + +/// Main struct for handling encryption and decryption operations. +/// Contains an initialized `ChaCha20Poly1305` cipher instance. +#[derive(Clone)] +pub struct CookieCipher { + cipher: ChaCha20Poly1305, +} + +/// Possible errors that can occur during cryptographic operations. +#[derive(Debug, thiserror::Error)] +pub enum CryptoCookieError { + /// Errors that occur during encryption operations + #[error("Encryption failed: {0}")] + Encryption(String), + /// Errors that occur during decryption operations + #[error("Decryption failed: {0}")] + Decryption(String), + /// Errors that occur during key creation/initialization + #[error("Key creation failed: {0}")] + KeyCreation(String), +} + +impl CookieCipher { + /// Creates a new `CookieCipher` instance with the provided 32-byte key. + /// + /// # Arguments + /// * `key` - A 32-byte array used as the encryption/decryption key + /// + /// # Returns + /// * `Result` - A new `CookieCipher` instance or an error + pub fn new(key: &[u8; 32]) -> Result { + debug!("Initializing CookieCipher with provided key"); + let key = Key::try_from(key.as_slice()).map_err(|e| { + error!("Failed to create key: {}", e); + CryptoCookieError::KeyCreation(e.to_string()) + })?; + + let cipher = ChaCha20Poly1305::new(&key); + debug!("CookieCipher initialized successfully"); + Ok(Self { cipher }) + } + + /// Encrypts the provided data using `ChaCha20Poly1305`. + /// + /// # Arguments + /// * `data` - The data to encrypt + /// + /// # Returns + /// * `Result, CryptoCookieError>` - The encrypted data with prepended nonce, or an error + pub fn encrypt(&self, data: &[u8]) -> Result, CryptoCookieError> { + debug!("Starting encryption of {} bytes", data.len()); + + let nonce = ChaCha20Poly1305::generate_nonce_with_rng(&mut aead::OsRng).map_err(|e| { + error!("Nonce generation failed: {}", e); + CryptoCookieError::Encryption(e.to_string()) + })?; + debug!("Generated new nonce for encryption"); + + let ciphertext = self.cipher.encrypt(&nonce, data).map_err(|e| { + error!("Encryption failed: {}", e); + CryptoCookieError::Encryption(e.to_string()) + })?; + + let mut combined = nonce.to_vec(); + combined.extend(ciphertext); + + debug!("Successfully encrypted data to {} bytes", combined.len()); + Ok(combined) + } + + /// Validates that the encrypted data meets the minimum length requirement. + /// + /// # Arguments + /// * `data` - The encrypted data to validate + /// + /// # Returns + /// * `Result<(), CryptoCookieError>` - Ok if valid, Error if too short + fn validate_data_length(data: &[u8]) -> Result<(), CryptoCookieError> { + if data.len() < 12 { + error!("Encrypted data too short: {} bytes", data.len()); + return Err(CryptoCookieError::Decryption("Data too short".into())); + } + Ok(()) + } + + /// Extracts and validates the nonce from the encrypted data. + /// + /// # Arguments + /// * `nonce_bytes` - The bytes containing the nonce + /// + /// # Returns + /// * `Result` - The extracted nonce or an error + fn extract_nonce(nonce_bytes: &[u8]) -> Result { + chacha20poly1305::Nonce::try_from(nonce_bytes).map_err(|e| { + error!("Failed to create nonce: {}", e); + CryptoCookieError::Decryption(e.to_string()) + }) + } + + /// Performs the actual decryption operation. + /// + /// # Arguments + /// * `nonce` - The nonce to use for decryption + /// * `ciphertext` - The encrypted data to decrypt + /// + /// # Returns + /// * `Result, CryptoCookieError>` - The decrypted data or an error + fn perform_decryption( + &self, + nonce: &chacha20poly1305::Nonce, + ciphertext: &[u8], + ) -> Result, CryptoCookieError> { + self.cipher.decrypt(nonce, ciphertext).map_err(|e| { + error!("Decryption failed: {}", e); + CryptoCookieError::Decryption(e.to_string()) + }) + } + + /// Decrypts the provided encrypted data. + /// + /// # Arguments + /// * `encrypted_data` - The data to decrypt (including nonce) + /// + /// # Returns + /// * `Result, CryptoCookieError>` - The decrypted data or an error + pub fn decrypt(&self, encrypted_data: &[u8]) -> Result, CryptoCookieError> { + debug!("Starting decryption of {} bytes", encrypted_data.len()); + + Self::validate_data_length(encrypted_data)?; + + let (nonce_bytes, ciphertext) = encrypted_data.split_at(12); + let nonce = Self::extract_nonce(nonce_bytes)?; + + debug!("Extracted nonce and ciphertext for decryption"); + + let plaintext = self.perform_decryption(&nonce, ciphertext)?; + + debug!("Successfully decrypted data to {} bytes", plaintext.len()); + Ok(plaintext) + } + + /// Generates a 32-byte key from a string input using BLAKE3 hashing. + /// + /// # Arguments + /// * `string` - The input string (typically a UUID) to generate the key from + /// + /// # Returns + /// * `Result<[u8; 32], CryptoCookieError>` - A 32-byte key or an error + pub fn generate_key_from_string(string: &str) -> Result<[u8; 32], CryptoCookieError> { + debug!("Generating key from string: {}", string); + + if string.is_empty() { + error!("Input string is empty"); + return Err(CryptoCookieError::KeyCreation( + "Input string is empty".into(), + )); + } + + // Hash the input string to get a fixed-size output + let hash = blake3::hash(string.as_bytes()); + + // Convert the hash bytes directly to an array + let key_array: [u8; 32] = *hash.as_bytes(); + + debug!("Key generated successfully"); + Ok(key_array) + } + + /// Encodes binary data to base64 string. + /// + /// # Arguments + /// * `data` - The binary data to encode + /// + /// # Returns + /// * `String` - The base64 encoded string + #[must_use] + pub fn base64_encode(data: &[u8]) -> String { + base64::engine::general_purpose::STANDARD.encode(data) + } + + /// Decodes base64 string to binary data. + /// + /// # Arguments + /// * `data` - The base64 string to decode + /// + /// # Returns + /// * `Result, base64::DecodeError>` - The decoded binary data or an error + pub fn base64_decode(data: &str) -> Result, base64::DecodeError> { + base64::engine::general_purpose::STANDARD.decode(data) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_key_generation() { + let key = CookieCipher::generate_key_from_string("0193b34e-0ad9-70e0-a3dd-8ec30b73a90a") + .expect("Failed to generate key"); + + assert_eq!(key.len(), 32); + } + + #[test] + fn test_encryption_decryption() { + let key = CookieCipher::generate_key_from_string("0193b34e-0ad9-70e0-a3dd-8ec30b73a90a") + .expect("Failed to generate key"); + let cipher = CookieCipher::new(&key).expect("Failed to create cipher"); + + let data = b"Hello, world!"; + let encrypted = cipher.encrypt(data).expect("Failed to encrypt data"); + let decrypted = cipher.decrypt(&encrypted).expect("Failed to decrypt data"); + + assert_eq!(data, decrypted.as_slice()); + } + + #[test] + fn test_base64_encoding() { + let data = b"Hello, world!"; + let encoded = CookieCipher::base64_encode(data); + let decoded = CookieCipher::base64_decode(&encoded).expect("Failed to decode base64"); + + assert_eq!(data, decoded.as_slice()); + } + + #[test] + fn test_invalid_data_length() { + let key = CookieCipher::generate_key_from_string("0193b34e-0ad9-70e0-a3dd-8ec30b73a90a") + .expect("Failed to generate key"); + let cipher = CookieCipher::new(&key).expect("Failed to create cipher"); + + let encrypted = vec![0; 10]; + let result = cipher.decrypt(&encrypted); + + assert!(result.is_err()); + } + + #[test] + fn test_invalid_nonce() { + let key = CookieCipher::generate_key_from_string("0193b34e-0ad9-70e0-a3dd-8ec30b73a90a") + .expect("Failed to generate key"); + let cipher = CookieCipher::new(&key).expect("Failed to create cipher"); + + let encrypted = vec![0; 12]; + let result = cipher.decrypt(&encrypted); + + assert!(result.is_err()); + } + + #[test] + fn test_invalid_decryption() { + let key = CookieCipher::generate_key_from_string("0193b34e-0ad9-70e0-a3dd-8ec30b73a90a") + .expect("Failed to generate key"); + let cipher = CookieCipher::new(&key).expect("Failed to create cipher"); + + let encrypted = vec![0; 24]; + let result = cipher.decrypt(&encrypted); + + assert!(result.is_err()); + } + + #[test] + fn test_invalid_base64() { + let result = CookieCipher::base64_decode("invalid_base64"); + + assert!(result.is_err()); + } + + #[test] + fn test_invalid_key_generation() { + let result = CookieCipher::generate_key_from_string(""); + + assert!(result.is_err()); + } + + #[test] + fn test_invalid_decryption_operation() { + let key = CookieCipher::generate_key_from_string("0193b34e-0ad9-70e0-a3dd-8ec30b73a90a") + .expect("Failed to generate key"); + let cipher = CookieCipher::new(&key).expect("Failed to create cipher"); + + let nonce = chacha20poly1305::Nonce::default(); + let ciphertext = vec![0; 1024]; + let result = cipher.perform_decryption(&nonce, &ciphertext); + + assert!(result.is_err()); + } +} diff --git a/crates/crypto/src/lib.rs b/crates/crypto/src/lib.rs index 8238e9e21..d53a0460d 100644 --- a/crates/crypto/src/lib.rs +++ b/crates/crypto/src/lib.rs @@ -41,3 +41,5 @@ pub use protected::Protected; pub use rng::CryptoRng; pub use rand_core::{RngCore, SeedableRng}; + +pub mod cookie; diff --git a/crates/tauri-plugin-cors-fetch/CHANGELOG.md b/crates/tauri-plugin-cors-fetch/CHANGELOG.md new file mode 100644 index 000000000..578d1973a --- /dev/null +++ b/crates/tauri-plugin-cors-fetch/CHANGELOG.md @@ -0,0 +1,11 @@ +# v2.1.0 + +- Fix: Exclude Tauri IPC requests from the request hook. + +# v2.0.0 + +- New: Hook `fetch` requests and redirect them to [tauri-plugin-http](https://crates.io/crates/tauri-plugin-http). + +# v1.0.0 + +- New: Hook `fetch` requests and redirect them to `x-http` and `x-https` custom protocols. diff --git a/crates/tauri-plugin-cors-fetch/Cargo.toml b/crates/tauri-plugin-cors-fetch/Cargo.toml new file mode 100644 index 000000000..4cba12e69 --- /dev/null +++ b/crates/tauri-plugin-cors-fetch/Cargo.toml @@ -0,0 +1,33 @@ +[package] +authors = ["Arnab Chakraborty ", "Del Wang "] +description = "Enabling Cross-Origin Resource Sharing (CORS) for Fetch Requests within Tauri applications. Modified to work with Spacedrive and Supertokens for authentication." +documentation = "https://docs.rs/crate/tauri-plugin-cors-fetch" +edition = "2021" +keywords = ["CORS", "fetch", "tauri-plugin", "unofficial"] +license = "MIT" +links = "tauri-plugin-cors-fetch" +name = "tauri-plugin-cors-fetch" +readme = "README.md" +repository = "https://github.com/idootop/tauri-plugin-cors-fetch" +rust-version = "1.70" +version = "2.1.1-sd-custom" + +[dependencies] +http = "0.2" +once_cell = "1.19.0" +reqwest = "0.11" +sd-crypto = { path = "../crypto" } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +tauri = "2.0.0-beta.11" +thiserror = "1" +tokio = { version = "1.36.0", features = ["macros"] } +tracing = { workspace = true } +url = "2" + +[build-dependencies] +tauri-plugin = { version = "2.0.0-beta.9", features = ["build"] } + +[package.metadata.docs.rs] +rustc-args = ["--cfg", "docsrs"] +rustdoc-args = ["--cfg", "docsrs"] diff --git a/crates/tauri-plugin-cors-fetch/LICENSE b/crates/tauri-plugin-cors-fetch/LICENSE new file mode 100644 index 000000000..3a2f1f923 --- /dev/null +++ b/crates/tauri-plugin-cors-fetch/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Del.Wang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/crates/tauri-plugin-cors-fetch/README.md b/crates/tauri-plugin-cors-fetch/README.md new file mode 100644 index 000000000..eb9e89128 --- /dev/null +++ b/crates/tauri-plugin-cors-fetch/README.md @@ -0,0 +1,88 @@ +![tauri-plugin-cors-fetch](https://github.com/idootop/tauri-plugin-cors-fetch/raw/main/banner.png) + +[![crates.io](https://img.shields.io/crates/v/tauri-plugin-cors-fetch.svg)](https://crates.io/crates/tauri-plugin-cors-fetch) +[![Documentation](https://docs.rs/tauri-plugin-cors-fetch/badge.svg)](https://docs.rs/crate/tauri-plugin-cors-fetch) +[![MIT licensed](https://img.shields.io/crates/l/tauri-plugin-cors-fetch.svg)](./LICENSE) + +An **unofficial** Tauri plugin that enables seamless cross-origin resource sharing (CORS) for web fetch requests within Tauri applications. + +## Overview + +When building cross-platform desktop applications with [Tauri](https://tauri.app), we often need to access services like [OpenAI](https://openai.com/product) that are restricted by **Cross-Origin Resource Sharing (CORS)** policies in web environments. + +However, on the desktop, we can bypass CORS and access these services directly. While the official [tauri-plugin-http](https://crates.io/crates/tauri-plugin-http) can bypass CORS, it requires modifying your network requests and might not be compatible with third-party dependencies that rely on the standard `fetch` API. + +## How it Works + +This plugin extends the official [tauri-plugin-http](https://crates.io/crates/tauri-plugin-http) by hooking into the browser's native `fetch` method during webpage initialization. It transparently redirects requests to the [tauri-plugin-http](https://crates.io/crates/tauri-plugin-http), allowing you to use the standard `fetch` API without additional code changes or workarounds. + +## Installation + +1. Add the plugin to your Tauri project's dependencies: + +```shell +# src-tauri +cargo add tauri-plugin-cors-fetch +``` + +2. Initialize the plugin in your Tauri application setup: + +```rust +// src-tauri/main.rs +fn main() { + tauri::Builder::default() + .plugin(tauri_plugin_cors_fetch::init()) + .run(tauri::generate_context!()) + .expect("failed to run app"); +} +``` + +3. Add permissions in your `capabilities` configuration: + +```json +// src-tauri/capabilities/default.json +{ + "permissions": ["cors-fetch:default"] +} +``` + +4. Enable `withGlobalTauri` in your Tauri configuration: + +```json +// src-tauri/tauri.conf.json +{ + "app": { + "withGlobalTauri": true + } +} +``` + +## Usage + +After installing and initializing the plugin, you can start making `fetch` requests from your Tauri application without encountering CORS-related errors. + +```javascript +// Enable CORS for the hooked fetch globally (default is true on app start) +window.enableCORSFetch(true); + +// Use the hooked fetch with CORS support +fetch('https://example.com/api') + .then((response) => response.json()) + .then((data) => console.log(data)) + .catch((error) => console.error(error)); + +// Use the hooked fetch directly +window.hookedFetch('https://example.com/api'); + +// Use the original, unhooked fetch +window.originalFetch('https://example.com/api'); +``` + +## Limitation + +1. **No Custom CSP Policy Support**: By default, all HTTP/HTTPS requests will be redirected to local native requests. +2. **No XMLHttpRequest Support**: The plugin is designed specifically to work with the modern `fetch` API and does not support `XMLHttpRequest` (XHR) requests. + +## License + +This project is licensed under the MIT License. diff --git a/crates/tauri-plugin-cors-fetch/api-iife.js b/crates/tauri-plugin-cors-fetch/api-iife.js new file mode 100644 index 000000000..99e949ac1 --- /dev/null +++ b/crates/tauri-plugin-cors-fetch/api-iife.js @@ -0,0 +1,118 @@ +class CORSFetch { + _requestId = 1; + + constructor() { + window.originalFetch = fetch.bind(window); + window.hookedFetch = this.hookedFetch.bind(this); + this.enableCORS(true); + } + + enableCORS(enable) { + window.fetch = enable ? window.hookedFetch : window.originalFetch; + } + + async hookedFetch(input, init) { + const _url = input instanceof Request ? input.url : input.toString(); + const isHttpRequests = /^https?:\/\//i.test(_url); + + // `ipc://localhost/${path}` and `http://ipc.localhost/${path}` are used for Tauri IPC requests + // https://github.com/tauri-apps/tauri/blob/7898b601d14ed62053dd24011fabadf31ec1af45/core/tauri/scripts/core.js#L12 + const isTauriIpcRequests = + /^ipc:\/\/localhost\//i.test(_url) || /^http:\/\/ipc.localhost\//i.test(_url); + + if (!isHttpRequests || isTauriIpcRequests) { + return window.originalFetch(input, init); + } + + return new Promise(async (resolve, reject) => { + const requestId = this._requestId++; + + const maxRedirections = init?.maxRedirections; + const connectTimeout = init?.connectTimeout; + const proxy = init?.proxy; + + // Remove these fields before creating the request + if (init) { + delete init.maxRedirections; + delete init.connectTimeout; + delete init.proxy; + } + + const signal = init?.signal; + + const headers = !init?.headers + ? [] + : init.headers instanceof Headers + ? Array.from(init.headers.entries()) + : Array.isArray(init.headers) + ? init.headers + : Object.entries(init.headers); + + const mappedHeaders = headers.map(([name, val]) => [ + name, + // we need to ensure we have all values as strings + typeof val === 'string' ? val : val.toString() + ]); + + const req = new Request(input, init); + const buffer = await req.arrayBuffer(); + const reqData = buffer.byteLength ? Array.from(new Uint8Array(buffer)) : null; + + signal?.addEventListener('abort', async (e) => { + const error = e.target.reason; + this._invoke('plugin:cors-fetch|cancel_cors_request', { + requestId + }).catch(() => {}); + reject(error); + }); + + const { + status, + statusText, + url, + body, + headers: responseHeaders + } = await this._invoke('plugin:cors-fetch|cors_request', { + request: { + requestId, + method: req.method, + url: req.url, + headers: mappedHeaders, + data: reqData, + maxRedirections, + connectTimeout, + proxy + } + }); + + const res = new Response( + body instanceof ArrayBuffer && body.byteLength + ? body + : body instanceof Array && body.length + ? new Uint8Array(body) + : null, + { + headers: responseHeaders, + status, + statusText + } + ); + + // url is read only but seems like we can do this + Object.defineProperty(res, 'url', { value: url }); + + resolve(res); + }); + } + + _invoke(cmd, args, options) { + if ('__TAURI__' in window) { + return window.__TAURI_INTERNALS__.invoke(cmd, args, options); + } + } +} + +(function () { + const cf = new CORSFetch(); + window.enableCORSFetch = cf.enableCORS.bind(cf); +})(); diff --git a/crates/tauri-plugin-cors-fetch/banner.png b/crates/tauri-plugin-cors-fetch/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..532e16dc515e00e65950837f80ee2049ad9fda8c GIT binary patch literal 23635 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 Vc3z*kC&&P&GsRc})rR<({{cIA+?4 + +Identifier +Description + + + + + + +`cors-fetch:allow-cancel-cors-request` + + + + +Enables the cancel_cors_request command without any pre-configured scope. + + + + + + + +`cors-fetch:deny-cancel-cors-request` + + + + +Denies the cancel_cors_request command without any pre-configured scope. + + + + + + + +`cors-fetch:allow-cors-request` + + + + +Enables the cors_request command without any pre-configured scope. + + + + + + + +`cors-fetch:deny-cors-request` + + + + +Denies the cors_request command without any pre-configured scope. + + + + diff --git a/crates/tauri-plugin-cors-fetch/permissions/default.toml b/crates/tauri-plugin-cors-fetch/permissions/default.toml new file mode 100644 index 000000000..8a65960fc --- /dev/null +++ b/crates/tauri-plugin-cors-fetch/permissions/default.toml @@ -0,0 +1,4 @@ +"$schema" = "schemas/schema.json" +[default] +description = "Allows all fetch operations" +permissions = ["allow-cancel-cors-request", "allow-cors-request"] diff --git a/crates/tauri-plugin-cors-fetch/permissions/schemas/schema.json b/crates/tauri-plugin-cors-fetch/permissions/schemas/schema.json new file mode 100644 index 000000000..9cbb2fe67 --- /dev/null +++ b/crates/tauri-plugin-cors-fetch/permissions/schemas/schema.json @@ -0,0 +1,325 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "PermissionFile", + "description": "Permission file that can define a default permission, a set of permissions or a list of inlined permissions.", + "type": "object", + "properties": { + "default": { + "description": "The default permission set for the plugin", + "anyOf": [ + { + "$ref": "#/definitions/DefaultPermission" + }, + { + "type": "null" + } + ] + }, + "set": { + "description": "A list of permissions sets defined", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionSet" + } + }, + "permission": { + "description": "A list of inlined permissions", + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Permission" + } + } + }, + "definitions": { + "DefaultPermission": { + "description": "The default permission set of the plugin.\n\nWorks similarly to a permission with the \"default\" identifier.", + "type": "object", + "required": [ + "permissions" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri convention is to use

headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PermissionSet": { + "description": "A set of direct permissions grouped together under a new name.", + "type": "object", + "required": [ + "description", + "identifier", + "permissions" + ], + "properties": { + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does.", + "type": "string" + }, + "permissions": { + "description": "All permissions this set contains.", + "type": "array", + "items": { + "$ref": "#/definitions/PermissionKind" + } + } + } + }, + "Permission": { + "description": "Descriptions of explicit privileges of commands.\n\nIt can enable commands to be accessible in the frontend of the application.\n\nIf the scope is defined it can be used to fine grain control the access of individual or multiple commands.", + "type": "object", + "required": [ + "identifier" + ], + "properties": { + "version": { + "description": "The version of the permission.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 1.0 + }, + "identifier": { + "description": "A unique identifier for the permission.", + "type": "string" + }, + "description": { + "description": "Human-readable description of what the permission does. Tauri internal convention is to use

headings in markdown content for Tauri documentation generation purposes.", + "type": [ + "string", + "null" + ] + }, + "commands": { + "description": "Allowed or denied commands when using this permission.", + "default": { + "allow": [], + "deny": [] + }, + "allOf": [ + { + "$ref": "#/definitions/Commands" + } + ] + }, + "scope": { + "description": "Allowed or denied scoped when using this permission.", + "allOf": [ + { + "$ref": "#/definitions/Scopes" + } + ] + }, + "platforms": { + "description": "Target platforms this permission applies. By default all platforms are affected by this permission.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Target" + } + } + } + }, + "Commands": { + "description": "Allowed and denied commands inside a permission.\n\nIf two commands clash inside of `allow` and `deny`, it should be denied by default.", + "type": "object", + "properties": { + "allow": { + "description": "Allowed command.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + }, + "deny": { + "description": "Denied command, which takes priority.", + "default": [], + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "Scopes": { + "description": "An argument for fine grained behavior control of Tauri commands.\n\nIt can be of any serde serializable type and is used to allow or prevent certain actions inside a Tauri command. The configured scope is passed to the command and will be enforced by the command implementation.\n\n## Example\n\n```json { \"allow\": [{ \"path\": \"$HOME/**\" }], \"deny\": [{ \"path\": \"$HOME/secret.txt\" }] } ```", + "type": "object", + "properties": { + "allow": { + "description": "Data that defines what is allowed by the scope.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + }, + "deny": { + "description": "Data that defines what is denied by the scope. This should be prioritized by validation logic.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/Value" + } + } + } + }, + "Value": { + "description": "All supported ACL values.", + "anyOf": [ + { + "description": "Represents a null JSON value.", + "type": "null" + }, + { + "description": "Represents a [`bool`].", + "type": "boolean" + }, + { + "description": "Represents a valid ACL [`Number`].", + "allOf": [ + { + "$ref": "#/definitions/Number" + } + ] + }, + { + "description": "Represents a [`String`].", + "type": "string" + }, + { + "description": "Represents a list of other [`Value`]s.", + "type": "array", + "items": { + "$ref": "#/definitions/Value" + } + }, + { + "description": "Represents a map of [`String`] keys to [`Value`]s.", + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/Value" + } + } + ] + }, + "Number": { + "description": "A valid ACL number.", + "anyOf": [ + { + "description": "Represents an [`i64`].", + "type": "integer", + "format": "int64" + }, + { + "description": "Represents a [`f64`].", + "type": "number", + "format": "double" + } + ] + }, + "Target": { + "description": "Platform target.", + "oneOf": [ + { + "description": "MacOS.", + "type": "string", + "enum": [ + "macOS" + ] + }, + { + "description": "Windows.", + "type": "string", + "enum": [ + "windows" + ] + }, + { + "description": "Linux.", + "type": "string", + "enum": [ + "linux" + ] + }, + { + "description": "Android.", + "type": "string", + "enum": [ + "android" + ] + }, + { + "description": "iOS.", + "type": "string", + "enum": [ + "iOS" + ] + } + ] + }, + "PermissionKind": { + "type": "string", + "oneOf": [ + { + "description": "Enables the cancel_cors_request command without any pre-configured scope.", + "type": "string", + "const": "allow-cancel-cors-request" + }, + { + "description": "Denies the cancel_cors_request command without any pre-configured scope.", + "type": "string", + "const": "deny-cancel-cors-request" + }, + { + "description": "Enables the cors_request command without any pre-configured scope.", + "type": "string", + "const": "allow-cors-request" + }, + { + "description": "Denies the cors_request command without any pre-configured scope.", + "type": "string", + "const": "deny-cors-request" + }, + { + "description": "Allows all fetch operations", + "type": "string", + "const": "default" + } + ] + } + } +} \ No newline at end of file diff --git a/crates/tauri-plugin-cors-fetch/src/commands.rs b/crates/tauri-plugin-cors-fetch/src/commands.rs new file mode 100644 index 000000000..f38e13462 --- /dev/null +++ b/crates/tauri-plugin-cors-fetch/src/commands.rs @@ -0,0 +1,434 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +// Source: tauri-plugin-http@2.0.0-beta.3 + +use std::{collections::HashMap, sync::Arc, time::Duration}; + +use http::{header, HeaderName, HeaderValue, Method}; +use reqwest::{redirect::Policy, NoProxy, RequestBuilder}; +use sd_crypto::cookie::CookieCipher; +use serde::{Deserialize, Serialize}; +use tauri::command; +use tracing::{debug, error}; + +use crate::{Error, Result, NODE_DATA_DIR}; + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RequestConfig { + request_id: u64, + method: String, + url: url::Url, + headers: Vec<(String, String)>, + data: Option>, + connect_timeout: Option, + max_redirections: Option, + proxy: Option, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FetchResponse { + status: u16, + status_text: String, + headers: Vec<(String, String)>, + url: String, + body: Option>, +} + +use once_cell::sync::Lazy; +use tokio::sync::oneshot; +type RequestPool = Arc>>>; +static REQUEST_POOL: Lazy = + Lazy::new(|| Arc::new(std::sync::Mutex::new(HashMap::new()))); + +#[command] +pub fn cancel_cors_request(request_id: u64) { + if let Some(tx) = REQUEST_POOL.lock().unwrap().remove(&request_id) { + tx.send(()).ok(); + } +} + +#[command] +pub async fn cors_request(request: RequestConfig) -> Result { + let request_id = request.request_id; + let (tx, rx) = oneshot::channel(); + REQUEST_POOL.lock().unwrap().insert(request_id, tx); + let request_config = build_request(request)?; + let response = get_response(request_config, rx).await; + if !REQUEST_POOL.lock().unwrap().contains_key(&request_id) { + return Err(Error::RequestCanceled); + } + REQUEST_POOL.lock().unwrap().remove(&request_id); + response +} + +pub fn build_request(request_config: RequestConfig) -> Result { + debug!("\n=== Starting Request Build Process ==="); + let RequestConfig { + request_id: _, + method, + url, + headers, + data, + connect_timeout, + max_redirections, + proxy, + } = request_config; + + debug!("\nRequest Details:"); + debug!(" Method: {}", method); + debug!(" URL: {}", url); + + let method = Method::from_bytes(method.as_bytes())?; + debug!("\nParsed HTTP method: {}", method); + + let headers: HashMap = HashMap::from_iter(headers); + debug!("\nHeaders:"); + for (key, value) in &headers { + debug!(" {} = {}", key, value); + } + + let mut builder = reqwest::ClientBuilder::new(); + debug!("\nBuilding Client Configuration:"); + + if let Some(timeout) = connect_timeout { + debug!(" Connect Timeout: {}ms", timeout); + builder = builder.connect_timeout(Duration::from_millis(timeout)); + } + + if let Some(max_redirections) = max_redirections { + debug!(" Redirect Policy:"); + builder = builder.redirect(if max_redirections == 0 { + debug!(" Redirects disabled"); + Policy::none() + } else { + debug!(" Max redirects: {}", max_redirections); + Policy::limited(max_redirections) + }); + } + + if let Some(proxy_config) = proxy { + debug!(" Configuring proxy settings"); + builder = attach_proxy(proxy_config, builder)?; + } + + debug!("\nFinalizing Request:"); + let client = builder.build()?; + let mut request = client.request(method.clone(), url.clone()); + + debug!("\nSetting Headers:"); + for (name, value) in &headers { + debug!(" Adding: {} = {}", name, value); + let name = HeaderName::from_bytes(name.as_bytes())?; + let value = HeaderValue::from_bytes(value.as_bytes())?; + request = request.header(name, value); + } + + if data.is_none() && matches!(method, Method::POST | Method::PUT) { + debug!( + " Adding empty content-length header for {} request", + method + ); + request = request.header(header::CONTENT_LENGTH, HeaderValue::from(0)); + } + + if headers.contains_key(header::RANGE.as_str()) { + debug!(" Range header present - Setting Accept-Encoding: identity"); + request = request.header( + header::ACCEPT_ENCODING, + HeaderValue::from_static("identity"), + ); + } + + if let Some(data) = data { + debug!("\nRequest Body:"); + debug!(" Size: {} bytes", data.len()); + request = request.body(data); + } + + debug!("\nRequest build completed successfully"); + debug!("=====================================\n"); + Ok(request) +} + +pub async fn get_response( + request: RequestBuilder, + rx: oneshot::Receiver<()>, +) -> Result { + debug!("\n=== Starting Response Fetch ==="); + + let response_or_none = tokio::select! { + _ = rx => { + debug!("\nRequest cancelled by receiver"); + None + }, + res = request.send() => { + debug!("\nRequest sent, awaiting response"); + Some(res) + }, + }; + + if let Some(response) = response_or_none { + debug!("\nProcessing Response:"); + match response { + Ok(res) => { + let status = res.status(); + debug!( + "\nStatus: {} ({})", + status.as_u16(), + status.canonical_reason().unwrap_or_default() + ); + + let url = res.url().to_string(); + debug!("Final URL: {}", url); + + let mut headers = Vec::new(); + debug!("\nResponse Headers:"); + for (key, val) in res.headers().iter() { + debug!(" {} = {:?}", key.as_str(), val); + headers.push(( + key.as_str().into(), + String::from_utf8(val.as_bytes().to_vec())?, + )); + } + + // Create cookies from headers + let mut cookie_store: HashMap = HashMap::new(); + + // Filter based on Supertokens' headers + for (key, val) in res.headers().iter() { + match key.as_str() { + "front-token" => { + if val.to_str().map(|v| v == "remove").unwrap_or(false) { + debug!("Removing front-token header (value: remove)"); + continue; + } + debug!("Adding front-token header"); + cookie_store.insert( + "front-token".to_string(), + val.to_str().unwrap_or_default().to_string(), + ); + } + "st-access-token" => { + if val.to_str().map(|v| v.is_empty()).unwrap_or(false) { + debug!("Removing empty st-access-token header"); + continue; + } + debug!("Setting st-access-token cookie"); + cookie_store.insert( + "st-access-token".to_string(), + val.to_str().unwrap_or_default().to_string(), + ); + } + "st-refresh-token" => { + if val.to_str().map(|v| v.is_empty()).unwrap_or(false) { + debug!("Removing empty st-refresh-token header"); + continue; + } + debug!("Setting st-refresh-token cookie"); + cookie_store.insert( + "st-refresh-token".to_string(), + val.to_str().unwrap_or_default().to_string(), + ); + } + // "set-cookie" => { + // if let Ok(cookie_str) = val.to_str() { + // if let Some((name, value)) = cookie_str.split_once('=') { + // if let Some(value) = value.split(';').next() { + // cookie_store.insert(name.trim().to_string(), value.trim().to_string()); + // } + // } + // } + // } + _ => {} + } + + debug!(" {} = {:?}", key.as_str(), val); + headers.push(( + key.as_str().into(), + String::from_utf8(val.as_bytes().to_vec())?, + )); + } + if !cookie_store.is_empty() { + let data_dir = NODE_DATA_DIR.get().unwrap().clone(); + let data_dir = data_dir.join("spacedrive").join("dev"); + + let node_config_path = data_dir.join("node_state.sdconfig"); + let node_config = std::fs::read_to_string(node_config_path).unwrap(); + + let node_config: serde_json::Value = + serde_json::from_str(&node_config).unwrap(); + let node_id = node_config["id"]["Uuid"].as_str().unwrap(); + debug!("Node ID: {:?}", node_id); + + // Create Cipher + let key = CookieCipher::generate_key_from_string(node_id).unwrap(); + let cipher = CookieCipher::new(&key).unwrap(); + + // Read .sdks file + let sdks_path = data_dir.join(".sdks"); + let data = std::fs::read(sdks_path.clone()).unwrap(); + + let data_str = String::from_utf8(data) + .map_err(|e| { + error!("Failed to convert data to string: {:?}", e.to_string()); + }) + .unwrap(); + let data = CookieCipher::base64_decode(&data_str) + .map_err(|e| { + error!("Failed to decode data: {:?}", e.to_string()); + }) + .unwrap(); + let de_data = cipher + .decrypt(&data) + .map_err(|e| { + error!("Failed to decrypt data: {:?}", e.to_string()); + }) + .unwrap(); + let de_data = String::from_utf8(de_data) + .map_err(|e| { + error!("Failed to convert data to string: {:?}", e.to_string()); + }) + .unwrap(); + + debug!("Decrypted Data: {:?}", de_data); + + debug!("\nCookies:"); + for (name, value) in &cookie_store { + debug!(" {} = {}", name, value); + } + + let mut de_data: Vec = serde_json::from_str(&de_data).unwrap(); + for cookie in &mut de_data { + for (name, value) in &cookie_store { + if cookie.starts_with(name) { + *cookie = format!("{}={};expires=Fri, 31 Dec 9999 23:59:59 GMT;path=/;samesite=lax", name, value); + } + } + } + + debug!("Updated Cookies: {:?}", de_data); + + // Now, we will encrypt the de_data and save it to the .sdks file + let de_data = serde_json::to_string(&de_data).unwrap(); + let en_data = cipher + .encrypt(de_data.as_bytes()) + .map_err(|e| { + error!("Failed to encrypt data: {:?}", e.to_string()); + }) + .unwrap(); + let en_data = CookieCipher::base64_encode(&en_data); + + std::fs::write(sdks_path, en_data).unwrap(); + } + + debug!("\nReading Response Body..."); + let body = res.bytes().await; + match body { + Ok(bytes) => { + debug!("Body received: {} bytes", bytes.len()); + Ok(FetchResponse { + status: status.as_u16(), + status_text: status.canonical_reason().unwrap_or_default().to_string(), + headers, + url, + body: Some(bytes.to_vec()), + }) + } + Err(e) => { + error!("Failed to read body: {}", e); + Err(Error::Network(e)) + } + } + } + Err(err) => { + error!("Network error: {}", err); + Err(Error::Network(err)) + } + } + } else { + debug!("Request was cancelled"); + Err(Error::RequestCanceled) + } +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Proxy { + all: Option, + http: Option, + https: Option, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(untagged)] +pub enum UrlOrConfig { + Url(String), + Config(ProxyConfig), +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ProxyConfig { + url: String, + basic_auth: Option, + no_proxy: Option, +} + +#[derive(Deserialize)] +pub struct BasicAuth { + username: String, + password: String, +} + +#[inline] +fn proxy_creator( + url_or_config: UrlOrConfig, + proxy_fn: fn(String) -> reqwest::Result, +) -> reqwest::Result { + match url_or_config { + UrlOrConfig::Url(url) => Ok(proxy_fn(url)?), + UrlOrConfig::Config(ProxyConfig { + url, + basic_auth, + no_proxy, + }) => { + let mut proxy = proxy_fn(url)?; + if let Some(basic_auth) = basic_auth { + proxy = proxy.basic_auth(&basic_auth.username, &basic_auth.password); + } + if let Some(no_proxy) = no_proxy { + proxy = proxy.no_proxy(NoProxy::from_string(&no_proxy)); + } + Ok(proxy) + } + } +} + +fn attach_proxy( + proxy: Proxy, + mut builder: reqwest::ClientBuilder, +) -> crate::Result { + let Proxy { all, http, https } = proxy; + + if let Some(all) = all { + let proxy = proxy_creator(all, reqwest::Proxy::all)?; + builder = builder.proxy(proxy); + } + + if let Some(http) = http { + let proxy = proxy_creator(http, reqwest::Proxy::http)?; + builder = builder.proxy(proxy); + } + + if let Some(https) = https { + let proxy = proxy_creator(https, reqwest::Proxy::https)?; + builder = builder.proxy(proxy); + } + + Ok(builder) +} diff --git a/crates/tauri-plugin-cors-fetch/src/error.rs b/crates/tauri-plugin-cors-fetch/src/error.rs new file mode 100644 index 000000000..a523058cc --- /dev/null +++ b/crates/tauri-plugin-cors-fetch/src/error.rs @@ -0,0 +1,49 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::{Serialize, Serializer}; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Json(#[from] serde_json::Error), + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + Network(#[from] reqwest::Error), + #[error(transparent)] + Http(#[from] http::Error), + #[error(transparent)] + HttpInvalidHeaderName(#[from] http::header::InvalidHeaderName), + #[error(transparent)] + HttpInvalidHeaderValue(#[from] http::header::InvalidHeaderValue), + #[error(transparent)] + UrlParseError(#[from] url::ParseError), + /// HTTP method error. + #[error(transparent)] + HttpMethod(#[from] http::method::InvalidMethod), + #[error("scheme {0} not supported")] + SchemeNotSupport(String), + #[error("Request canceled")] + RequestCanceled, + #[error("failed to process data url")] + DataUrlError, + #[error("failed to decode data url into bytes")] + DataUrlDecodeError, + #[error(transparent)] + Tauri(#[from] tauri::Error), + #[error(transparent)] + Utf8(#[from] std::string::FromUtf8Error), +} + +impl Serialize for Error { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: Serializer, + { + serializer.serialize_str(self.to_string().as_ref()) + } +} + +pub type Result = std::result::Result; diff --git a/crates/tauri-plugin-cors-fetch/src/lib.rs b/crates/tauri-plugin-cors-fetch/src/lib.rs new file mode 100644 index 000000000..c904fdeff --- /dev/null +++ b/crates/tauri-plugin-cors-fetch/src/lib.rs @@ -0,0 +1,41 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +//! ![tauri-plugin-cors-fetch](https://github.com/idootop/tauri-plugin-cors-fetch/raw/main/banner.png) +//! +//! Enabling Cross-Origin Resource Sharing (CORS) for Fetch Requests within Tauri applications. + +use std::path::PathBuf; + +use once_cell::sync::OnceCell; +pub use reqwest; +use tauri::{ + plugin::{Builder, TauriPlugin}, + Manager, Runtime, +}; + +pub use error::{Error, Result}; +mod commands; +mod error; + +pub static NODE_DATA_DIR: OnceCell = OnceCell::new(); + +pub fn init() -> TauriPlugin { + Builder::::new("cors-fetch") + .invoke_handler(tauri::generate_handler![ + commands::cors_request, + commands::cancel_cors_request, + ]) + .setup(|app_handle, _| { + let data_dir = app_handle + .path() + .data_dir() + .unwrap_or_else(|_| PathBuf::from("./")); + + NODE_DATA_DIR.set(data_dir).unwrap(); + + Ok(()) + }) + .build() +} diff --git a/interface/app/$libraryId/settings/client/account/Profile.tsx b/interface/app/$libraryId/settings/client/account/Profile.tsx index 0664b0a85..73adf0082 100644 --- a/interface/app/$libraryId/settings/client/account/Profile.tsx +++ b/interface/app/$libraryId/settings/client/account/Profile.tsx @@ -33,7 +33,16 @@ const Profile = ({ }) => { const emailName = user.email?.split('@')[0]; const capitalizedEmailName = (emailName?.charAt(0).toUpperCase() ?? '') + emailName?.slice(1); - const { accessToken, refreshToken } = getTokens(); + const [accessToken, setAccessToken] = useState(''); + const [refreshToken, setRefreshToken] = useState(''); + + useEffect(() => { + (async () => { + const tokens = await getTokens(); + setAccessToken(tokens.accessToken); + setRefreshToken(tokens.refreshToken); + })(); + }, []); const cloudBootstrap = useBridgeMutation('cloud.bootstrap'); const devices = useBridgeQuery(['cloud.devices.list']); diff --git a/interface/app/$libraryId/settings/client/account/handlers/cookieHandler.ts b/interface/app/$libraryId/settings/client/account/handlers/cookieHandler.ts index 4a7362d8e..f30d16c04 100644 --- a/interface/app/$libraryId/settings/client/account/handlers/cookieHandler.ts +++ b/interface/app/$libraryId/settings/client/account/handlers/cookieHandler.ts @@ -1,23 +1,38 @@ import { CookieHandlerInterface } from 'supertokens-website/utils/cookieHandler/types'; - -const frontendCookiesKey = 'frontendCookies'; +import { nonLibraryClient } from '@sd/client'; /** * Tauri handles cookies differently than in browser environments. The SuperTokens * SDK uses frontend cookies, to make sure everything work correctly we add custom - * cookie handling and store cookies in local storage instead (This is not a problem - * since these are frontend cookies and not server side cookies) + * cookie handling and store cookies in an encrypted file called `.sdks`. This file + * is stored in the data directory, and must be fetched via RSPC due to the encryption + * requirements. */ function getCookiesFromStorage(): string { - const cookiesFromStorage = window.localStorage.getItem(frontendCookiesKey); + let cookiesFromStorage: string = ''; - if (cookiesFromStorage === null) { - window.localStorage.setItem(frontendCookiesKey, '[]'); + nonLibraryClient + .query(['keys.get']) + .then((response) => { + // Debugging + // console.log("rspc response: ", response); + const cookiesArrayFromStorage: string[] = JSON.parse(response); + // console.log("Cookies fetched from storage: ", cookiesArrayFromStorage); + + // Actual + cookiesFromStorage = response; + }) + .catch((e) => { + console.error('Error fetching cookies from storage: ', e); + }); + + if (cookiesFromStorage.length === 0) { return ''; } /** - * Because we store cookies in local storage, we need to manually check + * Because we store cookies in a single string, we need to split them by + * the delimiter `;` and check the `expires=` part of the cookie string * for expiry before returning all cookies */ const cookieArrayInStorage: string[] = JSON.parse(cookiesFromStorage); @@ -25,47 +40,68 @@ function getCookiesFromStorage(): string { for (let cookieIndex = 0; cookieIndex < cookieArrayInStorage.length; cookieIndex++) { const currentCookieString = cookieArrayInStorage[cookieIndex]; - const parts = currentCookieString?.split(';'); + const parts = currentCookieString?.split(';') ?? []; let expirationString: string = ''; - for (let partIndex = 0; partIndex < parts!.length; partIndex++) { - const currentPart = parts![partIndex]; + for (let partIndex = 0; partIndex < parts.length; partIndex++) { + const currentPart = parts[partIndex]; - if (currentPart!.toLocaleLowerCase().includes('expires=')) { - expirationString = currentPart!; + if (currentPart?.toLocaleLowerCase().includes('expires=')) { + expirationString = currentPart; break; } } if (expirationString !== '') { const expirationValueString = expirationString.split('=')[1]; - const expirationDate = new Date(expirationValueString!); + const expirationDate = expirationValueString ? new Date(expirationValueString) : null; const currentTimeInMillis = Date.now(); // if the cookie has expired, we skip it - if (expirationDate.getTime() < currentTimeInMillis) { + if (expirationDate && expirationDate.getTime() < currentTimeInMillis) { continue; } } - cookieArrayToReturn.push(currentCookieString!); + if (currentCookieString !== undefined) { + cookieArrayToReturn.push(currentCookieString); + } } /** * After processing and removing expired cookies we need to update the cookies * in storage so we dont have to process the expired ones again */ - window.localStorage.setItem(frontendCookiesKey, JSON.stringify(cookieArrayToReturn)); + // window.localStorage.setItem(frontendCookiesKey, JSON.stringify(cookieArrayToReturn)); + try { + nonLibraryClient.mutation(['keys.save', JSON.stringify(cookieArrayToReturn)]); + // console.log("Cookies set successfully"); + } catch (e) { + console.error('Error setting cookies to storage: ', e); + } return cookieArrayToReturn.join('; '); } -function setCookieToStorage(cookieString: string) { - const cookieName = cookieString.split(';')[0]!.split('=')[0]; - const cookiesFromStorage = window.localStorage.getItem(frontendCookiesKey); +async function setCookieToStorage(cookieString: string) { + const cookieName = cookieString.split(';')[0]?.split('=')[0]; + + let cookiesFromStorage: string = ''; + try { + const response = await nonLibraryClient.query(['keys.get']); + // Debugging + const cookiesArrayFromStorage: string[] = JSON.parse(response); + // console.log("Cookies fetched from storage: ", cookiesArrayFromStorage); + + // Actual + cookiesFromStorage = response; + } catch (e) { + console.error('Error fetching cookies from storage: ', e); + } + let cookiesArray: string[] = []; - if (cookiesFromStorage !== null) { + if (cookiesFromStorage.length !== 0) { const cookiesArrayFromStorage: string[] = JSON.parse(cookiesFromStorage); cookiesArray = cookiesArrayFromStorage; } @@ -75,7 +111,7 @@ function setCookieToStorage(cookieString: string) { for (let i = 0; i < cookiesArray.length; i++) { const currentCookie = cookiesArray[i]; - if (currentCookie!.indexOf(`${cookieName}=`) !== -1) { + if (currentCookie?.indexOf(`${cookieName}=`) !== -1) { cookieIndex = i; break; } @@ -93,7 +129,15 @@ function setCookieToStorage(cookieString: string) { cookiesArray.push(cookieString); } - window.localStorage.setItem(frontendCookiesKey, JSON.stringify(cookiesArray)); + try { + await nonLibraryClient.mutation(['keys.save', JSON.stringify(cookiesArray)]); + // console.log("Cookies set successfully"); + } catch (e) { + console.error('Error setting cookies to storage: ', e); + return; + } + + // console.log("Setting cookies to storage: ", cookiesArray); } export default function getCookieHandler(original: CookieHandlerInterface): CookieHandlerInterface { @@ -104,7 +148,7 @@ export default function getCookieHandler(original: CookieHandlerInterface): Cook return cookies; }, setCookie: async function (cookieString: string) { - setCookieToStorage(cookieString); + await setCookieToStorage(cookieString); } }; } diff --git a/interface/app/$libraryId/settings/client/account/handlers/windowHandler.ts b/interface/app/$libraryId/settings/client/account/handlers/windowHandler.ts index 90a75c675..c4d16fc36 100644 --- a/interface/app/$libraryId/settings/client/account/handlers/windowHandler.ts +++ b/interface/app/$libraryId/settings/client/account/handlers/windowHandler.ts @@ -1,10 +1,9 @@ import { WindowHandlerInterface } from 'supertokens-website/utils/windowHandler/types'; /** - * This example app uses HashRouter from react-router-dom. The SuperTokens SDK relies on - * some window properties like location hash, query params etc. Because HashRouter places - * everything other than the website base in the location hash, we need to add custom - * handling for some of the properties of the Window API + * The SuperTokens SDK relies on some window properties like location hash, query params etc. + * This handler is used to override the default window object and provide custom implementations + * for these properties. */ export default function getWindowHandler(original: WindowHandlerInterface): WindowHandlerInterface { return { diff --git a/interface/app/$libraryId/settings/client/account/index.tsx b/interface/app/$libraryId/settings/client/account/index.tsx index 7037536dc..9c78b28f4 100644 --- a/interface/app/$libraryId/settings/client/account/index.tsx +++ b/interface/app/$libraryId/settings/client/account/index.tsx @@ -5,7 +5,7 @@ import { useBridgeMutation } from '@sd/client'; import { Button } from '@sd/ui'; import { Authentication } from '~/components'; import { useLocale } from '~/hooks'; -import { AUTH_SERVER_URL } from '~/util'; +import { AUTH_SERVER_URL, getTokens } from '~/util'; import { Heading } from '../../Layout'; import Profile from './Profile'; @@ -24,11 +24,16 @@ export const Component = () => { useEffect(() => { async function _() { + const tokens = await getTokens(); const user_data = await fetch(`${AUTH_SERVER_URL}/api/user`, { - method: 'GET' + method: 'GET', + headers: { + Authorization: `Bearer ${tokens.accessToken}` + } }); const data = await user_data.json(); + // console.log(data); setUserInfo(data.id ? data : null); } diff --git a/interface/components/Login.tsx b/interface/components/Login.tsx index a4ac8bf63..7bd76375f 100644 --- a/interface/components/Login.tsx +++ b/interface/components/Login.tsx @@ -44,8 +44,7 @@ async function signInClicked( } else if (response.status === 'SIGN_IN_NOT_ALLOWED') { toast.error(response.reason); } else { - const tokens = getTokens(); - console.log(cloudBootstrap); + const tokens = await getTokens(); cloudBootstrap.mutate([tokens.accessToken, tokens.refreshToken]); toast.success('Sign in successful'); reload(true); diff --git a/interface/util/index.tsx b/interface/util/index.tsx index 81ea777a8..379d106d9 100644 --- a/interface/util/index.tsx +++ b/interface/util/index.tsx @@ -1,4 +1,5 @@ import cryptoRandomString from 'crypto-random-string'; +import { nonLibraryClient } from '@sd/client'; // NOTE: `crypto` module is not available in RN so this can't be in client export const generatePassword = (length: number) => @@ -12,27 +13,20 @@ export const isNonEmptyObject = (input: object) => Object.keys(input).length > 0 export const AUTH_SERVER_URL = 'https://auth.spacedrive.com'; // export const AUTH_SERVER_URL = 'http://localhost:9420'; -export function getTokens() { - if (typeof window === 'undefined') { - return { - refreshToken: '', - accessToken: '' - }; - } +export async function getTokens(): Promise<{ accessToken: string; refreshToken: string }> { + const tokens = await nonLibraryClient.query(['keys.get']); + const tokensArray = JSON.parse(tokens); const refreshToken: string = - JSON.parse(window.localStorage.getItem('frontendCookies') ?? '[]') + tokensArray .find((cookie: string) => cookie.startsWith('st-refresh-token')) ?.split('=')[1] .split(';')[0] || ''; const accessToken: string = - JSON.parse(window.localStorage.getItem('frontendCookies') ?? '[]') + tokensArray .find((cookie: string) => cookie.startsWith('st-access-token')) ?.split('=')[1] .split(';')[0] || ''; - return { - refreshToken, - accessToken - }; + return { accessToken, refreshToken }; } diff --git a/packages/client/src/core.ts b/packages/client/src/core.ts index dc028a457..6d464efa6 100644 --- a/packages/client/src/core.ts +++ b/packages/client/src/core.ts @@ -24,6 +24,7 @@ export type Procedures = { { key: "invalidation.test-invalidate", input: never, result: number } | { key: "jobs.isActive", input: LibraryArgs, result: boolean } | { key: "jobs.reports", input: LibraryArgs, result: JobGroup[] } | + { key: "keys.get", input: never, result: string } | { key: "labels.count", input: LibraryArgs, result: number } | { key: "labels.get", input: LibraryArgs, result: Label | null } | { key: "labels.getForObject", input: LibraryArgs, result: Label[] } | @@ -108,6 +109,7 @@ export type Procedures = { { key: "jobs.objectValidator", input: LibraryArgs, result: null } | { key: "jobs.pause", input: LibraryArgs, result: null } | { key: "jobs.resume", input: LibraryArgs, result: null } | + { key: "keys.save", input: string, result: null } | { key: "labels.delete", input: LibraryArgs, result: null } | { key: "library.create", input: CreateLibraryArgs, result: LibraryConfigWrapped } | { key: "library.delete", input: string, result: null } | diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0b19456cae2326158cb1a0ac02a43dc8eaeaccc3..89c54054315f7df1dadeabdcee3532004b4ab748 100644 GIT binary patch delta 112 zcmZ4W)NRdEw+(9rIn`|q^bC#l4Aduo6cd@OJArfZw~4Wn6N-5z$4ugH_7rOO6k-Ho uCLm@8Viq7~1!6WJW(Q&pAm#*OE+FOxVjdvo1!6uR<_BVd?Vdt{i3ukL?QaAbftU%1nSq!Eh*^P{4T#x+m;;D8 cftU-3xq+Amh Date: Sun, 15 Dec 2024 17:53:46 -0500 Subject: [PATCH 21/34] Save email address for re-login It saves the email address in the login form so we don't have users always type in their email when they have to log in. --- core/src/api/keys.rs | 160 ++++++++++++++++++ core/src/library/config.rs | 25 ++- .../client/account/handlers/cookieHandler.ts | 6 - interface/components/Authentication.tsx | 2 +- interface/components/Login.tsx | 30 +++- interface/components/Register.tsx | 36 +++- packages/client/src/core.ts | 10 +- 7 files changed, 244 insertions(+), 25 deletions(-) diff --git a/core/src/api/keys.rs b/core/src/api/keys.rs index 82c671308..9904dfce5 100644 --- a/core/src/api/keys.rs +++ b/core/src/api/keys.rs @@ -1,6 +1,8 @@ +use super::utils::library; use super::{Ctx, SanitizedNodeConfig, R}; use rspc::{alpha::AlphaRouter, ErrorCode}; use sd_crypto::cookie::CookieCipher; +use serde_json::{json, Map, Value}; use std::path::{Path, PathBuf}; use std::sync::Arc; use tokio::io::AsyncWriteExt; @@ -183,4 +185,162 @@ pub(crate) fn mount() -> AlphaRouter { } }) }) + .procedure("saveEmailAddress", { + R.with2(library()) + .mutation(move |(node, library), args: String| async move { + let path = node + .libraries + .libraries_dir + .join(format!("{}.sdlibrary", library.id)); + + let mut config = serde_json::from_slice::>( + &tokio::fs::read(path.clone()).await.map_err(|e| { + rspc::Error::new( + ErrorCode::InternalServerError, + format!("Failed to read library config: {:?}", e.to_string()), + ) + })?, + ) + .map_err(|e: serde_json::Error| { + rspc::Error::new( + ErrorCode::InternalServerError, + format!("Failed to parse library config: {:?}", e.to_string()), + ) + })?; + + // Encrypt the email address + // Create new cipher with the library id as the key + let uuid_key = + CookieCipher::generate_key_from_string(library.id.to_string().as_str()) + .map_err(|e| { + error!("Failed to generate key: {:?}", e.to_string()); + rspc::Error::new( + ErrorCode::InternalServerError, + "Failed to generate key".to_string(), + ) + })?; + + let cipher = CookieCipher::new(&uuid_key).map_err(|e| { + error!("Failed to create cipher: {:?}", e.to_string()); + rspc::Error::new( + ErrorCode::InternalServerError, + "Failed to create cipher".to_string(), + ) + })?; + + let en_data = cipher.encrypt(args.as_bytes()).map_err(|e| { + error!("Failed to encrypt data: {:?}", e.to_string()); + rspc::Error::new( + ErrorCode::InternalServerError, + "Failed to encrypt data".to_string(), + ) + })?; + + let en_data = CookieCipher::base64_encode(&en_data); + + config.remove("cloud_email_address"); + config.insert("cloud_email_address".to_string(), json!(en_data)); + + tokio::fs::write( + path, + serde_json::to_vec(&config).map_err(|e| { + rspc::Error::new( + ErrorCode::InternalServerError, + format!("Failed to serialize library config: {:?}", e.to_string()), + ) + })?, + ) + .await + .map_err(|e| { + rspc::Error::new( + ErrorCode::InternalServerError, + format!("Failed to write library config: {:?}", e.to_string()), + ) + })?; + + Ok(()) + }) + }) + .procedure("getEmailAddress", { + R.with2(library()) + .query(move |(node, library), _: ()| async move { + let path = node + .libraries + .libraries_dir + .join(format!("{}.sdlibrary", library.id)); + + let config = serde_json::from_slice::>( + &tokio::fs::read(path.clone()).await.map_err(|e| { + rspc::Error::new( + ErrorCode::InternalServerError, + format!("Failed to read library config: {:?}", e.to_string()), + ) + })?, + ) + .map_err(|e: serde_json::Error| { + rspc::Error::new( + ErrorCode::InternalServerError, + format!("Failed to parse library config: {:?}", e.to_string()), + ) + })?; + + let en_data = config.get("cloud_email_address").ok_or_else(|| { + rspc::Error::new( + ErrorCode::InternalServerError, + "Failed to get cloud_email_address".to_string(), + ) + })?; + + let en_data = en_data.as_str().ok_or_else(|| { + rspc::Error::new( + ErrorCode::InternalServerError, + "Failed to get cloud_email_address".to_string(), + ) + })?; + + let en_data = CookieCipher::base64_decode(en_data).map_err(|e| { + error!("Failed to decode data: {:?}", e.to_string()); + rspc::Error::new( + ErrorCode::InternalServerError, + "Failed to decode data".to_string(), + ) + })?; + + let uuid_key = + CookieCipher::generate_key_from_string(library.id.to_string().as_str()) + .map_err(|e| { + error!("Failed to generate key: {:?}", e.to_string()); + rspc::Error::new( + ErrorCode::InternalServerError, + "Failed to generate key".to_string(), + ) + })?; + + let cipher = CookieCipher::new(&uuid_key).map_err(|e| { + error!("Failed to create cipher: {:?}", e.to_string()); + rspc::Error::new( + ErrorCode::InternalServerError, + "Failed to create cipher".to_string(), + ) + })?; + + let de_data = cipher.decrypt(&en_data).map_err(|e| { + error!("Failed to decrypt data: {:?}", e.to_string()); + rspc::Error::new( + ErrorCode::InternalServerError, + "Failed to decrypt data".to_string(), + ) + })?; + + let de_data = String::from_utf8(de_data).map_err(|e| { + error!("Failed to convert data to string: {:?}", e.to_string()); + rspc::Error::new( + ErrorCode::InternalServerError, + "Failed to convert data to string".to_string(), + ) + })?; + + Ok(de_data) + }) + }) } diff --git a/core/src/library/config.rs b/core/src/library/config.rs index 20c245d10..c185f8b29 100644 --- a/core/src/library/config.rs +++ b/core/src/library/config.rs @@ -46,6 +46,8 @@ pub struct LibraryConfig { #[serde(skip, default)] pub config_path: PathBuf, + /// cloud_email_address is the email address of the user who owns the cloud library this library is linked to. + pub cloud_email_address: Option, } #[derive( @@ -74,10 +76,11 @@ pub enum LibraryConfigVersion { V9 = 9, V10 = 10, V11 = 11, + V12 = 12, } impl ManagedVersion for LibraryConfig { - const LATEST_VERSION: LibraryConfigVersion = LibraryConfigVersion::V11; + const LATEST_VERSION: LibraryConfigVersion = LibraryConfigVersion::V12; const KIND: Kind = Kind::Json("version"); @@ -99,6 +102,7 @@ impl LibraryConfig { cloud_id: None, generate_sync_operations: Arc::new(AtomicBool::new(false)), config_path: path.as_ref().to_path_buf(), + cloud_email_address: None, }; this.save(path).await.map(|()| this) @@ -396,6 +400,25 @@ impl LibraryConfig { .await?; } + (LibraryConfigVersion::V11, LibraryConfigVersion::V12) => { + // Add the `cloud_email_address` field to the library config. + let mut config = serde_json::from_slice::>( + &fs::read(path).await.map_err(|e| { + VersionManagerError::FileIO(FileIOError::from((path, e))) + })?, + ) + .map_err(VersionManagerError::SerdeJson)?; + + config.insert(String::from("cloud_email_address"), Value::Null); + + fs::write( + path, + &serde_json::to_vec(&config).map_err(VersionManagerError::SerdeJson)?, + ) + .await + .map_err(|e| VersionManagerError::FileIO(FileIOError::from((path, e))))?; + } + _ => { error!(current_version = ?current, "Library config version is not handled;"); diff --git a/interface/app/$libraryId/settings/client/account/handlers/cookieHandler.ts b/interface/app/$libraryId/settings/client/account/handlers/cookieHandler.ts index f30d16c04..95f010f72 100644 --- a/interface/app/$libraryId/settings/client/account/handlers/cookieHandler.ts +++ b/interface/app/$libraryId/settings/client/account/handlers/cookieHandler.ts @@ -14,12 +14,6 @@ function getCookiesFromStorage(): string { nonLibraryClient .query(['keys.get']) .then((response) => { - // Debugging - // console.log("rspc response: ", response); - const cookiesArrayFromStorage: string[] = JSON.parse(response); - // console.log("Cookies fetched from storage: ", cookiesArrayFromStorage); - - // Actual cookiesFromStorage = response; }) .catch((e) => { diff --git a/interface/components/Authentication.tsx b/interface/components/Authentication.tsx index 22c4c7269..beff987e4 100644 --- a/interface/components/Authentication.tsx +++ b/interface/components/Authentication.tsx @@ -136,7 +136,7 @@ export const Authentication = ({ {activeTab === 'Login' ? ( ) : ( - + )}
Social auth and SSO (Single Sign On) available soon! diff --git a/interface/components/Login.tsx b/interface/components/Login.tsx index 7bd76375f..8cdf489ef 100644 --- a/interface/components/Login.tsx +++ b/interface/components/Login.tsx @@ -2,11 +2,11 @@ import { ArrowLeft } from '@phosphor-icons/react'; import { RSPCError } from '@spacedrive/rspc-client'; import { UseMutationResult } from '@tanstack/react-query'; import clsx from 'clsx'; -import { Dispatch, SetStateAction, useState } from 'react'; +import { Dispatch, SetStateAction, useEffect, useState } from 'react'; import { Controller } from 'react-hook-form'; import { signIn } from 'supertokens-web-js/recipe/emailpassword'; import { createCode } from 'supertokens-web-js/recipe/passwordless'; -import { useZodForm } from '@sd/client'; +import { useLibraryMutation, useLibraryQuery, useZodForm } from '@sd/client'; import { Button, Divider, Form, Input, toast, z } from '@sd/ui'; import { useLocale } from '~/hooks'; import { getTokens } from '~/util'; @@ -17,7 +17,8 @@ async function signInClicked( email: string, password: string, reload: Dispatch>, - cloudBootstrap: UseMutationResult // Cloud bootstrap mutation + cloudBootstrap: UseMutationResult, // Cloud bootstrap mutation + saveEmailAddress: UseMutationResult // Save email mutation ) { try { const response = await signIn({ @@ -46,6 +47,7 @@ async function signInClicked( } else { const tokens = await getTokens(); cloudBootstrap.mutate([tokens.accessToken, tokens.refreshToken]); + saveEmailAddress.mutate(email); toast.success('Sign in successful'); reload(true); } @@ -111,18 +113,34 @@ interface LoginProps { const LoginForm = ({ reload, cloudBootstrap, setContinueWithEmail }: LoginProps) => { const { t } = useLocale(); const [showPassword, setShowPassword] = useState(false); + const savedEmailAddress = useLibraryQuery(['keys.getEmailAddress']); + const saveEmailAddress = useLibraryMutation(['keys.saveEmailAddress']); + const form = useZodForm({ schema: LoginSchema, defaultValues: { - email: '', + email: savedEmailAddress.data ?? '', password: '' } }); + useEffect(() => { + savedEmailAddress.refetch(); + }, []); + + useEffect(() => { + if (savedEmailAddress.data) { + form.reset({ + email: savedEmailAddress.data, + password: '' + }); + } + }, [savedEmailAddress.data]); + return (
{ - await signInClicked(data.email, data.password, reload, cloudBootstrap); + await signInClicked(data.email, data.password, reload, cloudBootstrap, saveEmailAddress); })} className="w-full" form={form} @@ -193,7 +211,7 @@ const LoginForm = ({ reload, cloudBootstrap, setContinueWithEmail }: LoginProps) variant="accent" size="md" onClick={form.handleSubmit(async (data) => { - await signInClicked(data.email, data.password, reload, cloudBootstrap); + await signInClicked(data.email, data.password, reload, cloudBootstrap, saveEmailAddress); })} disabled={form.formState.isSubmitting} > diff --git a/interface/components/Register.tsx b/interface/components/Register.tsx index 1262274aa..74115cec3 100644 --- a/interface/components/Register.tsx +++ b/interface/components/Register.tsx @@ -1,12 +1,16 @@ import { zodResolver } from '@hookform/resolvers/zod'; +import { RSPCError } from '@spacedrive/rspc-client'; +import { UseMutationResult } from '@tanstack/react-query'; import clsx from 'clsx'; -import { useState } from 'react'; +import { Dispatch, SetStateAction, useState } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { signUp } from 'supertokens-web-js/recipe/emailpassword'; import { Button, Form, Input, toast, z } from '@sd/ui'; import { useLocale } from '~/hooks'; +import { useLibraryMutation } from '@sd/client'; import ShowPassword from './ShowPassword'; +import { getTokens } from '~/util'; const RegisterSchema = z .object({ @@ -26,7 +30,13 @@ const RegisterSchema = z }); type RegisterType = z.infer; -async function signUpClicked(email: string, password: string) { +async function signUpClicked( + email: string, + password: string, + reload: Dispatch>, + cloudBootstrap: UseMutationResult, + saveEmailAddress: UseMutationResult +) { try { const response = await signUp({ formFields: [ @@ -62,9 +72,11 @@ async function signUpClicked(email: string, password: string) { } else { // sign up successful. The session tokens are automatically handled by // the frontend SDK. + const tokens = await getTokens(); + cloudBootstrap.mutate([tokens.accessToken, tokens.refreshToken]); + saveEmailAddress.mutate(email); toast.success('Sign up successful'); - // FIXME: This is a temporary workaround. We will provide a better way to handle this. - window.location.reload(); + reload(true); } } catch (err: any) { if (err.isSuperTokensGeneralError === true) { @@ -76,7 +88,13 @@ async function signUpClicked(email: string, password: string) { } } -const Register = () => { +const Register = ({ + reload, + cloudBootstrap +}: { + reload: Dispatch>; + cloudBootstrap: UseMutationResult; // Cloud bootstrap mutation +}) => { const { t } = useLocale(); const [showPassword, setShowPassword] = useState(false); // useZodForm seems to be out-dated or needs @@ -89,12 +107,13 @@ const Register = () => { confirmPassword: '' } }); + const savedEmailAddress = useLibraryMutation(['keys.saveEmailAddress']); + return ( { // handle sign-up submission - console.log(data); - await signUpClicked(data.email, data.password); + await signUpClicked(data.email, data.password, reload, cloudBootstrap, savedEmailAddress); })} className="w-full" form={form} @@ -190,8 +209,7 @@ const Register = () => { size="md" variant="accent" onClick={form.handleSubmit(async (data) => { - console.log(data); - await signUpClicked(data.email, data.password); + await signUpClicked(data.email, data.password, reload, cloudBootstrap, savedEmailAddress); })} disabled={form.formState.isSubmitting} > diff --git a/packages/client/src/core.ts b/packages/client/src/core.ts index 6d464efa6..46f78cf1b 100644 --- a/packages/client/src/core.ts +++ b/packages/client/src/core.ts @@ -25,6 +25,7 @@ export type Procedures = { { key: "jobs.isActive", input: LibraryArgs, result: boolean } | { key: "jobs.reports", input: LibraryArgs, result: JobGroup[] } | { key: "keys.get", input: never, result: string } | + { key: "keys.getEmailAddress", input: LibraryArgs, result: string } | { key: "labels.count", input: LibraryArgs, result: number } | { key: "labels.get", input: LibraryArgs, result: Label | null } | { key: "labels.getForObject", input: LibraryArgs, result: Label[] } | @@ -110,6 +111,7 @@ export type Procedures = { { key: "jobs.pause", input: LibraryArgs, result: null } | { key: "jobs.resume", input: LibraryArgs, result: null } | { key: "keys.save", input: string, result: null } | + { key: "keys.saveEmailAddress", input: LibraryArgs, result: null } | { key: "labels.delete", input: LibraryArgs, result: null } | { key: "library.create", input: CreateLibraryArgs, result: LibraryConfigWrapped } | { key: "library.delete", input: string, result: null } | @@ -526,9 +528,13 @@ instance_id: number; * cloud_id is the ID of the cloud library this library is linked to. * If this is set we can assume the library is synced with the Cloud. */ -cloud_id?: string | null; generate_sync_operations?: boolean; version: LibraryConfigVersion } +cloud_id?: string | null; generate_sync_operations?: boolean; version: LibraryConfigVersion; +/** + * cloud_email_address is the email address of the user who owns the cloud library this library is linked to. + */ +cloud_email_address: string | null } -export type LibraryConfigVersion = "V0" | "V1" | "V2" | "V3" | "V4" | "V5" | "V6" | "V7" | "V8" | "V9" | "V10" | "V11" +export type LibraryConfigVersion = "V0" | "V1" | "V2" | "V3" | "V4" | "V5" | "V6" | "V7" | "V8" | "V9" | "V10" | "V11" | "V12" export type LibraryConfigWrapped = { uuid: string; instance_id: string; instance_public_key: RemoteIdentity; config: LibraryConfig } From 7fc34e007cd9b64749cd2d0b61b65502a566aa08 Mon Sep 17 00:00:00 2001 From: Arnab Chakraborty <11457760+Rocky43007@users.noreply.github.com> Date: Sun, 15 Dec 2024 18:25:16 -0500 Subject: [PATCH 22/34] Fix for styling --- interface/components/Login.tsx | 16 ++++++++++++++-- interface/package.json | 2 +- packages/ui/style/tailwind.js | 2 +- pnpm-lock.yaml | Bin 1159852 -> 1157139 bytes 4 files changed, 16 insertions(+), 4 deletions(-) diff --git a/interface/components/Login.tsx b/interface/components/Login.tsx index 8cdf489ef..72823eacd 100644 --- a/interface/components/Login.tsx +++ b/interface/components/Login.tsx @@ -140,7 +140,13 @@ const LoginForm = ({ reload, cloudBootstrap, setContinueWithEmail }: LoginProps) return ( { - await signInClicked(data.email, data.password, reload, cloudBootstrap, saveEmailAddress); + await signInClicked( + data.email, + data.password, + reload, + cloudBootstrap, + saveEmailAddress + ); })} className="w-full" form={form} @@ -211,7 +217,13 @@ const LoginForm = ({ reload, cloudBootstrap, setContinueWithEmail }: LoginProps) variant="accent" size="md" onClick={form.handleSubmit(async (data) => { - await signInClicked(data.email, data.password, reload, cloudBootstrap, saveEmailAddress); + await signInClicked( + data.email, + data.password, + reload, + cloudBootstrap, + saveEmailAddress + ); })} disabled={form.formState.isSubmitting} > diff --git a/interface/package.json b/interface/package.json index cbcb35c67..d3b132e7f 100644 --- a/interface/package.json +++ b/interface/package.json @@ -13,7 +13,6 @@ "@dnd-kit/utilities": "^3.2.2", "@headlessui/react": "^1.7.17", "@icons-pack/react-simple-icons": "^9.1.0", - "@spacedrive/rspc-client": "github:spacedriveapp/rspc#path:packages/client&6a77167495", "@phosphor-icons/react": "^2.0.13", "@radix-ui/react-dialog": "^1.0.5", "@radix-ui/react-dropdown-menu": "^2.0.6", @@ -27,6 +26,7 @@ "@sd/client": "workspace:*", "@sd/ui": "workspace:*", "@sentry/browser": "^7.74.1", + "@spacedrive/rspc-client": "github:spacedriveapp/rspc#path:packages/client&6a77167495", "@tanstack/react-query": "^5.59", "@tanstack/react-query-devtools": "^5.59", "@tanstack/react-table": "^8.20.5", diff --git a/packages/ui/style/tailwind.js b/packages/ui/style/tailwind.js index 318a1af40..e9e2091d8 100644 --- a/packages/ui/style/tailwind.js +++ b/packages/ui/style/tailwind.js @@ -11,7 +11,7 @@ module.exports = function (app, options) { content: [ `../../apps/${app}/src/**/*.{ts,tsx,html,stories.tsx}`, '../../packages/*/src/**/*.{ts,tsx,html,stories.tsx}', - '../../interface/{app,components}/*.{ts,tsx,html,stories.tsx}' + '../../interface/**/*.{ts,tsx,html,stories.tsx}', ], darkMode: 'class', theme: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 89c54054315f7df1dadeabdcee3532004b4ab748..994946ca837a8261a6697638de8b7073f4679b1d 100644 GIT binary patch delta 968 zcmb7?-%FEG7{_@x-*1w>i~t#b))XgX8U4IIj*kuIUgJtD}Ka zYFqQst7&H>?UXbO8b*x*4Ov6cFlm@I3MuWh_$?GDwVYdwB;8g@x?yKKx@Xva42NMa zHe3Wo{urE==+_H)z|;=kzCSK8Is`-c9bO)W0LH7JkopFp$D_kCr()gk-A_Ze_4GM`Rcqios5X7k4 zqTr=(Ozv#s73f`JwW{}kjQt^_4Rjo1XNu-g3J6ot-F9n@TEkzc`L! znw_DBAB;&@`&nk>nrBG^UP#F{YF%WV@>*8B3w$fqeX*9%;Z@cw{mX35Ki!de?;v%* z=UFGkzjBX>wk+^j9|l+X5$rIEBD!8Cx^Atf%#6Vm;lNDPY9c-;66?)r0y|Fh2s^e9 zi2JI$1+XOn+y3TrVvh^EJ_#4iy%w@DpH1|Rh*$V9A#&Hk|9O|RcO5dNM?|(-@=;1D H8h!K|5GRL5 delta 1354 zcmah}YiyHM7*5-Ly?p0<=Nv{ic4cwG@Dry9`V=yVYn>CvK#_K8iwn{&Nm~@zRSajs+P;^*z*mT%+IAoPaomSz$%-nKyh1x!!6Vd7;FZN{1h|HGJ zXfLw6RX4JOv;vp9Xe+$?ND;pLgeFlu!8K$*;{?x+ayw!nI*S+1i$ZvwvZzR$ql*0C zEWIl3=|*fPm55)V`wgAHo^Fxp5jyn9l1+J$o~16VpP_E}+o%=oH>nff-=di%-Hcga z`vP^!$ZeW$eyFw7r~HcPp}!ZGkgMUqk|stz**H%_mj5x-TiGBzf6;GE2==Nr)S9$) z@SoK89WrTkhID9wtm=Qy9@x}mMduOfxidxwf*)&2Pui@>w3)rsg^6cWedrEeg2%g= z6&>Adqf8%RyXx`br>q~hD^-EDNzEo(!pvvBN7;Ujl|VF*N0At1b&`y*jy1S3Lm$K1 z1H2Twl8W`%G%Gb~-1-NrF)FM5imyQJB-@ICQkI7|n@PTWXOivCgBnvr;ML8(MSnNi z4ig2LF5*I{gXF_D!+eXO6oi}9>bt(U5>r;do1rfwF~zzOi7EMupSdXaSD_fmzMEys zP55D!{b!+LbF2t&G?PO4^Bnupj4$TdlmBk$pHLST*a-!f-K-2Pm9!XNzecP>NxsMQ z;5w*p4b zDh(j~G_}jlVR7ApeL+!v=XCA&`;6qy-+|a_rXhNfYFHN%<9DqCiS>o@@USSiz<-lC bFet^ap3q((_l8BR9({X+A{+k{pP%|0ms1r* From 5935f7f51c8a061171ac18914846979db690e09a Mon Sep 17 00:00:00 2001 From: Arnab Chakraborty <11457760+Rocky43007@users.noreply.github.com> Date: Sun, 15 Dec 2024 18:34:20 -0500 Subject: [PATCH 23/34] Update tailwind.js --- packages/ui/style/tailwind.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ui/style/tailwind.js b/packages/ui/style/tailwind.js index e9e2091d8..59d22a478 100644 --- a/packages/ui/style/tailwind.js +++ b/packages/ui/style/tailwind.js @@ -11,7 +11,7 @@ module.exports = function (app, options) { content: [ `../../apps/${app}/src/**/*.{ts,tsx,html,stories.tsx}`, '../../packages/*/src/**/*.{ts,tsx,html,stories.tsx}', - '../../interface/**/*.{ts,tsx,html,stories.tsx}', + '../../interface/**/*.{ts,tsx,html,stories.tsx}' ], darkMode: 'class', theme: { From a445deb60b0298f46acfb83f9cf162b303d6a3b2 Mon Sep 17 00:00:00 2001 From: James Pine Date: Mon, 16 Dec 2024 17:07:47 -0800 Subject: [PATCH 24/34] more contributing guides --- CONTRIBUTING.md | 105 +++++++++++++--------- docs/developers/architecture/database.mdx | 14 +++ 2 files changed, 75 insertions(+), 44 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 45962bf1b..0175e153c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,17 +38,18 @@ This project uses [Cargo](https://doc.rust-lang.org/cargo/getting-started/instal To make changes locally, follow these steps: 1. Clone & enter the repository: - ``` - git clone https://github.com/spacedriveapp/spacedrive && cd spacedrive - ``` - Alternatively, if you’ve already cloned the repo locally, pull the latest changes with: `git pull` -> [!TIP] -> Consider running `pnpm clean` after pulling the repository if you're returning to it from previously to avoid old files conflicting. + ` git clone https://github.com/spacedriveapp/spacedrive && cd spacedrive + ` + Alternatively, if you’ve already cloned the repo locally, pull the latest changes with: `git pull` + > [!TIP] + > Consider running `pnpm clean` after pulling the repository if you're returning to it from previously to avoid old files conflicting. 2. Configure your system environment for Spacedrive development - - For Unix users (Linux / macOS), run: `./scripts/setup.sh` - - For Windows users, run: `.\scripts\setup.ps1` via PowerShell. -> [!NOTE] -> This script ([Unix](https://github.com/spacedriveapp/spacedrive/blob/main/scripts/setup.sh) / [Windows](https://github.com/spacedriveapp/spacedrive/blob/main/scripts/setup.ps1)) will check for if Rust and pnpm are installed then proceed to install any other required dependencies for Spacedrive to build via your system's respective package manager. + +- For Unix users (Linux / macOS), run: `./scripts/setup.sh` +- For Windows users, run: `.\scripts\setup.ps1` via PowerShell. + > [!NOTE] + > This script ([Unix](https://github.com/spacedriveapp/spacedrive/blob/main/scripts/setup.sh) / [Windows](https://github.com/spacedriveapp/spacedrive/blob/main/scripts/setup.ps1)) will check for if Rust and pnpm are installed then proceed to install any other required dependencies for Spacedrive to build via your system's respective package manager. + 3. Install NodeJS dependencies: `pnpm i` 4. Prepare the build: `pnpm prep`. This will run all necessary codegen and build required dependencies. @@ -58,13 +59,16 @@ To make changes locally, follow these steps: > The test files will be located in a directory called `test-data` in the root of the Spacedrive repository. To run the **desktop** app, run: + ``` pnpm tauri dev ``` + > [!NOTE] > The Tauri desktop app always runs its own instance of the backend and will not connect to a separately initiated `sd-server` instance. To run the **backend server**, run: + ``` cargo run -p sd-server ``` @@ -73,23 +77,29 @@ cargo run -p sd-server > If necessary, [DevTools](https://tauri.app/v1/guides/debugging/application/#webview-console) for the WebView can be opened by pressing Ctrl+Shift+I (Linux and Windows) or Command+Option+I (macOS) in the desktop app. > > Also, React DevTools can be launched using `pnpx react-devtools`. - However, it must be executed before starting the desktop app for it to connect. +> However, it must be executed before starting the desktop app for it to connect. To run the **web** app (requires the backend to be running), run: + ``` pnpm web dev ``` + > [!TIP] > You can also quickly launch the web interface together with the backend with: +> > ``` > pnpm dev:web > ``` To run the **e2e tests** for the web app: + ``` pnpm web test:e2e ``` + If you are developing a new test, you can execute Cypress in interactive mode with: + ``` pnpm web test:interactive ``` @@ -97,19 +107,21 @@ pnpm web test:interactive #### Troubleshooting - If you encounter any issues, ensure that you are using the following versions of Rust, Node.js and pnpm: - | tool | version | - | ---- | ------- | - | Rust | [`1.81`](rust-toolchain.toml) | + | tool | version | + | ---- | ------- | + | Rust | [`1.81`](rust-toolchain.toml) | | Node.js | [`18.18`](.nvmrc) | - | pnpm | `9.4.0` | + | pnpm | `9.4.0` | - [`rustup`](https://rustup.rs/) & [`nvm`](https://github.com/nvm-sh/nvm) should both pick up on the appropriate versions of the Rust Toolchain & Node respectively from the project automatically. +> **Note**: If you get a local migration error in development, you might need to set the following environment variables: [database documentation](docs/developers/architecture/database.mdx#environment-variables). - - After cleaning out your build artifacts using `pnpm clean`, it's necessary to re-run `pnpm prep`. +[`rustup`](https://rustup.rs/) & [`nvm`](https://github.com/nvm-sh/nvm) should both pick up on the appropriate versions of the Rust Toolchain & Node respectively from the project automatically. - - Make sure to read the [guidelines](https://spacedrive.com/docs/developers/prerequisites/guidelines) to ensure that your code follows a similar style to ours. +- After cleaning out your build artifacts using `pnpm clean`, it's necessary to re-run `pnpm prep`. - - After you finish making your changes and committing them to your branch, make sure to execute `pnpm autoformat` to fix any style inconsistency in your code. +- Make sure to read the [guidelines](https://spacedrive.com/docs/developers/prerequisites/guidelines) to ensure that your code follows a similar style to ours. + +- After you finish making your changes and committing them to your branch, make sure to execute `pnpm autoformat` to fix any style inconsistency in your code. ### Landing Page @@ -136,53 +148,58 @@ To run the mobile app: > Most modern phones use `arm64-v8a` while the Android Studio embedded emulator runs `x86_64` If you wish to debug directly on a local Android device: - - Install [ADB](https://developer.android.com/tools/adb) - - On macOS use [homebrew](https://brew.sh/): `brew install adb` - - [Configure debugging on your device](https://developer.android.com/tools/adb#Enabling) - - Select "Remember this device" & "Trust" when connecting over USB. - - Run `pnpm mobile android` with your device connected via USB. ->[!TIP] +- Install [ADB](https://developer.android.com/tools/adb) + - On macOS use [homebrew](https://brew.sh/): `brew install adb` +- [Configure debugging on your device](https://developer.android.com/tools/adb#Enabling) + - Select "Remember this device" & "Trust" when connecting over USB. +- Run `pnpm mobile android` with your device connected via USB. + +> [!TIP] > To access the logs from `sd-core` when running on device, run the following command: +> > ``` > adb logcat | grep -i com.spacedrive.app > ``` -#### iOS - - Install the latest version of [Xcode](https://apps.apple.com/au/app/xcode/id497799835) and Simulator if you wish to emulate an iOS device on your Mac. - - When running Xcode for the first time, make sure to select the latest version of iOS. - - Run `pnpm mobile ios` in the terminal to build & run the app on the Simulator. - - To run the app in debug mode with backend (`sd-core`) logging, comment out the following lines before running the above command: - https://github.com/spacedriveapp/spacedrive/blob/d180261ca5a93388486742e8f921e895e9ec26a4/apps/mobile/modules/sd-core/ios/build-rust.sh#L51-L54 +#### iOS + +- Install the latest version of [Xcode](https://apps.apple.com/au/app/xcode/id497799835) and Simulator if you wish to emulate an iOS device on your Mac. + - When running Xcode for the first time, make sure to select the latest version of iOS. +- Run `pnpm mobile ios` in the terminal to build & run the app on the Simulator. + - To run the app in debug mode with backend (`sd-core`) logging, comment out the following lines before running the above command: + https://github.com/spacedriveapp/spacedrive/blob/d180261ca5a93388486742e8f921e895e9ec26a4/apps/mobile/modules/sd-core/ios/build-rust.sh#L51-L54 You can now get backend (`sd-core`) logs from the Simulator by running the following command: - ``` - xcrun simctl launch --console booted com.spacedrive.app - ``` - - If you'd like to run the app on device, run: - ``` - pnpm mobile ios --device - ``` -> [!IMPORTANT] -> Note that you can only get `sd-core` logs from the app when running it on device by running the frontend and backend separately. + ``` + xcrun simctl launch --console booted com.spacedrive.app + ``` +- If you'd like to run the app on device, run: + ` pnpm mobile ios --device + ` + > [!IMPORTANT] + > Note that you can only get `sd-core` logs from the app when running it on device by running the frontend and backend separately. To run the backend (`sd-core`) separately, open up Xcode by running: + ``` xed apps/mobile/ios ``` + Select from the top if you wish to start on device or Simulator, and press play. -| Select Device | Run the App | Build & Core logs are found here | -| --- | --- | --- | -|![](./apps/landing/public/images/xcode-run-sd-core.01.png)|![](./apps/landing/public/images/xcode-run-sd-core.02.png)|![](./apps/landing/public/images/xcode-run-sd-core.03.png)| +| Select Device | Run the App | Build & Core logs are found here | +| ---------------------------------------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------- | +| ![](./apps/landing/public/images/xcode-run-sd-core.01.png) | ![](./apps/landing/public/images/xcode-run-sd-core.02.png) | ![](./apps/landing/public/images/xcode-run-sd-core.03.png) | To run the frontend, run the following: + ``` pnpm mobile start ``` + > [!IMPORTANT] > The frontend is not functional without the sd-core running as well. - ### Pull Request Once you have finished making your changes, create a pull request (PR) to submit them. diff --git a/docs/developers/architecture/database.mdx b/docs/developers/architecture/database.mdx index a0db0c78c..c41f646b1 100644 --- a/docs/developers/architecture/database.mdx +++ b/docs/developers/architecture/database.mdx @@ -22,3 +22,17 @@ Migrations are run by the Prisma migration engine on app launch. The databases file is SQLite and can be opened in any SQL viewer. ![A Spacedrive library database file open in Table Plus](/database-table-plus.webp) + +### Environment Variables + +The following environment variables can be used to control database behavior: + +- `SD_ACCEPT_DATA_LOSS`: When set to `"true"`, allows operations that might result in data loss. This should only be used in development or testing environments where data loss is acceptable. + +- `SD_FORCE_RESET_DB`: When set to `"true"`, forces a reset of the database. This will clear all existing data. Use with extreme caution and only in development environments. + +You can set these variables when running the app, e.g. `SD_ACCEPT_DATA_LOSS=true pnpm tauri dev`. + +These environment variables are primarily used during development and testing. They should not be used in production environments unless you fully understand the implications. + +⚠️ **Warning**: Setting these variables can result in permanent data loss. Use them only in development environments or when you explicitly need to reset or modify database behavior. From 14353bf0a227a8787b679f62d3ec3883858e8f29 Mon Sep 17 00:00:00 2001 From: Arnab Chakraborty <11457760+Rocky43007@users.noreply.github.com> Date: Tue, 17 Dec 2024 12:24:01 -0500 Subject: [PATCH 25/34] Fix note and tip blocks in `CONTRIBUTING.md` --- CONTRIBUTING.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0175e153c..855865dae 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,14 +41,14 @@ To make changes locally, follow these steps: ` git clone https://github.com/spacedriveapp/spacedrive && cd spacedrive ` Alternatively, if you’ve already cloned the repo locally, pull the latest changes with: `git pull` - > [!TIP] - > Consider running `pnpm clean` after pulling the repository if you're returning to it from previously to avoid old files conflicting. -2. Configure your system environment for Spacedrive development +> [!TIP] +> Consider running `pnpm clean` after pulling the repository if you're returning to it from previously to avoid old files conflicting. +3. Configure your system environment for Spacedrive development - For Unix users (Linux / macOS), run: `./scripts/setup.sh` - For Windows users, run: `.\scripts\setup.ps1` via PowerShell. - > [!NOTE] - > This script ([Unix](https://github.com/spacedriveapp/spacedrive/blob/main/scripts/setup.sh) / [Windows](https://github.com/spacedriveapp/spacedrive/blob/main/scripts/setup.ps1)) will check for if Rust and pnpm are installed then proceed to install any other required dependencies for Spacedrive to build via your system's respective package manager. +> [!NOTE] +> This script ([Unix](https://github.com/spacedriveapp/spacedrive/blob/main/scripts/setup.sh) / [Windows](https://github.com/spacedriveapp/spacedrive/blob/main/scripts/setup.ps1)) will check for if Rust and pnpm are installed then proceed to install any other required dependencies for Spacedrive to build via your system's respective package manager. 3. Install NodeJS dependencies: `pnpm i` 4. Prepare the build: `pnpm prep`. This will run all necessary codegen and build required dependencies. From e8c59bb7a403862c387adda4f43348cfdbe4a9d6 Mon Sep 17 00:00:00 2001 From: Arnab Chakraborty <11457760+Rocky43007@users.noreply.github.com> Date: Tue, 17 Dec 2024 12:25:45 -0500 Subject: [PATCH 26/34] Update CONTRIBUTING.md --- CONTRIBUTING.md | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 855865dae..8a6a160fb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -170,14 +170,12 @@ If you wish to debug directly on a local Android device: - To run the app in debug mode with backend (`sd-core`) logging, comment out the following lines before running the above command: https://github.com/spacedriveapp/spacedrive/blob/d180261ca5a93388486742e8f921e895e9ec26a4/apps/mobile/modules/sd-core/ios/build-rust.sh#L51-L54 You can now get backend (`sd-core`) logs from the Simulator by running the following command: - ``` - xcrun simctl launch --console booted com.spacedrive.app - ``` -- If you'd like to run the app on device, run: - ` pnpm mobile ios --device - ` - > [!IMPORTANT] - > Note that you can only get `sd-core` logs from the app when running it on device by running the frontend and backend separately. + ``` + xcrun simctl launch --console booted com.spacedrive.app + ``` +- If you'd like to run the app on device, run: `pnpm mobile ios --device` +> [!IMPORTANT] +> Note that you can only get `sd-core` logs from the app when running it on device by running the frontend and backend separately. To run the backend (`sd-core`) separately, open up Xcode by running: From 78afa0c014ccb07415f042c63e7d7183068d9ea1 Mon Sep 17 00:00:00 2001 From: Lynx <141365347+iLynxcat@users.noreply.github.com> Date: Tue, 17 Dec 2024 19:10:58 -0600 Subject: [PATCH 27/34] Fix font in landing and app --- apps/desktop/src/App.tsx | 2 +- apps/storybook/.storybook/preview.ts | 2 +- packages/ui/package.json | 2 +- packages/ui/style/tailwind.js | 13 +++++++++---- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 5ad32744b..e65825200 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -16,7 +16,7 @@ import { } from '@sd/interface'; import { RouteTitleContext } from '@sd/interface/hooks/useRouteTitle'; -import '@sd/ui/style/style.scss'; +import '@sd/ui/style'; import SuperTokens from 'supertokens-web-js'; import EmailPassword from 'supertokens-web-js/recipe/emailpassword'; diff --git a/apps/storybook/.storybook/preview.ts b/apps/storybook/.storybook/preview.ts index 79860c5ec..1384b87e1 100644 --- a/apps/storybook/.storybook/preview.ts +++ b/apps/storybook/.storybook/preview.ts @@ -1,6 +1,6 @@ import type { Preview } from '@storybook/react'; -import '@sd/ui/style/style.scss'; +import '@sd/ui/style'; const preview: Preview = { parameters: { diff --git a/packages/ui/package.json b/packages/ui/package.json index 60f785072..ec7609f55 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -4,7 +4,7 @@ "license": "GPL-3.0-only", "main": "src/index.ts", "types": "src/index.ts", - "sideEffects": false, + "sideEffects": ["./style/index.js", "./style/style.scss"], "exports": { ".": "./src/index.ts", "./src/forms": "./src/forms/index.ts", diff --git a/packages/ui/style/tailwind.js b/packages/ui/style/tailwind.js index 59d22a478..577449833 100644 --- a/packages/ui/style/tailwind.js +++ b/packages/ui/style/tailwind.js @@ -7,6 +7,9 @@ function alpha(variableName) { } module.exports = function (app, options) { + /** + * @type {import('tailwindcss').Config} + */ let config = { content: [ `../../apps/${app}/src/**/*.{ts,tsx,html,stories.tsx}`, @@ -41,10 +44,6 @@ module.exports = function (app, options) { '7xl': '5rem' }, extend: { - fontFamily: { - plex: ['var(--font-plex-sans)', ...defaultTheme.fontFamily.sans], - sans: ['var(--font-inter)', ...defaultTheme.fontFamily.sans] - }, colors: { accent: { DEFAULT: alpha('--color-accent'), @@ -178,6 +177,12 @@ module.exports = function (app, options) { ] }; + if (app === 'landing') { + console.log('CONFIGURING TAILWIND for Landing'); + config.theme.fontFamily.sans = ['var(--font-inter)', ...defaultTheme.fontFamily.sans]; + config.theme.fontFamily.plex = ['var(--font-plex-sans)', ...defaultTheme.fontFamily.sans]; + } + return config; }; From 5f2b57d59297b264310372559b183cdd954bc1d9 Mon Sep 17 00:00:00 2001 From: Arnab Chakraborty <11457760+Rocky43007@users.noreply.github.com> Date: Thu, 19 Dec 2024 17:51:56 -0500 Subject: [PATCH 28/34] Better UI/UX for accepting cloud sync connections --- interface/components/RequestAddDialog.tsx | 48 +++++++++++++++++------ interface/index.tsx | 22 +---------- 2 files changed, 37 insertions(+), 33 deletions(-) diff --git a/interface/components/RequestAddDialog.tsx b/interface/components/RequestAddDialog.tsx index e8d1bf035..630681c2e 100644 --- a/interface/components/RequestAddDialog.tsx +++ b/interface/components/RequestAddDialog.tsx @@ -1,18 +1,26 @@ import { ArrowRight } from '@phosphor-icons/react'; import { useQueryClient } from '@tanstack/react-query'; import { useNavigate } from 'react-router'; -import { HardwareModel, useBridgeMutation, useZodForm } from '@sd/client'; +import { + CloudDevice, + CloudP2PNotifyUser, + CloudP2PTicket, + CloudSyncGroupWithDevices, + HardwareModel, + useBridgeMutation, + useZodForm +} from '@sd/client'; import { Dialog, toast, useDialog, UseDialogProps, z } from '@sd/ui'; import { Icon } from '~/components'; import { useLocale } from '~/hooks'; import { hardwareModelToIcon } from '~/util/hardware'; import { usePlatform } from '~/util/Platform'; +type ReceivedJoinRequest = Extract; + export default ( props: { - device_name: string; - device_model: HardwareModel; - library_name: string; + data: ReceivedJoinRequest['data']; } & UseDialogProps ) => { // PROPS = device_name, device_model, library_name @@ -27,24 +35,40 @@ export default ( const queryClient = useQueryClient(); const form = useZodForm({ defaultValues: { libraryId: 'select_library' } }); + const userResponse = useBridgeMutation('cloud.userResponse'); // adapted from another dialog - we can change the form submit/remove form if needed but didn't want to // unnecessarily remove code - const onSubmit = form.handleSubmit(async (data) => { + const onSubmit = form.handleSubmit(async (_d) => { try { // const library = await joinLibrary.mutateAsync(data.libraryId); - const library = { uuid: '1234' }; // dummy data + userResponse.mutate({ + kind: 'AcceptDeviceInSyncGroup', + data: { + ticket: props.data.ticket, + accepted: { + id: props.data.sync_group.library.pub_id, + name: props.data.sync_group.library.name, + description: null + } + } + }); queryClient.setQueryData(['library.list'], (libraries: any) => { // The invalidation system beat us to it - if ((libraries || []).find((l: any) => l.uuid === library.uuid)) return libraries; + if ( + (libraries || []).find( + (l: any) => l.uuid === props.data.sync_group.library.pub_id + ) + ) + return libraries; - return [...(libraries || []), library]; + return [...(libraries || []), props.data.sync_group.library]; }); if (platform.refreshMenuBar) platform.refreshMenuBar(); - navigate(`/${library.uuid}`, { replace: true }); + navigate(`/${props.data.sync_group.library.pub_id}`, { replace: true }); } catch (e: any) { console.error(e); toast.error(e); @@ -71,12 +95,12 @@ export default (
-

{props.device_name}

+

{props.data.asking_device.name}

{/* library */} @@ -88,7 +112,7 @@ export default ( size={48} className="mr-2" /> -

{props.library_name}

+

{props.data.sync_group.library.name}

diff --git a/interface/index.tsx b/interface/index.tsx index da5e021a6..b6ffe671c 100644 --- a/interface/index.tsx +++ b/interface/index.tsx @@ -96,27 +96,7 @@ export function SpacedriveInterfaceRoot({ children }: PropsWithChildren) { console.log('Received cloud service notification', d); switch (d.kind) { case 'ReceivedJoinSyncGroupRequest': - // WARNING: This is a debug solution to accept the device into the sync group. THIS SHOULD NOT MAKE IT TO PRODUCTION - userResponse.mutate({ - kind: 'AcceptDeviceInSyncGroup', - data: { - ticket: d.data.ticket, - accepted: { - id: d.data.sync_group.library.pub_id, - name: d.data.sync_group.library.name, - description: null - } - } - }); - // TODO: Move the code above into the dialog below (@Rocky43007) - // dialogManager.create((dp) => ( - // - // )); + dialogManager.create((dp) => ); break; default: toast({ title: 'Cloud Service Notification', body: d.kind }, { type: 'info' }); From 0090af6b2fe5cf2636eab8d1c22d487ff4ee8c0a Mon Sep 17 00:00:00 2001 From: Arnab Chakraborty <11457760+Rocky43007@users.noreply.github.com> Date: Thu, 26 Dec 2024 00:43:31 +0300 Subject: [PATCH 29/34] [ENG-1402] Drag & drop into OS (#2839) * Wip but functional drag drop out. * Working transparent image for drag & drop * Clean up * Drag & Drop into OS from indexed locations * autoformat --- Cargo.lock | Bin 341015 -> 342323 bytes apps/desktop/package.json | 5 ++- apps/desktop/src-tauri/Cargo.toml | 3 +- .../src-tauri/capabilities/default.json | 1 + apps/desktop/src-tauri/src/main.rs | 1 + apps/desktop/src/App.tsx | 3 ++ .../Explorer/View/GridView/Item/index.tsx | 38 +++++++++++++++++- packages/assets/images/Transparent.png | Bin 0 -> 3083 bytes packages/assets/images/index.ts | 4 +- pnpm-lock.yaml | Bin 1157139 -> 1157501 bytes 10 files changed, 50 insertions(+), 5 deletions(-) create mode 100644 packages/assets/images/Transparent.png diff --git a/Cargo.lock b/Cargo.lock index 1904d0aeb90c58db344606c9e996f93f79f9b6f2..433b56b90d55008acfc815b74b27a14de1a2ed47 100644 GIT binary patch delta 630 zcmZ8f&r4KM6z0ym1Gma-OE9!7KiikEzWno?;KdJ%&k>kjklv2J>4v0 zeS{~JGD&#g+y|Z{Hl>Ep5G0e{X%+$tu^^0#*n62Osq^BA;ly|+gwf5t=Ld1aHZ;6k zimC_Pk&Cp}FJj zuh-zz>B0z1YP1bb_8^_ToDEFEB~&t$l{BdWq!4cDMv&v&kIT*@92y!IH%H_#S%Bcn1LtP}^bw;yo_{YzG| z&z~?9{wE)N!NYwpK9BqJBqrzau55M@|LlR4?|9-cp@d7z7!h0uW+WF%8BbGBH8naG zR%mUM;?gjYCM1wpCC)gZd2FRhnkU{5!c+yPCCn| zk7Jc;k%wZvak0;Due?fQ#dzwiVZm~36URduNRmGXXNszXI@oT|-x{}9H#Isdc>l5A D?`F#% delta 160 zcmdn|No4v9kq!P&HlKfby?XkC6^yde?Q-(%hGyOjBH@Ad){mQVAi2ZS+mOgFV+7T8{Og@u&~!Ji&=lSOp< tnrkf6kOa2_1$DWnpa0CFzx~4#mQ8Hap0WgO|MiAt7bBR(^MOU(69D&fLYx2q diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 7dc516d43..2d1f8da01 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -12,12 +12,13 @@ "lint": "eslint src --cache" }, "dependencies": { - "@spacedrive/rspc-client": "github:spacedriveapp/rspc#path:packages/client&6a77167495", - "@spacedrive/rspc-tauri": "github:spacedriveapp/rspc#path:packages/tauri&6a77167495", + "@crabnebula/tauri-plugin-drag": "^2.0.0", "@remix-run/router": "=1.13.1", "@sd/client": "workspace:*", "@sd/interface": "workspace:*", "@sd/ui": "workspace:*", + "@spacedrive/rspc-client": "github:spacedriveapp/rspc#path:packages/client&6a77167495", + "@spacedrive/rspc-tauri": "github:spacedriveapp/rspc#path:packages/tauri&6a77167495", "@t3-oss/env-core": "^0.7.1", "@tanstack/react-query": "^5.59", "@tauri-apps/api": "=2.0.3", diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index a01f07dee..0041164aa 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -47,7 +47,8 @@ tauri-plugin-shell = "=2.0.2" tauri-plugin-updater = "=2.0.2" # memory allocator -mimalloc = { workspace = true } +mimalloc = { workspace = true } +tauri-plugin-drag = "2.0.0" [dependencies.tauri] features = ["linux-libxdo", "macos-private-api", "native-tls-vendored", "unstable"] diff --git a/apps/desktop/src-tauri/capabilities/default.json b/apps/desktop/src-tauri/capabilities/default.json index eb899e7f2..1f163023b 100644 --- a/apps/desktop/src-tauri/capabilities/default.json +++ b/apps/desktop/src-tauri/capabilities/default.json @@ -27,6 +27,7 @@ "core:window:allow-start-dragging", "core:webview:allow-internal-toggle-devtools", "cors-fetch:default", + "drag:default", { "identifier": "http:default", "allow": [ diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index 2842f7210..c4d88ed45 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -358,6 +358,7 @@ async fn main() -> tauri::Result<()> { .plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_http::init()) + .plugin(tauri_plugin_drag::init()) // TODO: Bring back Tauri Plugin Window State - it was buggy so we removed it. .plugin(tauri_plugin_updater::Builder::new().build()) .plugin(updater::plugin()) diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index e65825200..880932f5a 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -18,6 +18,7 @@ import { RouteTitleContext } from '@sd/interface/hooks/useRouteTitle'; import '@sd/ui/style'; +import { startDrag } from '@crabnebula/tauri-plugin-drag'; import SuperTokens from 'supertokens-web-js'; import EmailPassword from 'supertokens-web-js/recipe/emailpassword'; import Passwordless from 'supertokens-web-js/recipe/passwordless'; @@ -46,6 +47,7 @@ import { createUpdater } from './updater'; declare global { interface Window { enableCORSFetch: (enable: boolean) => void; + startDrag: typeof startDrag; } } @@ -72,6 +74,7 @@ export default function App() { // This tells Tauri to show the current window because it's finished loading commands.appReady(); window.enableCORSFetch(true); + window.startDrag = startDrag; // .then(() => { // if (import.meta.env.PROD) window.fetch = fetch; // }); diff --git a/interface/app/$libraryId/Explorer/View/GridView/Item/index.tsx b/interface/app/$libraryId/Explorer/View/GridView/Item/index.tsx index a9d01994f..81e2e788d 100644 --- a/interface/app/$libraryId/Explorer/View/GridView/Item/index.tsx +++ b/interface/app/$libraryId/Explorer/View/GridView/Item/index.tsx @@ -1,9 +1,11 @@ +import { Transparent } from '@sd/assets/images'; import clsx from 'clsx'; -import { memo, useMemo } from 'react'; +import { memo, useEffect, useMemo } from 'react'; import { getItemFilePath, getItemObject, humanizeSize, + libraryClient, Tag, useExplorerLayoutStore, useLibraryQuery, @@ -11,6 +13,7 @@ import { type ExplorerItem } from '@sd/client'; import { useLocale } from '~/hooks'; +import { usePlatform } from '~/util/Platform'; import { useExplorerContext } from '../../../Context'; import { ExplorerDraggable } from '../../../ExplorerDraggable'; @@ -19,6 +22,7 @@ import { FileThumb } from '../../../FilePath/Thumb'; import { useFrame } from '../../../FilePath/useFrame'; import { explorerStore } from '../../../store'; import { useExplorerDraggable } from '../../../useExplorerDraggable'; +import { useExplorerItemData } from '../../../useExplorerItemData'; import { RenamableItemText } from '../../RenamableItemText'; import { ViewItem } from '../../ViewItem'; import { GridViewItemContext, useGridViewItemContext } from './Context'; @@ -107,6 +111,38 @@ const ItemMetadata = memo(() => { const item = useGridViewItemContext(); const { isDroppable } = useExplorerDroppableContext(); const explorerLayout = useExplorerLayoutStore(); + const dragState = useSelector(explorerStore, (s) => s.drag); + + useEffect(() => { + (async () => { + if (dragState?.type === 'dragging' && dragState.items.length > 0) { + const items = await Promise.all( + dragState.items.map(async (item) => { + const data = getItemFilePath(item); + if (!data) return; + + const file_path = + 'path' in data + ? data.path + : await libraryClient.query(['files.getPath', data.id]); + + return { + type: 'explorer-item', + file_path: file_path + }; + }) + ); + + // get image src from Transparent + const image = Transparent.split('/@fs')[1]; + + (window as any).startDrag({ + item: items.filter(Boolean).map((item) => item?.file_path), + icon: image + }); + } + })(); + }, [dragState]); const isRenaming = useSelector(explorerStore, (s) => s.isRenaming && item.selected); diff --git a/packages/assets/images/Transparent.png b/packages/assets/images/Transparent.png new file mode 100644 index 0000000000000000000000000000000000000000..4c847ef880a418930c82498f6b6e5b09ce1e0122 GIT binary patch literal 3083 zcmeAS@N?(olHy`uVBq!ia0y~yU~^z#U~b`H0g42>m}&wkmUKs7M+SzC{oH>NS%G|o zWRD45bDP46hOx7_4S6Fo+k-*%fF5lweBoc6VW5Sk%eSZW&>SQ!{ZTWuFY(U6;;l9^VC VTZ2|Q|2k0EdAjE{(D zy8C%rW`#NCWq6mBRs~s@W)(Q4XJtoa6<4@L1{?T>FL|7xcS0su2ik^%q# delta 159 zcmex+$!+olw+;QA(^nlA5So0xA!D+3;m&3$?(I_CjD6dt@37|PZI3y`2*gZ4%nZaV zK+FonY(UHo#2njW4sjk#pMF-It8M#EEv}D0wjWUD{=yH@D`UdlE@Q$2#Jt;OO!(FY zPCw_%$~JxGVSbV6e_Qyt+IP3|ZQtF>pE+~-z7_ml(+igKbGG-&3vBO|7yQr(05|_Z A`Tzg` From 09cd5a61839749e9f5cdac10383ea31ace165804 Mon Sep 17 00:00:00 2001 From: lzt1008 <75012451+lzt1008@users.noreply.github.com> Date: Thu, 26 Dec 2024 05:45:04 +0800 Subject: [PATCH 30/34] Refine i18n zh-CN translations for better accuracy (#2840) Refine zh-CN translations for better accuracy --- interface/locales/zh-CN/common.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/interface/locales/zh-CN/common.json b/interface/locales/zh-CN/common.json index dc3bc2799..709149dc3 100644 --- a/interface/locales/zh-CN/common.json +++ b/interface/locales/zh-CN/common.json @@ -29,7 +29,7 @@ "app_crashed_description": "出现了一些错误...", "appearance": "外观", "appearance_description": "调整客户端的外观。", - "apply": "申请", + "apply": "应用", "archive": "存档", "archive_coming_soon": "存档位置功能即将推出……", "archive_info": "将库中的数据作为存档提取,有利于保留位置的目录结构。", @@ -225,7 +225,7 @@ "export_library": "导出库", "export_library_coming_soon": "导出库功能即将推出", "export_library_description": "将这个库导出到一个文件。", - "extension": "扩大", + "extension": "扩展名", "extensions": "扩展", "extensions_description": "安装扩展来扩展这个客户端的功能。", "fahrenheit": "华氏度", @@ -301,7 +301,7 @@ "grid_gap": "间隙", "grid_view": "网格视图", "grid_view_notice_description": "网格视图以缩略图形式显示文件和文件夹,以便直观、快速识别要寻找的文件。", - "hidden": "隐", + "hidden": "已隐藏", "hidden_label": "阻止位置及其内容出现在汇总分类、搜索和标签中,除非启用了“显示隐藏项目”。", "hide_in_library_search": "在库搜索中隐藏", "hide_in_library_search_description": "在搜索整个库时从结果中隐藏带有此标签的文件。", @@ -390,7 +390,7 @@ "local": "本地", "local_locations": "本地位置", "local_node": "本地节点", - "location": "地点", + "location": "位置", "location_added_successfully": "位置添加成功。", "location_connected_tooltip": "位置正在监视变化", "location_deleted_successfully": "位置删除成功。", From 9de0c9423b43923e623b217345a079bd2adfc20b Mon Sep 17 00:00:00 2001 From: Arnab Chakraborty <11457760+Rocky43007@users.noreply.github.com> Date: Mon, 30 Dec 2024 21:47:28 +0300 Subject: [PATCH 31/34] Better Drag & Drop into OS Now it doesn't freeze the entire app and UI, and the drag & drop into OS system is only called when the mouse leaves the app's boundaries! However, the event/system doesn't cancel when you re-enter the application, and is something I'm still working on. --- .zed/settings.json | 66 ++++++ Cargo.lock | Bin 342323 -> 342350 bytes apps/desktop/src-tauri/Cargo.toml | 4 +- apps/desktop/src-tauri/src/drag.rs | 223 ++++++++++++++++++ apps/desktop/src-tauri/src/main.rs | 4 +- apps/desktop/src/App.tsx | 134 ++++++++++- apps/desktop/src/commands.ts | 28 +++ .../Explorer/View/GridView/Item/index.tsx | 32 --- interface/app/$libraryId/Explorer/index.tsx | 20 +- 9 files changed, 461 insertions(+), 50 deletions(-) create mode 100644 .zed/settings.json create mode 100644 apps/desktop/src-tauri/src/drag.rs diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 000000000..fc6bb453b --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,66 @@ +// Folder-specific settings +// +// For a full list of overridable settings, and general information on folder-specific settings, +// see the documentation: https://zed.dev/docs/configuring-zed#settings-files +{ + "inlay_hints": { + "enabled": false + }, + "languages": { + "Rust": { + "enable_language_server": true, + "formatter": "language_server", + "inlay_hints": { + "enabled": true, + "show_type_hints": true, + "show_parameter_hints": true, + "show_other_hints": true, + "show_background": false, + "edit_debounce_ms": 700, + "scroll_debounce_ms": 50 + } + }, + "TOML": { + "formatter": "language_server" + } + }, + "lsp": { + "rust-analyzer": { + "initialization_options": { + "procMacro": { + "enable": true + }, + "diagnostics": { + "experimental": { + "enable": false + } + }, + "showUnlinkedFileNotification": false + } + } + }, + "file_scan_exclusions": [ + "node_modules", + "**/node_modules", + "**/bower_components", + "**/*.code-search", + "**/*.contentlayer", + "**/*.next", + "**/dist", + "apps/mobile/ios/Pods", + "apps/mobile/android", + "apps/mobile/ios", + "**/.git", + "**/.svn", + "**/.hg", + "**/CVS", + "**/.DS_Store" + ], + "format_on_save": "on", + "ensure_final_newline_on_save": true, + "remove_trailing_whitespace_on_save": true, + "tab_size": 4, + "hard_tabs": false, + "show_whitespaces": "selection", + "show_completion_documentation": true +} diff --git a/Cargo.lock b/Cargo.lock index 433b56b90d55008acfc815b74b27a14de1a2ed47..74acb0b8230923f312656942cd18b0fb82d8917e 100644 GIT binary patch delta 54 zcmV-60LlNe@)XYU6o7;QgaWh!aif=|k^>SAWMXx5A}k6ZB4l!5XP4ip0U?)YnE?-n MrK1D4rK1Fnm0GzKn*aa+ delta 30 mcmX^2No4aUk%kt=7N#xCg^O8Il1hu)CoE>(K4CG-zNrA!*A0{a diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 0041164aa..e65b09eab 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -32,6 +32,7 @@ thiserror = { workspace = true } tokio = { workspace = true, features = ["sync"] } tracing = { workspace = true } uuid = { workspace = true, features = ["serde"] } +base64 = { workspace = true } # Specific Desktop dependencies # WARNING: Do NOT enable default features, as that vendors dbus (see below) @@ -45,10 +46,11 @@ tauri-plugin-http = "=2.0.3" tauri-plugin-os = "=2.0.1" tauri-plugin-shell = "=2.0.2" tauri-plugin-updater = "=2.0.2" +tauri-plugin-drag = "2.0.0" +drag = "2.0.0" # memory allocator mimalloc = { workspace = true } -tauri-plugin-drag = "2.0.0" [dependencies.tauri] features = ["linux-libxdo", "macos-private-api", "native-tls-vendored", "unstable"] diff --git a/apps/desktop/src-tauri/src/drag.rs b/apps/desktop/src-tauri/src/drag.rs new file mode 100644 index 000000000..e4f09b849 --- /dev/null +++ b/apps/desktop/src-tauri/src/drag.rs @@ -0,0 +1,223 @@ +// Import required dependencies for drag and drop operations, serialization, and async functionality +use drag::{DragItem, Image, Options}; +use serde::{Deserialize, Serialize}; +use specta::Type; +use std::path::PathBuf; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; +use tauri::{ipc::Channel, Manager, PhysicalPosition, State, WebviewWindow}; + +// DragState wraps a thread-safe boolean flag to track drag operation status +#[derive(Clone)] +pub struct DragState(pub Arc>); + +// Default implementation for DragState initializes with false +impl Default for DragState { + fn default() -> Self { + Self(Arc::new(Mutex::new(false))) + } +} + +// Enum to represent the result of a drag operation (serializable for IPC) +#[derive(Serialize, Deserialize, Type, Clone)] +pub enum WrappedDragResult { + Dropped, + Cancel, +} + +// Structure to hold cursor position coordinates (serializable for IPC) +#[derive(Serialize, Deserialize, Type, Clone)] +pub struct WrappedCursorPosition { + x: i32, + y: i32, +} + +// Combined structure for drag operation results (serializable for IPC) +#[derive(Serialize, Deserialize, Type, Clone)] +pub struct CallbackResult { + result: WrappedDragResult, + #[serde(rename = "cursorPos")] + cursor_pos: WrappedCursorPosition, +} + +// Conversion implementations for drag-rs types to our wrapped types +impl From for WrappedDragResult { + fn from(result: drag::DragResult) -> Self { + match result { + drag::DragResult::Dropped => WrappedDragResult::Dropped, + drag::DragResult::Cancel => WrappedDragResult::Cancel, + } + } +} + +impl From for WrappedCursorPosition { + fn from(pos: drag::CursorPosition) -> Self { + WrappedCursorPosition { x: pos.x, y: pos.y } + } +} + +// Global flag to track if position tracking is active +static TRACKING: AtomicBool = AtomicBool::new(false); + +#[tauri::command(async)] +/// Initiates a drag and drop operation with cursor position tracking +/// +/// # Arguments +/// * `window` - The Tauri window instance +/// * `_state` - Current drag state (unused) +/// * `files` - Vector of file paths to be dragged +/// * `icon_path` - Path to the preview icon for the drag operation +/// * `on_event` - Channel for communicating drag operation events back to the frontend +#[specta::specta] +pub async fn start_drag( + window: WebviewWindow, + _state: State<'_, DragState>, + files: Vec, + icon_path: String, + on_event: Channel, +) -> Result<(), String> { + // Fast atomic swap for tracking state + match TRACKING.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) { + Ok(_) => { + println!("Starting position tracking"); + } + Err(_) => { + // If already tracking, stop previous instance quickly + TRACKING.store(false, Ordering::SeqCst); + tokio::time::sleep(tokio::time::Duration::from_millis(16)).await; + TRACKING.store(true, Ordering::SeqCst); + println!("Restarting position tracking"); + } + } + + // Pre-allocate resources before spawning task + let window_handle = Arc::new(window); + let app_handle = window_handle.app_handle(); + + // Initialize control flags + let cancel_flag = Arc::new(AtomicBool::new(false)); + let is_completed = Arc::new(AtomicBool::new(false)); + + // Prepare resources once with minimal cloning + let tracking_resources = Arc::new((files.clone(), icon_path.clone(), Arc::new(on_event))); + + println!("Starting position tracking"); + + // Get handles for window and app management + let window_clone = window_handle.clone(); + let app_handle_owned = app_handle.to_owned(); + let window_owned = window_clone.to_owned(); + + // Control flags for operation state + let is_completed_clone = is_completed.clone(); + + // Spawn background task for cursor tracking + tokio::spawn(async move { + // Initialize tracking state + let mut last_position = (0.0, 0.0); + let mut last_message_time = Instant::now(); + let threshold = 1.0; // Minimum movement threshold + let message_debounce = Duration::from_millis(32); // State update interval + let mut was_inside = false; + + // Main tracking loop + while TRACKING.load(Ordering::SeqCst) && !is_completed.load(Ordering::SeqCst) { + let window_for_check = window_owned.clone(); + // Skip if window is not focused + if !window_for_check.is_focused().unwrap_or(false) { + tokio::time::sleep(tokio::time::Duration::from_millis(8)).await; + continue; + } + + // Get current cursor and window positions + if let (Ok(cursor_position), Ok(window_position), Ok(window_size)) = ( + window_for_check.cursor_position(), + window_for_check.outer_position(), + window_for_check.inner_size(), + ) { + // Calculate cursor position relative to window + let relative_position = PhysicalPosition::new( + cursor_position.x - window_position.x as f64, + cursor_position.y - window_position.y as f64, + ); + + // Check if cursor is inside window boundaries + let is_inside = relative_position.x >= 0.0 + && relative_position.y >= 0.0 + && relative_position.x <= window_size.width as f64 + && relative_position.y <= window_size.height as f64; + + // Process state changes if cursor moved enough + if is_inside != was_inside + && ((relative_position.x - last_position.0).abs() > threshold + || (relative_position.y - last_position.1).abs() > threshold) + { + let now = Instant::now(); + if now.duration_since(last_message_time) >= message_debounce { + // Prepare resources for drag operation + let files_for_drag = tracking_resources.0.clone(); + let icon_path_for_drag = tracking_resources.1.clone(); + let on_event_for_drag = tracking_resources.2.clone(); + let is_completed = is_completed_clone.clone(); + let cancel_flag_clone = cancel_flag.clone(); + let window_for_drag = window_owned.clone(); + + // Execute drag operation on main thread + app_handle_owned + .run_on_main_thread(move || { + if !is_inside { + println!("Starting drag operation"); + // Create drag items + let paths: Vec = + files_for_drag.iter().map(PathBuf::from).collect(); + let item = DragItem::Files(paths); + let preview_icon = + Image::File(PathBuf::from(&icon_path_for_drag)); + + // Start the drag operation + if let Ok(_) = drag::start_drag( + &window_for_drag, + item, + preview_icon, + move |result, cursor_pos| { + // Send result back to frontend + let _ = on_event_for_drag.send(CallbackResult { + result: result.into(), + cursor_pos: cursor_pos.into(), + }); + // Mark operation as completed + is_completed.store(true, Ordering::SeqCst); + TRACKING.store(false, Ordering::SeqCst); + }, + Options::default(), + ) { + println!("Drag operation started"); + } + } else { + println!("Cursor returned to window"); + cancel_flag_clone.store(true, Ordering::SeqCst); + // We have this for now, but technically, it doesn't do anything. + // I'm still trying to figure out how to cancel mid-drag without the user having to cancel the dragging on the frontend too. + // - @Rocky43007 + } + }) + .unwrap_or_default(); + + // Update tracking state + last_message_time = now; + was_inside = is_inside; + last_position = (relative_position.x, relative_position.y); + } + } + } + + // Prevent excessive CPU usage + tokio::time::sleep(tokio::time::Duration::from_millis(8)).await; + } + + println!("Tracking instance stopped"); + }); + + Ok(()) +} diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index c4d88ed45..6a821d260 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -22,6 +22,7 @@ use tokio::task::block_in_place; use tokio::time::sleep; use tracing::{debug, error}; +mod drag; mod file; mod menu; mod tauri_plugins; @@ -200,6 +201,7 @@ async fn main() -> tauri::Result<()> { set_menu_bar_item_state, request_fda_macos, open_trash_in_os_explorer, + drag::start_drag, file::open_file_paths, file::open_ephemeral_files, file::get_file_path_open_with_apps, @@ -358,11 +360,11 @@ async fn main() -> tauri::Result<()> { .plugin(tauri_plugin_os::init()) .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_http::init()) - .plugin(tauri_plugin_drag::init()) // TODO: Bring back Tauri Plugin Window State - it was buggy so we removed it. .plugin(tauri_plugin_updater::Builder::new().build()) .plugin(updater::plugin()) .manage(updater::State::default()) + .manage(drag::DragState::default()) .build(tauri::generate_context!())? .run(|_, _| {}); diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index 880932f5a..d11921d79 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -3,7 +3,13 @@ import { QueryClientProvider } from '@tanstack/react-query'; import { listen } from '@tauri-apps/api/event'; import { PropsWithChildren, startTransition, useEffect, useMemo, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; -import { RspcProvider, useBridgeMutation } from '@sd/client'; +import { + getItemFilePath, + libraryClient, + RspcProvider, + useBridgeMutation, + useSelector +} from '@sd/client'; import { createRoutes, DeeplinkEvent, @@ -18,12 +24,13 @@ import { RouteTitleContext } from '@sd/interface/hooks/useRouteTitle'; import '@sd/ui/style'; -import { startDrag } from '@crabnebula/tauri-plugin-drag'; +import { Channel, invoke } from '@tauri-apps/api/core'; import SuperTokens from 'supertokens-web-js'; import EmailPassword from 'supertokens-web-js/recipe/emailpassword'; import Passwordless from 'supertokens-web-js/recipe/passwordless'; import Session from 'supertokens-web-js/recipe/session'; import ThirdParty from 'supertokens-web-js/recipe/thirdparty'; +import { explorerStore } from '@sd/interface/app/$libraryId/Explorer/store'; // TODO: Bring this back once upstream is fixed up. // const client = hooks.createClient({ // links: [ @@ -38,6 +45,7 @@ import getWindowHandler from '@sd/interface/app/$libraryId/settings/client/accou import { useLocale } from '@sd/interface/hooks'; import { AUTH_SERVER_URL, getTokens } from '@sd/interface/util'; +import { Transparent } from '../../../packages/assets/images'; import { commands } from './commands'; import { platform } from './platform'; import { queryClient } from './query'; @@ -47,7 +55,7 @@ import { createUpdater } from './updater'; declare global { interface Window { enableCORSFetch: (enable: boolean) => void; - startDrag: typeof startDrag; + useDragAndDrop: () => void; } } @@ -69,12 +77,89 @@ SuperTokens.init({ const startupError = (window as any).__SD_ERROR__ as string | undefined; +function useDragAndDrop() { + const dragState = useSelector(explorerStore, (s) => s.drag); + + useEffect(() => { + console.log('Drag effect triggered:', { + dragStateType: dragState?.type, + itemCount: dragState?.type === 'dragging' ? dragState?.items?.length : undefined + }); + + (async () => { + if (dragState?.type === 'dragging' && dragState.items.length > 0) { + console.log('Starting drag operation with items:', dragState.items); + + const items = await Promise.all( + dragState.items.map(async (item) => { + const data = getItemFilePath(item); + if (!data) { + console.log('No file path data for item:', item); + return; + } + + const file_path = + 'path' in data ? data.path : await libraryClient.query(['files.getPath', data.id]); + + console.log('Resolved file path:', file_path); + return { + type: 'explorer-item', + file_path: file_path + }; + }) + ); + + const image = Transparent.split('/@fs')[1]!; + console.log('Using preview image:', image); + + const validFiles = items.filter(Boolean).map((item) => item?.file_path); + console.log('Invoking start_drag with files:', validFiles); + + try { + const channel = new Channel<{ + result: 'Dropped' | 'Cancelled'; + cursorPos: { x: number; y: number }; + }>(); + + channel.onmessage = (payload) => { + console.log('Drag completed:', { + result: payload.result, + position: payload.cursorPos, + timestamp: new Date().toISOString() + }); + + if (payload.result === 'Dropped') { + console.log('Drop location:', { + x: payload.cursorPos.x, + y: payload.cursorPos.y, + screen: window.screen + }); + } + + explorerStore.drag = null; + }; + + await invoke('start_drag', { + files: validFiles, + iconPath: image, + onEvent: channel + }); + console.log('start_drag invoked successfully'); + } catch (error) { + console.error('Failed to start drag:', error); + explorerStore.drag = null; + } + } + })(); + }, [dragState]); +} + export default function App() { useEffect(() => { // This tells Tauri to show the current window because it's finished loading commands.appReady(); window.enableCORSFetch(true); - window.startDrag = startDrag; + window.useDragAndDrop = useDragAndDrop; // .then(() => { // if (import.meta.env.PROD) window.fetch = fetch; // }); @@ -209,6 +294,43 @@ function AppInner() { }; }, [selectedTab.element]); + const SizeDisplay = () => { + const [size, setSize] = useState({ + width: window.innerWidth, + height: window.innerHeight + }); + + useEffect(() => { + const handleResize = () => { + setSize({ + width: window.innerWidth, + height: window.innerHeight + }); + }; + + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, []); + + return ( +
+ {size.width} x {size.height} +
+ ); + }; + return ( { startTransition(() => { setTabs((tabs) => { - const { pathname, search } = - selectedTab.router.state.location; + const { pathname, search } = selectedTab.router.state.location; const newTab = createTab({ pathname, search }); const newTabs = [...tabs, newTab]; @@ -308,6 +429,7 @@ function AppInner() { tab.element ) )} +
diff --git a/apps/desktop/src/commands.ts b/apps/desktop/src/commands.ts index 81581cf81..cd3b7005f 100644 --- a/apps/desktop/src/commands.ts +++ b/apps/desktop/src/commands.ts @@ -49,6 +49,31 @@ export const commands = { else return { status: 'error', error: e as any }; } }, + /** + * Initiates a drag and drop operation with cursor position tracking + * + * # Arguments + * * `window` - The Tauri window instance + * * `_state` - Current drag state (unused) + * * `files` - Vector of file paths to be dragged + * * `icon_path` - Path to the preview icon for the drag operation + * * `on_event` - Channel for communicating drag operation events back to the frontend + */ + async startDrag( + files: string[], + iconPath: string, + onEvent: TAURI_CHANNEL + ): Promise> { + try { + return { + status: 'ok', + data: await TAURI_INVOKE('start_drag', { files, iconPath, onEvent }) + }; + } catch (e) { + if (e instanceof Error) throw e; + else return { status: 'error', error: e as any }; + } + }, async openFilePaths( library: string, ids: number[] @@ -162,6 +187,7 @@ export const events = __makeEvents__<{ /** user-defined types **/ export type AppThemeType = 'Auto' | 'Light' | 'Dark'; +export type CallbackResult = { result: WrappedDragResult; cursorPos: WrappedCursorPosition }; export type DragAndDropEvent = | { type: 'Hovered'; paths: string[]; x: number; y: number } | { type: 'Dropped'; paths: string[]; x: number; y: number } @@ -199,6 +225,8 @@ export type RevealItem = | { FilePath: { id: number } } | { Ephemeral: { path: string } }; export type Update = { version: string }; +export type WrappedCursorPosition = { x: number; y: number }; +export type WrappedDragResult = 'Dropped' | 'Cancel'; type __EventObj__ = { listen: (cb: TAURI_API_EVENT.EventCallback) => ReturnType>; diff --git a/interface/app/$libraryId/Explorer/View/GridView/Item/index.tsx b/interface/app/$libraryId/Explorer/View/GridView/Item/index.tsx index 81e2e788d..dc3dc3d12 100644 --- a/interface/app/$libraryId/Explorer/View/GridView/Item/index.tsx +++ b/interface/app/$libraryId/Explorer/View/GridView/Item/index.tsx @@ -111,38 +111,6 @@ const ItemMetadata = memo(() => { const item = useGridViewItemContext(); const { isDroppable } = useExplorerDroppableContext(); const explorerLayout = useExplorerLayoutStore(); - const dragState = useSelector(explorerStore, (s) => s.drag); - - useEffect(() => { - (async () => { - if (dragState?.type === 'dragging' && dragState.items.length > 0) { - const items = await Promise.all( - dragState.items.map(async (item) => { - const data = getItemFilePath(item); - if (!data) return; - - const file_path = - 'path' in data - ? data.path - : await libraryClient.query(['files.getPath', data.id]); - - return { - type: 'explorer-item', - file_path: file_path - }; - }) - ); - - // get image src from Transparent - const image = Transparent.split('/@fs')[1]; - - (window as any).startDrag({ - item: items.filter(Boolean).map((item) => item?.file_path), - icon: image - }); - } - })(); - }, [dragState]); const isRenaming = useSelector(explorerStore, (s) => s.isRenaming && item.selected); diff --git a/interface/app/$libraryId/Explorer/index.tsx b/interface/app/$libraryId/Explorer/index.tsx index 2aa5971c0..2bddff35d 100644 --- a/interface/app/$libraryId/Explorer/index.tsx +++ b/interface/app/$libraryId/Explorer/index.tsx @@ -34,6 +34,11 @@ interface Props { contextMenu?: () => ReactNode; } +declare global { + interface Window { + useDragAndDrop: () => void; + } +} /** * This component is used in a few routes and acts as the reference demonstration of how to combine * all the elements of the explorer except for the context, which must be used in the parent component. @@ -82,6 +87,8 @@ export default function Explorer(props: PropsWithChildren) { explorer.settingsStore.showHiddenFiles = !explorer.settingsStore.showHiddenFiles; }); + window.useDragAndDrop(); + useKeyRevealFinder(); useExplorerDnd(); @@ -111,18 +118,13 @@ export default function Explorer(props: PropsWithChildren) { contextMenu={props.contextMenu ? props.contextMenu() : } emptyNotice={ props.emptyNotice ?? ( - + ) } listViewOptions={{ hideHeaderBorder: true }} scrollPadding={{ top: topBar.topBarHeight, - bottom: showPathBar - ? PATH_BAR_HEIGHT + (showTagBar ? TAG_BAR_HEIGHT : 0) - : undefined + bottom: showPathBar ? PATH_BAR_HEIGHT + (showTagBar ? TAG_BAR_HEIGHT : 0) : undefined }} />
@@ -142,9 +144,7 @@ export default function Explorer(props: PropsWithChildren) { )} style={{ paddingTop: topBar.topBarHeight + 12, - bottom: showPathBar - ? PATH_BAR_HEIGHT + (showTagBar ? TAG_BAR_HEIGHT : 0) - : 0 + bottom: showPathBar ? PATH_BAR_HEIGHT + (showTagBar ? TAG_BAR_HEIGHT : 0) : 0 }} /> )} From 26a9f30b0d26c7b38cf30f899ee1c99ccb2663c4 Mon Sep 17 00:00:00 2001 From: Arnab Chakraborty <11457760+Rocky43007@users.noreply.github.com> Date: Mon, 30 Dec 2024 21:55:48 +0300 Subject: [PATCH 32/34] Remove debug `` component --- apps/desktop/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/App.tsx b/apps/desktop/src/App.tsx index d11921d79..05dbdc4ed 100644 --- a/apps/desktop/src/App.tsx +++ b/apps/desktop/src/App.tsx @@ -429,7 +429,7 @@ function AppInner() { tab.element ) )} - + {/* */}
From bcf82614e6dbe2701e28b52d95a982f5a39cacce Mon Sep 17 00:00:00 2001 From: Arnab Chakraborty <11457760+Rocky43007@users.noreply.github.com> Date: Wed, 1 Jan 2025 13:57:01 +0300 Subject: [PATCH 33/34] Scripts switching prod and dev service endpoints A script that switches the prod and dev endpoints for the auth & relay services. --- scripts/switch_servers.ps1 | 163 +++++++++++++++++++++++++++++++++++++ scripts/switch_servers.sh | 113 +++++++++++++++++++++++++ 2 files changed, 276 insertions(+) create mode 100644 scripts/switch_servers.ps1 create mode 100755 scripts/switch_servers.sh diff --git a/scripts/switch_servers.ps1 b/scripts/switch_servers.ps1 new file mode 100644 index 000000000..e8169fe09 --- /dev/null +++ b/scripts/switch_servers.ps1 @@ -0,0 +1,163 @@ +# Usage +# .\script.ps1 dev # Will prompt for relay server modification +# .\script.ps1 prod # Will prompt for relay server modification +# .\script.ps1 dev -r # Will automatically modify relay servers +# .\script.ps1 prod -r # Will automatically modify relay servers +# .\script.ps1 dev -s # Will skip relay server modification without prompting +# .\script.ps1 prod -s # Will skip relay server modification without prompting + + +# File paths +$rustFile = "core/crates/cloud-services/src/lib.rs" +$tsxFile = "interface/util/index.tsx" +$coreFile = "core/src/lib.rs" + +# Function to prompt for relay servers change +function Prompt-RelayServers { + while ($true) { + $response = Read-Host "Do you want to modify relay servers as well? (y/n)" + switch ($response.ToLower()) { + 'y' { return $true } + 'n' { return $false } + default { Write-Host "Please answer y or n." } + } + } +} + +# Check arguments +if ($args.Count -lt 1 -or $args.Count -gt 2) { + Write-Host "Usage: