Skip to content

Commit

Permalink
Merge pull request #133 from alphamanuscript/master
Browse files Browse the repository at this point in the history
v0.3.0
  • Loading branch information
habbes authored Sep 14, 2020
2 parents 31dfd7d + 0381e99 commit 3ea1b73
Show file tree
Hide file tree
Showing 30 changed files with 485 additions and 71 deletions.
6 changes: 3 additions & 3 deletions .github/actions/app-selector/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions server/src/core/payment/validation-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ export const sendDonationInputSchema = joi.object().keys({
.required()
.max(2000)
.messages({
'any.required': `amount is required`,
'any.required': `Amount is required`,
'number.base': 'Invalid type, amount must be a number',
'number.max': 'amount cannot be more than 2000'
'number.max': 'Amount cannot be more than 2000'
})
});

Expand Down
29 changes: 27 additions & 2 deletions server/src/core/user/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export interface User {
phone: string,
email?: string,
name: string,
isAnonymous?: boolean,
addedBy: string,
/**
* the donors from whom this beneficiary can receive funds
Expand Down Expand Up @@ -73,7 +74,8 @@ export interface UserCreateArgs {
name: string,
email: string,
password: string,
googleIdToken: string
isAnonymous?: boolean,
googleIdToken?: string
};

export interface UserNominateArgs {
Expand Down Expand Up @@ -183,6 +185,12 @@ export interface UserService {
* @param userId
*/
getNew(userId: string): Promise<User>;
/**
* retrieves the anonymous user
* corresponding to the specified userId
* @param userId
*/
getAnonymous(userId: string): Promise<User>;
/**
* invalidates the specified access token
* @param token
Expand Down Expand Up @@ -219,4 +227,21 @@ export interface UserService {
* @param pipeline
*/
aggregate(pipeline: any[]): Promise<any[]>;
};
/**
* initiates a donation
* from anonymous user args.phone
* @param args
*/
donateAnonymously(args: UserDonateAnonymouslyArgs): Promise<Transaction>;
};

export interface UserCreateAnonymousArgs {
name: string,
phone: string,
email: string
}


export interface UserDonateAnonymouslyArgs extends UserCreateAnonymousArgs {
amount: number
}
54 changes: 51 additions & 3 deletions server/src/core/user/user-service.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Db, Collection } from 'mongodb';
import { generateId, hashPassword, verifyPassword, verifyGoogleIdToken, generateToken, validateId } from '../util';
import { generateId, generatePassword, hashPassword, verifyPassword, verifyGoogleIdToken, generateToken, validateId } from '../util';
import {
User, DbUser, UserCreateArgs, UserService, UserPutArgs,
AccessToken, UserLoginArgs, UserLoginResult, UserNominateArgs, UserRole,
UserActivateArgs, UserActivateBeneficiaryArgs, UserActivateMiddlemanArgs
UserActivateArgs, UserActivateBeneficiaryArgs, UserActivateMiddlemanArgs,
UserCreateAnonymousArgs, UserDonateAnonymouslyArgs
} from './types';
import * as messages from '../messages';
import {
Expand Down Expand Up @@ -139,6 +140,10 @@ export class Users implements UserService {
updatedAt: now
};

if(args.isAnonymous) {
user.isAnonymous = true;
}

try {
if (args.password) {
user.password = await hashPassword(args.password);
Expand Down Expand Up @@ -471,7 +476,21 @@ export class Users implements UserService {
const user = await this.collection.findOne({ _id: userId, password: '' }, { projection: SAFE_USER_PROJECTION });
if (!user) throw createResourceNotFoundError(messages.ERROR_USER_NOT_FOUND);

return getSafeUser(user);
return user;
}
catch(e) {
rethrowIfAppError(e);
throw createDbOpFailedError(e.message);
}
}

async getAnonymous(userId: string): Promise<User> {
validators.validatesGetAnonymous(userId);
try {
const user = await this.collection.findOne({ _id: userId, isAnonymous: true }, { projection: SAFE_USER_PROJECTION });
if (!user) throw createResourceNotFoundError(messages.ERROR_USER_NOT_FOUND);

return user;
}
catch(e) {
rethrowIfAppError(e);
Expand Down Expand Up @@ -650,4 +669,33 @@ export class Users implements UserService {
throw createDbOpFailedError(e.message);
}
}

public async donateAnonymously(args: UserDonateAnonymouslyArgs): Promise<Transaction> {
validators.validateDonateAnonymously(args);
const { amount, name, phone, email } = args;
try {
const user = await this.createAnonymous({ name, phone, email });
const transaction = await this.initiateDonation(user._id, { amount });
return transaction;
}
catch (e) {
rethrowIfAppError(e);
}
}

async createAnonymous(args: UserCreateAnonymousArgs): Promise<User> {
const { name, phone, email } = args;
try {
let user = await this.collection.findOne({ phone }, { projection: SAFE_USER_PROJECTION });
if (!user) {
const password = generatePassword();
user = await this.create({ name, phone, email, password, isAnonymous: true });
}

return user;
}
catch (e) {
rethrowIfAppError(e);
}
}
}
38 changes: 24 additions & 14 deletions server/src/core/user/validation-schemas.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as joi from '@hapi/joi';
import { phoneValidationSchema, passwordValidationSchema, googleIdTokenValidationSchema, emailValidationSchema, idValidationSchema } from '../util/validation-util';
import { phoneValidationSchema, passwordValidationSchema, googleIdTokenValidationSchema, emailValidationSchema, idValidationSchema, isAnonymousSchema } from '../util/validation-util';

const emailSchema = joi.string()
.pattern(/\S+@\S+\.\S+/) // Simplest pattern ([email protected]) of email validation. Should be updated with a more rigorous, thorough pattern
Expand Down Expand Up @@ -30,17 +30,34 @@ const userIdSchema = joi.object().keys({
'string.empty': `Please enter userId`,
'string.pattern.base': `Invalid userId. Must contain hexadecimals only and be 32 characters long`
}),
})
});

const amountSchema = joi.number()
.required()
.min(100)
.messages({
'any.required': `Amount is required`,
'number.base': 'Invalid type, amount must be a number',
'number.min': `Amount must be 100 or more`,
})

export const createInputSchema = joi.alternatives().try(
joi.object().keys({
phone: phoneValidationSchema,
password: passwordValidationSchema,
name: joi.string().required(),
email: emailSchema
email: emailSchema,
isAnonymous: isAnonymousSchema,
}),
joi.object().keys({ phone: phoneValidationSchema, googleIdToken: googleIdTokenValidationSchema }),
);;
);

export const donateAnonymouslyInputSchema = joi.object().keys({
amount: amountSchema,
name: joi.string().required(),
phone: phoneValidationSchema,
email: emailSchema,
});

export const loginInputSchema = joi.alternatives().try(
joi.object().keys({ phone: phoneValidationSchema, password: passwordValidationSchema }),
Expand Down Expand Up @@ -110,14 +127,7 @@ export const logoutAllInputSchema = userIdSchema;

export const initiateDonationInputSchema = joi.object().keys({
userId: idValidationSchema,
amount: joi.number()
.required()
.min(100)
.messages({
'any.required': `amount is required`,
'number.base': 'Invalid type, amount must be a number',
'number.min': `amount must be 100 or more`,
})
amount: amountSchema
});

export const putInputSchema = joi.object().keys({
Expand All @@ -133,8 +143,8 @@ export const putInputSchema = joi.object().keys({
name: joi.string()
.required()
.messages({
'any.required': 'name is required',
'string.empty': 'name is required'
'any.required': 'Name is required',
'string.empty': 'Name is required'
}),
email: emailValidationSchema,
password: passwordValidationSchema
Expand Down
10 changes: 9 additions & 1 deletion server/src/core/user/validator.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { UserCreateArgs, UserNominateArgs, UserLoginArgs, UserActivateArgs, UserPutArgs } from './types'
import { UserCreateArgs, UserNominateArgs, UserLoginArgs, UserActivateArgs, UserPutArgs, UserDonateAnonymouslyArgs } from './types'
import { createValidationError } from '../error';
import * as schemas from './validation-schemas';
import { makeValidatorFromJoiSchema } from '../util';
Expand All @@ -9,6 +9,12 @@ export const validatesCreate = (args: UserCreateArgs) => {
if (args.phone[0] === '0') createValidationError('Phone number cannot start with 0');
}

export const validateDonateAnonymously = (args: UserDonateAnonymouslyArgs) => {
const { error } = schemas.donateAnonymouslyInputSchema.validate(args);
if (error) throw createValidationError(error.details[0].message);
if (args.phone[0] === '0') createValidationError('Phone number cannot start with 0');
}

export const validatesLogin = (args: UserLoginArgs) => {
const { error } = schemas.loginInputSchema.validate(args);
if (error) throw createValidationError(error.details[0].message);
Expand Down Expand Up @@ -56,6 +62,8 @@ export const validatesInitiateDonation = ({ userId, amount } : { userId: string;

export const validatesGetNew = validatesLogoutAll;

export const validatesGetAnonymous = validatesGetNew;

export const validatesPut = ({ userId, args } : { userId: string, args: UserPutArgs}) => {
const { name, email, password } = args;
const { error } = schemas.putInputSchema.validate({ userId, name, email, password });
Expand Down
4 changes: 4 additions & 0 deletions server/src/core/util/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ export function generateToken(): string {
return randomBytes(64).toString('hex');
}

export function generatePassword(): string {
return randomBytes(8).toString('hex');
}


export function hasOnlyAllowedKeys (arg: any, allowedKeys: string[]): boolean {
return arg ? !Object.keys(arg).some(key => !allowedKeys.includes(key)) : false;
Expand Down
8 changes: 7 additions & 1 deletion server/src/core/util/validation-util.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as joi from '@hapi/joi';
import { createValidationError } from '../error';
import { messages } from '..';

export function makeValidatorFromJoiSchema<TArgs = any>(schema: joi.Schema) {
return (args: TArgs) => {
Expand Down Expand Up @@ -36,7 +37,7 @@ export const passwordValidationSchema = joi.string()
'any.required': 'Password is required',
'string.base': 'Invalid type, password must be a string',
'string.empty': 'Please enter your password',
'string.pattern.base': 'Invalid password. Must range between 8 and 18 characters'
'string.pattern.base': 'Invalid password. Must be at least one character'
});

export const googleIdTokenValidationSchema = joi.string()
Expand All @@ -57,4 +58,9 @@ export const emailValidationSchema = joi.string()
'string.email.base': 'Invalid e-mail'
});

export const isAnonymousSchema = joi.boolean()
.messages({
'boolean.base': 'Invalid type, isAnonymous must be a boolean',
})

export const validateEmail = makeValidatorFromJoiSchema(emailValidationSchema);
8 changes: 5 additions & 3 deletions server/src/rest/routes/donations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import { Router } from 'express';
import { wrapResponse, requireAuth } from '../middleware';

export const donations = Router();
donations.use(requireAuth());

donations.post('/initiate', wrapResponse(
req => req.core.users.initiateDonation(req.user._id, req.body)));
donations.post('/initiate', requireAuth(), wrapResponse(
req => req.core.users.initiateDonation(req.user._id, req.body)));

donations.post('/anonymous/initiate', wrapResponse(
req => req.core.users.donateAnonymously(req.body)));
12 changes: 9 additions & 3 deletions server/src/rest/routes/transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@ import { wrapResponse, requireAuth } from '../middleware';

export const transactions = Router();

transactions.use(requireAuth());
// transactions.use(requireAuth());

transactions.get('/', wrapResponse(
transactions.get('/', requireAuth(), wrapResponse(
req => req.core.transactions.getAllByUser(req.user._id)));

transactions.get('/:id', wrapResponse(
transactions.get('/:id', requireAuth(), wrapResponse(
req => req.core.transactions.checkUserTransactionStatus(req.user._id, req.params.id)));

transactions.get('/anonymous/:userId', wrapResponse(
req => req.core.transactions.getAllByUser(req.params.userId)));

transactions.get('/:id/anonymous/:userId', wrapResponse(
req => req.core.transactions.checkUserTransactionStatus(req.params.userId, req.params.id)));
3 changes: 3 additions & 0 deletions server/src/rest/routes/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,8 @@ users.post('/activate-invitee', wrapResponse(
users.get('/:id', wrapResponse(
req => req.core.users.getNew(req.params.id)));

users.get('/anonymous/:id', wrapResponse(
req => req.core.users.getAnonymous(req.params.id)));

users.put('/:id', wrapResponse(
req => req.core.users.put(req.params.id, { ... req.body })));
6 changes: 3 additions & 3 deletions server/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3667,9 +3667,9 @@ node-addon-api@^2.0.0:
integrity sha512-ASCL5U13as7HhOExbT6OlWJJUV/lLzL2voOSP1UVehpRD8FbSrSDjfScK/KwAvVTI5AS6r4VwbOMlIqtvRidnA==

node-fetch@^2.3.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA==
version "2.6.1"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==

node-forge@^0.9.0:
version "0.9.1"
Expand Down
9 changes: 8 additions & 1 deletion webapp/src/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,27 @@
<GoogleSignUpModal :googleUser="googleUser"/>
<PaymentListener/>
<DonateModal/>
<DonateAnonymouslyModal/>
</div>
</template>
<script>
import LoginModal from './components/login-modal.vue';
import SignUpModal from './components/sign-up-modal.vue';
import DonateModal from './components/donate-modal.vue';
import DonateAnonymouslyModal from './components/donate-anonymously-modal.vue';
import GoogleSignUpModal from './components/google-sign-up-modal.vue';
import { PaymentListener } from './components/payment';
import LoggedOutStructure from './views/logged-out-structure.vue';
import LoggedInStructure from './views/logged-in-structure.vue';
import { DEFAULT_SIGNED_OUT_PAGE } from './router/defaults';
import { AnonymousUser } from '@/services';
export default {
components: {
LoginModal,
SignUpModal,
DonateModal,
DonateAnonymouslyModal,
GoogleSignUpModal,
LoggedInStructure,
LoggedOutStructure,
Expand All @@ -38,8 +42,11 @@ export default {
showLoggedInNavigation () {
if (this.$route.name === DEFAULT_SIGNED_OUT_PAGE ||
this.$route.name === 'accept-invitation' ||
this.$route.name === 'signup-new-user')
this.$route.name === 'signup-new-user' ||
(this.$route.name === 'post-payment-flutterwave' && AnonymousUser.isSet())) {
return false
}
return true
},
imageUrl () {
Expand Down
Loading

0 comments on commit 3ea1b73

Please sign in to comment.