Unverified Commit 9472f915 authored by Philipp Berger's avatar Philipp Berger
Browse files

chore: release v2.2.0

parent b1ad548f
# Changelog
### 2.2.0 (2021-10-01)
* **backend** feat: add operator device support
* **backend** feat: add operator device feature flag
* **locations** feat: add devices list screen
* **locations** feat: add device reactivation flow
* **locations** feat: add device registration flow
### 2.1.0 (2021-10-01)
* **backend** feat: add signed location transfer support
* **backend** feat: add average checkin time
......
......@@ -45,6 +45,18 @@ node {
triggerDeploy('hotfix', GIT_VERSION)
}
if (env.BRANCH_NAME.startsWith('preview-1/')) {
triggerDeploy('p1', GIT_VERSION)
}
if (env.BRANCH_NAME.startsWith('preview-2/')) {
triggerDeploy('p2', GIT_VERSION)
}
if (env.BRANCH_NAME.startsWith('preview-3/')) {
triggerDeploy('p3', GIT_VERSION)
}
if (env.BRANCH_NAME == 'master') {
triggerDeploy('preprod', GIT_VERSION)
}
......
{
"name": "e2e",
"version": "2.1.0",
"version": "2.2.0",
"main": "index.js",
"private": true,
"engines": {
......
{
"name": "@lucaapp/web",
"version": "2.1.0",
"version": "2.2.0",
"private": true,
"license": "Apache-2.0",
"author": "Culture4Life <hello@luca-app.de> (https://www.luca-app.de/)",
......
......@@ -53,6 +53,10 @@ module.exports = {
v4: 'BADGE_ATTESTATION_KEY_PUBLIC_V4',
},
},
operatorDevice: {
publicKey: 'OPERATOR_DEVICE_PUBLIC_KEY',
privateKey: 'OPERATOR_DEVICE_PRIVATE_KEY',
}
},
proxy: {
http: 'http_proxy',
......
......@@ -68,6 +68,11 @@ module.exports = {
gateway: '',
},
luca: {
challenges: {
operatorDeviceCreation: {
maxAgeMinutes: moment.duration(30, 'minutes'),
},
},
traces: {
maximumRequestablePeriod: moment.duration(24, 'hours').as('hours'),
maxAge: moment.duration(28, 'days').as('hours'),
......@@ -90,6 +95,11 @@ module.exports = {
maxAgeHours: moment.duration(28, 'days').as('hours'),
},
},
operatorDevice: {
unactivated: {
maxAgeMinutes: moment.duration(30, 'minutes'),
},
},
users: {
maxAge: moment.duration(28, 'days').as('hours'),
},
......@@ -127,6 +137,21 @@ module.exports = {
max: 28,
minKeyAge: moment.duration(24, 'hours').as('hours'),
},
operatorDevice: {
expire: moment.duration(31, 'days').as('millisecond'),
maxReactivationAge: moment.duration(20, 'minutes').as('millisecond'),
// DEV ONLY TOKEN
publicKey: `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEc0JU9Xhlom553niIAc4K9C/1ZXOT
AQp4BE3MdB9LqeGgVw78Krp0/YoQRPZmvBzBwXUZFmB+ZmcMcywB7aAXTw==
-----END PUBLIC KEY-----`,
// DEV ONLY TOKEN
privateKey: `-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIGEXxQ0ksJNT0AV4srZvxR86UTSUv63yuvfdqv5+ZyTfoAoGCCqGSM49
AwEHoUQDQgAEc0JU9Xhlom553niIAc4K9C/1ZXOTAQp4BE3MdB9LqeGgVw78Krp0
/YoQRPZmvBzBwXUZFmB+ZmcMcywB7aAXTw==
-----END EC PRIVATE KEY-----`,
},
badge: {
targetKeyId: 2,
keyLength: 64,
......@@ -192,6 +217,11 @@ module.exports = {
operator_email_patch_ratelimit_hour: 5,
keys_daily_rotate_post_ratelimit_hour: 5,
location_transfer_post_ratelimit_hour: 5,
challenges_operatorDevice_get_ratelimit_hour: 50,
challenges_operatorDevice_post_ratelimit_day: 1000,
challenges_operatorDevice_post_ratelimit_hour: 100,
challenges_operatorDevice_patch_ratelimit_day: 8000,
challenges_operatorDevice_patch_ratelimit_hour: 800,
notifications_traces_get_ratelimit_hour: 5,
notifications_v4_health_departments_get_ratelimit_hour: 1000,
notifications_v4_traces_active_chunk_get_ratelimit_hour: 1000,
......
{
"name": "@lucaapp/backend",
"version": "2.1.0",
"version": "2.2.0",
"private": true,
"license": "Apache-2.0",
"author": "Culture4Life <hello@luca-app.de> (https://www.luca-app.de/)",
......@@ -29,11 +29,14 @@
"@tsconfig/node14": "1.0.1",
"@types/config": "0.0.39",
"@types/etag": "1.8.1",
"@types/passport": "1.0.7",
"@types/express": "4.17.13",
"@types/lodash": "4.14.172",
"@types/jsonwebtoken": "8.5.4",
"@types/node-forge": "0.10.3",
"@types/validator": "13.6.3",
"@types/node-forge": "0.10.5",
"@types/jsonwebtoken": "8.5.5",
"axios": "0.21.4",
"bloomit": "1.1.1",
"colors": "1.4.0",
......@@ -65,6 +68,7 @@
"node-forge": "0.10.0",
"passport": "0.4.1",
"passport-custom": "1.1.1",
"passport-jwt": "4.0.0",
"passport-local": "1.0.0",
"pg": "8.7.1",
"prom-client": "13.2.0",
......
......@@ -4,17 +4,28 @@ interface IUser {
interface IOperator extends IUser {
type: 'Operator';
allowOperatorDevices: boolean;
}
interface IOperatorDevice extends IOperator {
type: 'OperatorDevice';
device: {
uuid: string;
name: string;
os: 'android' | 'ios';
role: 'scanner' | 'employee' | 'manager';
};
}
interface IHealthDepartmentEmployee extends IUser {
HealthDepartment: unknown;
departmentId: string;
isAdmin: boolean;
departmentId: string;
HealthDepartment: unknown;
type: 'HealthDepartmentEmployeee';
}
declare namespace Express {
export interface Request {
user?: IUser | IOperator | IHealthDepartmentEmployee;
user?: IUser | IOperator | IOperatorDevice | IHealthDepartmentEmployee;
}
}
......@@ -16,6 +16,7 @@ const { noCache } = require('./middlewares/noCache');
const passportSession = require('./passport/session');
const bearerBadgeGeneratorStrategy = require('./passport/bearerBadgeGenerator');
const localOperatorStrategy = require('./passport/localOperator');
const operatorDeviceStrategy = require('./passport/operatorDevice');
const localHealthDepartmentEmployeeStrategy = require('./passport/localHealthDepartmentEmployee');
const requestMetricsMiddleware = require('./middlewares/requestMetrics');
......@@ -25,7 +26,7 @@ const licensesRouter = require('./routes/licenses');
const internalRouter = require('./routes/internal');
const v2Router = require('./routes/v2');
const v3Router = require('./routes/v3');
const v4Router = require('./routes/v4');
const v4Router = require('./routes/v4').default;
let app;
......@@ -43,6 +44,7 @@ const configureApp = () => {
'local-healthDepartmentEmployee',
localHealthDepartmentEmployeeStrategy
);
passport.use('jwt-operatorDevice', operatorDeviceStrategy);
app = express();
const router = express.Router();
......
export enum ChallengeType {
OperatorDeviceCreation = 'OperatorDeviceCreation',
}
export enum OperatorDeviceCreationChallengeState {
Ready = 'READY',
Canceled = 'CANCELED',
AuthenticationPINRequired = 'AUTHENTICATION_PIN_REQUIRED',
PrivateKeyRequired = 'PRIVATE_KEY_REQUIRED',
PrivateKeyPINRequired = 'PRIVATE_KEY_PIN_REQUIRED',
Done = 'DONE',
}
// eslint-disable-next-line no-shadow
export enum OperatorDevice {
scanner = 'scanner',
employee = 'employee',
manager = 'manager',
}
// eslint-disable-next-line no-shadow
export enum OperatorDeviceSupportedOSTypes {
IOS = 'ios',
Android = 'android',
}
module.exports = {
up: async (queryInterface, DataTypes) => {
await queryInterface.createTable('OperatorDevices', {
uuid: {
type: DataTypes.UUID,
allowNull: false,
primaryKey: true,
defaultValue: DataTypes.UUIDV4,
},
activated: {
allowNull: false,
defaultValue: false,
type: DataTypes.BOOLEAN,
},
name: {
type: DataTypes.STRING(64),
},
os: {
defaultValue: 'unknown',
type: DataTypes.STRING(8),
},
role: {
allowNull: false,
type: DataTypes.ENUM(['scanner', 'employee', 'manager']),
},
operatorId: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'Operators',
key: 'uuid',
},
onUpdate: 'CASCADE',
onDelete: 'CASCADE',
},
refreshedAt: {
allowNull: false,
type: DataTypes.DATE,
},
reactivatedAt: {
allowNull: true,
type: DataTypes.DATE,
},
createdAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.literal('CURRENT_TIMESTAMP'),
},
updatedAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.literal('CURRENT_TIMESTAMP'),
},
});
},
down: queryInterface => {
return queryInterface.dropTable('OperatorDevices');
},
};
module.exports = {
up: async (queryInterface, DataTypes) => {
return queryInterface.sequelize.transaction(transaction => {
return Promise.all([
queryInterface.addColumn(
'FeatureFlags',
'locationFrontend',
{
allowNull: false,
defaultValue: false,
type: DataTypes.BOOLEAN,
},
{ transaction }
),
queryInterface.addColumn(
'FeatureFlags',
'healthDepartmentFrontend',
{
allowNull: false,
defaultValue: false,
type: DataTypes.BOOLEAN,
},
{ transaction }
),
queryInterface.addColumn(
'FeatureFlags',
'webapp',
{
allowNull: false,
defaultValue: false,
type: DataTypes.BOOLEAN,
},
{ transaction }
),
queryInterface.addColumn(
'FeatureFlags',
'ios',
{
allowNull: false,
defaultValue: false,
type: DataTypes.BOOLEAN,
},
{ transaction }
),
queryInterface.addColumn(
'FeatureFlags',
'android',
{
allowNull: false,
defaultValue: false,
type: DataTypes.BOOLEAN,
},
{ transaction }
),
queryInterface.addColumn(
'FeatureFlags',
'operatorApp',
{
allowNull: false,
defaultValue: false,
type: DataTypes.BOOLEAN,
},
{ transaction }
),
]);
});
},
down: async queryInterface => {
return queryInterface.sequelize.transaction(transaction => {
return Promise.all([
queryInterface.removeColumn('FeatureFlags', 'locationFrontend', {
transaction,
}),
queryInterface.removeColumn(
'FeatureFlags',
'healthDepartmentFrontend',
{ transaction }
),
queryInterface.removeColumn('FeatureFlags', 'webapp', { transaction }),
queryInterface.removeColumn('FeatureFlags', 'ios', { transaction }),
queryInterface.removeColumn('FeatureFlags', 'android', { transaction }),
queryInterface.removeColumn('FeatureFlags', 'operatorApp', {
transaction,
}),
]);
});
},
};
module.exports = {
up: async (queryInterface, DataTypes) =>
queryInterface.addColumn('Operators', 'allowOperatorDevices', {
allowNull: false,
defaultValue: false,
type: DataTypes.BOOLEAN,
}),
down: queryInterface => {
return queryInterface.removeColumn('Operators', 'allowOperatorDevices');
},
};
module.exports = {
up: async (queryInterface, DataTypes) => {
await queryInterface.createTable('Challenges', {
uuid: {
type: DataTypes.UUID,
allowNull: false,
primaryKey: true,
defaultValue: DataTypes.UUIDV4,
},
type: {
allowNull: false,
type: DataTypes.STRING(32),
},
state: {
allowNull: false,
type: DataTypes.STRING(32),
},
createdAt: {
type: DataTypes.DATE,
allowNull: false,
defaultValue: DataTypes.literal('CURRENT_TIMESTAMP'),
},
updatedAt: {
allowNull: false,
type: DataTypes.DATE,
defaultValue: DataTypes.literal('CURRENT_TIMESTAMP'),
},
});
},
down: queryInterface => {
return queryInterface.dropTable('Challenges');
},
};
module.exports = (Sequelize, DataTypes) => {
return Sequelize.define('Challenge', {
uuid: {
type: DataTypes.UUID,
allowNull: false,
primaryKey: true,
defaultValue: DataTypes.UUIDV4,
},
type: {
allowNull: false,
type: DataTypes.STRING(32),
},
state: {
allowNull: false,
type: DataTypes.STRING(32),
},
});
};
......@@ -8,5 +8,35 @@ module.exports = (Sequelize, DataTypes) => {
value: {
type: DataTypes.STRING,
},
locationFrontend: {
allowNull: false,
defaultValue: false,
type: DataTypes.BOOLEAN,
},
healthDepartmentFrontend: {
allowNull: false,
defaultValue: false,
type: DataTypes.BOOLEAN,
},
webapp: {
allowNull: false,
defaultValue: false,
type: DataTypes.BOOLEAN,
},
ios: {
allowNull: false,
defaultValue: false,
type: DataTypes.BOOLEAN,
},
android: {
allowNull: false,
defaultValue: false,
type: DataTypes.BOOLEAN,
},
operatorApp: {
allowNull: false,
defaultValue: false,
type: DataTypes.BOOLEAN,
},
});
};
......@@ -83,6 +83,11 @@ module.exports = (Sequelize, DataTypes) => {
defaultValue: '1.0.0',
type: DataTypes.STRING(32),
},
allowOperatorDevices: {
allowNull: false,
defaultValue: false,
type: DataTypes.BOOLEAN,
},
isTrusted: {
allowNull: false,
defaultValue: false,
......
const {
OperatorDevice: OperatorDeviceType,
// eslint-disable-next-line node/no-missing-require
} = require('constants/operatorDevice');
module.exports = (Sequelize, DataTypes) => {
const OperatorDevice = Sequelize.define('OperatorDevice', {
uuid: {
type: DataTypes.UUID,
allowNull: false,
primaryKey: true,
defaultValue: DataTypes.UUIDV4,
},
activated: {
allowNull: false,
defaultValue: false,
type: DataTypes.BOOLEAN,
},
name: {
type: DataTypes.STRING(64),
},
os: {
defaultValue: 'unknown',
type: DataTypes.STRING(8),
},
role: {
allowNull: false,
type: DataTypes.ENUM([
OperatorDeviceType.scanner,
OperatorDeviceType.employee,
OperatorDeviceType.manager,
]),
},
reactivatedAt: {
allowNull: true,
type: DataTypes.DATE,
},
refreshedAt: {
allowNull: false,
type: DataTypes.DATE,
},
});
OperatorDevice.associate = models => {
OperatorDevice.belongsTo(models.Operator, {
foreignKey: 'operatorId',
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
});
};
return OperatorDevice;
};
const config = require('config');
const status = require('http-status');
const ApiError = require('../utils/apiError');
const { ApiError } = require('../utils/apiError');
const { SessionError } = require('../passport/session');
const handle404 = (request, response) => response.sendStatus(status.NOT_FOUND);
......@@ -25,7 +25,7 @@ const handle500 = (error, request, response, next) => {
return response.status(error.statusCode).send(errorDTO);
}
if (config.get('debug')) {
if (config.get('debug') && next) {
return next(error);
}
if (error.statusCode) {
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment