From 07e61bd34794fb8db3d5424bcecb531bb051e1ec Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Fri, 2 Nov 2018 18:50:13 -0700 Subject: [PATCH] First pass, can create and update --- app/scenes/Settings/Details.js | 24 +++++++++- server/api/team.js | 3 +- server/domains.js | 25 ++++++++++ .../20181031015046-add-subdomain-to-team.js | 14 ++++++ server/models/Team.js | 47 ++++++++++--------- 5 files changed, 88 insertions(+), 25 deletions(-) create mode 100644 server/domains.js create mode 100644 server/migrations/20181031015046-add-subdomain-to-team.js diff --git a/app/scenes/Settings/Details.js b/app/scenes/Settings/Details.js index 686d6d745..55586a71f 100644 --- a/app/scenes/Settings/Details.js +++ b/app/scenes/Settings/Details.js @@ -25,6 +25,7 @@ class Details extends React.Component { form: ?HTMLFormElement; @observable name: string; + @observable subdomain: string; @observable avatarUrl: ?string; componentDidMount() { @@ -51,12 +52,16 @@ class Details extends React.Component { this.name = ev.target.value; }; + handleSubdomainChange = (ev: SyntheticInputEvent<*>) => { + this.subdomain = ev.target.value.toLowerCase(); + }; + handleAvatarUpload = (avatarUrl: string) => { this.avatarUrl = avatarUrl; }; handleAvatarError = (error: ?string) => { - this.props.ui.showToast(error || 'Unable to upload new avatar'); + this.props.ui.showToast(error || 'Unable to upload new logo'); }; get isValid() { @@ -104,11 +109,28 @@ class Details extends React.Component {
(this.form = ref)}> + + + {this.subdomain && ( + + You will be able to access your wiki at{' '} + {this.subdomain}.getoutline.com + + )} + diff --git a/server/api/team.js b/server/api/team.js index 3bc4a1b8a..3c83ee675 100644 --- a/server/api/team.js +++ b/server/api/team.js @@ -12,7 +12,7 @@ const { authorize } = policy; const router = new Router(); router.post('team.update', auth(), async ctx => { - const { name, avatarUrl, sharing } = ctx.body; + const { name, avatarUrl, subdomain, sharing } = ctx.body; const endpoint = publicS3Endpoint(); const user = ctx.state.user; @@ -20,6 +20,7 @@ router.post('team.update', auth(), async ctx => { authorize(user, 'update', team); if (name) team.name = name; + if (subdomain) team.subdomain = subdomain; if (sharing !== undefined) team.sharing = sharing; if (avatarUrl && avatarUrl.startsWith(`${endpoint}/uploads/${user.id}`)) { team.avatarUrl = avatarUrl; diff --git a/server/domains.js b/server/domains.js new file mode 100644 index 000000000..ddac4a73b --- /dev/null +++ b/server/domains.js @@ -0,0 +1,25 @@ +// @flow +export const RESERVED_SUBDOMAINS = [ + 'admin', + 'api', + 'beta', + 'blog', + 'cdn', + 'community', + 'developer', + 'forum', + 'help', + 'imap', + 'localhost', + 'mail', + 'ns1', + 'ns2', + 'ns3', + 'ns4', + 'smtp', + 'support', + 'status', + 'static', + 'test', + 'www', +]; diff --git a/server/migrations/20181031015046-add-subdomain-to-team.js b/server/migrations/20181031015046-add-subdomain-to-team.js new file mode 100644 index 000000000..75f805a12 --- /dev/null +++ b/server/migrations/20181031015046-add-subdomain-to-team.js @@ -0,0 +1,14 @@ +module.exports = { + up: async (queryInterface, Sequelize) => { + await queryInterface.addColumn('teams', 'subdomain', { + type: Sequelize.STRING, + allowNull: true, + unique: true + }); + await queryInterface.addIndex('teams', ['subdomain']); + }, + down: async (queryInterface, Sequelize) => { + await queryInterface.removeColumn('teams', 'subdomain'); + await queryInterface.removeIndex('teams', ['subdomain']); + } +} \ No newline at end of file diff --git a/server/models/Team.js b/server/models/Team.js index 933303854..79b90d5ee 100644 --- a/server/models/Team.js +++ b/server/models/Team.js @@ -2,33 +2,34 @@ import uuid from 'uuid'; import { DataTypes, sequelize, Op } from '../sequelize'; import { publicS3Endpoint, uploadToS3FromUrl } from '../utils/s3'; +import { RESERVED_SUBDOMAINS } from '../domains'; import Collection from './Collection'; import User from './User'; -const Team = sequelize.define( - 'team', - { - id: { - type: DataTypes.UUID, - defaultValue: DataTypes.UUIDV4, - primaryKey: true, - }, - name: DataTypes.STRING, - slackId: { type: DataTypes.STRING, allowNull: true }, - googleId: { type: DataTypes.STRING, allowNull: true }, - avatarUrl: { type: DataTypes.STRING, allowNull: true }, - sharing: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true }, - slackData: DataTypes.JSONB, +const Team = sequelize.define('team', { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + primaryKey: true, }, - { - indexes: [ - { - unique: true, - fields: ['slackId'], - }, - ], - } -); + name: DataTypes.STRING, + subdomain: { + type: DataTypes.STRING, + allowNull: true, + validate: { + isLowercase: true, + isAlphanumeric: true, + len: [4, 32], + notIn: [RESERVED_SUBDOMAINS], + }, + unique: true, + }, + slackId: { type: DataTypes.STRING, allowNull: true }, + googleId: { type: DataTypes.STRING, allowNull: true }, + avatarUrl: { type: DataTypes.STRING, allowNull: true }, + sharing: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: true }, + slackData: DataTypes.JSONB, +}); Team.associate = models => { Team.hasMany(models.Collection, { as: 'collections' });