diff --git a/Readme.md b/Readme.md
index 2fe6008..642a31a 100644
--- a/Readme.md
+++ b/Readme.md
@@ -60,6 +60,14 @@ This fixes the causes of lint warnings (where possible).
- MAIL_SERVER_BLACKLIST
- list of domains, separated by comma
- if the user tries to use such a mail service, then he will get the notification that this provider is not supported
+- MAIL_WHITELIST
+ - list of mail addresses (``someone@somewhere.com``) or domains (``mailbox.org``), separated by comma
+ - if a user requests signing in with a mail address which is not in this list, then the request is rejected
+ - if the list is empty, then any mail address (except with domains from the blacklist) is allowed
+ - note: this allows a third party who knows the server url to check if a certain mail address is allowed by trying to sign in with it
+- DISABLE_SIGNUP
+ - ``yes`` or ``no`` (default: no)
+ - disables creating new families if ``yes`` is selected
## HTTPS
diff --git a/src/api/auth.ts b/src/api/auth.ts
index fb4b588..985c598 100644
--- a/src/api/auth.ts
+++ b/src/api/auth.ts
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
- * Copyright (C) 2019 Jonas Lochmann
+ * Copyright (C) 2019 - 2020 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -20,7 +20,7 @@ import { Router } from 'express'
import { BadRequest } from 'http-errors'
import { Database } from '../database'
import { sendLoginCode, signInByMailCode } from '../function/authentication/login-by-mail'
-import { isMailServerBlacklisted, sanitizeMailAddress } from '../util/mail'
+import { isMailAddressCoveredByWhitelist, isMailServerBlacklisted, sanitizeMailAddress } from '../util/mail'
import {
isSendMailLoginCodeRequest,
isSignInByMailCodeRequest
@@ -41,7 +41,9 @@ export const createAuthRouter = (database: Database) => {
throw new BadRequest()
}
- if (isMailServerBlacklisted(mail)) {
+ if (!isMailAddressCoveredByWhitelist(mail)) {
+ res.json({ mailAddressNotWhitelisted: true })
+ } else if (isMailServerBlacklisted(mail)) {
res.json({ mailServerBlacklisted: true })
} else {
const { mailLoginToken } = await sendLoginCode({
diff --git a/src/api/parent.ts b/src/api/parent.ts
index ef534cd..04ea3aa 100644
--- a/src/api/parent.ts
+++ b/src/api/parent.ts
@@ -1,6 +1,6 @@
/*
* server component for the TimeLimit App
- * Copyright (C) 2019 Jonas Lochmann
+ * Copyright (C) 2019 - 2020 Jonas Lochmann
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@@ -17,7 +17,8 @@
import { json } from 'body-parser'
import { Router } from 'express'
-import { BadRequest, Unauthorized } from 'http-errors'
+import { BadRequest, Forbidden, Unauthorized } from 'http-errors'
+import { config } from '../config'
import { Database } from '../database'
import { removeDevice } from '../function/device/remove-device'
import { canRecoverPassword } from '../function/parent/can-recover-password'
@@ -47,7 +48,11 @@ export const createParentRouter = ({ database, websocket }: {database: Database,
const { mailAuthToken } = req.body
const { status, mail } = await getStatusByMailToken({ database, mailAuthToken })
- res.json({ status, mail })
+ res.json({
+ status,
+ mail,
+ canCreateFamily: !config.disableSignup
+ })
} catch (ex) {
next(ex)
}
@@ -55,6 +60,10 @@ export const createParentRouter = ({ database, websocket }: {database: Database,
router.post('/create-family', json(), async (req, res, next) => {
try {
+ if (config.disableSignup) {
+ throw new Forbidden()
+ }
+
if (!isCreateFamilyByMailTokenRequest(req.body)) {
throw new BadRequest()
}
diff --git a/src/config.ts b/src/config.ts
new file mode 100644
index 0000000..508c89f
--- /dev/null
+++ b/src/config.ts
@@ -0,0 +1,37 @@
+/*
+ * server component for the TimeLimit App
+ * Copyright (C) 2019 - 2020 Jonas Lochmann
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, version 3 of the License.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+// note: soem configuration parameters are read directly at the location where they are used
+interface Config {
+ mailWhitelist: Array
+ disableSignup: boolean
+}
+
+function parseYesNo (value: string) {
+ if (value === 'yes') {
+ return true
+ } else if (value === 'no') {
+ return false
+ } else {
+ throw new Error('invalid value "' + value + '", expected "" or "no"')
+ }
+}
+
+export const config: Config = {
+ mailWhitelist: (process.env.MAIL_WHITELIST || '').split(',').map((item) => item.trim()).filter((item) => item.length > 0),
+ disableSignup: parseYesNo(process.env.DISABLE_SIGNUP || 'no')
+}
diff --git a/src/util/mail.ts b/src/util/mail.ts
index 9a32002..989ef75 100644
--- a/src/util/mail.ts
+++ b/src/util/mail.ts
@@ -18,6 +18,7 @@
import { parseOneAddress } from 'email-addresses'
import * as Email from 'email-templates'
import { join } from 'path'
+import { config } from '../config'
const mailimprint = process.env.MAIL_IMPRINT || 'not defined'
const mailServerBlacklist = (process.env.MAIL_SERVER_BLACKLIST || '').split(',').filter((item) => !!item)
@@ -84,13 +85,40 @@ export const sendUninstallWarningMail = async ({ receiver, deviceName }: {
})
}
-export function isMailServerBlacklisted (mail: string) {
+export function isMailServerBlacklisted (mail: string): boolean {
const parts = mail.split('@')
const domain = parts[parts.length - 1]
return mailServerBlacklist.indexOf(domain.toLowerCase()) !== -1
}
+export function isMailAddressCoveredByWhitelist (mail: string): boolean {
+ if (config.mailWhitelist.length === 0) {
+ return true
+ }
+
+ const mailParts = mail.split('@')
+ const mailDomain = mailParts[mailParts.length - 1]
+
+ for (let i = 0; i < config.mailWhitelist.length; i++) {
+ const whtielistItem = config.mailWhitelist[i]
+
+ const isDomain = whtielistItem.indexOf('@') === -1
+
+ if (isDomain) {
+ if (mailDomain === whtielistItem) {
+ return true
+ }
+ } else {
+ if (mail === whtielistItem) {
+ return true
+ }
+ }
+ }
+
+ return false
+}
+
export function sanitizeMailAddress (input: string): string | null {
const parsed = parseOneAddress(input)