diff --git a/docs/content/rctf/configuration.md b/docs/content/rctf/configuration.md
index e070e96..502f264 100644
--- a/docs/content/rctf/configuration.md
+++ b/docs/content/rctf/configuration.md
@@ -81,6 +81,7 @@ Configuration for advanced users - sane defaults are automatically set by the in
| `proxy.trust` | _(none)_ | yes | `false` | boolean, string, string array, or integer | X-Forwarded-For trust: the trust parameter to [proxy-addr](https://www.npmjs.com/package/proxy-addr) |
| `loginTimeout` | `RCTF_LOGIN_TIMEOUT` | yes | 3600000 | integer | lifetime of registration, email update, and recovery links, in milliseconds |
| `userMembers` | `RCTF_USER_MEMBERS` | yes | `true` | boolean | whether to allow a user to provide emails for individual members |
+| `registrationsEnabled` | _(none)_ | yes | `true` | boolean | whether to allow new teams creation |
| `database.migrate` | `RCTF_DATABASE_MIGRATE` | yes | `never` | `before` \| `only` \| `never` | how to run postgreSQL migrations. [documentation](management/migration.md) |
| `instanceType` | `RCTF_INSTANCE_TYPE` | yes | `all` | `all` \| `frontend` \| `leaderboard` | what type of instance to run. [documentation](management/scaling.md) |
| `challengeProvider` | _(none)_ | yes | `database` | provider | provider for challenges. [documentation](providers/challenges/index.md) |
diff --git a/packages/api-types/src/responses/badRegistrationsDisabled.yaml b/packages/api-types/src/responses/badRegistrationsDisabled.yaml
new file mode 100644
index 0000000..3ac4686
--- /dev/null
+++ b/packages/api-types/src/responses/badRegistrationsDisabled.yaml
@@ -0,0 +1,2 @@
+status: 400
+message: The registrations are disabled.
diff --git a/packages/api-types/src/responses/goodClientConfig.yml b/packages/api-types/src/responses/goodClientConfig.yml
index 492e46e..d1b994d 100644
--- a/packages/api-types/src/responses/goodClientConfig.yml
+++ b/packages/api-types/src/responses/goodClientConfig.yml
@@ -49,6 +49,8 @@ data:
type: string
emailEnabled:
type: boolean
+ registrationsEnabled:
+ type: boolean
ctftime:
type: object
properties:
diff --git a/packages/api-types/src/routes/auth/register/post.yml b/packages/api-types/src/routes/auth/register/post.yml
index e8b6d53..926476a 100644
--- a/packages/api-types/src/routes/auth/register/post.yml
+++ b/packages/api-types/src/routes/auth/register/post.yml
@@ -28,3 +28,4 @@ responses:
- badKnownCtftimeId
- badKnownEmail
- badKnownName
+ - badRegistrationsDisabled
diff --git a/packages/client/src/api/auth.js b/packages/client/src/api/auth.js
index 3b3a78e..cead164 100644
--- a/packages/client/src/api/auth.js
+++ b/packages/client/src/api/auth.js
@@ -82,6 +82,7 @@ export const register = async ({
case 'badEmail':
case 'badKnownEmail':
case 'badCompetitionNotAllowed':
+ case 'badRegistrationsDisabled':
return {
errors: {
email: resp.message,
diff --git a/packages/client/src/app.js b/packages/client/src/app.js
index d599109..c74b484 100644
--- a/packages/client/src/app.js
+++ b/packages/client/src/app.js
@@ -27,19 +27,25 @@ import { ToastProvider } from './components/toast'
import { navigateRef } from './history-hack'
import { hasChallsReadPermission } from './util/permissions'
+import config from './config'
const LoggedOutRedir =
const LoggedInRedir =
function App({ classes }) {
const loggedOut = !localStorage.token
+ const registerItems = config.registrationsEnabled
+ ? [
+ {
+ element: ,
+ path: '/register',
+ name: 'Register',
+ },
+ ]
+ : []
const loggedOutPaths = [
- {
- element: ,
- path: '/register',
- name: 'Register',
- },
+ ...registerItems,
{
element: ,
path: '/login',
diff --git a/packages/server/src/api/auth/register.js b/packages/server/src/api/auth/register.js
index b4f87f8..fca3cd4 100644
--- a/packages/server/src/api/auth/register.js
+++ b/packages/server/src/api/auth/register.js
@@ -10,6 +10,10 @@ import { getUserByNameOrEmail } from '../../database/users'
import { sendVerification } from '../../email'
export default makeFastifyRoute(authRegisterPost, async ({ req, res }) => {
+ if (!config.registrationsEnabled) {
+ return res.badRegistrationsDisabled()
+ }
+
let email
let ctftimeId
if (req.body.ctftimeToken !== undefined) {
diff --git a/packages/server/src/config/client.ts b/packages/server/src/config/client.ts
index de80622..e94545a 100644
--- a/packages/server/src/config/client.ts
+++ b/packages/server/src/config/client.ts
@@ -15,6 +15,7 @@ const config: ClientConfig = {
faviconUrl: server.faviconUrl,
emailEnabled: server.email != null,
userMembers: server.userMembers,
+ registrationsEnabled: server.registrationsEnabled,
ctftime:
server.ctftime == null
? undefined
diff --git a/packages/server/src/config/load.ts b/packages/server/src/config/load.ts
index 5e3c26a..f87f471 100644
--- a/packages/server/src/config/load.ts
+++ b/packages/server/src/config/load.ts
@@ -131,6 +131,7 @@ export const defaultConfig: PartialDeep = {
},
instanceType: 'all',
userMembers: true,
+ registrationsEnabled: true,
sponsors: [],
homeContent: '',
faviconUrl: 'https://redpwn.storage.googleapis.com/branding/rctf-favicon.ico',
diff --git a/packages/server/src/config/types.ts b/packages/server/src/config/types.ts
index 58dc142..75f13d7 100644
--- a/packages/server/src/config/types.ts
+++ b/packages/server/src/config/types.ts
@@ -49,6 +49,8 @@ export interface ServerConfig {
}
userMembers: boolean
+ registrationsEnabled: boolean
+
sponsors: Sponsor[]
homeContent: string
ctfName: string
@@ -98,6 +100,7 @@ export type ClientConfig = Pick<
| 'startTime'
| 'endTime'
| 'userMembers'
+ | 'registrationsEnabled'
| 'faviconUrl'
> & {
emailEnabled: boolean
diff --git a/packages/server/test/integration/auth.js b/packages/server/test/integration/auth.js
index af80b48..a42dd71 100644
--- a/packages/server/test/integration/auth.js
+++ b/packages/server/test/integration/auth.js
@@ -10,6 +10,7 @@ import {
goodRegister,
goodToken,
goodUserUpdate,
+ badRegistrationsDisabled,
} from '@rctf/api-types/responses'
import * as auth from '../../src/auth'
@@ -139,3 +140,18 @@ test('succeeds with goodUserUpdate', async () => {
expect(respUser.email).toBe(testUser.email)
expect(respUser.division).toBe(nextUser.division)
})
+
+test('fails with badRegistrationsDisabled', async () => {
+ const oldRegistrations = config.registrationsEnabled
+ config.registrationsEnabled = false
+
+ const resp = await request(app.server)
+ .post(process.env.API_ENDPOINT + '/auth/register')
+ .send({
+ ...testUser,
+ })
+ .expect(badRegistrationsDisabled.status)
+
+ expect(resp.body.kind).toBe('badRegistrationsDisabled')
+ config.registrationsEnabled = oldRegistrations
+})