mirror of https://github.com/keeweb/keeweb.git
366 lines
13 KiB
JavaScript
366 lines
13 KiB
JavaScript
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 };
|