@@ -16,21 +16,20 @@ export default defineStore('delegation', {
1616 state : ( ) => {
1717 return {
1818 /**
19- * List of principal objects that the current user has delegated to
20- * (members of the current user's calendar-proxy-write group ).
19+ * List of principal objects that the current user has delegated to.
20+ * Each entry has the principal fields plus a `permission` field ('write'|'read' ).
2121 *
2222 * @type {object[] }
2323 */
2424 delegates : [ ] ,
2525
2626 /**
27- * Principal URLs of users who have granted the current user proxy access.
28- * Stored as full absolute principal URLs so calendar-home discovery can
29- * be performed without reconstructing paths from user IDs.
27+ * Users who have granted the current user proxy access, with permission level.
28+ * Each entry: { principalUrl: string, permission: 'write'|'read' }
3029 *
31- * @type {string[] }
30+ * @type {Array<{principalUrl: string, permission: 'write'|'read'}> }
3231 */
33- delegatorPrincipalUrls : [ ] ,
32+ delegators : [ ] ,
3433 }
3534 } ,
3635
@@ -41,13 +40,13 @@ export default defineStore('delegation', {
4140 * @param {object } state The store state
4241 * @return {boolean }
4342 */
44- hasDelegatedCalendars : ( state ) => state . delegatorPrincipalUrls . length > 0 ,
43+ hasDelegatedCalendars : ( state ) => state . delegators . length > 0 ,
4544 } ,
4645
4746 actions : {
4847 /**
49- * Fetch the current user's delegates (members of their calendar-proxy-write group)
50- * and resolve their principal details.
48+ * Fetch the current user's delegates (members of their calendar-proxy-write
49+ * and calendar-proxy-read groups) and resolve their principal details.
5150 *
5251 * @return {Promise<void> }
5352 */
@@ -58,33 +57,53 @@ export default defineStore('delegation', {
5857 return
5958 }
6059
61- const proxyWriteGroupUrl = currentUser . url . replace ( / \/ ? $ / , '' ) + '/calendar-proxy-write'
60+ const baseUrl = currentUser . url . replace ( / \/ ? $ / , '' )
61+ const proxyWriteGroupUrl = baseUrl + '/calendar-proxy-write'
62+ const proxyReadGroupUrl = baseUrl + '/calendar-proxy-read'
6263
63- let memberUrls
64+ let writeUrls = [ ]
65+ let readUrls = [ ]
6466 try {
65- memberUrls = await getClient ( ) . getDelegateUrls ( proxyWriteGroupUrl )
67+ [ writeUrls , readUrls ] = await Promise . all ( [
68+ getClient ( ) . getDelegateUrls ( proxyWriteGroupUrl ) ,
69+ getClient ( ) . getDelegateUrls ( proxyReadGroupUrl ) ,
70+ ] )
6671 } catch ( error ) {
6772 logger . error ( 'Could not fetch delegate URLs' , { error } )
6873 return
6974 }
7075
7176 const delegates = [ ]
72- for ( const url of memberUrls ) {
77+
78+ for ( const url of writeUrls ) {
7379 try {
7480 const dav = await findPrincipalByUrl ( url )
7581 if ( dav ) {
76- delegates . push ( mapDavToPrincipal ( dav ) )
82+ delegates . push ( { ... mapDavToPrincipal ( dav ) , permission : 'write' } )
7783 }
7884 } catch ( error ) {
79- logger . error ( 'Could not resolve delegate principal' , { url, error } )
85+ logger . error ( 'Could not resolve write delegate principal' , { url, error } )
8086 }
8187 }
88+
89+ for ( const url of readUrls ) {
90+ try {
91+ const dav = await findPrincipalByUrl ( url )
92+ if ( dav ) {
93+ delegates . push ( { ...mapDavToPrincipal ( dav ) , permission : 'read' } )
94+ }
95+ } catch ( error ) {
96+ logger . error ( 'Could not resolve read delegate principal' , { url, error } )
97+ }
98+ }
99+
82100 this . delegates = delegates
83101 logger . debug ( 'Fetched delegates' , { delegates : this . delegates } )
84102 } ,
85103
86104 /**
87- * Fetch the principal URLs of users who have granted the current user proxy access.
105+ * Fetch the principal URLs and permission level of users who have granted
106+ * the current user proxy access (both read and write).
88107 *
89108 * @return {Promise<void> }
90109 */
@@ -96,34 +115,38 @@ export default defineStore('delegation', {
96115 }
97116
98117 try {
99- this . delegatorPrincipalUrls = await getClient ( ) . getDelegatorPrincipalUrls ( currentUser . url )
100- logger . debug ( 'Fetched delegators' , { delegatorPrincipalUrls : this . delegatorPrincipalUrls } )
118+ this . delegators = await getClient ( ) . getDelegatorsWithPermission ( currentUser . url )
119+ logger . debug ( 'Fetched delegators' , { delegators : this . delegators } )
101120 } catch ( error ) {
102- logger . error ( 'Could not fetch delegator principal URLs ' , { error } )
121+ logger . error ( 'Could not fetch delegator information ' , { error } )
103122 }
104123 } ,
105124
106125 /**
107- * Add a user as a delegate (add them to the current user's calendar-proxy-write group) .
126+ * Add a user as a delegate with the given permission level .
108127 *
109128 * @param {object } data The destructuring object
110129 * @param {string } data.principalUrl Absolute URL of the principal to add
130+ * @param {'write'|'read' } data.permission The permission level to grant
111131 * @return {Promise<void> }
112132 */
113- async addDelegate ( { principalUrl } ) {
133+ async addDelegate ( { principalUrl, permission = 'write' } ) {
114134 const principalsStore = usePrincipalsStore ( )
115135 const currentUser = principalsStore . getCurrentUserPrincipal
116136 if ( ! currentUser ?. url ) {
117137 return
118138 }
119139
120- const proxyWriteGroupUrl = currentUser . url . replace ( / \/ ? $ / , '' ) + '/calendar-proxy-write'
121- await getClient ( ) . addDelegate ( proxyWriteGroupUrl , principalUrl )
140+ const baseUrl = currentUser . url . replace ( / \/ ? $ / , '' )
141+ const proxyGroupUrl = permission === 'read'
142+ ? baseUrl + '/calendar-proxy-read'
143+ : baseUrl + '/calendar-proxy-write'
144+ await getClient ( ) . addDelegate ( proxyGroupUrl , principalUrl )
122145 await this . fetchDelegates ( )
123146 } ,
124147
125148 /**
126- * Remove a delegate (remove them from the current user's calendar- proxy-write group) .
149+ * Remove a delegate from whichever proxy group(s) they are in .
127150 *
128151 * @param {object } data The destructuring object
129152 * @param {string } data.principalUrl Absolute URL of the principal to remove
@@ -136,8 +159,15 @@ export default defineStore('delegation', {
136159 return
137160 }
138161
139- const proxyWriteGroupUrl = currentUser . url . replace ( / \/ ? $ / , '' ) + '/calendar-proxy-write'
140- await getClient ( ) . removeDelegate ( proxyWriteGroupUrl , principalUrl )
162+ const baseUrl = currentUser . url . replace ( / \/ ? $ / , '' )
163+ // Find the delegate's current permission so we remove from the right group.
164+ const existing = this . delegates . find ( ( d ) => d . url === principalUrl )
165+ const permission = existing ?. permission ?? 'write'
166+
167+ const proxyGroupUrl = permission === 'read'
168+ ? baseUrl + '/calendar-proxy-read'
169+ : baseUrl + '/calendar-proxy-write'
170+ await getClient ( ) . removeDelegate ( proxyGroupUrl , principalUrl )
141171 this . delegates = this . delegates . filter ( ( d ) => d . url !== principalUrl )
142172 } ,
143173
@@ -147,24 +177,21 @@ export default defineStore('delegation', {
147177 * The calendars are tagged with isDelegated=true so CalendarList can show them
148178 * in their own section.
149179 *
150- * Calendar home URLs are discovered via CalDAV PROPFIND on each delegator's
151- * principal (RFC 4791 §6.2.1) rather than being constructed from user IDs,
152- * which ensures correctness regardless of server path conventions.
180+ * Read-only delegators' calendars are additionally marked readOnly=true so they
181+ * are excluded from the calendar picker (which only lists writable calendars).
153182 *
154183 * @return {Promise<void> }
155184 */
156185 async fetchDelegatedCalendars ( ) {
157- if ( ! this . delegatorPrincipalUrls . length ) {
186+ if ( ! this . delegators . length ) {
158187 return
159188 }
160189
161190 const principalsStore = usePrincipalsStore ( )
162191 const calendarsStore = useCalendarsStore ( )
163192 const currentUser = principalsStore . getCurrentUserPrincipal
164193
165- for ( const delegatorPrincipalUrl of this . delegatorPrincipalUrls ) {
166- // Discover the delegator's calendar home URL via CalDAV principal PROPFIND.
167- // This follows RFC 4791 §6.2.1 and avoids hard-coding any URL path convention.
194+ for ( const { principalUrl : delegatorPrincipalUrl , permission } of this . delegators ) {
168195 const calendarHomeUrl = await getClient ( ) . getCalendarHomeUrlForPrincipal ( delegatorPrincipalUrl )
169196 if ( ! calendarHomeUrl ) {
170197 logger . warn ( 'Could not determine calendar home URL for delegator' , { delegatorPrincipalUrl } )
@@ -176,16 +203,20 @@ export default defineStore('delegation', {
176203 const rawCalendars = await findCalendarsAtUrl ( calendarHomeUrl )
177204 const mappedCalendars = rawCalendars
178205 . map ( ( cal ) => mapDavCollectionToCalendar ( cal , currentUser ) )
179- . map ( ( cal ) => ( { ...cal , isDelegated : true } ) )
206+ . map ( ( cal ) => ( {
207+ ...cal ,
208+ isDelegated : true ,
209+ // Read-only proxy access: prevent editing and hide from calendar picker
210+ ...( permission === 'read' ? { readOnly : true } : { } ) ,
211+ } ) )
180212
181213 for ( const calendar of mappedCalendars ) {
182- // Only add if not already present
183214 if ( ! calendarsStore . getCalendarById ( calendar . id ) ) {
184215 calendarsStore . addCalendarMutation ( { calendar } )
185216 }
186217 }
187218
188- logger . debug ( 'Fetched delegated calendars from' , { calendarHomeUrl, count : mappedCalendars . length } )
219+ logger . debug ( 'Fetched delegated calendars from' , { calendarHomeUrl, permission , count : mappedCalendars . length } )
189220 } catch ( error ) {
190221 logger . error ( 'Could not fetch calendars for delegator' , { delegatorPrincipalUrl, error } )
191222 showError ( t ( 'calendar' , 'Could not load delegated calendars. Make sure the server supports calendar delegation.' ) )
0 commit comments