diff --git a/.github/workflows/bearer.yaml b/.github/workflows/bearer.yaml new file mode 100644 index 0000000..c7337c1 --- /dev/null +++ b/.github/workflows/bearer.yaml @@ -0,0 +1,19 @@ +name: 🧸 Bearer + +on: + push: + branches: + - main + +permissions: + contents: read + +jobs: + rule_check: + runs-on: ubuntu-latest + steps: + - name: ⬇️ Checkout + uses: actions/checkout@v4 + + - name: 🧸 Bearer + uses: bearer/bearer-action@v2 diff --git a/graphql.schema.json b/graphql.schema.json index 141081c..5556b6e 100644 --- a/graphql.schema.json +++ b/graphql.schema.json @@ -3591,6 +3591,33 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "removeUser", + "description": "", + "args": [ + { + "name": "phoneNumber", + "description": "", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null + } + ], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "requestCode", "description": "", diff --git a/src/config/index.ts b/src/config/index.ts index 1daab2d..58edd50 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -55,24 +55,9 @@ const optionalConfigs = { LOGGING_DISCORD: process.env.LOGGING_DISCORD, LOGGING_DISCORD_TOKEN: process.env.LOGGING_DISCORD_TOKEN, LOGGING_DISCORD_WEBHOOK: process.env.LOGGING_DISCORD_WEBHOOK, - LOGGING_MONGO: process.env.LOGGING_MONGO || false, + LOGGING_MONGO: process.env.LOGGING_MONGO !== 'false' || false, }; -if (process.env.NODE_ENV === 'development') { - console.log( - 'CONFIG', - JSON.stringify( - { - ...requiredConfigs, - ...recommendedConfigs, - ...optionalConfigs, - }, - null, - 2, - ), - ); -} - export default { ...requiredConfigs, ...recommendedConfigs, diff --git a/src/express/auth/index.ts b/src/express/auth/index.ts index 6689af5..e827bc8 100644 --- a/src/express/auth/index.ts +++ b/src/express/auth/index.ts @@ -136,10 +136,16 @@ export const authMiddleware = async (req: ExpressReqContext, res: Response, next } // Set new timestamps req.user.markModified('updatedAt'); - await req.user.save(); + await req.user.save().catch((e) => { + logger.error("Couldn't save User 1", req.user); + logger.error(e); + }); if (req.device) { req.device.markModified('updatedAt'); - await req.device.save(); + await req.device.save().catch((e) => { + logger.error("Couldn't save Device 1", req.device); + logger.error(e); + }); } if (req.phone) { req.phone.markModified('updatedAt'); @@ -168,10 +174,16 @@ export const authMiddleware = async (req: ExpressReqContext, res: Response, next } // Set new timestamps req.user.markModified('updatedAt'); - await req.user.save(); + await req.user.save().catch((e) => { + logger.error("Couldn't save User 2", req.user); + logger.error(e); + }); if (req.device) { req.device.markModified('updatedAt'); - await req.device.save(); + await req.device.save().catch((e) => { + logger.error("Couldn't save Device 2", req.device); + logger.error(e); + }); } if (req.phone) { req.phone.markModified('updatedAt'); @@ -193,7 +205,7 @@ export const authMiddleware = async (req: ExpressReqContext, res: Response, next // logger.jwt('JWT: Credentials present'); // User device = await DeviceModel.findOne({ - deviceHash: crypto.createHash('sha256').update(deviceHash).digest('hex'), + deviceHash, }); phone = phoneHash ? await PhoneModel.findOne({ @@ -205,33 +217,46 @@ export const authMiddleware = async (req: ExpressReqContext, res: Response, next // logger.jwt('JWT: Create new User'); device = await DeviceModel.findOne({ - deviceHash: crypto.createHash('sha256').update(deviceHash).digest('hex'), + deviceHash, }); // Device if (!device) { device = new DeviceModel({ - deviceHash: crypto.createHash('sha256').update(deviceHash).digest('hex'), + deviceHash, }); - await device.save().catch((e) => { - logger.error("Couldn't save Device") + try { + device = await device.save(); + } catch (e) { + logger.error("Couldn't save Device 3", device.toObject(), { deviceHash }); logger.error(e); - }); + } } // Create user logger.debug('Create new User'); user = new UserModel({ device, phone }); - await user.save(); + try { + user = await user.save(); + } catch (e) { + logger.error("Couldn't save User 3", user.toObject()); + logger.error(e); + } } // logger.jwt(`JWT: Token New for User: ${user._id}`); const [createToken, createRefreshToken] = await createTokens(user._id); headerToken({ res, token: createToken, refreshToken: createRefreshToken }); // Set new timestamps user.markModified('updatedAt'); - await user.save(); + await user.save().catch((e) => { + logger.error("Couldn't save User 4", user.toObject()); + logger.error(e); + }); if (device) { device.markModified('updatedAt'); - await device.save(); + await device.save().catch((e) => { + logger.error("Couldn't save Device 4", device.toObject()); + logger.error(e); + }); } if (phone) { phone.markModified('updatedAt'); diff --git a/src/generated/graphql.ts b/src/generated/graphql.ts index a877f73..4e91170 100644 --- a/src/generated/graphql.ts +++ b/src/generated/graphql.ts @@ -441,6 +441,7 @@ export type Mutation = { addToken: TokenResult; finishSearch: SearchTerm; increaseActivity?: Maybe; + removeUser?: Maybe; requestCode: CodeResult; requestVerification: VerificationResult; signUp?: Maybe; @@ -466,6 +467,11 @@ export type MutationIncreaseActivityArgs = { }; +export type MutationRemoveUserArgs = { + phoneNumber: Scalars['String']['input']; +}; + + export type MutationRequestCodeArgs = { newPhone: Scalars['String']['input']; oldPhoneHash?: InputMaybe; @@ -956,6 +962,7 @@ export type MutationResolvers>; finishSearch?: Resolver>; increaseActivity?: Resolver, ParentType, ContextType, RequireFields>; + removeUser?: Resolver, ParentType, ContextType, RequireFields>; requestCode?: Resolver>; requestVerification?: Resolver>; signUp?: Resolver, ParentType, ContextType, RequireFields>; diff --git a/src/graphql/resolvers/Activity.integ.ts b/src/graphql/resolvers/Activity.integ.ts new file mode 100644 index 0000000..ddf9528 --- /dev/null +++ b/src/graphql/resolvers/Activity.integ.ts @@ -0,0 +1,140 @@ +import { + IProcedure, + Device, + Phone, + User, + ProcedureModel, + DeviceModel, + PhoneModel, + UserModel, + ActivityModel, +} from '@democracy-deutschland/democracy-common'; +import axios from 'axios'; +import crypto from 'crypto'; +import { connectDB, disconnectDB } from '../../services/mongoose'; +import config from '../../config'; + +const GRAPHQL_API_URL = process.env.GRAPHQL_API_URL || 'http://localhost:3000'; + +describe('Activity Resolvers', () => { + describe('Mutations', () => { + describe('increaseActivity', () => { + const DEVICE_HASH = 'SOME_DEVICE_HASH_ACTIVITY_RESOLVER_INCREASE_ACTIVITY'; + const PHONE_NUMBER = `+49111111111`; + const xPhoneHash = crypto.createHash('sha256').update(PHONE_NUMBER).digest('hex'); + const phoneHash = crypto.createHash('sha256').update(xPhoneHash).digest('hex'); + let procedure: IProcedure; + let device: Device; + let phone: Phone; + let user: User; + + beforeAll(async () => { + await connectDB(config.DB_URL, { debug: false }); + + // create tmp procedure + procedure = await ProcedureModel.create({ + procedureId: '0000000', + title: 'tmp procedure for increaseActivity test', + period: 1, + type: 'Antrag', + voteResults: { + yes: 0, + no: 0, + abstination: 0, + }, + }); + + device = await DeviceModel.create({ + deviceHash: DEVICE_HASH, + }); + + phone = await PhoneModel.create({ + phoneHash, + }); + + // create tmp user + user = await UserModel.create({ + verified: true, + device, + phone, + }); + }); + + afterAll(async () => { + await Promise.all([ + ActivityModel.deleteOne({ procedure: procedure }), + procedure.remove(), + phone.remove(), + device.remove(), + user.remove(), + ]); + + await disconnectDB(); + }); + + it('fail to increase activity on non-existing procedure', async () => { + const response = await axios.post( + GRAPHQL_API_URL, + { + query: ` + mutation IncreaseActivity($procedureId: String!) { + increaseActivity(procedureId: $procedureId) { + activityIndex + active + } + } + `, + variables: { + procedureId: 'non-existing-procedure-id', + }, + }, + { + headers: { + 'Content-Type': 'application/json', + 'x-device-hash': device.deviceHash, + 'x-phone-hash': xPhoneHash, + }, + }, + ); + + const { data } = response.data; + + expect(data).toBeDefined(); + expect(data.increaseActivity).toBeNull(); + }); + + it('increase activity', async () => { + const response = await axios.post( + GRAPHQL_API_URL, + { + query: ` + mutation IncreaseActivity($procedureId: String!) { + increaseActivity(procedureId: $procedureId) { + activityIndex + active + } + } + `, + variables: { + procedureId: '0000000', + }, + }, + { + headers: { + 'Content-Type': 'application/json', + 'x-device-hash': device.deviceHash, + 'x-phone-hash': xPhoneHash, + }, + }, + ); + + const { data } = response.data; + + expect(data).toBeDefined(); + expect(data.increaseActivity).toBeDefined(); + expect(data.increaseActivity.activityIndex).toBeDefined(); + expect(data.increaseActivity.active).toBeTruthy(); + }); + }); + }); +}); diff --git a/src/graphql/resolvers/Activity.ts b/src/graphql/resolvers/Activity.ts index 227b643..7faae73 100644 --- a/src/graphql/resolvers/Activity.ts +++ b/src/graphql/resolvers/Activity.ts @@ -38,8 +38,10 @@ const ActivityApi: Resolvers = { searchQuery = { procedureId }; } const procedure = await ProcedureModel.findOne(searchQuery); + if (!procedure) { - throw new Error('Procedure not found'); + logger.error('Procedure not found', { procedureId }); + throw new Error(`Procedure not found: ${procedureId}`); } let active = await ActivityModel.findOne({ actor: CONFIG.SMS_VERIFICATION ? phone._id : device._id, diff --git a/src/graphql/resolvers/Device.integ.ts b/src/graphql/resolvers/Device.integ.ts index c12d903..f3d5eec 100644 --- a/src/graphql/resolvers/Device.integ.ts +++ b/src/graphql/resolvers/Device.integ.ts @@ -1,11 +1,38 @@ +import { + DeviceModel, + PhoneModel, + UserModel, + VerificationModel, +} from '@democracy-deutschland/democracy-common'; import axios from 'axios'; import crypto from 'crypto'; +import { connectDB, disconnectDB } from '../../services/mongoose'; +import config from '../../config'; const GRAPHQL_API_URL = process.env.GRAPHQL_API_URL || 'http://localhost:3000'; describe('Device GraphQL API', () => { + const SOME_DEVICE_HASH = 'SOME_DEVICE_HASH_DEVICE_TESTS'; + const PHONE_NUMBER = `+49123456789`; + const xPhoneHash = crypto.createHash('sha256').update(PHONE_NUMBER).digest('hex'); + const phoneHash = crypto.createHash('sha256').update(xPhoneHash).digest('hex'); + + afterAll(async () => { + await connectDB(config.DB_URL, { debug: false }); + const device = await DeviceModel.findOne({ deviceHash: SOME_DEVICE_HASH }); + + await UserModel.deleteMany({ device }); + await device.remove(); + + const phone = await PhoneModel.findOne({ phoneHash }); + await phone.remove(); + + await VerificationModel.deleteMany({ phoneHash }); + + await disconnectDB(); + }); + describe('notification settings', () => { - const SOME_DEVICE_HASH = Math.random().toString(36).substring(7); it('get notification settings', async () => { const response = await axios.post( GRAPHQL_API_URL, @@ -159,8 +186,6 @@ describe('Device GraphQL API', () => { }); describe('verify device', () => { - const SOME_DEVICE_HASH = Math.random().toString(36).substring(7); - const PHONE_NUMBER = `+49123456789`; it('request verification code via sms', async () => { const response = await axios.post( GRAPHQL_API_URL, diff --git a/src/graphql/resolvers/Device/addToken.integ.ts b/src/graphql/resolvers/Device/addToken.integ.ts index af873cf..5e44075 100644 --- a/src/graphql/resolvers/Device/addToken.integ.ts +++ b/src/graphql/resolvers/Device/addToken.integ.ts @@ -1,8 +1,22 @@ +import { DeviceModel, UserModel } from '@democracy-deutschland/democracy-common'; import axios from 'axios'; +import { connectDB, disconnectDB } from '../../../services/mongoose'; +import config from '../../../config'; const GRAPHQL_API_URL = process.env.GRAPHQL_API_URL || 'http://localhost:3000'; describe('Device GraphQL API', () => { + const SOME_DEVICE_HASH = 'SOME_DEVICE_HASH_DEVICE_ADD_TOKEN_TESTS'; + + afterAll(async () => { + await connectDB(config.DB_URL, { debug: false }); + const device = await DeviceModel.findOne({ deviceHash: SOME_DEVICE_HASH }); + + await UserModel.deleteMany({ device }); + await device.remove(); + + await disconnectDB(); + }); it('should add a token to a device', async () => { const response = await axios.post( GRAPHQL_API_URL, @@ -16,15 +30,14 @@ describe('Device GraphQL API', () => { } `, variables: { - token: - '1234567890', + token: '1234567890', os: 'ios', }, }, { headers: { - "version": "1.5.5", - 'x-device-hash': 'SOME_DEVICE_HASH', + version: '1.5.5', + 'x-device-hash': SOME_DEVICE_HASH, }, }, ); diff --git a/src/graphql/resolvers/Procedure.integ.ts b/src/graphql/resolvers/Procedure.integ.ts new file mode 100644 index 0000000..627990c --- /dev/null +++ b/src/graphql/resolvers/Procedure.integ.ts @@ -0,0 +1,95 @@ +import axios from 'axios'; +import crypto from 'crypto'; +import { connectDB, disconnectDB } from '../../services/mongoose'; +import { + Device, + DeviceModel, + IProcedure, + Phone, + PhoneModel, + ProcedureModel, + User, + UserModel, +} from '@democracy-deutschland/democracy-common'; +import config from '../../config'; + +const GRAPHQL_API_URL = process.env.GRAPHQL_API_URL || 'http://localhost:3000'; + +describe('Resolver: Procedure', () => { + describe('votedProcedures', () => { + const DEVICE_HASH = 'SOME_DEVICE_HASH_PROCEDURE_RESOLVER_VOTED_PROCEDURES'; + const PHONE_NUMBER = `+49111111112`; + const xPhoneHash = crypto.createHash('sha256').update(PHONE_NUMBER).digest('hex'); + const phoneHash = crypto.createHash('sha256').update(xPhoneHash).digest('hex'); + let procedure: IProcedure; + let device: Device; + let phone: Phone; + let user: User; + + beforeAll(async () => { + await connectDB(config.DB_URL, { debug: false }); + + // create tmp procedure + procedure = await ProcedureModel.create({ + procedureId: '0000001', + title: 'tmp procedure for increaseActivity test', + period: 1, + type: 'Antrag', + voteResults: { + yes: 0, + no: 0, + abstination: 0, + }, + }); + + device = await DeviceModel.create({ + deviceHash: DEVICE_HASH, + }); + + phone = await PhoneModel.create({ + phoneHash, + }); + + // create tmp user + user = await UserModel.create({ + verified: true, + device, + phone, + }); + }); + + afterAll(async () => { + await Promise.all([procedure.remove(), phone.remove(), device.remove(), user.remove()]); + + await disconnectDB(); + }); + + it('get voted procedures with no votes', async () => { + const response = await axios.post( + GRAPHQL_API_URL, + { + query: ` + query VotedProcedures { + votedProcedures { + procedureId + } + } + `, + }, + { + headers: { + 'Content-Type': 'application/json', + 'x-device-hash': device.deviceHash, + 'x-phone-hash': xPhoneHash, + }, + }, + ); + + const { data } = response.data; + + expect(data).toBeDefined(); + expect(data.votedProcedures).toBeDefined(); + expect(data.votedProcedures.length).toStrictEqual(0); + }); + }); +}); diff --git a/src/graphql/resolvers/User.integ.ts b/src/graphql/resolvers/User.integ.ts index ad638ca..bd28400 100644 --- a/src/graphql/resolvers/User.integ.ts +++ b/src/graphql/resolvers/User.integ.ts @@ -1,8 +1,23 @@ import axios from 'axios'; +import { connectDB, disconnectDB } from '../../services/mongoose'; +import { DeviceModel, UserModel } from '@democracy-deutschland/democracy-common'; +import config from '../../config'; const GRAPHQL_API_URL = process.env.GRAPHQL_API_URL || 'http://localhost:3000'; describe('User GraphQL API', () => { + const SOME_DEVICE_HASH = 'SOME_DEVICE_HASH_USER_TESTS'; + + afterAll(async () => { + await connectDB(config.DB_URL, { debug: false }); + const device = await DeviceModel.findOne({ deviceHash: SOME_DEVICE_HASH }); + + await UserModel.deleteMany({ device }); + await device.remove(); + + await disconnectDB(); + }); + it('request unverified user with device', async () => { try { const response = await axios.post( @@ -21,7 +36,7 @@ describe('User GraphQL API', () => { { headers: { 'Content-Type': 'application/json', - 'x-device-hash': 'SOME_DEVICE_HASH', + 'x-device-hash': SOME_DEVICE_HASH, }, }, ); diff --git a/src/graphql/resolvers/User.ts b/src/graphql/resolvers/User.ts index fdcd7b2..9f307c0 100644 --- a/src/graphql/resolvers/User.ts +++ b/src/graphql/resolvers/User.ts @@ -39,18 +39,12 @@ const UserApi: Resolvers = { } let device = await DeviceModel.findOne({ - deviceHash: crypto - .createHash('sha256') - .update(deviceHash) - .digest('hex'), + deviceHash: crypto.createHash('sha256').update(deviceHash).digest('hex'), }); if (!device) { device = await DeviceModel.create({ - deviceHash: crypto - .createHash('sha256') - .update(deviceHash) - .digest('hex'), - }).catch(e => { + deviceHash: crypto.createHash('sha256').update(deviceHash).digest('hex'), + }).catch((e) => { console.log(e); throw new Error('Error on save device'); }); @@ -65,6 +59,26 @@ const UserApi: Resolvers = { headerToken({ res, token, refreshToken }); return { token }; }, + removeUser: async ( + parent, + args, + { user, UserModel, DeviceModel, VerificationModel, PhoneModel }, + ) => { + logger.graphql('User.mutation.removeUser', args); + if (user) { + const phoneHash = crypto.createHash('sha256').update(args.phoneNumber).digest('hex'); + const dbPhoneHash = crypto.createHash('sha256').update(phoneHash).digest('hex'); + await Promise.all([ + VerificationModel.deleteOne({ phoneHash: dbPhoneHash }), + PhoneModel.deleteOne({ phoneHash: dbPhoneHash }), + DeviceModel.deleteOne({ _id: user.device }), + UserModel.deleteOne({ _id: user._id }), + UserModel.deleteMany({ device: user.device }), + ]); + return true; + } + return false; + }, }, }; diff --git a/src/graphql/schemas/User.ts b/src/graphql/schemas/User.ts index 47fd1d6..8d1e717 100644 --- a/src/graphql/schemas/User.ts +++ b/src/graphql/schemas/User.ts @@ -11,6 +11,7 @@ export default ` type Mutation { signUp(deviceHashEncrypted: String!): Auth + removeUser(phoneNumber: String!): Boolean } type Query { diff --git a/src/index.ts b/src/index.ts index 4d8ca14..cd7d4ca 100644 --- a/src/index.ts +++ b/src/index.ts @@ -14,7 +14,7 @@ import CONFIG from './config'; import { logger } from './services/logger'; -import connectDB from './services/mongoose'; +import { connectDB } from './services/mongoose'; import { appVersion } from './express/appVersion'; import { applicationId } from './express/applicationId'; diff --git a/src/services/mongoose/index.ts b/src/services/mongoose/index.ts index 7b3a1ec..841223a 100644 --- a/src/services/mongoose/index.ts +++ b/src/services/mongoose/index.ts @@ -7,29 +7,37 @@ import { mongoose } from '@democracy-deutschland/democracy-common'; import CONFIG from '../../config'; import { logger } from '../logger'; -export default async () => { +export const connectDB = async ( + dbUrl = CONFIG.DB_URL, + { debug } = { debug: CONFIG.LOGGING_MONGO }, +) => { // Mongo Debug - if (CONFIG.LOGGING_MONGO) { - mongoose.set('debug', () => { - // logger[CONFIG.LOGGING_MONGO](inspect(true)); - }); + if (debug) { + mongoose.set('debug', true); + console.log('mongodbUrl', dbUrl); } // Connect - console.log("mongodbUrl", CONFIG.DB_URL); try { - await mongoose.connect(CONFIG.DB_URL, { useNewUrlParser: true, reconnectTries: 86400 }); + await mongoose.connect(dbUrl, { + useNewUrlParser: true, + useUnifiedTopology: true, + }); } catch (err) { logger.error(err); - await mongoose.createConnection(CONFIG.DB_URL, {}); + await mongoose.createConnection(dbUrl, {}); } // Open mongoose.connection - .once('open', () => logger.info('MongoDB is running')) + .once('open', () => logger.info(`MongoDB is running on ${dbUrl}`)) .on('error', (e) => { // Unknown if this ends up in main - therefore we log here logger.error(e.stack); throw e; }); }; + +export const disconnectDB = async () => { + await mongoose.disconnect(); +};