diff --git a/docs/api/cozy-client/modules/models.sharing.md b/docs/api/cozy-client/modules/models.sharing.md index 22bf792f2..24012e493 100644 --- a/docs/api/cozy-client/modules/models.sharing.md +++ b/docs/api/cozy-client/modules/models.sharing.md @@ -6,9 +6,9 @@ ## Functions -### getSharingLink +### makeSharingLink -▸ **getSharingLink**(`client`, `filesIds`, `options?`): `Promise`<`string`> +▸ **makeSharingLink**(`client`, `filesIds`, `options?`): `Promise`<`string`> Generate Sharing link for one or many files @@ -21,6 +21,7 @@ Generate Sharing link for one or many files | `options` | `Object` | Options | | `options.password` | `string` | - | | `options.ttl` | `string` | - | +| `options.verbs` | `string`\[] | - | *Returns* @@ -30,4 +31,4 @@ Shared link *Defined in* -[packages/cozy-client/src/models/sharing.js:15](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/sharing.js#L15) +[packages/cozy-client/src/models/sharing.js:16](https://github.com/cozy/cozy-client/blob/master/packages/cozy-client/src/models/sharing.js#L16) diff --git a/docs/api/cozy-stack-client.md b/docs/api/cozy-stack-client.md index 83f78be9f..548f41d93 100644 --- a/docs/api/cozy-stack-client.md +++ b/docs/api/cozy-stack-client.md @@ -1667,7 +1667,7 @@ Implements `DocumentCollection` API along with specific methods for `io.cozy.per * [PermissionCollection](#PermissionCollection) * [.create(permission)](#PermissionCollection+create) - * [.add(document, permission)](#PermissionCollection+add) ⇒ Promise + * [.add(document, permission, options)](#PermissionCollection+add) ⇒ Promise * ~~[.findApps()](#PermissionCollection+findApps)~~ * [.createSharingLink(document, options)](#PermissionCollection+createSharingLink) * [.fetchPermissionsByLink(permissions)](#PermissionCollection+fetchPermissionsByLink) @@ -1692,7 +1692,7 @@ It can also associates one or more codes to it, via the codes parameter -### permissionCollection.add(document, permission) ⇒ Promise +### permissionCollection.add(document, permission, options) ⇒ Promise Adds a permission to the given document. Document type must be `io.cozy.apps`, `io.cozy.konnectors` or `io.cozy.permissions` @@ -1702,18 +1702,23 @@ Adds a permission to the given document. Document type must be | --- | --- | --- | | document | object | Document which receives the permission | | permission | object | Describes the permission | +| options | object | options | +| [options.expiresAt] | string | Date at which the permission will expire. Set to "" to remove it. | +| [options.password] | string | To generate a password-protected link. Set to "" to remove it. | **Example** ``` -const permissions = await client - .collection('io.cozy.permissions') - .add(konnector, { +const permissions = await client.collection('io.cozy.permissions').add( + konnector, + { folder: { type: 'io.cozy.files', verbs: ['GET', 'PUT'], values: [`io.cozy.files.bc57b60eb2954537b0dcdc6ebd8e9d23`] } - }) + }, + { expiresAt: '2100-01-01T00:00:00Z', password: 'password' } +) ``` @@ -1732,7 +1737,11 @@ Create a share link | --- | --- | --- | | document | Object | cozy document | | options | object | options | -| options.verbs | Array.<string> | explicit permissions to use | +| [options.ttl] | string | Time to live (bigduration format, e.g. "4Y3M2D1h30m15s") | +| [options.password] | string | To generate a password-protected link | +| [options.verbs] | Array.<string> | explicit permissions to use | +| [options.codes] | string | A comma separed list of values (defaulted to code) | +| [options.tiny] | boolean | If set to true then the generated shortcode will be 6 digits Cozy-Stack has a few conditions to be able to use this tiny shortcode ATM you have to specifiy a ttl < 1h, but it can change. see https://docs.cozy.io/en/cozy-stack/permissions/#post-permissions for exact informations | @@ -2305,7 +2314,7 @@ Build a permission set | document | Object | cozy document | | publicLink | boolean | are the permissions for a public link ? | | options | object | options | -| options.verbs | Array.<string> | explicit permissions to use | +| [options.verbs] | Array.<string> | explicit permissions to use | diff --git a/packages/cozy-client/src/models/sharing.js b/packages/cozy-client/src/models/sharing.js index a1ff58d92..6f4e05117 100644 --- a/packages/cozy-client/src/models/sharing.js +++ b/packages/cozy-client/src/models/sharing.js @@ -10,17 +10,18 @@ import { generateWebLink } from '../helpers' * @param {object} options - Options * @param {string} [options.ttl] - Time to live (bigduration format, e.g. "4Y3M2D1h30m15s") * @param {string} [options.password] - To generate a password-protected link + * @param {string[]} [options.verbs] - Array of verbs to allow on the shared link. Default is ['GET'] * @returns {Promise} Shared link */ -export const getSharingLink = async ( +export const makeSharingLink = async ( client, filesIds, - { ttl, password } = {} + { ttl, password, verbs = ['GET'] } = {} ) => { const PERMS = { _type: DOCTYPE_PERMISSIONS, permissions: { - files: { type: DOCTYPE_FILES, values: filesIds, verbs: ['GET'] } + files: { type: DOCTYPE_FILES, values: filesIds, verbs } }, ...(ttl && { ttl }), ...(password && { password }) diff --git a/packages/cozy-client/src/models/sharing.spec.js b/packages/cozy-client/src/models/sharing.spec.js index 37d4d76b3..d02738b93 100644 --- a/packages/cozy-client/src/models/sharing.spec.js +++ b/packages/cozy-client/src/models/sharing.spec.js @@ -1,6 +1,6 @@ -import { getSharingLink } from './sharing' +import { makeSharingLink } from './sharing' -describe('getSharingLink', () => { +describe('makeSharingLink', () => { const mockSharecode = { attributes: { shortcodes: { code: 'shortcode' } } } const mockClient = ({ isFlatDomain = false } = {}) => ({ save: jest.fn(() => ({ data: mockSharecode })), @@ -10,7 +10,7 @@ describe('getSharingLink', () => { const mockFiles = ['fileId01', 'fileId02'] it('should generate the right share link if "isFlatDomain" param is not defined', async () => { - const sharingLink = await getSharingLink(mockClient(), mockFiles) + const sharingLink = await makeSharingLink(mockClient(), mockFiles) expect(sharingLink).toBe( 'http://drive.cozy.cloud/public?sharecode=shortcode#/' @@ -18,7 +18,7 @@ describe('getSharingLink', () => { }) it('should generate the right share link to a nested cozy', async () => { - const sharingLink = await getSharingLink( + const sharingLink = await makeSharingLink( mockClient({ isFlatDomain: false }), mockFiles ) @@ -29,7 +29,7 @@ describe('getSharingLink', () => { }) it('should generate the right share link to a flat cozy', async () => { - const sharingLink = await getSharingLink( + const sharingLink = await makeSharingLink( mockClient({ isFlatDomain: true }), mockFiles ) @@ -40,14 +40,14 @@ describe('getSharingLink', () => { }) it('should generate the right share link with an correct sharecode', async () => { - const sharingLink = await getSharingLink(mockClient(), mockFiles) + const sharingLink = await makeSharingLink(mockClient(), mockFiles) expect(sharingLink).toContain('sharecode=shortcode') }) it('should save called with the "ttl" param', async () => { const mockSave = jest.fn().mockReturnValue({ data: mockSharecode }) const client = { ...mockClient(), save: mockSave } - await getSharingLink(client, mockFiles, { + await makeSharingLink(client, mockFiles, { ttl: '1d' }) @@ -67,7 +67,7 @@ describe('getSharingLink', () => { it('should save called with the "password" param', async () => { const mockSave = jest.fn().mockReturnValue({ data: mockSharecode }) const client = { ...mockClient(), save: mockSave } - await getSharingLink(client, mockFiles, { + await makeSharingLink(client, mockFiles, { password: 'password' }) @@ -84,10 +84,29 @@ describe('getSharingLink', () => { }) }) + it('should save called with the "verbs" param', async () => { + const mockSave = jest.fn().mockReturnValue({ data: mockSharecode }) + const client = { ...mockClient(), save: mockSave } + await makeSharingLink(client, mockFiles, { + verbs: ['GET', 'POST'] + }) + + expect(mockSave).toBeCalledWith({ + _type: 'io.cozy.permissions', + permissions: { + files: { + type: 'io.cozy.files', + values: ['fileId01', 'fileId02'], + verbs: ['GET', 'POST'] + } + } + }) + }) + it('should save called without the "ttl" or "password" params', async () => { const mockSave = jest.fn().mockReturnValue({ data: mockSharecode }) const client = { ...mockClient(), save: mockSave } - await getSharingLink(client, mockFiles) + await makeSharingLink(client, mockFiles) expect(mockSave).toBeCalledWith({ _type: 'io.cozy.permissions', diff --git a/packages/cozy-client/types/models/sharing.d.ts b/packages/cozy-client/types/models/sharing.d.ts index 111ea8d03..0a5621b4a 100644 --- a/packages/cozy-client/types/models/sharing.d.ts +++ b/packages/cozy-client/types/models/sharing.d.ts @@ -1,5 +1,6 @@ -export function getSharingLink(client: CozyClient, filesIds: string[], { ttl, password }?: { +export function makeSharingLink(client: CozyClient, filesIds: string[], { ttl, password, verbs }?: { ttl: string; password: string; + verbs: string[]; }): Promise; import CozyClient from "../CozyClient"; diff --git a/packages/cozy-stack-client/src/PermissionCollection.js b/packages/cozy-stack-client/src/PermissionCollection.js index 7b966fb04..73a4c5b09 100644 --- a/packages/cozy-stack-client/src/PermissionCollection.js +++ b/packages/cozy-stack-client/src/PermissionCollection.js @@ -61,22 +61,27 @@ class PermissionCollection extends DocumentCollection { * * @param {object} document - Document which receives the permission * @param {object} permission - Describes the permission + * @param {object} options - options + * @param {string} [options.expiresAt] - Date at which the permission will expire. Set to "" to remove it. + * @param {string} [options.password] - To generate a password-protected link. Set to "" to remove it. * @returns {Promise} * * @example * ``` - * const permissions = await client - * .collection('io.cozy.permissions') - * .add(konnector, { + * const permissions = await client.collection('io.cozy.permissions').add( + * konnector, + * { * folder: { * type: 'io.cozy.files', * verbs: ['GET', 'PUT'], * values: [`io.cozy.files.bc57b60eb2954537b0dcdc6ebd8e9d23`] * } - * }) + * }, + * { expiresAt: '2100-01-01T00:00:00Z', password: 'password' } + * ) * ``` */ - async add(document, permission) { + async add(document, permission, options = {}) { let endpoint switch (document._type) { case 'io.cozy.apps': @@ -94,11 +99,19 @@ class PermissionCollection extends DocumentCollection { ) } + const { expiresAt, password } = options + // We need to pass password and expires_at even if they are empty strings because the API expects them. + // If value is a empty string, the stack will remove the password or expires_at. + const hasPassword = password || password === '' + const hasExpiresAt = expiresAt || expiresAt === '' + const resp = await this.stackClient.fetchJSON('PATCH', endpoint, { data: { type: 'io.cozy.permissions', attributes: { - permissions: permission + permissions: permission, + ...(hasPassword && { password }), + ...(hasExpiresAt && { expires_at: expiresAt }) } } }) @@ -143,13 +156,25 @@ class PermissionCollection extends DocumentCollection { * * @param {{_id, _type}} document - cozy document * @param {object} options - options - * @param {string[]} options.verbs - explicit permissions to use + * @param {string} [options.ttl] - Time to live (bigduration format, e.g. "4Y3M2D1h30m15s") + * @param {string} [options.password] - To generate a password-protected link + * @param {string[]} [options.verbs] - explicit permissions to use + * @param {string} [options.codes] A comma separed list of values (defaulted to code) + * @param {boolean} [options.tiny] If set to true then the generated shortcode will be 6 digits + * Cozy-Stack has a few conditions to be able to use this tiny shortcode ATM you have to specifiy + * a ttl < 1h, but it can change. + * see https://docs.cozy.io/en/cozy-stack/permissions/#post-permissions for exact informations */ async createSharingLink(document, options = {}) { - const { verbs } = options + const { ttl, password, verbs, tiny, codes = 'code' } = options + const searchParams = new URLSearchParams() + searchParams.append('codes', codes) + if (ttl) searchParams.append('ttl', ttl) + if (tiny) searchParams.append('tiny', true) + const resp = await this.stackClient.fetchJSON( 'POST', - `/permissions?codes=email`, + `/permissions?${searchParams}`, { data: { type: 'io.cozy.permissions', @@ -158,7 +183,8 @@ class PermissionCollection extends DocumentCollection { document, true, verbs ? { verbs } : {} - ) + ), + ...(password && { password }) } } } @@ -245,7 +271,7 @@ class PermissionCollection extends DocumentCollection { * @param {{_id, _type}} document - cozy document * @param {boolean} publicLink - are the permissions for a public link ? * @param {object} options - options - * @param {string[]} options.verbs - explicit permissions to use + * @param {string[]} [options.verbs] - explicit permissions to use * @returns {object} permissions object that can be sent through /permissions/* */ export const getPermissionsFor = ( diff --git a/packages/cozy-stack-client/src/PermissionCollection.spec.js b/packages/cozy-stack-client/src/PermissionCollection.spec.js index a10390892..8c773ca68 100644 --- a/packages/cozy-stack-client/src/PermissionCollection.spec.js +++ b/packages/cozy-stack-client/src/PermissionCollection.spec.js @@ -99,6 +99,81 @@ describe('PermissionCollection', () => { ) }) + it('should call with options if they are defined and filled in', async () => { + await collection.add( + { + _type: 'io.cozy.permissions', + _id: 'a340d5e0d64711e6b66c5fc9ce1e17c6' + }, + fixtures.permission, + { expiresAt: '2019-01-01T00:00:00Z', password: 'password' } + ) + + expect(client.fetchJSON).toHaveBeenCalledWith( + 'PATCH', + '/permissions/a340d5e0d64711e6b66c5fc9ce1e17c6', + { + data: { + type: 'io.cozy.permissions', + attributes: { + permissions: fixtures.permission, + expires_at: '2019-01-01T00:00:00Z', + password: 'password' + } + } + } + ) + }) + + it('should call with options if they are defined but not filled in', async () => { + await collection.add( + { + _type: 'io.cozy.permissions', + _id: 'a340d5e0d64711e6b66c5fc9ce1e17c6' + }, + fixtures.permission, + { expiresAt: '', password: '' } + ) + + expect(client.fetchJSON).toHaveBeenCalledWith( + 'PATCH', + '/permissions/a340d5e0d64711e6b66c5fc9ce1e17c6', + { + data: { + type: 'io.cozy.permissions', + attributes: { + permissions: fixtures.permission, + expires_at: '', + password: '' + } + } + } + ) + }) + + it('should call without options if they are undefined', async () => { + await collection.add( + { + _type: 'io.cozy.permissions', + _id: 'a340d5e0d64711e6b66c5fc9ce1e17c6' + }, + fixtures.permission + ) + + expect(client.fetchJSON).toHaveBeenCalledWith( + 'PATCH', + '/permissions/a340d5e0d64711e6b66c5fc9ce1e17c6', + { + data: { + type: 'io.cozy.permissions', + attributes: { + permissions: fixtures.permission + } + } + } + ) + }) + it('uses expected apps endpoint', async () => { await collection.add( { @@ -186,7 +261,7 @@ describe('PermissionCollection', () => { await collection.createSharingLink(document) expect(client.fetchJSON).toHaveBeenCalledWith( 'POST', - '/permissions?codes=email', + '/permissions?codes=code', { data: { type: 'io.cozy.permissions', @@ -211,7 +286,7 @@ describe('PermissionCollection', () => { await collection.createSharingLink(document, options) expect(client.fetchJSON).toHaveBeenCalledWith( 'POST', - '/permissions?codes=email', + '/permissions?codes=code', { data: { type: 'io.cozy.permissions', @@ -228,6 +303,20 @@ describe('PermissionCollection', () => { } ) }) + + it('Should be call with the right params', async () => { + const document = { _type: 'io.cozy.files', _id: '1234' } + await collection.createSharingLink(document, { + ttl: '1D', + tiny: true, + codes: 'a,b' + }) + expect(client.fetchJSON).toHaveBeenCalledWith( + 'POST', + '/permissions?codes=a%2Cb&ttl=1D&tiny=true', + { data: { attributes: {}, type: 'io.cozy.permissions' } } + ) + }) }) })