Skip to content

Commit

Permalink
- Add boostrap logic for newcomers
Browse files Browse the repository at this point in the history
- Remove account POST and account assigning endpoints
  • Loading branch information
Andrew Nikitin committed Nov 2, 2023
1 parent c7b3284 commit 1446983
Show file tree
Hide file tree
Showing 12 changed files with 192 additions and 324 deletions.
2 changes: 0 additions & 2 deletions src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,9 +166,7 @@ class App {
app.get('/resource/search/:did', new IssuerController().getResource);

// Account API
app.post('/account', new AccountController().create);
app.get('/account', new AccountController().get);
app.post('/account/assign', new AccountController().assign);

// LogTo webhooks
app.post('/account/bootstrap', LogToWebHook.verifyHookSignature, new AccountController().bootstrap);
Expand Down
308 changes: 142 additions & 166 deletions src/controllers/customer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Request, Response } from 'express';
import { checkBalance } from '@cheqd/sdk';
import { CheqdNetwork, checkBalance } from '@cheqd/sdk';
import { TESTNET_MINIMUM_BALANCE, DEFAULT_DENOM_EXPONENT } from '../types/constants.js';
import { CustomerService } from '../services/customer.js';
import { LogToHelper } from '../middleware/auth/logto.js';
Expand All @@ -8,62 +8,13 @@ import { StatusCodes } from 'http-status-codes';
import { LogToWebHook } from '../middleware/hook.js';
import { UserService } from '../services/user.js';
import { RoleService } from '../services/role.js';
import { PaymentAccountService } from '../services/payment_account.js';
import type { CustomerEntity } from '../database/entities/customer.entity.js';
import type { UserEntity } from '../database/entities/user.entity.js';
import type { PaymentAccountEntity } from '../database/entities/payment.account.entity.js';
import { IdentityServiceStrategySetup } from '../services/identity/index.js';

export class AccountController {
/**
* @openapi
*
* /account:
* post:
* tags: [Account]
* summary: Create a new custodian-mode client.
* description: This endpoint creates a new custodian-mode client and creates issuer DIDs and Cosmos/cheqd accounts for the client.
* requestBody:
* content:
* application/json:
* schema:
* type: object
* properties:
* name:
* type: string
* responses:
* 200:
* description: The request was successful.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Customer'
* 400:
* $ref: '#/components/schemas/InvalidRequest'
* 401:
* $ref: '#/components/schemas/UnauthorizedError'
* 500:
* $ref: '#/components/schemas/InternalError'
*/
public async create(request: Request, response: Response) {
const name = (request.body.name as string).toLowerCase();
if (!name) {
return response.status(StatusCodes.BAD_REQUEST).json({
error: 'Missing required parameter: name',
});
}
try {
const customer = await CustomerService.instance.create(name);
if (!customer) {
return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
error: `Internal server error. Please try again later.`,
});
}
return response.status(StatusCodes.OK).json({
customerId: customer.customerId,
name: customer.name,
});
} catch (error) {
return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
error: `${error}`,
});
}
}

/**
* @openapi
Expand Down Expand Up @@ -97,93 +48,26 @@ export class AccountController {
*/
public async get(request: Request, response: Response) {
try {
const name = (request.query.name as string).toLowerCase();
const result = await CustomerService.instance.findOne(name);
if (result) {
return response.status(StatusCodes.OK).json({
customerId: result.customerId,
// address: result.address,
});
}

return response.status(StatusCodes.BAD_REQUEST).json({
error: 'Customer not found',
});
} catch (error) {
return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
error: `${error}`,
});
}
}

/**
* @openapi
*
* /account/assign:
* post:
* tags: [Account]
* summary: Assign logTo user with Customer
* description: This endpoint allows to assign logTo users with CredentialService customer
* requestBody:
* content:
* application/json:
* schema:
* type: object
* properties:
* logToId:
* type: string
* customerName:
* type: string
* responses:
* 200:
* description: The request was successful.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/AssignResponse'
* 400:
* $ref: '#/components/schemas/InvalidRequest'
* 401:
* $ref: '#/components/schemas/UnauthorizedError'
* 500:
* $ref: '#/components/schemas/InternalError'
*/
public async assign(request: Request, response: Response) {
try {
const userId = request.body.logToId;
const customerName = (request.body.customerName as string).toLowerCase();
const customer = await CustomerService.instance.findOne(customerName);
const user = await UserService.instance.findOne({ logToId: userId});
if (!customer) {
return response.status(StatusCodes.BAD_REQUEST).json({
error: 'Customer not found',
});
}
// If there is no user with such userId - let's create it.
if (!user) {
const defaultRole = await RoleService.instance.findOne({ name: 'default' });
if (!defaultRole) {
return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
error: `Default role not found`,
});
}
const res = await UserService.instance.create(userId, customer, defaultRole);
if (!res) {
return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
error: `User with id: ${userId} record could not be created with the customer ${customer.name}`,
});
}
}
const result = await UserService.instance.update(userId, customer);
if (!result) {
if (!response.locals.customer) {
// It's not ok, seems like there no any customer assigned to the user yet
// But it's not an expectede behaviour cause it should be done on bootstrap phase of after migration
return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
error: `User record could not be updated with the customer`,
error: "Bad state cause there is no customer assigned to the user yet. Please contact administrator."
});
}
return response.status(StatusCodes.OK).json({
customerId: result.customer.customerId,
name: result.customer.name,
});
const paymentAccount = await PaymentAccountService.instance.find({customer: response.locals.customer});
const result = {
customer: {
customerId: response.locals.customer.customerId,
name: response.locals.customer.name,
},
paymentAccount: {
address: paymentAccount[0].address,
},

};

return response.status(StatusCodes.OK).json(result);

} catch (error) {
return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
Expand Down Expand Up @@ -213,68 +97,160 @@ export class AccountController {
}

public async bootstrap(request: Request, response: Response) {
// 1. Check that the customer exists
// 1.1 If not, create it
// 2. Get the user Roles
// 2.1 If list of roles is empty - assign default role
// 2.2 Create custom_data and update the userInfo (send it to the LogTo)
// 3. Check the token balance for Testnet account
// 3.1 If it's less then required for DID creation - assign new portion from testnet-faucet
const customerId: string = response.locals.customer.customerId || LogToWebHook.getCustomerId(request);
// For now we keep temporary 1-1 relatioin between user and customer
// So the flow is:
// 1. Get LogTo user id from request body
// 2. Check if such row exists in the DB
// 2.1. If no - create it
// 3. If yes - check that there is customer associated with such user
// 3.1. If no:
// 3.1.1. Create customer
// 3.1.2. Assign customer to the user

// 4. Check is paymentAccount exists for the customer
// 4.1. If no - create it

// 5. Assign default role on LogTo
// 5.1 Get user's roles
// 5.2 If list of roles is empty and the user is not suspended - assign default role
// 6. Create custom_data and update the userInfo (send it to the LogTo)
// 6.1 If custom_data is empty - create it
// 7. Check the token balance for Testnet account

let customer: CustomerEntity | null;
let user: UserEntity | null;
let paymentAccount: PaymentAccountEntity | null;

// 1. Get logTo UserId from request body
if (!request.body.user || !request.body.user.id || !request.body.user.primaryEmail) {
return response.status(StatusCodes.BAD_REQUEST).json({
error: 'User id is not specified or primaryEmail is not set',
});
}
const logToUserId = request.body.user.id;
const logToUserEmail = request.body.user.primaryEmail;

const defaultRole = await RoleService.instance.getDefaultRole();
if (!defaultRole) {
return response.status(StatusCodes.BAD_REQUEST).json({
error: 'Default role is not set on Credential Service side',
});
}

// 2. Check if such row exists in the DB
user = await UserService.instance.get(logToUserId);
if (!user) {
// 2.1. If no - create customer first
// Cause for now we assume only 1-1 connection between user and customer
// We think here that if now user row - no customer also, cause customer should be created before user
// Even if customer was created before for such user but the process was interruted somehow - we need to create it again
// Cause we don't know the state of the customer in this case
// 2.1.1. Create customer
customer = await CustomerService.instance.create(logToUserEmail) as CustomerEntity;
if (!customer) {
return response.status(StatusCodes.BAD_REQUEST).json({
error: 'User is not found in db: Customer was not created',
});
}
// 2.2. Create user
user = await UserService.instance.create(logToUserId, customer, defaultRole);
if (!user) {
return response.status(StatusCodes.BAD_REQUEST).json({
error: 'User is not found in db: User was not created',
});
}
}
// 3. If yes - check that there is customer associated with such user
if (!user.customer) {
// 3.1. If no:
// 3.1.1. Create customer
customer = await CustomerService.instance.create(logToUserEmail) as CustomerEntity;
if (!customer) {
return response.status(StatusCodes.BAD_REQUEST).json({
error: 'User exists in db: Customer was not created',
});
}
// 3.1.2. Assign customer to the user
user.customer = customer;
await UserService.instance.update(user.logToId, customer);
} else {
customer = user.customer;
}

// 4. Check is paymentAccount exists for the customer
const accounts = await PaymentAccountService.instance.find({customer});
if (accounts.length === 0) {
const key = await new IdentityServiceStrategySetup(customer.customerId).agent.createKey('Secp256k1', customer);
if (!key) {
return response.status(StatusCodes.BAD_REQUEST).json({
error: 'PaymentAccount is not found in db: Key was not created',
});
}
paymentAccount = await PaymentAccountService.instance.create(CheqdNetwork.Testnet, true, customer, key) as PaymentAccountEntity;
if (!paymentAccount) {
return response.status(StatusCodes.BAD_REQUEST).json({
error: 'PaymentAccount is not found in db: Payment account was not created',
});
}
} else {
paymentAccount = accounts[0];
}


const logToHelper = new LogToHelper();
const _r = await logToHelper.setup();
if (_r.status !== StatusCodes.OK) {
return response.status(StatusCodes.BAD_GATEWAY).json({
error: _r.error,
});
}
// 1. Check that the customer exists
let customer: any = await CustomerService.instance.get(customerId);
if (!customer) {
customer = await CustomerService.instance.create(customerId);
}
// 2. Get the user's roles
const roles = await logToHelper.getRolesForUser(customerId);
// 5. Assign default role on LogTo
// 5.1 Get user's roles
const roles = await logToHelper.getRolesForUser(user.logToId);
if (roles.status !== StatusCodes.OK) {
return response.status(StatusCodes.BAD_GATEWAY).json({
error: roles.error,
});
}

// 2.1 If list of roles is empty and the user is not suspended - assign default role
// 5.2 If list of roles is empty and the user is not suspended - assign default role
if (roles.data.length === 0 && !LogToWebHook.isUserSuspended(request)) {
const _r = await logToHelper.setDefaultRoleForUser(customerId);
const _r = await logToHelper.setDefaultRoleForUser(user.logToId);
if (_r.status !== StatusCodes.OK) {
return response.status(StatusCodes.BAD_GATEWAY).json({
error: _r.error,
});
}
}

const customDataFromLogTo = await logToHelper.getCustomData(customerId);
const customDataFromLogTo = await logToHelper.getCustomData(user.logToId);

// 2.2 Create custom_data and update the userInfo (send it to the LogTo)
if (Object.keys(customDataFromLogTo.data).length === 0 && customer.address) {
// 6. Create custom_data and update the userInfo (send it to the LogTo)
if (Object.keys(customDataFromLogTo.data).length === 0 && paymentAccount.address) {
const customData = {
cosmosAccounts: {
testnet: customer.address,
customer: {
id: customer.customerId,
name: customer.name,
},
paymentAccount: {
address: paymentAccount.address,
}
};
const _r = await logToHelper.updateCustomData(customerId, customData);
const _r = await logToHelper.updateCustomData(user.logToId, customData);
if (_r.status !== 200) {
return response.status(_r.status).json({
error: _r.error,
});
}
}

// 3. Check the token balance for Testnet account
if (customer.address && process.env.ENABLE_ACCOUNT_TOPUP === 'true') {
const balances = await checkBalance(customer.address, process.env.TESTNET_RPC_URL);
// 7. Check the token balance for Testnet account
if (paymentAccount.address && process.env.ENABLE_ACCOUNT_TOPUP === 'true') {
const balances = await checkBalance(paymentAccount.address, process.env.TESTNET_RPC_URL);
const balance = balances[0];
if (!balance || +balance.amount < TESTNET_MINIMUM_BALANCE * Math.pow(10, DEFAULT_DENOM_EXPONENT)) {
// 3.1 If it's less then required for DID creation - assign new portion from testnet-faucet
const resp = await FaucetHelper.delegateTokens(customer.address);
const resp = await FaucetHelper.delegateTokens(paymentAccount.address);
if (resp.status !== StatusCodes.OK) {
return response.status(StatusCodes.BAD_GATEWAY).json({
error: resp.error,
Expand Down
Loading

0 comments on commit 1446983

Please sign in to comment.