@@ -14,6 +14,14 @@ import { getBaseUrl } from '@/lib/core/utils/urls'
1414
1515const logger = createLogger ( 'PublicShareManager' )
1616
17+ /** Thrown when share auth config is invalid (e.g. enabling a password share with no password). Maps to a 400. */
18+ export class ShareValidationError extends Error {
19+ constructor ( message : string ) {
20+ super ( message )
21+ this . name = 'ShareValidationError'
22+ }
23+ }
24+
1725type ShareResourceType = z . infer < typeof shareResourceTypeSchema >
1826
1927type PublicShareRow = typeof publicShare . $inferSelect
@@ -94,10 +102,11 @@ interface UpsertFileShareInput {
94102 * a fresh unguessable token; subsequent calls flip `isActive`/`authType` and keep
95103 * the token stable (so an existing link resolves again after re-enable).
96104 *
97- * Auth handling (fail-fast): `password` requires a plaintext `password` unless the
98- * share already has one (encrypted at rest via {@link encryptSecret}); `email`/`sso`
99- * require a non-empty `allowedEmails` (provided or already stored); any other type
100- * clears both the password and the allow-list.
105+ * Auth validation only applies when **enabling** (`isActive: true`): `password`
106+ * requires a plaintext `password` unless one is already stored (encrypted via
107+ * {@link encryptSecret}); `email`/`sso` require a non-empty `allowedEmails`.
108+ * Disabling (going Private) always succeeds and preserves the stored config so a
109+ * later re-enable restores it. Validation failures throw {@link ShareValidationError}.
101110 */
102111export async function upsertFileShare ( {
103112 workspaceId,
@@ -117,23 +126,35 @@ export async function upsertFileShare({
117126
118127 const finalAuthType : ShareAuthType =
119128 authType ?? ( existing ?. authType as ShareAuthType | undefined ) ?? 'public'
120-
121- let finalPassword : string | null = null
122- let finalAllowedEmails : string [ ] = [ ]
123- if ( finalAuthType === 'password' ) {
124- if ( password ) {
125- finalPassword = ( await encryptSecret ( password ) ) . encrypted
126- } else if ( existing ?. password ) {
127- finalPassword = existing . password
129+ const existingAllowedEmails = Array . isArray ( existing ?. allowedEmails )
130+ ? ( existing . allowedEmails as string [ ] )
131+ : [ ]
132+
133+ // Disabling preserves the stored config (and skips validation) so turning
134+ // sharing off always succeeds; only enabling validates the chosen auth mode.
135+ let finalPassword : string | null = existing ?. password ?? null
136+ let finalAllowedEmails : string [ ] = existingAllowedEmails
137+ if ( isActive ) {
138+ if ( finalAuthType === 'password' ) {
139+ if ( password ) {
140+ finalPassword = ( await encryptSecret ( password ) ) . encrypted
141+ } else if ( existing ?. password ) {
142+ finalPassword = existing . password
143+ } else {
144+ throw new ShareValidationError ( 'Password is required for password-protected shares' )
145+ }
146+ finalAllowedEmails = [ ]
147+ } else if ( finalAuthType === 'email' || finalAuthType === 'sso' ) {
148+ finalAllowedEmails = allowedEmails ?? existingAllowedEmails
149+ if ( finalAllowedEmails . length === 0 ) {
150+ throw new ShareValidationError (
151+ 'At least one allowed email is required for email/SSO shares'
152+ )
153+ }
154+ finalPassword = null
128155 } else {
129- throw new Error ( 'Password is required for password-protected shares' )
130- }
131- } else if ( finalAuthType === 'email' || finalAuthType === 'sso' ) {
132- finalAllowedEmails =
133- allowedEmails ??
134- ( Array . isArray ( existing ?. allowedEmails ) ? ( existing . allowedEmails as string [ ] ) : [ ] )
135- if ( finalAllowedEmails . length === 0 ) {
136- throw new Error ( 'At least one allowed email is required for email/SSO shares' )
156+ finalPassword = null
157+ finalAllowedEmails = [ ]
137158 }
138159 }
139160
0 commit comments