import { StorageBase } from 'storage/storage-base'; import { MsTeamsApps } from 'const/cloud-storage-apps'; import { Features } from 'util/features'; // https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow // https://graph.microsoft.com/v1.0/me/transitiveMemberOf/microsoft.graph.group?$count=true // https://graph.microsoft.com/v1.0/groups?$filter=groupTypes/any(c:c+eq+'Unified') // https://graph.microsoft.com/v1.0/groups?$filter=groupTypes/any(c:c+eq+'Unified')&$orderby=displayName // https://graph.microsoft.com/v1.0/groups?$orderby=displayName // /me/joinedTeams // https://graph.microsoft.com/v1.0/groups/{group id}/drive/root/children class StorageTeams extends StorageBase { name = 'msteams'; enabled = true; uipos = 50; icon = 'user-friends'; _graphUrl = 'https://graph.microsoft.com/v1.0'; _groupsUrl = `${this._graphUrl}/me/joinedTeams`; _baseUrl = `${this._graphUrl}/groups`; getPathForName(fileName) { return '/drive/root:/' + fileName + '.kdbx'; } genUrlAddress(groupId, path) { if (groupId) { return this._baseUrl + '/' + groupId + (path ? '/' + path.replace(/^\/+/, '') : ''); } else { return this._groupsUrl; } } genUrl(path) { if (!path) { const groupId = null; const dir = null; const url = this.genUrlAddress(groupId, dir); return [groupId, dir, url]; } const parts = path.replace(/^\/+/, '').split('/'); if (parts.length === 0) { const groupId = null; const dir = null; const url = this.genUrlAddress(groupId, dir); return [groupId, dir, url]; } else if (parts.length === 1) { const groupId = parts[0]; const dir = null; const url = this.genUrlAddress(groupId, dir); return [groupId, dir, url]; } else { path = path.replace(/\/drive\/root\:/, ''); const groupId = parts[0]; const dir = ('/' + parts.slice(1).join('/')).replace(/^\/+/, ''); const url = this.genUrlAddress(groupId, dir); return [groupId, dir, url]; } } load(path, opts, callback) { this._oauthAuthorize((err) => { if (err) { return callback && callback(err); } this.logger.debug('Load', path); const ts = this.logger.ts(); const urlParts = this.genUrl(path); const groupId = urlParts[0]; path = urlParts[1]; const url = urlParts[2]; if (!groupId) { const err = 'no group id defined'; return callback && callback(err); } this._xhr({ url, responseType: 'json', success: (response) => { const downloadUrl = response['@microsoft.graph.downloadUrl']; let rev = response.eTag; if (!downloadUrl || !response.eTag) { this.logger.debug( 'Load error', path, 'no download url', response, this.logger.ts(ts) ); return callback && callback('no download url'); } this._xhr({ url: downloadUrl, responseType: 'arraybuffer', skipAuth: true, success: (response, xhr) => { rev = xhr.getResponseHeader('ETag') || rev; this.logger.debug('Loaded', path, rev, this.logger.ts(ts)); return callback && callback(null, response, { rev }); }, error: (err) => { this.logger.error('Load error', path, err, this.logger.ts(ts)); return callback && callback(err); } }); }, error: (err) => { this.logger.error('Load error', path, err, this.logger.ts(ts)); return callback && callback(err); } }); }); } stat(path, opts, callback) { this._oauthAuthorize((err) => { if (err) { return callback && callback(err); } this.logger.debug('Stat', path); const ts = this.logger.ts(); const urlParts = this.genUrl(path); const groupId = urlParts[0]; path = urlParts[1]; const url = urlParts[2]; if (!groupId) { const err = 'no group id defined'; return callback && callback(err); } this._xhr({ url, responseType: 'json', success: (response) => { const rev = response.eTag; if (!rev) { this.logger.error('Stat error', path, 'no eTag', this.logger.ts(ts)); return callback && callback('no eTag'); } this.logger.debug('Stated', path, rev, this.logger.ts(ts)); return callback && callback(null, { rev }); }, error: (err, xhr) => { if (xhr.status === 404) { this.logger.debug('Stated not found', path, this.logger.ts(ts)); return callback && callback({ notFound: true }); } this.logger.error('Stat error', path, err, this.logger.ts(ts)); return callback && callback(err); } }); }); } save(path, opts, data, callback, rev) { this._oauthAuthorize((err) => { if (err) { return callback && callback(err); } this.logger.debug('Save', path, rev); const ts = this.logger.ts(); const urlParts = this.genUrl(path); const groupId = urlParts[0]; path = urlParts[1]; const url = urlParts[2] + ':/content'; if (!groupId) { const err = 'no group id defined'; return callback && callback(err); } this._xhr({ url, method: 'PUT', responseType: 'json', headers: rev ? { 'If-Match': rev } : null, data, statuses: [200, 201, 412], success: (response, xhr) => { rev = response.eTag; if (!rev) { this.logger.error('Save error', path, 'no eTag', this.logger.ts(ts)); return callback && callback('no eTag'); } if (xhr.status === 412) { this.logger.debug('Save conflict', path, rev, this.logger.ts(ts)); return callback && callback({ revConflict: true }, { rev }); } this.logger.debug('Saved', path, rev, this.logger.ts(ts)); return callback && callback(null, { rev }); }, error: (err) => { this.logger.error('Save error', path, err, this.logger.ts(ts)); return callback && callback(err); } }); }); } list(dir, callback) { this._oauthAuthorize((err) => { if (err) { return callback && callback(err); } this.logger.debug('List', dir); const ts = this.logger.ts(); const urlParts = this.genUrl(dir); const groupId = urlParts[0]; dir = urlParts[1]; const urlPath = groupId ? (dir ? ':/children' : '/drive/root/children') : ''; const url = urlParts[2] + urlPath; const self = this; self._groupId = groupId; this._xhr({ url, responseType: 'json', success: (response) => { if (!response || !response.value) { this.logger.error('List error', this.logger.ts(ts), response); return callback && callback('list error'); } this.logger.debug('Listed', this.logger.ts(ts)); let fileList; if (!self._groupId) { fileList = response.value .filter((f) => f.displayName) .map((f) => ({ name: f.displayName, path: '/' + f.id, rev: f.id, dir: true })); } else { fileList = response.value .filter((f) => f.name) .map((f) => ({ name: f.name, path: `/${self._groupId}${f.parentReference.path}/${f.name}`, rev: f.eTag, dir: !!f.folder })); } return callback && callback(null, fileList); }, error: (err) => { this.logger.error('List error', this.logger.ts(ts), err); return callback && callback(err); } }); }); } remove(path, callback) { this.logger.debug('Remove', path); const ts = this.logger.ts(); const urlParts = this.genUrl(path); const groupId = urlParts[0]; path = urlParts[1]; const url = urlParts[2]; if (!groupId) { const err = 'no group id defined'; return callback && callback(err); } this._xhr({ url, method: 'DELETE', responseType: 'json', statuses: [200, 204], success: () => { this.logger.debug('Removed', path, this.logger.ts(ts)); return callback && callback(); }, error: (err) => { this.logger.error('Remove error', path, err, this.logger.ts(ts)); return callback && callback(err); } }); } mkdir(path, callback) { this._oauthAuthorize((err) => { if (err) { return callback && callback(err); } this.logger.debug('Make dir', path); const ts = this.logger.ts(); const urlParts = this.genUrl(path); const groupId = urlParts[0]; path = urlParts[1]; const url = urlParts[2] + '/drive/root/children'; if (!groupId) { const err = 'no group id defined'; return callback && callback(err); } const data = JSON.stringify({ name: path.replace('/drive/root:/', ''), folder: {} }); this._xhr({ url, method: 'POST', responseType: 'json', statuses: [200, 204], data, dataType: 'application/json', success: () => { this.logger.debug('Made dir', path, this.logger.ts(ts)); return callback && callback(); }, error: (err) => { this.logger.error('Make dir error', path, err, this.logger.ts(ts)); return callback && callback(err); } }); }); } logout(enabled) { this._oauthRevokeToken(); } _getOAuthConfig() { let clientId = this.appSettings.msteamsClientId; let clientSecret = this.appSettings.msteamsClientSecret; let tenant = this.appSettings.msteamsTenantId; if (!clientId) { if (Features.isDesktop) { ({ id: clientId, secret: clientSecret, tenantId: tenant } = MsTeamsApps.Desktop); } else if (Features.isLocal) { ({ id: clientId, secret: clientSecret, tenantId: tenant } = MsTeamsApps.Local); } else { ({ id: clientId, secret: clientSecret, tenantId: tenant } = MsTeamsApps.Production); } } tenant = tenant || 'common'; let scope = 'Sites.ReadWrite.All Team.ReadBasic.All'; if (!this.appSettings.shortLivedStorageToken) { scope += ' offline_access'; } return { url: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/authorize`, tokenUrl: `https://login.microsoftonline.com/${tenant}/oauth2/v2.0/token`, scope, clientId, clientSecret, pkce: true, width: 600, height: 500 }; } } export { StorageTeams };