mirror of
https://codeberg.org/timelimit/timelimit-server.git
synced 2026-08-31 19:03:45 +02:00
Initial commit
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* server component for the TimeLimit App
|
||||
* Copyright (C) 2019 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { split } from 'lodash'
|
||||
|
||||
export const serializedBitmaskRegex = /^(\d*,\d*(,\d*,\d*)*)?$/
|
||||
|
||||
export const validateBitmask = (bitmask: string, maxLength: number) => {
|
||||
if (!serializedBitmaskRegex.test(bitmask)) {
|
||||
throw new Error('bitmask does not match regex')
|
||||
}
|
||||
|
||||
if (bitmask === '') {
|
||||
return
|
||||
}
|
||||
|
||||
const splitpoints = split(bitmask, ',').map((item) => parseInt(item, 10))
|
||||
|
||||
if (splitpoints.findIndex((item) => !Number.isSafeInteger(item)) !== -1) {
|
||||
throw new Error('bitmask contains non-safe integers')
|
||||
}
|
||||
|
||||
if (splitpoints.findIndex((item) => item < 0) !== -1) {
|
||||
throw new Error('bitmask contains negative integers')
|
||||
}
|
||||
|
||||
if (splitpoints.findIndex((item) => item > maxLength) !== -1) {
|
||||
throw new Error('bitmask contains integers bigger than maxSize')
|
||||
}
|
||||
|
||||
let previousValue = -1
|
||||
|
||||
splitpoints.forEach((item) => {
|
||||
if (item <= previousValue) {
|
||||
throw new Error('bitmask numbers are not strictly sorted')
|
||||
}
|
||||
|
||||
previousValue = item
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* server component for the TimeLimit App
|
||||
* Copyright (C) 2019 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export const enumMax = <T> (a: T, b: T, values: Array<T>): T => (
|
||||
values[Math.max(values.indexOf(a), values.indexOf(b))]
|
||||
)
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* server component for the TimeLimit App
|
||||
* Copyright (C) 2019 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export function assertIsHexString (value: string) {
|
||||
if (value.length % 2 !== 0) {
|
||||
throw new Error('expected hex string but has got uneven length')
|
||||
}
|
||||
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const char = value[i]
|
||||
|
||||
if ('0123456789abcdef'.indexOf(char) === -1) {
|
||||
throw new Error('expected hex string but got invalid char')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* server component for the TimeLimit App
|
||||
* Copyright (C) 2019 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { uniq } from 'lodash'
|
||||
|
||||
export function assertNonEmptyListWithoutDuplicates (list: Array<string>) {
|
||||
if (list.length === 0) {
|
||||
throw new Error('expected not empty list')
|
||||
}
|
||||
|
||||
if (uniq(list).length !== list.length) {
|
||||
throw new Error('expected list without duplicates')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* server component for the TimeLimit App
|
||||
* Copyright (C) 2019 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as Email from 'email-templates'
|
||||
import { join } from 'path'
|
||||
|
||||
const mailimprint = process.env.MAIL_IMPRINT || 'not defined'
|
||||
|
||||
const email = new Email({
|
||||
message: {
|
||||
from: process.env.MAIL_SENDER || ''
|
||||
},
|
||||
transport: JSON.parse(process.env.MAIL_TRANSPORT || 'null') || undefined,
|
||||
views: {
|
||||
options: {
|
||||
extension: 'ejs'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export const sendAuthenticationMail = async ({ receiver, code, locale }: {receiver: string, code: string, locale: string}) => {
|
||||
await email.send({
|
||||
template: join(__dirname, '../../other/mail/login'),
|
||||
message: {
|
||||
to: receiver
|
||||
},
|
||||
locals: {
|
||||
subject: locale === 'de' ? 'Anmeldung bei TimeLimit' : 'Sign in at TimeLimit',
|
||||
introtext: locale === 'de' ? 'Geben Sie zum Authentifizieren folgenden Code in TimeLimit ein' : 'To authenticate, enter the following code in TimeLimit',
|
||||
code,
|
||||
outrotext: locale === 'de' ? 'Geben Sie diesen Code nicht an Dritte weiter.' : 'Do not share this code with third parties.',
|
||||
mailimprint
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const sendManipulationWarningMail = async ({ receiver, deviceName }: {
|
||||
receiver: string
|
||||
deviceName: string
|
||||
}) => {
|
||||
await email.send({
|
||||
template: join(__dirname, '../../other/mail/manipulation'),
|
||||
message: {
|
||||
to: receiver
|
||||
},
|
||||
locals: {
|
||||
subject: 'TimeLimit@' + deviceName + ' - Manipulation',
|
||||
deviceName,
|
||||
mailimprint
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const sendUninstallWarningMail = async ({ receiver, deviceName }: {
|
||||
receiver: string
|
||||
deviceName: string
|
||||
}) => {
|
||||
await email.send({
|
||||
template: join(__dirname, '../../other/mail/uninstall'),
|
||||
message: {
|
||||
to: receiver
|
||||
},
|
||||
locals: {
|
||||
subject: 'TimeLimit removed from ' + deviceName,
|
||||
deviceName,
|
||||
mailimprint
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* server component for the TimeLimit App
|
||||
* Copyright (C) 2019 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export const optionalPasswordRegex = /^(\$2a\$[123][0-9]\$[./A-Za-z0-9]{53})?$/
|
||||
export const optionalSaltRegex = /^(\$2a\$[123][0-9]\$[./A-Za-z0-9]{22})?$/
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* server component for the TimeLimit App
|
||||
* Copyright (C) 2019 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { readFileSync } from 'fs'
|
||||
import { range } from 'lodash'
|
||||
import { resolve } from 'path'
|
||||
|
||||
const wordlist = readFileSync(resolve(__dirname, '../../other/wordlist/de.txt'))
|
||||
.toString()
|
||||
.split('\n')
|
||||
.filter((item) => item.trim().length > 0)
|
||||
|
||||
const randomWord = () => wordlist[Math.floor(Math.random() * (wordlist.length - 1))]
|
||||
|
||||
export const randomWords = (numberOfWords: number) => (
|
||||
range(numberOfWords)
|
||||
.map((item) => randomWord())
|
||||
.join(' ')
|
||||
)
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* server component for the TimeLimit App
|
||||
* Copyright (C) 2019 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { RateLimiterAbstract, RateLimiterMemory } from 'rate-limiter-flexible'
|
||||
|
||||
const globalMailSendLimitMinute: RateLimiterAbstract = new RateLimiterMemory({
|
||||
keyPrefix: 'timelimit:sendmail-auth:global:minute',
|
||||
points: 5,
|
||||
duration: 60 // 1 minute
|
||||
})
|
||||
|
||||
const globalMailSendLimitHour: RateLimiterAbstract = new RateLimiterMemory({
|
||||
keyPrefix: 'timelimit:sendmail-auth:global:hour',
|
||||
points: 30,
|
||||
duration: 60 * 60 // 1 hour
|
||||
})
|
||||
|
||||
const gloablMailSendLimitDay: RateLimiterAbstract = new RateLimiterMemory({
|
||||
keyPrefix: 'timelimit:sendmail-auth:global:day',
|
||||
points: 100,
|
||||
duration: 60 * 60 * 24 // 1 day
|
||||
})
|
||||
|
||||
const checkGlobalMailSendLimit = async () => {
|
||||
await globalMailSendLimitMinute.consume('global')
|
||||
await globalMailSendLimitHour.consume('global')
|
||||
await gloablMailSendLimitDay.consume('global')
|
||||
}
|
||||
|
||||
const individualMailLimitFiveMinutes: RateLimiterAbstract = new RateLimiterMemory({
|
||||
keyPrefix: 'timelimit:sendmail-auth:individual:5minutes',
|
||||
points: 2,
|
||||
duration: 60 * 5 // 5 minutes
|
||||
})
|
||||
|
||||
const individualMailLimitDay: RateLimiterAbstract = new RateLimiterMemory({
|
||||
keyPrefix: 'timelimit:sendmail-auth:individual:day',
|
||||
points: 6,
|
||||
duration: 60 * 60 * 24 // 1 day
|
||||
})
|
||||
|
||||
const checkIndividualMailSendLimit = async (receiver: string) => {
|
||||
await individualMailLimitFiveMinutes.consume(receiver)
|
||||
await individualMailLimitDay.consume(receiver)
|
||||
}
|
||||
|
||||
export const checkMailSendLimit = async (receiver: string) => {
|
||||
await checkIndividualMailSendLimit(receiver)
|
||||
await checkGlobalMailSendLimit()
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* server component for the TimeLimit App
|
||||
* Copyright (C) 2019 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { RateLimiterAbstract, RateLimiterMemory } from 'rate-limiter-flexible'
|
||||
|
||||
const individualMailLimitMinute: RateLimiterAbstract = new RateLimiterMemory({
|
||||
keyPrefix: 'timelimit:sendmail-warning:individual:minute',
|
||||
points: 2,
|
||||
duration: 60 // 1 minute
|
||||
})
|
||||
|
||||
const individualMailLimitDay: RateLimiterAbstract = new RateLimiterMemory({
|
||||
keyPrefix: 'timelimit:sendmail-warning:individual:day',
|
||||
points: 20,
|
||||
duration: 60 * 60 * 24 // 1 day
|
||||
})
|
||||
|
||||
const checkIndividualMailSendLimit = async (receiver: string) => {
|
||||
await individualMailLimitMinute.consume(receiver)
|
||||
await individualMailLimitDay.consume(receiver)
|
||||
}
|
||||
|
||||
const checkMailSendLimit = async (receiver: string) => {
|
||||
await checkIndividualMailSendLimit(receiver)
|
||||
}
|
||||
|
||||
export const canSendWarningMail = async (receiver: string) => {
|
||||
try {
|
||||
await checkMailSendLimit(receiver)
|
||||
|
||||
return true
|
||||
} catch (ex) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* server component for the TimeLimit App
|
||||
* Copyright (C) 2019 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import * as TokenGenerator from 'tokgen'
|
||||
|
||||
const authTokenGenerator = new TokenGenerator({
|
||||
length: 32,
|
||||
chars: 'a-zA-Z0-9'
|
||||
})
|
||||
|
||||
export const generateAuthToken = () => authTokenGenerator.generate()
|
||||
|
||||
const idWithinFamilyGenerator = new TokenGenerator({
|
||||
length: 6,
|
||||
chars: 'a-zA-Z0-9'
|
||||
})
|
||||
|
||||
export const generateIdWithinFamily = () => idWithinFamilyGenerator.generate()
|
||||
export const isIdWithinFamily = (id: string) => id.length === 6 && /^[a-zA-Z0-9]+$/.test(id)
|
||||
export const assertIdWithinFamily = (id: string) => {
|
||||
if (!isIdWithinFamily(id)) {
|
||||
throw new Error('invalid id within family')
|
||||
}
|
||||
}
|
||||
|
||||
const versionIdGenerator = new TokenGenerator({
|
||||
length: 4,
|
||||
chars: 'a-zA-Z0-9'
|
||||
})
|
||||
|
||||
export const generateVersionId = () => versionIdGenerator.generate()
|
||||
|
||||
const familyIdGenerator = new TokenGenerator({
|
||||
length: 10,
|
||||
chars: 'a-zA-Z0-9'
|
||||
})
|
||||
|
||||
export const generateFamilyId = () => familyIdGenerator.generate()
|
||||
Reference in New Issue
Block a user