Compare commits

...

19 Commits

Author SHA1 Message Date
Alex Ant c0d236dff3 chore(release): publish
- coopback@1.7.30
 - coopdoc-generator-ts@1.0.74
 - cooptypes@1.0.19
 - coop-notificator@0.0.23
 - terminal@0.1.34
2024-08-13 23:03:13 +05:00
Alex Ant 743d5fc306 разделение формы для ввода персональных данных с регистратором; зародыш формы добавления пайщика в реестр. 2024-08-13 23:00:09 +05:00
Alex Ant 2f51516163 step to install | works on add user by api 2024-08-13 15:21:23 +05:00
Alex Ant b606b46aa4 страница настроек с кнопками выхода для пайщика 2024-08-05 14:31:27 +05:00
Alex Ant 5b65ff99a0 chore(release): publish
- coopback@1.7.29
 - coopdoc-generator-ts@1.0.73
 - cooptypes@1.0.18
 - coop-notificator@0.0.22
 - terminal@0.1.33
2024-08-05 12:36:01 +05:00
Alex Ant 74a3f52324 update 2024-08-05 12:32:23 +05:00
Alex Ant 7c2a4fdfa8 удаление потерявшейся папки 2024-08-05 12:05:05 +05:00
Alex Ant fecee3a098 fix naming 2024-08-05 11:56:29 +05:00
Alex Ant 45f7111fb9 chore(release): publish
- coopback@1.7.29-alpha.0
 - coopdoc-generator-ts@1.0.73-alpha.0
 - cooptypes@1.0.18-alpha.0
 - coop-notificator@0.0.22-alpha.0
 - terminal@0.1.33-alpha.0
2024-08-05 11:51:37 +05:00
Alex Ant f2e23a8062 перевыпуск ключа доступа 2024-08-05 11:38:10 +05:00
Alex Ant 36a3631f5e login with signature 2024-08-02 17:11:01 +05:00
Alex Ant a758d19a9f обновление mongoose, интеграция eslint и активация typescript проверок 2024-08-01 14:06:01 +05:00
Alex Ant aac3e53a30 eslint repair 2024-08-01 13:06:28 +05:00
Alex Ant 85d87ee210 FIX !!! 2024-07-31 20:48:38 +05:00
Alex Ant 2f3ee80ed4 Merge branch 'main' into dev 2024-07-31 17:26:20 +05:00
Alex Ant 3632bea54e chore(release): publish
- terminal@0.1.32
2024-07-31 17:26:08 +05:00
Alex Ant 4a0521751a update 2024-07-31 17:24:44 +05:00
Alex Ant 7d6266c241 chore(release): publish
- terminal@0.1.32-alpha.0
2024-07-31 16:28:53 +05:00
Alex Ant 1d69b9ffea Рабочие столы v1 2024-07-31 16:24:50 +05:00
261 changed files with 5322 additions and 3623 deletions
-31
View File
@@ -1,31 +0,0 @@
{
"env": {
"node": true,
"jest": true
},
"extends": ["airbnb-base", "plugin:jest/recommended", "plugin:security/recommended", "plugin:prettier/recommended"],
"plugins": ["jest", "security", "prettier", "import"],
"parserOptions": {
"ecmaVersion": 2018
},
"rules": {
"@typescript-eslint/no-explicit-any":"off",
"no-console": "error",
"func-names": "off",
"no-underscore-dangle": "off",
"consistent-return": "off",
"jest/expect-expect": "off",
"security/detect-object-injection": "off",
"import/extensions": [
"error",
"ignorePackages",
{
"js": "never",
"jsx": "never",
"ts": "never",
"tsx": "never"
}
]
}
}
+29
View File
@@ -0,0 +1,29 @@
{
"env": {
"node": true,
"jest": true,
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended"
],
"plugins": ["@typescript-eslint", "prettier"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 2018,
"sourceType": "module",
"project": "./tsconfig.json"
},
"rules": {
"no-console": "warn",
"func-names": "off",
"no-underscore-dangle": "off",
"consistent-return": "off",
"jest/expect-expect": "off",
"security/detect-object-injection": "off",
"@typescript-eslint/no-unused-vars": "warn"
}
}
-1
View File
@@ -1,5 +1,4 @@
{
"CodeGPT.apiKey": "OpenAI",
"cSpell.words": [
"subcode"
]
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.7.30](https://github.com/hagopj13/node-express-boilerplate/compare/coopback@1.7.29...coopback@1.7.30) (2024-08-13)
**Note:** Version bump only for package coopback
## [1.7.29](https://github.com/hagopj13/node-express-boilerplate/compare/coopback@1.7.29-alpha.0...coopback@1.7.29) (2024-08-05)
**Note:** Version bump only for package coopback
## [1.7.28](https://github.com/hagopj13/node-express-boilerplate/compare/coopback@1.7.28-alpha.0...coopback@1.7.28) (2024-07-26)
**Note:** Version bump only for package coopback
+4 -4
View File
@@ -1,12 +1,12 @@
{
"name": "create-nodejs-express-app",
"version": "1.7.28",
"version": "1.7.30",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "create-nodejs-express-app",
"version": "1.7.28",
"version": "1.7.30",
"license": "MIT",
"dependencies": {
"@a2seven/yoo-checkout": "^1.1.4",
@@ -32,7 +32,7 @@
"jsonwebtoken": "^8.5.1",
"jspdf": "^2.5.1",
"moment": "^2.24.0",
"mongoose": "^5.7.7",
"mongoose": "^8.5.2",
"morgan": "^1.9.1",
"nodemailer": "^6.3.1",
"nunjucks": "^3.2.4",
@@ -54,7 +54,7 @@
"@types/express": "^4.17.21",
"@types/node": "^20.11.26",
"coveralls": "^3.0.7",
"eslint": "^7.0.0",
"eslint": "^7.32.0",
"eslint-config-airbnb-base": "^14.0.0",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-import": "^2.29.1",
+7 -3
View File
@@ -1,6 +1,6 @@
{
"name": "coopback",
"version": "1.7.28",
"version": "1.7.30",
"description": "",
"private": true,
"bin": "bin/createNodejsApp.js",
@@ -8,6 +8,7 @@
"repository": "https://github.com/hagopj13/node-express-boilerplate.git",
"author": "Alex Ant <dacom.dark.sun@gmail.com>",
"license": "MIT",
"types": "src/types/index.ts",
"engines": {
"node": ">=12.0.0"
},
@@ -88,7 +89,7 @@
"jsonwebtoken": "^8.5.1",
"jspdf": "^2.5.1",
"moment": "^2.24.0",
"mongoose": "^5.7.7",
"mongoose": "^8.5.2",
"morgan": "^1.9.1",
"nodemailer": "^6.3.1",
"nunjucks": "^3.2.4",
@@ -107,13 +108,16 @@
"xss-clean": "^0.1.1"
},
"devDependencies": {
"@antfu/eslint-config": "^2.24.1",
"@redocly/cli": "^1.18.0",
"@types/express": "^4.17.21",
"@types/faker": "^6.6.9",
"@types/node": "^20.11.26",
"@typescript-eslint/eslint-plugin": "^5.10.0",
"@typescript-eslint/parser": "^8.0.0",
"concurrently": "^8.2.2",
"coveralls": "^3.0.7",
"eslint": "^7.0.0",
"eslint": "^7.32.0",
"eslint-config-airbnb-base": "^14.0.0",
"eslint-config-prettier": "^8.1.0",
"eslint-plugin-import": "^2.29.1",
@@ -1,4 +1,5 @@
import config from '../src/config/config';
import logger from '../src/config/logger';
import { User } from '../src/models';
import { tokenService } from '../src/services';
import { userService } from '../src/services';
@@ -7,7 +8,7 @@ import mongoose from 'mongoose';
const args = process.argv.slice(2);
if (args.length < 1) {
console.error('Ошибка: Необходимо указать имя пользователя');
logger.error('Ошибка: Необходимо указать имя пользователя');
process.exit(1);
}
@@ -19,9 +20,9 @@ async function createServiceUser(username: string) {
const user = await userService.createServiceUser(username);
const token = await tokenService.generateServiceAccessToken(user);
console.log('token:', token.access.token);
logger.log('token:', token.access.token);
} catch (e: any) {
console.log('Ошибка: ', e.message);
logger.log('Ошибка: ', e.message);
}
process.exit(0);
+1 -1
View File
@@ -17,7 +17,7 @@ async function deleteUser(username: string) {
try {
await mongoose.connect(config.mongoose.url, config.mongoose.options);
await userService.deleteUserById(username);
await userService.deleteUserByUsername(username);
console.log('Пользователь удалён: ', username);
} catch (e: any) {
+1 -1
View File
@@ -25,7 +25,7 @@ async function init() {
email: 'dacom.dark.sun@gmail.com',
password,
public_key: 'EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV',
role: 'chairman',
role: 'user',
type: 'individual',
individual_data: {
first_name: 'Иван',
+1 -1
View File
@@ -7,7 +7,7 @@ async function types(): Promise<void> {
// Configure your settings here
const result = await convertFromDirectory({
schemaDirectory: './src/validations',
typeOutputDirectory: './src/types',
typeOutputDirectory: './src/types/auto-generated',
debug: true,
});
+16 -8
View File
@@ -1,17 +1,15 @@
import dotenv from 'dotenv';
import Joi from 'joi';
import path from 'path';
// import { fileURLToPath } from 'url';
// import { dirname, join } from 'path';
// const __filename = fileURLToPath(import.meta.url);
// const __dirname = dirname(__filename);
dotenv.config({ path: path.join(__dirname, '../../.env') });
const envVarsSchema = Joi.object()
.keys({
NODE_ENV: Joi.string().valid('production', 'development', 'test').required(),
BASE_URL: Joi.string().required(),
SERVICE_USERNAME: Joi.string().required(),
SERVICE_WIF: Joi.string().required(),
PORT: Joi.number().default(3000),
MONGODB_URL: Joi.string().required().description('Mongo DB url'),
JWT_SECRET: Joi.string().required().description('JWT secret key'),
@@ -23,11 +21,16 @@ const envVarsSchema = Joi.object()
JWT_VERIFY_EMAIL_EXPIRATION_MINUTES: Joi.number()
.default(10)
.description('minutes after which verify email token expires'),
JWT_INVITE_EXPIRATION_MINUTES: Joi.number().default(3600).description('minutes after invite token expires'),
SMTP_HOST: Joi.string().description('server that will send the emails'),
SMTP_PORT: Joi.number().description('port to connect to the email server'),
SMTP_USERNAME: Joi.string().description('username for email server'),
SMTP_PASSWORD: Joi.string().description('password for email server'),
EMAIL_FROM: Joi.string().description('the from field in the emails sent by the app'),
COOPNAME: Joi.string().description('имя аккаунта кооператива'),
})
.unknown();
@@ -39,13 +42,16 @@ if (error) {
export default {
env: envVars.NODE_ENV,
base_url: envVars.BASE_URL,
port: envVars.PORT,
service_wif: envVars.SERVICE_WIF,
service_username: envVars.SERVICE_USERNAME,
mongoose: {
url: envVars.MONGODB_URL + (envVars.NODE_ENV === 'test' ? '-test' : ''),
options: {
useCreateIndex: true,
useNewUrlParser: true,
useUnifiedTopology: true,
// useCreateIndex: true,
// useNewUrlParser: true,
// useUnifiedTopology: true,
},
},
jwt: {
@@ -54,6 +60,7 @@ export default {
refreshExpirationDays: envVars.JWT_REFRESH_EXPIRATION_DAYS,
resetPasswordExpirationMinutes: envVars.JWT_RESET_PASSWORD_EXPIRATION_MINUTES,
verifyEmailExpirationMinutes: envVars.JWT_VERIFY_EMAIL_EXPIRATION_MINUTES,
inviteExpirationMinutes: envVars.JWT_INVITE_EXPIRATION_MINUTES,
},
email: {
smtp: {
@@ -66,4 +73,5 @@ export default {
},
from: envVars.EMAIL_FROM,
},
coopname: envVars.COOPNAME,
};
+2 -2
View File
@@ -1,5 +1,5 @@
import winston from 'winston'
import config from './config'
import winston from 'winston';
import config from './config';
const enumerateErrorFormat = winston.format((info) => {
if (info instanceof Error) {
+2 -2
View File
@@ -1,7 +1,7 @@
const allRoles = {
user: [''],
service: ['sendNotification'],
chairman: ['getUsers', 'manageUsers', 'loadAgenda', 'loadStaff', 'getDocuments', 'loadInfo'],
service: ['addUser', 'sendNotification'],
chairman: ['addUser', 'getUsers', 'manageUsers', 'loadAgenda', 'loadStaff', 'getDocuments', 'loadInfo'],
member: ['getUsers', 'manageUsers', 'loadAgenda', 'loadStaff', 'getDocuments', 'loadInfo'],
};
+1
View File
@@ -3,4 +3,5 @@ export const tokenTypes = {
REFRESH: 'refresh',
RESET_PASSWORD: 'resetPassword',
VERIFY_EMAIL: 'verifyEmail',
INVITE: 'invite',
};
@@ -1,17 +1,15 @@
import httpStatus from 'http-status';
import catchAsync from '../utils/catchAsync';
import { authService, userService, tokenService, emailService, blockchainService } from '../services';
import { RForgotPassword, RRefreshTokens, RResetPassword, RSendNotification, RVerifyEmail } from '../types';
import { authService, tokenService, emailService } from '../services';
import { RForgotKey, RRefreshTokens, RResetKey, RVerifyEmail } from '../types';
const { CREATED, NO_CONTENT } = httpStatus;
const { NO_CONTENT } = httpStatus;
export const login = catchAsync(async (req, res) => {
const { email, password } = req.body;
const user = await authService.loginUserWithEmailAndPassword(email, password);
const { now, signature, email } = req.body;
// if (user.is_registered === false)
// throw new Error('Регистрация не была завершена. Пожалуйста, подождите решения совета или начните с начала. ');
const user = await authService.loginUserWithSignature(email, now, signature);
const tokens = await tokenService.generateAuthTokens(user);
res.send({ user, tokens });
@@ -27,14 +25,14 @@ export const refreshTokens = catchAsync(async (req: RRefreshTokens, res: any) =>
res.send(tokens);
});
export const forgotPassword = catchAsync(async (req: RForgotPassword, res) => {
const resetPasswordToken = await tokenService.generateResetPasswordToken(req.body.email);
await emailService.sendResetPasswordEmail(req.body.email, resetPasswordToken);
export const lostKey = catchAsync(async (req: RForgotKey, res) => {
const resetKeyToken = await tokenService.generateResetKeyToken(req.body.email);
await emailService.sendResetKeyEmail(req.body.email, resetKeyToken);
res.status(NO_CONTENT).send();
});
export const resetPassword = catchAsync(async (req: RResetPassword, res) => {
await authService.resetPassword(req.query.token, req.body.password);
export const resetKey = catchAsync(async (req: RResetKey, res) => {
await authService.resetKey(req.body.token, req.body.public_key);
res.status(NO_CONTENT).send();
});
@@ -1,24 +1,22 @@
import catchAsync from '../utils/catchAsync';
import { coopService, documentService } from '../services';
import { Cooperative } from 'cooptypes';
import { generator } from '../services/document.service';
import ApiError from '../utils/ApiError';
import httpStatus from 'http-status';
import { Request, Response } from 'express';
export const loadInfo = catchAsync(async (req, res) => {
export const loadInfo = catchAsync(async (req: Request, res: Response) => {
const cooperative = await coopService.loadInfo(String(process.env.COOPNAME));
res.send(cooperative);
});
export const loadContacts = catchAsync(async (req, res) => {
export const loadContacts = catchAsync(async (req: Request, res: Response) => {
const contacts = await coopService.loadContacts(String(process.env.COOPNAME));
res.send(contacts);
});
export const loadAgenda = catchAsync(async (req, res) => {
export const loadAgenda = catchAsync(async (req: Request, res: Response) => {
const { coopname } = req.query;
const agenda = await coopService.loadAgenda(coopname);
const agenda = await coopService.loadAgenda(coopname as string);
const complexAgenda: Cooperative.Documents.IComplexAgenda[] = [];
@@ -30,7 +28,7 @@ export const loadAgenda = catchAsync(async (req, res) => {
res.send(complexAgenda);
});
export const loadStaff = catchAsync(async (req, res) => {
export const loadStaff = catchAsync(async (req, res: Response) => {
const { coopname } = req.query;
const staff = await coopService.loadStaff(coopname);
res.send(staff);
@@ -1,18 +1,8 @@
import catchAsync from '../utils/catchAsync';
import { documentService } from '../services/index';
import type {
IGetDocuments,
RDeletePaymentMethod,
RGenerate,
RGetDocuments,
RGetListPaymentMethods,
RSavePaymentMethod,
} from '../types';
import type { RGenerate, RGetDocuments } from '../types';
import httpStatus from 'http-status';
import pick from '../utils/pick';
import { IGetResponse } from '../types/common';
import { Cooperative } from 'cooptypes';
import ApiError from '../utils/ApiError';
export const generateDocument = catchAsync(async (req: RGenerate, res) => {
const document = await documentService.generateDocument(req.body);
@@ -21,9 +11,9 @@ export const generateDocument = catchAsync(async (req: RGenerate, res) => {
});
export const getDocuments = catchAsync(async (req: RGetDocuments, res) => {
let filter = req.query?.filter;
let limit = req.query?.limit as number;
let page = req.query?.page as number;
const filter = req.query?.filter;
const limit = req.query?.limit as number;
const page = req.query?.page as number;
const documents = await documentService.queryDocuments(filter, page, limit);
@@ -4,3 +4,4 @@ export * as documentController from './document.controller';
export * as paymentController from './payment.controller';
export * as coopController from './coop.controller';
export * as notifyController from './notify.controller';
export * as monoController from './mono.controller';
@@ -0,0 +1,18 @@
import httpStatus from 'http-status';
import { getMonoStatus } from '../services/mono.service';
import { getBlockchainInfo } from '../services/blockchain.service';
import { IHealthResponse } from '../types';
import { Request, Response } from 'express';
import catchAsync from '../utils/catchAsync';
export const getHealth = catchAsync(async (req: Request, res: Response) => {
const status = await getMonoStatus();
const blockchain = await getBlockchainInfo();
const result: IHealthResponse = {
status,
blockchain,
};
res.status(httpStatus.OK).send(result);
});
@@ -1,12 +1,16 @@
import http from 'http-status';
import ApiError from '../utils/ApiError';
import catchAsync from '../utils/catchAsync';
import { userService, tokenService } from '../services';
import { RCreateUser, RJoinCooperative } from '../types';
import { userService, tokenService, emailService, blockchainService } from '../services';
import { IAddUser, ICreateUser, RCreateUser, RJoinCooperative } from '../types';
import httpStatus from 'http-status';
import pick from '../utils/pick';
import { IGetResponse } from '../types/common';
import { IUser } from '../models/user.model';
import { Request, Response } from 'express';
import { generateUsername } from '../../tests/utils/generateUsername';
import config from '../config/config';
import logger from '../config/logger';
/**
* Порядок регистрации:
@@ -30,6 +34,38 @@ export const createUser = catchAsync(async (req: RCreateUser, res) => {
res.status(http.CREATED).send({ user, tokens });
});
export const addUser = catchAsync(async (req: Request, res: Response) => {
const body: IAddUser = req.body;
const newUser: ICreateUser = {
...body,
public_key: '',
role: 'user',
username: generateUsername(),
};
const user = await userService.createUser(newUser);
try {
await blockchainService.addUser({
...body,
...newUser,
registrator: config.service_username,
referer: body.referer ? body.referer : '',
coopname: config.coopname,
meta: '',
});
const token = await tokenService.generateInviteToken(user.email);
await emailService.sendInviteEmail(req.body.email, token);
} catch (e) {
logger.warn('error on add user: ', e);
await userService.deleteUserByUsername(newUser.username);
}
res.status(httpStatus.CREATED).send({ user });
});
export const joinCooperative = catchAsync(async (req: RJoinCooperative, res) => {
await userService.joinCooperative(req.body);
@@ -44,7 +80,7 @@ export const getUsers = catchAsync(async (req, res) => {
const result = {} as IGetResponse<IUser>;
const data = [] as any;
for await (let user of users.results) {
for await (const user of users.results) {
const json = user.toJSON();
if (user.type != 'service') {
json.private_data = (await user.getPrivateData()) || {};
@@ -82,6 +118,6 @@ export const updateUser = catchAsync(async (req, res) => {
});
export const deleteUser = catchAsync(async (req, res) => {
await userService.deleteUserById(req.params.username);
await userService.deleteUserByUsername(req.params.username);
res.status(http.NO_CONTENT).send();
});
+8 -4
View File
@@ -1,22 +1,26 @@
export * from './types';
import mongoose from 'mongoose';
import app from './app';
import config from './config/config';
import logger from './config/logger';
import { updateAuth } from './services/auth.service';
import { connectGenerator } from './services/document.service';
import { initSocketConnection } from './controllers/ws.controller';
import { monoService } from './services';
const SERVER_URL: string = process.env.SOCKET_SERVER || 'http://localhost:2222';
let server: any;
mongoose.connect(config.mongoose.url, config.mongoose.options).then(async () => {
mongoose.connect(config.mongoose.url).then(async () => {
logger.info('Connected to MongoDB');
//подключаемся к хранилищу приватных данных
await monoService.init();
// подключаемся к хранилищу приватных данных
await connectGenerator();
//подключаемся к ws-серверу
// подключаемся к ws-серверу
await initSocketConnection(SERVER_URL);
server = app.listen(config.port, () => {
+1
View File
@@ -1,3 +1,4 @@
export { default as Token } from './token.model';
export { default as User } from './user.model';
export { default as Order } from './order.model';
export { default as Mono } from './mono.model';
@@ -0,0 +1,26 @@
import mongoose, { Schema } from 'mongoose';
import { paginate, toJSON } from './plugins';
import { IHealthStatus } from '../types';
export interface IMono {
coopname: string;
status: IHealthStatus;
}
const Status = {
Install: 'install',
Active: 'active',
Maintenance: 'maintenance',
};
const MonoSchema = new Schema<IMono>({
coopname: { type: String, required: true },
status: { type: String, required: true, enum: Object.values(Status) },
});
MonoSchema.plugin(toJSON);
MonoSchema.plugin(paginate);
const MonoModel = mongoose.model<IMono>('Mono', MonoSchema);
export default MonoModel;
+52 -50
View File
@@ -5,9 +5,9 @@ interface IOrder extends Document {
creator: string;
type: string;
data: {
username: string
provider: string
quantity: string
username: string;
provider: string;
quantity: string;
};
order_id?: number;
payed?: boolean;
@@ -16,59 +16,61 @@ interface IOrder extends Document {
expired_at?: Date;
}
const orderSchema = new Schema<IOrder>({
creator: {
type: String,
required: true,
},
type: {
type: String,
required: true,
},
data: {
provider: {
type: String,
required: true
},
username: {
const orderSchema = new Schema<IOrder>(
{
creator: {
type: String,
required: true,
},
quantity: {
type: {
type: String,
required: true
}
},
order_id: {
type: Number,
required: false,
},
payed: {
type: Boolean,
required: false,
default: false,
},
delivered: {
type: Boolean,
required: false,
default: false,
},
error: {
type: Object,
required: false,
},
expired_at: {
type: Date,
default: () => {
const now = new Date();
now.setDate(now.getDate() + 1); // Увеличиваем на одни сутки
return now;
required: true,
},
data: {
provider: {
type: String,
required: true,
},
username: {
type: String,
required: true,
},
quantity: {
type: String,
required: true,
},
},
order_id: {
type: Number,
required: false,
},
payed: {
type: Boolean,
required: false,
default: false,
},
delivered: {
type: Boolean,
required: false,
default: false,
},
error: {
type: Object,
required: false,
},
expired_at: {
type: Date,
default: () => {
const now = new Date();
now.setDate(now.getDate() + 1); // Увеличиваем на одни сутки
return now;
},
},
},
},
{
timestamps: true,
});
{
timestamps: true,
}
);
// add plugin that converts mongoose to json
orderSchema.plugin(toJSON);
@@ -25,7 +25,7 @@ const tokenSchema = new Schema<IToken>(
},
type: {
type: String,
enum: [tokenTypes.REFRESH, tokenTypes.RESET_PASSWORD, tokenTypes.VERIFY_EMAIL],
enum: [tokenTypes.REFRESH, tokenTypes.RESET_PASSWORD, tokenTypes.VERIFY_EMAIL, tokenTypes.INVITE],
required: true,
},
expires: {
+4 -30
View File
@@ -1,4 +1,4 @@
import mongoose, { Document, Schema, model, Model } from 'mongoose';
import mongoose, { Schema, model, Model } from 'mongoose';
import validator from 'validator/index';
import bcryptjs from 'bcryptjs';
import { toJSON, paginate } from './plugins/index';
@@ -14,16 +14,16 @@ export interface IUser {
status: 'created' | 'joined' | 'payed' | 'registered' | 'active' | 'failed' | 'blocked';
message: string;
is_registered: boolean;
has_account: boolean;
type: 'individual' | 'entrepreneur' | 'organization';
public_key: string;
referer: string;
email: string;
password: string;
role: string;
is_email_verified: boolean;
statement: {
hash: string;
meta: Object;
meta: object;
public_key: string;
signature: string;
};
@@ -35,7 +35,6 @@ export interface IUser {
getPrivateData(): Promise<
Cooperative.Users.IIndividualData | Cooperative.Users.IEntrepreneurData | Cooperative.Users.IOrganizationData | null
>;
isPasswordMatch(password: string): Promise<boolean>;
}
interface IUserModel extends Model<IUser> {
@@ -71,7 +70,7 @@ const userSchema = new Schema<IUser, IUserModel>(
},
public_key: {
type: String,
required: true,
default: '',
},
referer: {
type: String,
@@ -89,18 +88,6 @@ const userSchema = new Schema<IUser, IUserModel>(
}
},
},
password: {
type: String,
required: true,
trim: true,
minlength: 8,
validate(value) {
if (!value.match(/\d/) || !value.match(/[a-zA-Z]/)) {
throw new Error('Password must contain at least one letter and one number');
}
},
private: true, // used by the toJSON plugin
},
role: {
type: String,
enum: roles,
@@ -162,19 +149,6 @@ userSchema.methods.getPrivateData = async function (): Promise<
return result;
};
userSchema.methods.isPasswordMatch = async function (password) {
const user = this;
return compare(password, user.password);
};
userSchema.pre('save', async function (next) {
const user = this;
if (user.isModified('password')) {
user.password = await hash(user.password, 8);
}
next();
});
const User = model<IUser, IUserModel>('User', userSchema);
export default User;
@@ -10,8 +10,8 @@ const router = Router();
router.post('/login', validate(authValidation.RLogin), authController.login);
router.post('/logout', validate(authValidation.RLogout), authController.logout);
router.post('/refresh-tokens', validate(authValidation.RRefreshTokens), authController.refreshTokens);
router.post('/forgot-password', validate(authValidation.RForgotPassword), authController.forgotPassword);
router.post('/reset-password', validate(authValidation.RResetPassword), authController.resetPassword);
router.post('/lost-key', validate(authValidation.RForgotKey), authController.lostKey);
router.post('/reset-key', validate(authValidation.RResetKey), authController.resetKey);
router.post('/send-verification-email', auth(), authController.sendVerificationEmail);
router.post('/verify-email', validate(authValidation.RVerifyEmail), authController.verifyEmail);
@@ -2,15 +2,14 @@ import { Router } from 'express';
import auth from '../../middlewares/auth';
import validate from '../../middlewares/validate';
import * as coopValidation from '../../validations/coop.validation';
// import { loadAgenda, loadStaff, } from '../../validations/coop.validation';
import * as coopController from '../../controllers/coop.controller';
const router = Router();
router.route('/agenda').get(auth('loadAgenda'), validate(coopValidation.loadAgenda), coopController.loadAgenda);
router.route('/staff').get(auth('loadStaff'), validate(coopValidation.loadStaff), coopController.loadStaff);
router.route('/contacts').get(coopController.loadContacts);
router.route('/info').get(auth('loadInfo'), coopController.loadInfo);
router.route('/contacts').get(coopController.loadContacts);
export default router;
@@ -4,6 +4,7 @@ import userRoute from './user.route';
import docsRoute from './docs.route';
import paymentRoute from './payment.route';
import coopRoute from './coop.route';
import monoRoute from './mono.route';
import dataRoute from './document.route';
import notifyRoute from './notify.route';
@@ -12,6 +13,10 @@ import config from '../../config/config';
const router = Router();
const defaultRoutes = [
{
path: '/mono',
route: monoRoute,
},
{
path: '/auth',
route: authRoute,
@@ -0,0 +1,8 @@
import { Router } from 'express';
import { monoController } from '../../controllers';
const router = Router();
router.route('/health').get(monoController.getHealth);
export default router;
@@ -11,9 +11,9 @@ router
.post(validate(userValidation.RCreateUser), userController.createUser)
.get(auth('getUsers'), validate(userValidation.RGetUsers), userController.getUsers);
router
.route('/join-cooperative')
.post(validate(userValidation.RJoinCooperative), userController.joinCooperative)
router.route('/join-cooperative').post(validate(userValidation.RJoinCooperative), userController.joinCooperative);
router.route('/add').post(auth('addUser'), validate(userValidation.RAddUser), userController.addUser);
router
.route('/:username')
@@ -1,68 +1,50 @@
import httpStatus from 'http-status';
import * as tokenService from './token.service';
import * as userService from './user.service';
import { getSoviet } from './blockchain.service';
import Token from '../models/token.model';
import ApiError from '../utils/ApiError';
import { tokenTypes } from '../config/tokens';
import { IRefreshTokens } from '../types';
import { getUserByEmail } from './user.service';
import { Bytes, Checksum256, Signature } from '@wharfkit/antelope';
import { getBlockchainAccount, getBlockchainInfo, hasActiveKey } from '../services/blockchain.service';
import config from '../config/config';
import { blockchainService } from '.';
export const updateAuth = async () => {
try {
const board = await getSoviet(process.env.COOPNAME);
// TODO снимать права с тех, кто уже не в совете
// eslint-disable-next-line no-restricted-syntax
for (const member of board.members) {
// eslint-disable-next-line no-await-in-loop
const user = await userService.getUserByUsername(member.username);
if (member.position === 'chairman' && !user) {
// eslint-disable-next-line no-await-in-loop
// await userService.createUser({
// username: member.username,
// public_key: '-',
// email: process.env.CHAIRMAN_EMAIL ,
// password: process.env.CHAIRMAN_PASSWORD as string,
// is_registered: true,
// is_organization: false,
// user_profile: {
// first_name: 'Имя',
// last_name: 'Фамилия',
// middle_name: 'Отчество',
// birthday: '23-42-3423',
// phone: '+7902294404',
// },
// signature: '-',
// signature_hash: '-',
// role: 'chairman',
// });
} else if (member.position === 'chairman' && user) {
user.role = 'chairman';
user.password = process.env.CHAIRMAN_PASSWORD || 'password';
// eslint-disable-next-line no-await-in-loop
await user.save();
} else if (user) {
user.role = 'admin';
// eslint-disable-next-line no-await-in-loop
await user.save();
}
export const loginUserWithSignature = async (email, now, signature) => {
const user = await getUserByEmail(email);
if (!user) {
throw new ApiError(httpStatus.UNAUTHORIZED, 'Пользователь не найден');
}
const bytes = Bytes.fromString(now, 'utf8');
const checksum = Checksum256.hash(bytes);
const wharf_signature = Signature.from(signature);
const publicKey = wharf_signature.recoverDigest(checksum).toLegacyString();
const info = await getBlockchainInfo();
const blockchainDate = new Date(info.head_block_time).getTime();
const userData = new Date(now).getTime();
const differenceInSeconds = (blockchainDate - userData) / 1000;
if (differenceInSeconds > 30) {
throw new ApiError(httpStatus.BAD_REQUEST, 'Время подписи и время блокчейна превышает допустимое расхождение');
}
if (config.env !== 'test') {
try {
const blockchainAccount = await getBlockchainAccount(user.username);
const hasKey = hasActiveKey(blockchainAccount, publicKey);
if (!hasKey) throw new ApiError(httpStatus.UNAUTHORIZED, 'Неверный приватный ключ');
} catch (e) {
throw new ApiError(httpStatus.UNAUTHORIZED, 'Неверный приватный ключ');
}
} catch (e: any) {
// eslint-disable-next-line no-console
console.log('Ошибка при автоматической проверке целевой авторизации: ', e.message);
}
};
/**
* Login with email and password
* @param {string} email
* @param {string} password
* @returns {Promise<User>}
*/
export const loginUserWithEmailAndPassword = async (email, password) => {
const user = await userService.getUserByEmail(email);
if (!user || !(await user.isPasswordMatch(password))) {
throw new ApiError(httpStatus.UNAUTHORIZED, 'Incorrect email or password');
}
return user;
};
@@ -76,7 +58,7 @@ export const logout = async (refreshToken) => {
if (!refreshTokenDoc) {
throw new ApiError(httpStatus.NOT_FOUND, 'Not found');
}
await refreshTokenDoc.remove();
await refreshTokenDoc.deleteOne();
};
/**
@@ -91,7 +73,7 @@ export const refreshAuth = async (data: IRefreshTokens) => {
if (!user) {
throw new Error();
}
await refreshTokenDoc.remove();
await refreshTokenDoc.deleteOne();
return tokenService.generateAuthTokens(user);
} catch (error) {
throw new ApiError(httpStatus.UNAUTHORIZED, 'Please authenticate');
@@ -99,22 +81,33 @@ export const refreshAuth = async (data: IRefreshTokens) => {
};
/**
* Reset password
* @param {string} resetPasswordToken
* @param {string} newPassword
* Reset key
* @param {string} resetKeyToken
* @param {string} publicKey
* @returns {Promise}
*/
export const resetPassword = async (resetPasswordToken, newPassword) => {
export const resetKey = async (resetKeyToken, publicKey) => {
try {
const resetPasswordTokenDoc = await tokenService.verifyToken(resetPasswordToken, tokenTypes.RESET_PASSWORD);
const user = await userService.getUserById(resetPasswordTokenDoc.user);
const resetKeyTokenDoc = await tokenService.verifyToken(resetKeyToken, tokenTypes.RESET_PASSWORD);
const user = await userService.getUserById(resetKeyTokenDoc.user);
if (!user) {
throw new Error();
}
await userService.updateUserById(user._id, { password: newPassword });
if (config.env !== 'test')
await blockchainService.changeKey({
coopname: config.coopname,
changer: config.service_username,
username: user.username,
public_key: publicKey,
});
await userService.updateUserById(user._id, { public_key: publicKey });
await Token.deleteMany({ user: user._id, type: tokenTypes.RESET_PASSWORD });
} catch (error) {
throw new ApiError(httpStatus.UNAUTHORIZED, 'Password reset failed');
throw new ApiError(httpStatus.UNAUTHORIZED, 'Token reset failed');
}
};
@@ -6,9 +6,30 @@ import EosApi from 'eosjs-api';
import getInternalAction from '../utils/getInternalAction';
import { GatewayContract, RegistratorContract, SovietContract } from 'cooptypes';
import { IUser } from '../models/user.model';
import { GetAccountResult, GetInfoResult } from 'eosjs/dist/eosjs-rpc-interfaces';
import config from '../config/config';
const rpc = new JsonRpc(process.env.BLOCKCHAIN_RPC as string, { fetch });
function hasActiveKey(account, publicKey) {
const activePermissions = account.permissions.find((p: any) => p.perm_name === 'active');
if (!activePermissions) return false;
return activePermissions.required_auth.keys.some((key: any) => key.key === publicKey);
}
async function getBlockchainInfo(): Promise<GetInfoResult> {
const api = getApi();
return await api.getInfo({});
}
async function getBlockchainAccount(username): Promise<GetAccountResult> {
const api = getApi();
return await api.getAccount(username);
}
/**
* Получить инстанс для осуществления транзакции в блокчейн.
* @param {username} - авторизация от аккаунта
@@ -22,7 +43,7 @@ async function getInstance(wif) {
return api;
}
async function getApi() {
function getApi() {
const options = {
httpEndpoint: process.env.BLOCKCHAIN_RPC, // default, null for cold-storage
verbose: false, // API logging
@@ -245,6 +266,62 @@ async function failOrder(data) {
);
}
export async function addUser(data: RegistratorContract.Actions.AddUser.IAddUser) {
const eos = await getInstance(config.service_wif);
const actions = [
{
account: RegistratorContract.contractName.production,
name: RegistratorContract.Actions.AddUser.actionName,
authorization: [
{
actor: config.service_username,
permission: 'active',
},
],
data,
},
];
await eos.transact(
{
actions,
},
{
blocksBehind: 3,
expireSeconds: 30,
}
);
}
export async function changeKey(data: RegistratorContract.Actions.ChangeKey.IChangeKey) {
const eos = await getInstance(config.service_wif);
const actions = [
{
account: RegistratorContract.contractName.production,
name: RegistratorContract.Actions.ChangeKey.actionName,
authorization: [
{
actor: config.service_username,
permission: 'active',
},
],
data,
},
];
await eos.transact(
{
actions,
},
{
blocksBehind: 3,
expireSeconds: 30,
}
);
}
async function getSoviet(coopname) {
const api = await getApi();
@@ -271,4 +348,7 @@ export {
failOrder,
completeOrder,
getSoviet,
getBlockchainInfo,
getBlockchainAccount,
hasActiveKey,
};
@@ -4,6 +4,7 @@ import { userService, blockchainService } from './index';
import { Cooperative, RegistratorContract, SovietContract } from 'cooptypes';
import ApiError from '../utils/ApiError';
import httpStatus from 'http-status';
import logger from '../config/logger';
export const loadAgenda = async (coopname: string): Promise<Cooperative.Documents.IAgenda[]> => {
const api = await blockchainService.getApi();
@@ -85,7 +86,9 @@ export const loadContacts = async (coopname: string) => {
if (coopAccount.meta) {
try {
announce = JSON.parse(coopAccount.meta);
} catch (e: any) {}
} catch (e: any) {
logger.warn(`Ошибка при получении контактов: ${e.message}`);
}
}
return {
@@ -1,14 +1,9 @@
import eosjsecc from 'eosjs-ecc';
import ApiError from '../utils/ApiError';
import { getActions, getTables } from '../utils/getFetch';
import * as coopService from './coop.service';
const { verify, sha256 } = eosjsecc;
import { getActions } from '../utils/getFetch';
import config from '../config/config';
import { Generator } from 'coopdoc-generator-ts';
import type { IGenerate, IGetDocuments } from '../types';
import type { IGenerate } from '../types';
import { Cooperative, SovietContract } from 'cooptypes';
import { User } from '../models';
import { IUser } from '../models/user.model';
export const generator = new Generator();
@@ -99,8 +94,8 @@ export async function buildComplexDocument(
export const queryDocuments = async (
filter: any,
page: number = 1,
limit: number = 100
page = 1,
limit = 100
): Promise<Cooperative.Documents.IGetComplexDocuments> => {
const actions = await getActions<SovietContract.Actions.Registry.NewResolved.INewResolved>(
`${process.env.SIMPLE_EXPLORER_API}/get-actions`,
@@ -116,7 +111,7 @@ export const queryDocuments = async (
}
);
let response: Cooperative.Documents.IGetComplexDocuments = {
const response: Cooperative.Documents.IGetComplexDocuments = {
results: [],
page,
limit,
@@ -2,9 +2,9 @@ import { createTransport } from 'nodemailer';
import config from '../config/config';
import logger from '../config/logger';
const { email, env } = config
const { email, env } = config;
const transport = createTransport(email.smtp);
export const transport = createTransport(email.smtp);
/* istanbul ignore next */
if (env !== 'test') {
@@ -21,7 +21,7 @@ if (env !== 'test') {
* @param {string} text
* @returns {Promise}
*/
const sendEmail = async (to, subject, text) => {
export const sendEmail = async (to, subject, text) => {
const msg = { from: email.from, to, subject, text };
await transport.sendMail(msg);
};
@@ -32,13 +32,31 @@ const sendEmail = async (to, subject, text) => {
* @param {string} token
* @returns {Promise}
*/
const sendResetPasswordEmail = async (to, token) => {
const subject = 'Reset password';
// replace this url with the link to the reset password page of your front-end app
const resetPasswordUrl = `http://link-to-app/reset-password?token=${token}`;
const text = `Dear user,
To reset your password, click on this link: ${resetPasswordUrl}
If you did not request any password resets, then ignore this email.`;
export const sendResetKeyEmail = async (to, token) => {
const subject = 'Восстановление доступа';
const resetPasswordUrl = `${config.base_url}/#/${config.coopname}/auth/reset-key?token=${token}`;
const text = `Мы получили запрос на перевыпуск приватного ключа,
Для перевыпуска нажмите на ссылку: ${resetPasswordUrl}. Время действия ссылки - 10 минут.
Если вы не запрашивали перевыпуск ключа - проигнорируйте это сообщение.`;
await sendEmail(to, subject, text);
};
/**
* Send reset password email
* @param {string} to
* @param {string} token
* @returns {Promise}
*/
export const sendInviteEmail = async (to, token) => {
const subject = 'Восстановление доступа';
const inviteUrl = `${config.base_url}/#/${config.coopname}/auth/reset-key?token=${token}`;
const text = `Вам отправлено приглашение на подключение к Цифровому Кооперативу в качестве пайщика.
Для того, чтобы воспользоваться приглашени, нажмите на ссылку: ${token}. Время действия ссылки - 24 часа.`;
await sendEmail(to, subject, text);
};
@@ -48,7 +66,7 @@ If you did not request any password resets, then ignore this email.`;
* @param {string} token
* @returns {Promise}
*/
const sendVerificationEmail = async (to, token) => {
export const sendVerificationEmail = async (to, token) => {
const subject = 'Email Verification';
// replace this url with the link to the email verification page of your front-end app
const verificationEmailUrl = `http://link-to-app/verify-email?token=${token}`;
@@ -57,10 +75,3 @@ To verify your email, click on this link: ${verificationEmailUrl}
If you did not create an account, then ignore this email.`;
await sendEmail(to, subject, text);
};
export {
transport,
sendEmail,
sendResetPasswordEmail,
sendVerificationEmail,
};
@@ -7,3 +7,4 @@ export * as paymentService from './payment.service';
export * as documentService from './document.service';
export * as coopService from './coop.service';
export * as wsService from './ws.service';
export * as monoService from './mono.service';
@@ -0,0 +1,26 @@
import httpStatus from 'http-status';
import { Mono } from '../models';
import ApiError from '../utils/ApiError';
import config from '../config/config';
import logger from '../config/logger';
import { IHealthStatus } from '../types';
export const init = async (): Promise<void> => {
const mono = await Mono.findOne({ coopname: config.coopname });
if (!mono)
await Mono.create({
coopname: config.coopname,
status: 'install',
});
logger.info('MONO инициализирован');
};
export const getMonoStatus = async (): Promise<IHealthStatus> => {
const mono = await Mono.findOne({ coopname: config.coopname });
if (!mono) throw new ApiError(httpStatus.BAD_REQUEST, 'Установщик не найден');
return mono.status;
};
@@ -1 +1 @@
export default {};
@@ -40,7 +40,7 @@ function getAmountPlusFee(amount: number, provider: string): number {
}
export async function createDeposit(username: string, data: ICreateDeposit): Promise<ICreatedPayment> {
let cooperative = await generator.constructCooperative(process.env.COOPNAME as string);
const cooperative = await generator.constructCooperative(process.env.COOPNAME as string);
if (!cooperative) throw new Error('Кооператив не найден');
@@ -50,7 +50,7 @@ export async function createDeposit(username: string, data: ICreateDeposit): Pro
type: 'deposit',
data: { username, provider: data.provider, quantity: data.quantity },
});
const internal_id = db_order._id;
const internal_id = db_order.id;
let order_id = 0;
@@ -118,9 +118,9 @@ export async function createDeposit(username: string, data: ICreateDeposit): Pro
}
export async function createInitialOrder(username: string, data: ICreateInitialPayment): Promise<ICreatedPayment> {
let cooperative = await generator.constructCooperative(process.env.COOPNAME as string);
const cooperative = await generator.constructCooperative(process.env.COOPNAME as string);
if (!cooperative) throw new Error('Кооператив не найден');
if (!cooperative) throw new ApiError(httpStatus.BAD_REQUEST, 'Кооператив не найден');
const user = await getUserByUsername(username);
let amount = '';
@@ -139,7 +139,7 @@ export async function createInitialOrder(username: string, data: ICreateInitialP
type: 'registration',
data: { provider: data.provider, username, quantity: amount },
});
const internal_id = db_order._id;
const internal_id = db_order.id;
if (data.provider === 'yookassa') {
// 2. Используешь внутренний айди для получения ордера в системе платежей
@@ -237,7 +237,7 @@ export async function catchIPN(ipnBody: IYandexIPN) {
memo: '',
});
console.log('Зарегистрирован новый пользователь: ', user.username);
logger.info('Зарегистрирован новый пользователь: ', user.username);
user.status = 'registered';
user.is_registered = true;
} else if (order.type === 'deposit') {
@@ -248,7 +248,7 @@ export async function catchIPN(ipnBody: IYandexIPN) {
memo: '',
});
const quantity = parseFloat(order.data.quantity);
console.log(`Принят паевый взнос пользователя: ${user.username} на сумму ${order.data.quantity}`);
logger.info(`Принят паевый взнос пользователя: ${user.username} на сумму ${order.data.quantity}`);
}
await user.save();
@@ -256,7 +256,7 @@ export async function catchIPN(ipnBody: IYandexIPN) {
order.delivered = true;
await order.save();
} catch (e: any) {
console.error(e);
logger.warn(e);
order.error = e;
order.delivered = false;
await order.save();
@@ -281,6 +281,7 @@ export async function catchIPN(ipnBody: IYandexIPN) {
});
}
} else {
logger.warn(`Ордер не найден: `, ipnBody);
throw new Error(`Ордер не найден`);
}
}
@@ -107,15 +107,31 @@ export const generateAuthTokens = async (user) => {
* @param {string} email
* @returns {Promise<string>}
*/
export const generateResetPasswordToken = async (email) => {
export const generateResetKeyToken = async (email) => {
const user = await userService.getUserByEmail(email);
if (!user) {
throw new ApiError(httpStatus.NOT_FOUND, 'No users found with this email');
}
const expires = moment().add(config.jwt.resetPasswordExpirationMinutes, 'minutes');
const resetPasswordToken = generateToken(user.id, expires, tokenTypes.RESET_PASSWORD);
await saveToken(resetPasswordToken, user.id, expires, tokenTypes.RESET_PASSWORD);
return resetPasswordToken;
const resetKeyToken = generateToken(user.id, expires, tokenTypes.RESET_PASSWORD);
await saveToken(resetKeyToken, user.id, expires, tokenTypes.RESET_PASSWORD);
return resetKeyToken;
};
/**
* Generate invite token
* @param {string} email
* @returns {Promise<string>}
*/
export const generateInviteToken = async (email) => {
const user = await userService.getUserByEmail(email);
if (!user) {
throw new ApiError(httpStatus.NOT_FOUND, 'No users found with this email');
}
const expires = moment().add(config.jwt.inviteExpirationMinutes, 'minutes');
const inviteToken = generateToken(user.id, expires, tokenTypes.INVITE);
await saveToken(inviteToken, user.id, expires, tokenTypes.INVITE);
return inviteToken;
};
/**
@@ -2,8 +2,7 @@ import http from 'http-status';
import { User } from '../models';
import ApiError from '../utils/ApiError';
import { generator } from './document.service';
import { ICreateUser, IJoinCooperative } from '../types/user.validation';
import ecc from 'eosjs-ecc';
import { ICreateUser, IJoinCooperative } from '../types/auto-generated/user.validation';
import { PublicKey, Signature } from '@wharfkit/antelope';
import faker from 'faker';
import { randomBytes } from 'crypto';
@@ -28,6 +27,7 @@ export const createServiceUser = async (username: string) => {
export const createUser = async (userBody: ICreateUser) => {
//TODO проверяем на существование пользователя
//допускаем обновление личных данных, если пользователь находится в статусе 'created'
const exist = await User.findOne({ email: userBody.email });
if (userBody.type === 'individual' && !userBody.individual_data)
@@ -186,11 +186,11 @@ export const updateUserByUsername = async (username, updateBody) => {
* @param {string} username
* @returns {Promise<User>}
*/
export const deleteUserById = async (username) => {
export const deleteUserByUsername = async (username) => {
const user = await getUserByUsername(username);
if (!user) {
throw new ApiError(http.NOT_FOUND, 'Пользователь не найден');
}
await user.remove();
await user.deleteOne();
return user;
};
@@ -9,7 +9,8 @@ export interface IForgotPassword {
export interface ILogin {
email: string;
password: string;
now: string;
signature: string;
}
export interface ILogout {
@@ -20,11 +21,8 @@ export interface IRefreshTokens {
refreshToken: string;
}
export interface IResetPasswordBody {
password: string;
}
export interface IResetPasswordQuery {
export interface IResetKeyBody {
public_key: string;
token: string;
}
@@ -32,7 +30,7 @@ export interface IVerifyEmail {
token: string;
}
export interface RForgotPassword {
export interface RForgotKey {
body: {
email: string;
};
@@ -41,7 +39,8 @@ export interface RForgotPassword {
export interface RLogin {
body: {
email: string;
password: string;
now: string;
signature: string;
};
}
@@ -57,11 +56,9 @@ export interface RRefreshTokens {
};
}
export interface RResetPassword {
export interface RResetKey {
body: {
password: string;
};
query: {
public_key: string;
token: string;
};
}
@@ -0,0 +1,11 @@
/**
* This file was automatically generated by joi-to-typescript
* Do not modify this file manually
*/
export * from './auth.validation';
export * from './coop.validation';
export * from './document.validation';
export * from './notify.validation';
export * from './payment.validation';
export * from './user.validation';
@@ -3,6 +3,84 @@
* Do not modify this file manually
*/
export interface IAddUser {
created_at: string;
email: string;
entrepreneur_data?: {
bank_account: {
account_number: string;
bank_name: string;
card_number?: string;
currency: 'RUB' | 'Other';
details: {
bik: string;
corr: string;
kpp: string;
};
};
birthdate: string;
city: string;
country: 'Russia' | 'Other';
details: {
inn: string;
ogrn: string;
};
email: string;
first_name: string;
full_address: string;
last_name: string;
middle_name: string;
phone: string;
};
individual_data?: {
birthdate: string;
email: string;
first_name: string;
full_address: string;
last_name: string;
middle_name: string;
phone: string;
};
initial: string;
minimum: string;
organization_data?: {
bank_account: {
account_number: string;
bank_name: string;
card_number?: string;
currency: 'RUB' | 'Other';
details: {
bik: string;
corr: string;
kpp: string;
};
};
city: string;
country: 'Russia' | 'Other';
details: {
inn: string;
ogrn: string;
};
email: string;
full_address: string;
full_name: string;
is_cooperative: boolean;
phone: string;
represented_by: {
based_on: string;
first_name: string;
last_name: string;
middle_name: string;
position: string;
};
short_name: string;
type: 'coop' | 'ooo' | 'oao' | 'zao' | 'pao' | 'ao';
};
referer?: string;
spread_initial: boolean;
type: 'individual' | 'entrepreneur' | 'organization';
}
export interface ICreateUser {
email: string;
entrepreneur_data?: {
@@ -73,10 +151,9 @@ export interface ICreateUser {
short_name: string;
type: 'coop' | 'ooo' | 'oao' | 'zao' | 'pao' | 'ao';
};
password: string;
public_key: string;
referer?: string;
role: 'user' | 'chairman' | 'member';
role: 'user';
type: 'individual' | 'entrepreneur' | 'organization';
username: string;
}
@@ -169,6 +246,87 @@ export interface IOrganizationData {
type: 'coop' | 'ooo' | 'oao' | 'zao' | 'pao' | 'ao';
}
export interface RAddUser {
body: {
created_at: string;
email: string;
entrepreneur_data?: {
bank_account: {
account_number: string;
bank_name: string;
card_number?: string;
currency: 'RUB' | 'Other';
details: {
bik: string;
corr: string;
kpp: string;
};
};
birthdate: string;
city: string;
country: 'Russia' | 'Other';
details: {
inn: string;
ogrn: string;
};
email: string;
first_name: string;
full_address: string;
last_name: string;
middle_name: string;
phone: string;
};
individual_data?: {
birthdate: string;
email: string;
first_name: string;
full_address: string;
last_name: string;
middle_name: string;
phone: string;
};
initial: string;
meta?: string;
minimum: string;
organization_data?: {
bank_account: {
account_number: string;
bank_name: string;
card_number?: string;
currency: 'RUB' | 'Other';
details: {
bik: string;
corr: string;
kpp: string;
};
};
city: string;
country: 'Russia' | 'Other';
details: {
inn: string;
ogrn: string;
};
email: string;
full_address: string;
full_name: string;
is_cooperative: boolean;
phone: string;
represented_by: {
based_on: string;
first_name: string;
last_name: string;
middle_name: string;
position: string;
};
short_name: string;
type: 'coop' | 'ooo' | 'oao' | 'zao' | 'pao' | 'ao';
};
referer?: string;
spread_initial: boolean;
type: 'individual' | 'entrepreneur' | 'organization';
};
}
export interface RCreateUser {
body: {
email: string;
@@ -240,10 +398,9 @@ export interface RCreateUser {
short_name: string;
type: 'coop' | 'ooo' | 'oao' | 'zao' | 'pao' | 'ao';
};
password: string;
public_key: string;
referer?: string;
role: 'user' | 'chairman' | 'member';
role: 'user';
type: 'individual' | 'entrepreneur' | 'organization';
username: string;
};
@@ -280,7 +437,6 @@ export interface RJoinCooperative {
export interface RUpdateUser {
body?: {
email?: string;
password?: string;
};
params?: {
username: string;
+1 -1
View File
@@ -122,7 +122,7 @@ export interface IAction {
elapsed: number;
}
export interface IExtendedTable extends ITable {}
export type IExtendedTable = ITable;
export interface IExtendedAction extends IAction {
user: IIndividualData | IEntrepreneurData | IOrganizationData | null;
@@ -1,74 +0,0 @@
/**
* This file was automatically generated by joi-to-typescript
* Do not modify this file manually
*/
export interface IGenerate {
/**
* Unknown Property
*/
[x: string]: unknown;
action: string;
block_num?: number;
code: string;
coopname: string;
created_at?: string;
generator?: string;
lang?: 'ru';
timezone?: string;
username: string;
version?: string;
}
export interface IGetDocuments {
/**
* Unknown Property
*/
[x: string]: unknown;
filter?: {
receiver?: string;
};
limit?: number;
page?: number;
sortBy?: string;
}
export interface RGenerate {
body: {
/**
* Unknown Property
*/
[x: string]: unknown;
action: string;
block_num?: number;
code: string;
coopname: string;
created_at?: string;
generator?: string;
lang?: 'ru';
timezone?: string;
username: string;
version?: string;
};
}
export interface RGetDocuments {
query?: {
/**
* Unknown Property
*/
[x: string]: unknown;
filter?: {
receiver?: string;
};
limit?: number;
page?: number;
sortBy?: string;
};
}
export interface RGetMyDocuments {
query?: {
username?: string;
};
}
+3 -11
View File
@@ -1,11 +1,3 @@
/**
* This file was automatically generated by joi-to-typescript
* Do not modify this file manually
*/
export * from './auth.validation';
export * from './coop.validation';
export * from './document.validation';
export * from './notify.validation';
export * from './payment.validation';
export * from './user.validation';
export * from './auto-generated';
export * from './mono.types';
export * from './common';
@@ -0,0 +1,8 @@
import { GetInfoResult } from 'eosjs/dist/eosjs-rpc-interfaces';
export type IHealthStatus = 'install' | 'active' | 'maintenance';
export interface IHealthResponse {
status: IHealthStatus;
blockchain: GetInfoResult;
}
@@ -1,100 +0,0 @@
/**
* This file was automatically generated by joi-to-typescript
* Do not modify this file manually
*/
export interface ICreateDeposit {
provider: 'yookassa';
quantity: string;
}
export interface ICreateInitialPayment {
provider: 'yookassa';
}
export interface IRecieveIPN {
event: string;
object: {
/**
* Unknown Property
*/
[x: string]: unknown;
amount: {
/**
* Unknown Property
*/
[x: string]: unknown;
currency: string;
value: string;
};
authorization_details?: {
/**
* Unknown Property
*/
[x: string]: unknown;
auth_code?: string;
rrn?: string;
three_d_secure?: {
/**
* Unknown Property
*/
[x: string]: unknown;
applied?: boolean;
};
};
created_at?: string;
description?: string;
expires_at?: string;
id: string;
income_amount: {
/**
* Unknown Property
*/
[x: string]: unknown;
currency: string;
value: string;
};
metadata?: {
/**
* Unknown Property
*/
[x: string]: unknown;
};
paid: boolean;
payment_method: {
/**
* Unknown Property
*/
[x: string]: unknown;
card?: {
/**
* Unknown Property
*/
[x: string]: unknown;
card_type?: string;
expiry_month?: string;
expiry_year?: string;
first6?: string;
issuer_country?: string;
issuer_name?: string;
last4?: string;
};
id: string;
saved?: boolean;
title?: string;
type: string;
};
refundable?: boolean;
refunded_amount: {
/**
* Unknown Property
*/
[x: string]: unknown;
currency: string;
value: string;
};
status: string;
test?: boolean;
};
type: string;
}
@@ -14,28 +14,26 @@ export const IForgotPassword = Joi.object({
email: Joi.string().email().required(),
});
export const RForgotPassword = Joi.object({
export const RForgotKey = Joi.object({
body: IForgotPassword.required(),
});
// Внутренние параметры
export const ILogin = Joi.object({
signature: Joi.string().required(),
email: Joi.string().email().required(),
password: Joi.string().required(),
now: Joi.string().required(),
});
export const ILogout = Joi.object({
refreshToken: Joi.string().required(),
});
export const IResetPasswordQuery = Joi.object({
export const IResetKeyBody = Joi.object({
public_key: Joi.string().required(),
token: Joi.string().required(),
});
export const IResetPasswordBody = Joi.object({
password: Joi.string().required().custom(password),
});
export const IVerifyEmail = Joi.object({
token: Joi.string().required(),
});
@@ -49,9 +47,8 @@ export const RLogout = Joi.object({
body: ILogout.required(),
});
export const RResetPassword = Joi.object({
query: IResetPasswordQuery.required(),
body: IResetPasswordBody.required(),
export const RResetKey = Joi.object({
body: IResetKeyBody.required(),
});
export const RVerifyEmail = Joi.object({
@@ -1,5 +1,5 @@
import * as Joi from 'joi';
import { password, objectId } from './custom.validation';
import { password } from './custom.validation';
import { IBankAccount } from './payment.validation';
export const IIndividualData = Joi.object({
@@ -57,8 +57,7 @@ export const IEntrepreneurData = Joi.object({
export const ICreateUser = Joi.object({
email: Joi.string().required().email(),
password: Joi.string().required().custom(password),
role: Joi.string().required().valid('user', 'chairman', 'member'),
role: Joi.string().required().valid('user'),
public_key: Joi.string().required(),
username: Joi.string().required().length(12),
referer: Joi.string().length(12).allow('').optional(),
@@ -68,6 +67,25 @@ export const ICreateUser = Joi.object({
entrepreneur_data: IEntrepreneurData.optional(),
});
export const IAddUser = Joi.object({
email: Joi.string().required().email(),
referer: Joi.string().length(12).allow('').optional(),
type: Joi.string().required().valid('individual', 'entrepreneur', 'organization'),
individual_data: IIndividualData.optional(),
organization_data: IOrganizationData.optional(),
entrepreneur_data: IEntrepreneurData.optional(),
spread_initial: Joi.boolean().required(),
created_at: Joi.string().required(),
initial: Joi.string().required(),
minimum: Joi.string().required(),
});
export const RAddUser = Joi.object({
body: IAddUser.required(),
});
export const IDocument = Joi.object().keys({
hash: Joi.string().required(),
signature: Joi.string().required(),
@@ -104,7 +122,6 @@ export const RUpdateUser = Joi.object({
body: Joi.object()
.keys({
email: Joi.string().email(),
password: Joi.string().custom(password),
})
.min(1),
});
+6 -6
View File
@@ -27,12 +27,12 @@ export const installInitialCooperativeData = async () => {
is_cooperative: true,
is_branched: false,
coop_type: 'conscoop',
registration: '2.0000 TRUB',
initial: '1.0000 TRUB',
minimum: '1.0000 TRUB',
org_registration: '20.0000 TRUB',
org_initial: '10.0000 TRUB',
org_minimum: '10.0000 TRUB',
registration: '200.0000 TRUB',
initial: '100.0000 TRUB',
minimum: '100.0000 TRUB',
org_registration: '2000.0000 TRUB',
org_initial: '1000.0000 TRUB',
org_minimum: '1000.0000 TRUB',
} as RegistratorContract.Tables.Cooperatives.ICooperative);
await insertDelta(delta1);
@@ -1,17 +1,11 @@
import mongoose from 'mongoose';
import bcrypt from 'bcryptjs';
import faker from 'faker';
import User from '../../src/models/user.model';
import { generateUsername } from '../utils/generateUsername';
import { Cooperative } from 'cooptypes';
import { ICreateUser } from '../../src/types';
const password = 'password1';
const email1 = faker.internet.email().toLowerCase();
export const participantOne: ICreateUser = {
email: email1,
password,
username: generateUsername(),
public_key: 'EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV',
role: 'user',
+6 -10
View File
@@ -7,10 +7,6 @@ import { Cooperative } from 'cooptypes';
import { ICreateUser } from '../../src/types';
import { ObjectId } from 'mongodb';
const password = 'password1';
const salt = bcrypt.genSaltSync(8);
const hashedPassword = bcrypt.hashSync(password, salt);
const generateRandomId = () => new mongoose.Types.ObjectId();
type testUser = Omit<IUser, 'getPrivateData' | 'isPasswordMatch' | 'private_data'> & {
@@ -33,8 +29,8 @@ const adminUsername = generateUsername();
export const admin: testUser = {
_id: generateRandomId(),
email: email1,
password,
status: 'active',
has_account: false,
message: '',
is_registered: true,
role: 'chairman',
@@ -67,7 +63,7 @@ const usernameOne = generateUsername();
export const userOne: testUser = {
_id: generateRandomId(),
email: email2,
password,
has_account: false,
status: 'active',
message: '',
is_registered: true,
@@ -101,7 +97,7 @@ const usernameTwo = generateUsername();
export const userTwo: testUser = {
_id: generateRandomId(),
email: email3,
password,
has_account: false,
status: 'active',
message: '',
is_registered: true,
@@ -134,7 +130,7 @@ export const userTwo: testUser = {
export const chairman: testUser = {
_id: generateRandomId(),
email: email4,
password,
has_account: false,
status: 'active',
message: '',
is_registered: true,
@@ -167,7 +163,7 @@ export const chairman: testUser = {
export const voskhod: testUser = {
_id: generateRandomId(),
email: email5,
password,
has_account: false,
status: 'active',
message: '',
is_registered: true,
@@ -254,7 +250,7 @@ export const insertUsers = async (users: testUser[]) => {
for (const user of users) {
const { type, individual_data, organization_data, entrepreneur_data, block_num, ...rest } = user;
await User.insertMany([{ ...rest, type, password: hashedPassword }]);
await User.insertMany([{ ...rest, type }]);
if (type === 'individual' && individual_data) {
await insertPrivateIndividualUserData({
@@ -16,6 +16,8 @@ import { tokenTypes } from '../../src/config/tokens';
import { userOne, admin, insertUsers } from '../fixtures/user.fixture';
import { userOneAccessToken, adminAccessToken } from '../fixtures/token.fixture';
import { generateUsername } from '../utils/generateUsername';
import { getBlockchainInfo } from '../../src/services/blockchain.service';
import { Bytes, Checksum256, PrivateKey } from '@wharfkit/session';
setupTestDB();
@@ -27,7 +29,6 @@ describe('Auth routes', () => {
newUser = {
email,
password: 'password1',
role: 'user',
public_key: 'EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV',
username: generateUsername(),
@@ -48,8 +49,6 @@ describe('Auth routes', () => {
test('should return 201 and successfully register user if request data is ok', async () => {
const res = await request(app).post('/v1/users').send(newUser).expect(httpStatus.CREATED);
expect(res.body.user).not.toHaveProperty('password');
expect(res.body.user).toEqual({
id: expect.anything(),
email: newUser.email,
@@ -74,7 +73,6 @@ describe('Auth routes', () => {
expect(dbUser).toBeDefined();
expect(dbUser?.password).not.toBe(newUser.password);
expect(dbUser).toMatchObject({ email: newUser.email, role: 'user', is_email_verified: false });
expect(res.body.tokens).toEqual({
@@ -95,30 +93,24 @@ describe('Auth routes', () => {
await request(app).post('/v1/users').send(newUser).expect(httpStatus.BAD_REQUEST);
});
test('should return 400 error if password length is less than 8 characters', async () => {
newUser.password = 'passwo1';
await request(app).post('/v1/users').send(newUser).expect(httpStatus.BAD_REQUEST);
});
test('should return 400 error if password does not contain both letters and numbers', async () => {
newUser.password = 'password';
await request(app).post('/v1/users').send(newUser).expect(httpStatus.BAD_REQUEST);
newUser.password = '11111111';
await request(app).post('/v1/users').send(newUser).expect(httpStatus.BAD_REQUEST);
});
});
describe('POST /v1/auth/login', () => {
test('should return 200 and login user if email and password match', async () => {
test('should return 200 and login user if email and signature match', async () => {
await insertUsers([userOne]);
const now = (await getBlockchainInfo()).head_block_time;
const privateKey = PrivateKey.fromString('5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3');
const bytes = Bytes.fromString(now, 'utf8');
const checksum = Checksum256.hash(bytes);
const signature = privateKey.signDigest(checksum);
const loginCredentials = {
email: userOne.email,
password: userOne.password,
now,
signature,
};
const res = await request(app).post('/v1/auth/login').send(loginCredentials);
@@ -140,27 +132,46 @@ describe('Auth routes', () => {
});
test('should return 401 error if there are no users with that email', async () => {
const now = (await getBlockchainInfo()).head_block_time;
const privateKey = PrivateKey.fromString('5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3');
const bytes = Bytes.fromString(now, 'utf8');
const checksum = Checksum256.hash(bytes);
const signature = privateKey.signDigest(checksum);
const loginCredentials = {
now,
email: userOne.email,
password: userOne.password,
signature,
};
const res = await request(app).post('/v1/auth/login').send(loginCredentials).expect(httpStatus.UNAUTHORIZED);
expect(res.body).toEqual({ code: httpStatus.UNAUTHORIZED, message: 'Incorrect email or password' });
expect(res.body).toEqual({ code: httpStatus.UNAUTHORIZED, message: 'Пользователь не найден' });
});
test('should return 401 error if password is wrong', async () => {
await insertUsers([userOne]);
const loginCredentials = {
email: userOne.email,
password: 'wrongPassword1',
};
// test('should return 401 error if signature is wrong', async () => {
const res = await request(app).post('/v1/auth/login').send(loginCredentials).expect(httpStatus.UNAUTHORIZED);
// Восстановить проверку. Необходимо подложить аккаунт тестового юзера в коде вместо обращения к блокчейну.
expect(res.body).toEqual({ code: httpStatus.UNAUTHORIZED, message: 'Incorrect email or password' });
});
// await insertUsers([userOne]);
// const now = (await getBlockchainInfo()).head_block_time;
// const privateKey = PrivateKey.fromString('5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3');
// const bytes = Bytes.fromString(now, 'utf8');
// const checksum = Checksum256.hash(bytes);
// const signature = privateKey.signDigest(checksum);
// const now2 = '2024-08-02T11:50:40.000';
// const loginCredentials = {
// email: userOne.email,
// now: now2,
// signature,
// };
// const res = await request(app).post('/v1/auth/login').send(loginCredentials);
// console.log(res.body);
// expect(res.status).toBe(httpStatus.UNAUTHORIZED);
// expect(res.body).toEqual({ code: httpStatus.UNAUTHORIZED, message: 'Incorrect email or password' });
// });
});
describe('POST /v1/auth/logout', () => {
@@ -260,47 +271,46 @@ describe('Auth routes', () => {
});
});
describe('POST /v1/auth/forgot-password', () => {
describe('POST /v1/auth/lost-key', () => {
beforeEach(() => {
jest.spyOn(emailService.transport, 'sendMail').mockResolvedValue();
});
test('should return 204 and send reset password email to the user', async () => {
test('should return 204 and send reset key email to the user', async () => {
await insertUsers([userOne]);
const sendResetPasswordEmailSpy = jest.spyOn(emailService, 'sendResetPasswordEmail');
const sendResetKeyEmailSpy = jest.spyOn(emailService, 'sendResetKeyEmail');
await request(app).post('/v1/auth/forgot-password').send({ email: userOne.email }).expect(httpStatus.NO_CONTENT);
await request(app).post('/v1/auth/lost-key').send({ email: userOne.email }).expect(httpStatus.NO_CONTENT);
expect(sendResetPasswordEmailSpy).toHaveBeenCalledWith(userOne.email, expect.any(String));
const resetPasswordToken = sendResetPasswordEmailSpy.mock.calls[0][1];
const dbResetPasswordTokenDoc = await Token.findOne({ token: resetPasswordToken, user: userOne._id.toString() });
expect(sendResetKeyEmailSpy).toHaveBeenCalledWith(userOne.email, expect.any(String));
const resetKeyToken = sendResetKeyEmailSpy.mock.calls[0][1];
const dbResetPasswordTokenDoc = await Token.findOne({ token: resetKeyToken, user: userOne._id.toString() });
expect(dbResetPasswordTokenDoc).toBeDefined();
});
test('should return 400 if email is missing', async () => {
await insertUsers([userOne]);
const res = await request(app).post('/v1/auth/forgot-password').send();
const res = await request(app).post('/v1/auth/lost-key').send();
expect(res.status).toBe(httpStatus.BAD_REQUEST);
});
test('should return 404 if email does not belong to any user', async () => {
await request(app).post('/v1/auth/forgot-password').send({ email: userOne.email }).expect(httpStatus.NOT_FOUND);
await request(app).post('/v1/auth/lost-key').send({ email: userOne.email }).expect(httpStatus.NOT_FOUND);
});
});
describe('POST /v1/auth/reset-password', () => {
test('should return 204 and reset the password', async () => {
describe('POST /v1/auth/reset-key', () => {
test('should return 204 and reset the key', async () => {
await insertUsers([userOne]);
const expires = moment().add(config.jwt.resetPasswordExpirationMinutes, 'minutes');
const resetPasswordToken = tokenService.generateToken(userOne._id, expires, tokenTypes.RESET_PASSWORD);
await tokenService.saveToken(resetPasswordToken, userOne._id, expires, tokenTypes.RESET_PASSWORD);
const resetKeyToken = tokenService.generateToken(userOne._id, expires, tokenTypes.RESET_PASSWORD);
await tokenService.saveToken(resetKeyToken, userOne._id, expires, tokenTypes.RESET_PASSWORD);
const res = await request(app)
.post('/v1/auth/reset-password')
.query({ token: resetPasswordToken })
.send({ password: 'password2' });
.post('/v1/auth/reset-key')
.send({ token: resetKeyToken, public_key: userOne.public_key });
expect(res.status).toBe(httpStatus.NO_CONTENT);
@@ -308,9 +318,6 @@ describe('Auth routes', () => {
expect(dbUser).not.toBeUndefined();
if (dbUser) {
const isPasswordMatch = await bcrypt.compare('password2', dbUser.password);
expect(isPasswordMatch).toBe(true);
const dbResetPasswordTokenCount = await Token.countDocuments({
user: userOne._id.toString(),
type: tokenTypes.RESET_PASSWORD,
@@ -319,76 +326,49 @@ describe('Auth routes', () => {
}
});
test('should return 400 if reset password token is missing', async () => {
test('should return 400 if reset key token is missing', async () => {
await insertUsers([userOne]);
await request(app).post('/v1/auth/reset-password').send({ password: 'password2' }).expect(httpStatus.BAD_REQUEST);
});
test('should return 401 if reset password token is blacklisted', async () => {
await insertUsers([userOne]);
const expires = moment().add(config.jwt.resetPasswordExpirationMinutes, 'minutes');
const resetPasswordToken = tokenService.generateToken(userOne._id, expires, tokenTypes.RESET_PASSWORD);
await tokenService.saveToken(resetPasswordToken, userOne._id, expires, tokenTypes.RESET_PASSWORD, true);
await request(app)
.post('/v1/auth/reset-password')
.query({ token: resetPasswordToken })
.send({ password: 'password2' })
.post('/v1/auth/reset-key')
.send({ public_key: 'EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV' })
.expect(httpStatus.BAD_REQUEST);
});
test('should return 401 if reset key token is blacklisted', async () => {
await insertUsers([userOne]);
const expires = moment().add(config.jwt.resetPasswordExpirationMinutes, 'minutes');
const resetKeyToken = tokenService.generateToken(userOne._id, expires, tokenTypes.RESET_PASSWORD);
await tokenService.saveToken(resetKeyToken, userOne._id, expires, tokenTypes.RESET_PASSWORD, true);
await request(app)
.post('/v1/auth/reset-key')
.send({ token: resetKeyToken, public_key: 'EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV' })
.expect(httpStatus.UNAUTHORIZED);
});
test('should return 401 if reset password token is expired', async () => {
test('should return 401 if reset key token is expired', async () => {
await insertUsers([userOne]);
const expires = moment().subtract(1, 'minutes');
const resetPasswordToken = tokenService.generateToken(userOne._id, expires, tokenTypes.RESET_PASSWORD);
await tokenService.saveToken(resetPasswordToken, userOne._id, expires, tokenTypes.RESET_PASSWORD);
const resetKeyToken = tokenService.generateToken(userOne._id, expires, tokenTypes.RESET_PASSWORD);
await tokenService.saveToken(resetKeyToken, userOne._id, expires, tokenTypes.RESET_PASSWORD);
await request(app)
.post('/v1/auth/reset-password')
.query({ token: resetPasswordToken })
.send({ password: 'password2' })
.post('/v1/auth/reset-key')
.send({ token: resetKeyToken, public_key: 'EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV' })
.expect(httpStatus.UNAUTHORIZED);
});
test('should return 401 if user is not found', async () => {
const expires = moment().add(config.jwt.resetPasswordExpirationMinutes, 'minutes');
const resetPasswordToken = tokenService.generateToken(userOne._id, expires, tokenTypes.RESET_PASSWORD);
await tokenService.saveToken(resetPasswordToken, userOne._id, expires, tokenTypes.RESET_PASSWORD);
const resetKeyToken = tokenService.generateToken(userOne._id, expires, tokenTypes.RESET_PASSWORD);
await tokenService.saveToken(resetKeyToken, userOne._id, expires, tokenTypes.RESET_PASSWORD);
await request(app)
.post('/v1/auth/reset-password')
.query({ token: resetPasswordToken })
.send({ password: 'password2' })
.post('/v1/auth/reset-key')
.send({ token: resetKeyToken, public_key: 'EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV' })
.expect(httpStatus.UNAUTHORIZED);
});
test('should return 400 if password is missing or invalid', async () => {
await insertUsers([userOne]);
const expires = moment().add(config.jwt.resetPasswordExpirationMinutes, 'minutes');
const resetPasswordToken = tokenService.generateToken(userOne._id, expires, tokenTypes.RESET_PASSWORD);
await tokenService.saveToken(resetPasswordToken, userOne._id, expires, tokenTypes.RESET_PASSWORD);
await request(app).post('/v1/auth/reset-password').query({ token: resetPasswordToken }).expect(httpStatus.BAD_REQUEST);
await request(app)
.post('/v1/auth/reset-password')
.query({ token: resetPasswordToken })
.send({ password: 'short1' })
.expect(httpStatus.BAD_REQUEST);
await request(app)
.post('/v1/auth/reset-password')
.query({ token: resetPasswordToken })
.send({ password: 'password' })
.expect(httpStatus.BAD_REQUEST);
await request(app)
.post('/v1/auth/reset-password')
.query({ token: resetPasswordToken })
.send({ password: '11111111' })
.expect(httpStatus.BAD_REQUEST);
});
});
describe('POST /v1/auth/send-verification-email', () => {
@@ -56,7 +56,7 @@ describe('Проверка получения документов', () => {
skip_save: false,
};
let res = await request(app)
const res = await request(app)
.post('/v1/documents/generate')
.set('Authorization', `Bearer ${registeredUser.body.tokens.access.token}`)
.send(options);
@@ -26,7 +26,6 @@ describe('Проверка данных', () => {
const email = faker.internet.email().toLowerCase();
newUser = {
email: email,
password: 'password1',
role: 'user',
public_key: 'EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV',
username: generateUsername(),
@@ -51,7 +50,6 @@ describe('Проверка данных', () => {
const email = faker.internet.email().toLowerCase();
newUser = {
email: email,
password: 'password1',
role: 'user',
public_key: 'EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV',
username: generateUsername(),
@@ -67,7 +67,6 @@ describe('Проверка получения документов', () => {
const email = faker.internet.email().toLowerCase();
const newUser = {
email: email,
password: 'password1',
role: 'user',
public_key: 'EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV',
username: generateUsername(),
@@ -144,7 +143,7 @@ describe('Проверка получения документов', () => {
statement: signedDocument,
};
let joincoop_result = await request(app)
const joincoop_result = await request(app)
.post('/v1/users/join-cooperative')
.set('Authorization', `Bearer ${registeredUser.body.tokens.access.token}`)
.send(joinCoopData);
@@ -4,10 +4,10 @@ import app from '../../src/app';
import { setupTestDB } from '../utils/setupTestDB';
import { User } from '../../src/models';
import { userOne, userTwo, admin, insertUsers } from '../fixtures/user.fixture';
import { userOneAccessToken, adminAccessToken } from '../fixtures/token.fixture';
import { userOneAccessToken, adminAccessToken, userTwoAccessToken } from '../fixtures/token.fixture';
import request from 'supertest';
import { generateUsername } from '../utils/generateUsername';
import { IDocument, IIndividualData, IJoinCooperative } from '../../src/types';
import { IIndividualData } from '../../src/types';
setupTestDB();
@@ -19,7 +19,6 @@ describe('User routes', () => {
const email = faker.internet.email().toLowerCase();
newUser = {
email: email,
password: 'password1',
role: 'user',
public_key: 'EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV',
username: generateUsername(),
@@ -51,11 +50,9 @@ describe('User routes', () => {
expect(res.body.user).toBeDefined();
expect(res.body.tokens).toBeDefined();
expect(res.body.user).not.toHaveProperty('password');
const dbUser = await User.findOne({ username: newUser.username });
expect(dbUser).toBeDefined();
expect(dbUser?.password).not.toBe(newUser.password);
const privateData = (await dbUser?.getPrivateData()) as IIndividualData;
expect(privateData).toBeDefined();
@@ -65,23 +62,15 @@ describe('User routes', () => {
expect(dbUser?.toObject()).toMatchObject({ email: newUser.email, role: newUser.role, is_email_verified: false });
});
test('should be able to create an admin as well', async () => {
test('should not be able to create an admin as well', async () => {
await insertUsers([admin]);
newUser.role = 'member';
const res = await request(app)
await request(app)
.post('/v1/users')
.set('Authorization', `Bearer ${adminAccessToken}`)
.send(newUser)
.expect(httpStatus.CREATED);
expect(res.body.user).toBeDefined();
expect(res.body.tokens).toBeDefined();
expect(res.body.user.role).toBe('member');
const dbUser = await User.findOne({ username: newUser.username });
expect(dbUser?.role).toBe('member');
.expect(httpStatus.BAD_REQUEST);
});
test('успешная регистрация без токена доступа', async () => {
@@ -122,36 +111,6 @@ describe('User routes', () => {
.expect(httpStatus.BAD_REQUEST);
});
test('should return 400 error if password length is less than 8 characters', async () => {
await insertUsers([admin]);
newUser.password = 'passwo1';
await request(app)
.post('/v1/users')
.set('Authorization', `Bearer ${adminAccessToken}`)
.send(newUser)
.expect(httpStatus.BAD_REQUEST);
});
test('should return 400 error if password does not contain both letters and numbers', async () => {
await insertUsers([admin]);
newUser.password = 'password';
await request(app)
.post('/v1/users')
.set('Authorization', `Bearer ${adminAccessToken}`)
.send(newUser)
.expect(httpStatus.BAD_REQUEST);
newUser.password = '1111111';
await request(app)
.post('/v1/users')
.set('Authorization', `Bearer ${adminAccessToken}`)
.send(newUser)
.expect(httpStatus.BAD_REQUEST);
});
test('should return 400 error if role is neither user nor admin', async () => {
await insertUsers([admin]);
newUser.role = 'invalid';
@@ -164,6 +123,71 @@ describe('User routes', () => {
});
});
describe('POST /v1/users/add', () => {
let newUser;
beforeEach(() => {
const email = faker.internet.email().toLowerCase();
newUser = {
email: email,
referer: '',
type: 'individual',
individual_data: {
first_name: faker.name.firstName(),
last_name: faker.name.lastName(),
middle_name: '',
birthdate: '2023-04-01',
phone: '+1234567890',
email: email,
full_address: 'Russia, Moscow, Tverskaya street, 10',
},
};
});
test('should return 201 and successfully add new user if data is ok', async () => {
await insertUsers([admin]);
const res = await request(app).post('/v1/users/add').set('Authorization', `Bearer ${adminAccessToken}`).send(newUser);
// if (res.status !== httpStatus.CREATED) {
// console.error(res.body); // Выводит тело ответа, если статус не CREATED
// }
expect(res.status).toBe(httpStatus.CREATED);
expect(res.body.user).toBeDefined();
expect(res.body.user.username).toBeDefined();
const dbUser = await User.findOne({ username: res.body.user.username });
expect(dbUser).toBeDefined();
const privateData = (await dbUser?.getPrivateData()) as IIndividualData;
expect(privateData).toBeDefined();
expect(privateData.first_name).toEqual(newUser.individual_data?.first_name);
expect(privateData.last_name).toEqual(newUser.individual_data?.last_name);
expect(dbUser?.toObject()).toMatchObject({ email: newUser.email, role: 'user', is_email_verified: false });
});
test('should return 403 if service user is not admin but data new user is ok', async () => {
await insertUsers([userTwo]);
const res = await request(app)
.post('/v1/users/add')
.set('Authorization', `Bearer ${userTwoAccessToken}`)
.send(newUser);
expect(res.status).toBe(httpStatus.FORBIDDEN);
});
test('should return 401 if service user is not have access token', async () => {
const res = await request(app).post('/v1/users/add').send(newUser);
expect(res.status).toBe(httpStatus.UNAUTHORIZED);
});
});
describe('GET /v1/users', () => {
test('should return 200 and apply the default query options', async () => {
await insertUsers([userOne, userTwo, admin]);
@@ -513,7 +537,6 @@ describe('User routes', () => {
await insertUsers([userOne]);
const updateBody = {
email: faker.internet.email().toLowerCase(),
password: 'newPassword1',
};
const res = await request(app)
@@ -528,7 +551,6 @@ describe('User routes', () => {
await insertUsers([userOne, admin]);
const updateBody = {
email: faker.internet.email().toLowerCase(),
password: 'newPassword1',
};
const res = await request(app)
@@ -542,7 +564,6 @@ describe('User routes', () => {
const dbUser = await User.findById(userOne._id);
expect(dbUser).toBeDefined();
expect(dbUser?.password).not.toBe(updateBody.password);
expect(dbUser).toMatchObject({ email: updateBody.email, role: 'user' });
});
@@ -550,7 +571,6 @@ describe('User routes', () => {
await insertUsers([userOne]);
const updateBody = {
email: faker.internet.email().toLowerCase(),
password: 'newPassword1',
};
await request(app).patch(`/v1/users/${userOne.username}`).send(updateBody).expect(httpStatus.UNAUTHORIZED);
@@ -560,7 +580,6 @@ describe('User routes', () => {
await insertUsers([userOne, userTwo]);
const updateBody = {
email: faker.internet.email().toLowerCase(),
password: 'newPassword1',
};
await request(app)
@@ -574,7 +593,6 @@ describe('User routes', () => {
await insertUsers([userOne, admin]);
const updateBody = {
email: faker.internet.email().toLowerCase(),
password: 'newPassword1',
};
await request(app)
@@ -588,7 +606,6 @@ describe('User routes', () => {
await insertUsers([admin]);
const updateBody = {
email: faker.internet.email().toLowerCase(),
password: 'newPassword1',
};
await request(app)
@@ -600,7 +617,7 @@ describe('User routes', () => {
test('should return 400 if email is invalid', async () => {
await insertUsers([userOne, admin]);
const updateBody = { email: 'invalidEmail', password: 'GoodPassword!23' };
const updateBody = { email: 'invalidEmail' };
await request(app)
.patch(`/v1/users/${userOne.username}`)
@@ -611,7 +628,7 @@ describe('User routes', () => {
test('should return 400 if email is already taken', async () => {
await insertUsers([userOne, userTwo, admin]);
const updateBody = { email: userTwo.email, password: 'GoodPassword!23' };
const updateBody = { email: userTwo.email };
await request(app)
.patch(`/v1/users/${userOne.username}`)
@@ -620,41 +637,9 @@ describe('User routes', () => {
.expect(httpStatus.BAD_REQUEST);
});
test('should return 400 if password length is less than 8 characters', async () => {
await insertUsers([userOne, admin]);
const updateBody = { password: 'passwo1' };
await request(app)
.patch(`/v1/users/${userOne.username}`)
.set('Authorization', `Bearer ${adminAccessToken}`)
.send(updateBody)
.expect(httpStatus.BAD_REQUEST);
});
test('should return 400 if password does not contain both letters and numbers', async () => {
await insertUsers([userOne, admin]);
const updateBody = { password: 'password' };
await request(app)
.patch(`/v1/users/${userOne.username}`)
.set('Authorization', `Bearer ${adminAccessToken}`)
.send(updateBody)
.expect(httpStatus.BAD_REQUEST);
updateBody.password = '11111111';
await request(app)
.patch(`/v1/users/${userOne.username}`)
.set('Authorization', `Bearer ${adminAccessToken}`)
.send(updateBody)
.expect(httpStatus.BAD_REQUEST);
});
//
test('should return 400 if email is my email then taken', async () => {
await insertUsers([userOne, admin]);
const updateBody = { email: userOne.email, password: 'veryStrongPassword!2' };
const updateBody = { email: userOne.email };
await request(app)
.patch(`/v1/users/${userOne.username}`)
@@ -10,7 +10,6 @@ describe('User model', () => {
newUser = {
name: faker.name.findName(),
email: faker.internet.email().toLowerCase(),
password: 'password1',
role: 'user',
public_key: 'EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV',
type: 'individual',
@@ -27,36 +26,9 @@ describe('User model', () => {
await expect(new User(newUser).validate()).rejects.toThrow();
});
test('should throw a validation error if password length is less than 8 characters', async () => {
newUser.password = 'passwo1';
await expect(new User(newUser).validate()).rejects.toThrow();
});
test('should throw a validation error if password does not contain numbers', async () => {
newUser.password = 'password';
await expect(new User(newUser).validate()).rejects.toThrow();
});
test('should throw a validation error if password does not contain letters', async () => {
newUser.password = '11111111';
await expect(new User(newUser).validate()).rejects.toThrow();
});
test('should throw a validation error if role is unknown', async () => {
newUser.role = 'invalid';
await expect(new User(newUser).validate()).rejects.toThrow();
});
});
describe('User toJSON()', () => {
test('should not return user password when toJSON is called', () => {
const newUser = {
name: faker.name.findName(),
email: faker.internet.email().toLowerCase(),
password: 'password1',
role: 'user',
};
expect(new User(newUser).toJSON()).not.toHaveProperty('password');
});
});
});
+2
View File
@@ -5,6 +5,8 @@
"lib": ["ES2015"],
"outDir": "dist",
"strict": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"esModuleInterop": true,
"allowJs": false,
"noImplicitAny": false,
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.0.74](https://github.com/copenomics/coopdoc-generator-ts/compare/coopdoc-generator-ts@1.0.73...coopdoc-generator-ts@1.0.74) (2024-08-13)
**Note:** Version bump only for package coopdoc-generator-ts
## [1.0.73](https://github.com/copenomics/coopdoc-generator-ts/compare/coopdoc-generator-ts@1.0.73-alpha.0...coopdoc-generator-ts@1.0.73) (2024-08-05)
**Note:** Version bump only for package coopdoc-generator-ts
## [1.0.72](https://github.com/copenomics/coopdoc-generator-ts/compare/coopdoc-generator-ts@1.0.72-alpha.3...coopdoc-generator-ts@1.0.72) (2024-07-25)
**Note:** Version bump only for package coopdoc-generator-ts
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "coopdoc-generator-ts",
"type": "module",
"version": "1.0.72",
"version": "1.0.74",
"private": true,
"packageManager": "pnpm@9.0.6",
"description": "",
@@ -40,8 +40,18 @@ export class PDFService implements IPDFService {
private static async generatePDFBuffer(htmlContent: string): Promise<Uint8Array> {
const browser = await puppeteer.launch({
args: ['--no-sandbox', '--disable-setuid-sandbox'],
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu',
'--disable-extensions',
'--disable-software-rasterizer',
],
timeout: 120000, // увеличиваем общее время ожидания до 2 минут
protocolTimeout: 120000, // увеличиваем время ожидания протокола до 2 минут
})
const page = await browser.newPage()
// const data = await inlineCss(htmlContent, { url: 'about:blank' })
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.0.19](https://github.com/coopenomics/cooptypes/compare/cooptypes@1.0.18...cooptypes@1.0.19) (2024-08-13)
**Note:** Version bump only for package cooptypes
## [1.0.18](https://github.com/coopenomics/cooptypes/compare/cooptypes@1.0.18-alpha.0...cooptypes@1.0.18) (2024-08-05)
**Note:** Version bump only for package cooptypes
## [1.0.17](https://github.com/coopenomics/cooptypes/compare/cooptypes@1.0.17-alpha.3...cooptypes@1.0.17) (2024-07-25)
**Note:** Version bump only for package cooptypes
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "cooptypes",
"type": "module",
"version": "1.0.17",
"version": "1.0.19",
"description": "_description_",
"author": "Alex Ant <dacom.dark.sun@gmail.com>",
"license": "MIT",
@@ -4,7 +4,7 @@ import * as ContractNames from '../../../common/names'
/**
* Имя таблицы
*/
export const tableName = 'fundwallet'
export const tableName = 'coopwallet'
/**
* Таблица хранится в {@link ContractNames._fund | области памяти контракта}.
@@ -14,4 +14,4 @@ export const scope = ContractNames._fund
/**
* @interface
*/
export type IFundWallet = Fund.IFundwallet
export type ICoopWallet = Fund.ICoopwallet
@@ -12,7 +12,7 @@ export * as ExpensedFunds from './expenseFunds'
/**
* Таблица содержит сводную информацию о балансе кошелька всех фондов кооператива.
*/
export * as FundWallet from './fundWallet'
export * as CoopWallet from './coopWallet'
/**
* Таблица содержит информацию о запросах на списание по статьям и выводах из фондов.
@@ -0,0 +1,15 @@
import * as Permissions from '../../../common/permissions'
import type * as Registrator from '../../../interfaces/registrator'
import { Actors } from '../../../common'
export const authorizations = [{ permissions: [Permissions.active, Permissions.special], actor: Actors._admin }] as const
/**
* Имя действия
*/
export const actionName = 'adduser'
/**
* @interface
*/
export type IAddUser = Registrator.IAdduser
@@ -49,3 +49,9 @@ export * as ConfirmJoin from './confirmJoin'
* @private
*/
export * as Init from './init'
/**
* Действие добавления пайщика в обход процедуры регистрации.
* @private
*/
export * as AddUser from './addUser'
@@ -36,6 +36,11 @@ export interface IAddexpense {
quantity: IAsset
}
export interface IAddinitial {
coopname: IName
quantity: IAsset
}
export interface IAuthorize {
coopname: IName
type: IName
@@ -48,6 +53,15 @@ export interface IComplete {
withdraw_id: IUint64
}
export interface ICoopwallet {
id: IUint64
coopname: IName
circulating_account: ISimpleWallet
initial_account: ISimpleWallet
accumulative_account: ISimpleWallet
accumulative_expense_account: ISimpleWallet
}
export interface ICounts extends ICountsBase {
}
@@ -140,6 +154,9 @@ export interface IInit {
initial: IAsset
}
export interface IMigrate {
}
export interface INewfund {
coopname: IName
type: IName
@@ -152,6 +169,11 @@ export interface INewwithdraw {
id: IUint64
}
export interface ISimpleWallet {
available: IAsset
withdrawed: IAsset
}
export interface ISpreadamount {
coopname: IName
quantity: IAsset
@@ -20,6 +20,19 @@ export interface IAccount {
registered_at: ITimePointSec
}
export interface IAdduser {
registrator: IName
coopname: IName
referer: IName
username: IName
type: IName
created_at: ITimePointSec
initial: IAsset
minimum: IAsset
spread_initial: boolean
meta: string
}
export interface IBalances extends IBalancesBase {
}
@@ -30,16 +43,12 @@ export interface IBalancesBase {
}
export interface IChangekey {
coopname: IName
changer: IName
username: IName
public_key: IPublicKey
}
export interface ICheck {
hash: IChecksum256
public_key: IPublicKey
signature: ISignature
}
export interface IConfirmreg {
coopname: IName
username: IName
@@ -69,9 +78,6 @@ export interface IDocument {
meta: string
}
export interface IFix {
}
export interface IInit {
}
@@ -82,6 +88,9 @@ export interface IJoincoop {
document: IDocument
}
export interface IMigrate {
}
export interface INewaccount {
registrator: IName
coopname: IName
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.0.23](https://github.com/copenomics/coop-notificator/compare/coop-notificator@0.0.22...coop-notificator@0.0.23) (2024-08-13)
**Note:** Version bump only for package coop-notificator
## [0.0.22](https://github.com/copenomics/coop-notificator/compare/coop-notificator@0.0.22-alpha.0...coop-notificator@0.0.22) (2024-08-05)
**Note:** Version bump only for package coop-notificator
## [0.0.21](https://github.com/copenomics/coop-notificator/compare/coop-notificator@0.0.21-alpha.3...coop-notificator@0.0.21) (2024-07-25)
**Note:** Version bump only for package coop-notificator
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "coop-notificator",
"type": "module",
"version": "0.0.21",
"version": "0.0.23",
"private": true,
"packageManager": "pnpm@9.1.1",
"description": "_description_",
+1
View File
@@ -2,6 +2,7 @@
"editor.bracketPairColorization.enabled": true,
"editor.guides.bracketPairs": true,
"editor.formatOnSave": true,
"editor.tabCompletion": "onlySnippets",
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": ["source.fixAll.eslint"],
"eslint.validate": ["javascript", "javascriptreact", "typescript", "vue"],
+24
View File
@@ -3,6 +3,30 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.1.34](https://github.com/coopenomics/monocoop/compare/terminal@0.1.33...terminal@0.1.34) (2024-08-13)
**Note:** Version bump only for package terminal
## [0.1.33](https://github.com/coopenomics/monocoop/compare/terminal@0.1.33-alpha.0...terminal@0.1.33) (2024-08-05)
**Note:** Version bump only for package terminal
## [0.1.32](https://github.com/coopenomics/monocoop/compare/terminal@0.1.32-alpha.0...terminal@0.1.32) (2024-07-31)
**Note:** Version bump only for package terminal
## [0.1.31](https://github.com/coopenomics/monocoop/compare/terminal@0.1.31-alpha.0...terminal@0.1.31) (2024-07-26)
**Note:** Version bump only for package terminal
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "terminal",
"version": "0.1.31",
"version": "0.1.34",
"description": "A Terminal Project",
"productName": "Terminal App",
"author": "Alex Ant <dacom.dark.sun@gmail.com>",
@@ -34,6 +34,7 @@
"@wharfkit/session": "^1.3.1",
"@wharfkit/wallet-plugin-privatekey": "^1.1.0",
"axios": "^1.2.1",
"coopback": "workspace:*",
"cooptypes": "workspace:*",
"dotenv": "^16.4.5",
"email-regex": "^5.0.0",
+2
View File
@@ -102,6 +102,7 @@ module.exports = configure(function (/* ctx */) {
eslint: {
lintCommand: 'eslint "./**/*.{js,ts,mjs,cjs,vue}"',
},
overlay: false
},
{ server: false },
],
@@ -111,6 +112,7 @@ module.exports = configure(function (/* ctx */) {
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
devServer: {
// https: true
vueDevtools: false,
open: false, // opens browser window automatically
port: 3005,
hmr:{
@@ -0,0 +1,59 @@
import { RenderError } from '@quasar/app-vite';
import { ssrMiddleware } from 'quasar/wrappers';
// This middleware should execute as last one
// since it captures everything and tries to
// render the page with Vue
export default ssrMiddleware(({ app, resolve, render, serve }) => {
// we capture any other Express route and hand it
// over to Vue and Vue Router to render our page
app.get(resolve.urlPath('*'), (req, res) => {
res.setHeader('Content-Type', 'text/html');
render(/* the ssrContext: */ { req, res })
.then((html) => {
// now let's send the rendered html to the client
res.send(html);
})
.catch((err: RenderError) => {
// oops, we had an error while rendering the page
// we were told to redirect to another URL
if (err.url) {
if (err.code) {
res.redirect(err.code, err.url);
} else {
res.redirect(err.url);
}
} else if (err.code === 404) {
// hmm, Vue Router could not find the requested route
// Should reach here only if no "catch-all" route
// is defined in /src/routes
res.status(404).send('404 | Page Not Found');
} else if (process.env.DEV) {
// well, we treat any other code as error;
// if we're in dev mode, then we can use Quasar CLI
// to display a nice error page that contains the stack
// and other useful information
// serve.error is available on dev only
serve.error({ err, req, res });
} else {
// we're in production, so we should have another method
// to display something to the client when we encounter an error
// (for security reasons, it's not ok to display the same wealth
// of information as we do in development)
// Render Error Page on production or
// create a route (/src/routes) for an error page and redirect to it
res.status(500).send('500 | Internal Server Error');
if (process.env.DEBUGGING) {
console.error(err.stack);
}
}
});
});
});
+10
View File
@@ -0,0 +1,10 @@
/* eslint-disable */
// THIS FEATURE-FLAG FILE IS AUTOGENERATED,
// REMOVAL OR CHANGES WILL CAUSE RELATED TYPES TO STOP WORKING
import "quasar/dist/types/feature-flag";
declare module "quasar/dist/types/feature-flag" {
interface QuasarFeatureFlags {
ssr: true;
}
}
+49 -5
View File
@@ -3,24 +3,62 @@ router-view(v-if="isLoaded")
</template>
<script setup lang="ts">
console.log('on start')
import { onMounted, ref } from 'vue'
import { onMounted, ref, watch } from 'vue'
import { FailAlert } from 'src/shared/api/alerts'
import { handleException } from 'src/shared/api';
import { useRouter } from 'vue-router'
import { useRoute, useRouter } from 'vue-router'
import 'src/app/styles/quasar-variables.sass'
import { useDesktopStore } from 'src/entities/Desktop/model';
import { Cookies, LocalStorage, QSpinner, useQuasar } from 'quasar';
import { COOPNAME } from 'src/shared/config';
import { useCardStore } from './providers/card/store';
const $q = useQuasar()
const router = useRouter()
const route = useRoute()
const desktop = useDesktopStore()
const card = useCardStore()
const isLoaded = ref(false)
const enableLoading = () => {
$q.loading.show({
spinner: QSpinner,
message: 'Цифровой Кооператив ушел оффлайн, но обещал вернуться. Пожалуйста, проверьте ваше интернет-соединение. А мы попытаемся его восстановить.',
spinnerSize: 50,
})
}
const disableLoading = () => {
$q.loading.hide()
}
const onlineCheck = () => {
if (desktop.online === false) {
enableLoading()
} else {
card.initWallet()
disableLoading()
}
}
onlineCheck()
// Watch the online status
watch(() => desktop.online, () => onlineCheck())
onMounted(async () => {
try {
removeLoader()
isLoaded.value = true
const ref = Cookies.get('referer') || String(route.query.r || '')
if (ref) {
LocalStorage.setItem(`${COOPNAME}:referer`, ref)
}
} catch (e: unknown) {
console.error(e)
@@ -29,6 +67,7 @@ onMounted(async () => {
isLoaded.value = true
removeLoader()
}
})
function removeLoader() {
@@ -40,3 +79,8 @@ function removeLoader() {
}
}
</script>
<style>
.q-loading__backdrop {
opacity: 1 !important
}
</style>
@@ -0,0 +1,32 @@
import { defineStore } from 'pinia'
import { useSessionStore } from 'src/entities/Session';
import { useCurrentUserStore } from 'src/entities/User';
import { COOPNAME } from 'src/shared/config';
const namespace = 'cardstore'
interface ICardStore {
initWallet: () => Promise<void>;
}
export const useCardStore = defineStore(namespace, (): ICardStore => {
const currentUser = useCurrentUserStore();
const session = useSessionStore();
async function initWallet(): Promise<void> {
if (!session.isAuth && !currentUser.userAccount){
await session.init()
if (session.isAuth)
try{
await currentUser.loadProfile(session.username, COOPNAME)
} catch(e: any){
console.error(e)
}
}
}
return {
initWallet
}
})
+40 -22
View File
@@ -1,6 +1,7 @@
import { route } from 'quasar/wrappers';
import {
RouteLocationNormalizedGeneric,
RouteRecordRaw,
createMemoryHistory,
createRouter,
createWebHashHistory,
@@ -9,20 +10,36 @@ import {
import { routes } from 'src/app/providers/routes';
import { useSessionStore } from 'src/entities/Session';
import {
AUTHORIZED_HOME_PAGE,
COOPNAME,
NOT_AUTHORIZED_HOME_PAGE,
} from 'src/shared/config';
import { IUserAccountData, useCurrentUserStore } from 'src/entities/User';
import { useMenuStore } from 'src/entities/Menu';
import { useDesktopStore } from '../../entities/Desktop/model';
import { useCardStore } from './card/store';
export default route(function (/* { store, ssrContext } */) {
export const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
export default route(async function (/* { store, ssrContext } */) {
const createHistory = process.env.SERVER
? createMemoryHistory
: process.env.VUE_ROUTER_MODE === 'history'
? createWebHistory
: createWebHashHistory;
const desktops = useDesktopStore()
await desktops.healthCheck()
const { initWallet } = useCardStore()
await initWallet()
await desktops.loadDesktops()
await desktops.setActiveDesktop(desktops.defaultDesktopHash)
desktops.currentDesktop?.routes.forEach(route => {
routes[0].children?.push(route as RouteRecordRaw)
})
const router = createRouter({
history: createHistory(process.env.VUE_ROUTER_BASE),
routes,
@@ -39,6 +56,7 @@ export default route(function (/* { store, ssrContext } */) {
},
});
router.beforeEach(
async (
to: RouteLocationNormalizedGeneric,
@@ -46,31 +64,26 @@ export default route(function (/* { store, ssrContext } */) {
next: any
) => {
desktops.healthCheck()
const { initWallet } = useCardStore()
await initWallet()
const currentUser = useCurrentUserStore();
const session = useSessionStore();
if (!session.isAuth && !currentUser.userAccount){
await session.init()
if (session.isAuth)
try{
await currentUser.loadProfile(session.username, COOPNAME)
} catch(e: any){
console.error(e)
}
}
const menuStore = useMenuStore()
menuStore.setRoutes(routes)
if (to.name == 'index') {
console.log('7')
/** проверяем установлено ли приложение и не переход ли это на страницу установки */
if (desktops.health?.status === 'install' && to.name !== 'install') {
/** переадресуем на страницу установки */
next({name: 'install', params: { coopname: COOPNAME }})
return
} else if (to.name == 'index') {
/** Если переходим на Главную, то определяем её на основе рабочего стола и имеющейся авторизации */
if (session.isAuth){
next({ name: AUTHORIZED_HOME_PAGE, params: { coopname: COOPNAME } });
next({ name: desktops.currentDesktop?.authorizedHome, params: { coopname: COOPNAME } });
return
} else {
next({
name: NOT_AUTHORIZED_HOME_PAGE,
name: desktops.currentDesktop?.nonAuthorizedHome,
params: { coopname: COOPNAME },
});
return;
@@ -78,10 +91,15 @@ export default route(function (/* { store, ssrContext } */) {
}
if (hasAccess(to, currentUser.userAccount)) {
/** Если переход на любую другую страницу, то проверяем права доступа к ней на основе роли и продолжаем загрузку */
next(); // Продолжить переход, если доступ разрешен
return
} else {
next({ name: 'permissionDenied' }); // Перенаправить на страницу с сообщением о недостатке прав доступа
return
}
}
);
@@ -2,24 +2,13 @@ import layout from 'src/pages/_layouts/default.vue';
import index from 'src/pages/index.vue';
import blank from 'src/pages/blank/blank.vue';
import permissionDenied from 'src/pages/_layouts/permissionDenied.vue';
// import sovietPage from 'src/components/soviet/index.vue'
import { CoopSettingsPage } from 'src/pages/CoopSettings';
import { UserHomePage } from 'src/pages/UserHome/ui';
import { ModerationPage } from 'src/pages/marketplace/Moderation';
import { CreateParentOfferPage } from 'src/pages/marketplace/CreateParentOffer';
import { ShowcasePage } from 'src/pages/marketplace/Showcase';
import { SuppliesListPage } from 'src/pages/marketplace/SuppliesList';
import { UserParentOffersPage } from 'src/pages/marketplace/UserParentOffers';
import { UserSuppliesListPage } from 'src/pages/marketplace/UserSuppliesList';
import { MainMarketplacePage } from 'src/pages/marketplace/MainPage';
import { SignUpPage } from 'src/pages/SignUp';
import { SignInPage } from 'src/pages/SignIn';
import { RouteRecordRaw } from 'vue-router';
import decisions from 'src/components/soviet/decisions.vue';
import participants from 'src/components/soviet/participants.vue';
import documents from 'src/components/soviet/documents.vue';
import { ContactsPage } from 'src/pages/Contacts';
import { InstallCooperativePage } from 'src/pages/Install';
import { LostKeyPage } from 'src/pages/Auth/LostKey/ui';
import { ResetKeyPage } from 'src/pages/Auth/ResetKey';
const baseRoutes = [
{
@@ -31,143 +20,6 @@ const baseRoutes = [
name: 'index',
component: index,
},
//страница кошелька пользователя
{
path: '/:coopname/home',
name: 'home',
component: UserHomePage,
children: [],
meta: {
is_desktop_menu: true,
title: 'Профиль',
icon: 'fa-solid fa-id-card',
roles: [],
},
},
//страница управления кооперативом
{
path: ':coopname/settings',
name: 'soviet',
component: CoopSettingsPage,
meta: {
is_desktop_menu: true,
title: 'Настройки',
icon: 'fa-solid fa-cog',
roles: ['chairman', 'member'],
},
},
{
path: ':coopname/agenda',
name: 'agenda',
component: decisions,
meta: {
is_desktop_menu: true,
title: 'Повестка',
icon: 'fa-solid fa-check-to-slot',
roles: ['chairman', 'member'],
},
},
{
path: ':coopname/participants',
name: 'participants',
component: participants,
meta: {
is_desktop_menu: true,
title: 'Пайщики',
icon: 'fa-solid fa-users',
roles: ['chairman', 'member'],
},
},
{
path: ':coopname/documents',
name: 'documents',
component: documents,
meta: {
is_desktop_menu: true,
title: 'Документы',
icon: 'fa-solid fa-file-invoice',
roles: ['chairman', 'member'],
},
},
{
path: ':coopname/auth/signin',
name: 'signin',
component: SignInPage,
children: [],
},
{
path: ':coopname/auth/signup',
name: 'signup',
component: SignUpPage,
children: [],
},
{
path: '/:coopname/marketplace',
name: 'marketplace',
component: MainMarketplacePage,
children: [
{
path: 'moderation',
name: 'marketplace-moderation',
component: ModerationPage,
},
{
path: 'create-offer',
name: 'marketplace-create-offer',
component: CreateParentOfferPage,
},
{
path: 'showcase',
name: 'marketplace-showcase',
component: ShowcasePage,
children: [
{
path: ':id',
name: 'marketplace-showcase-id',
component: ShowcasePage,
},
],
},
{
path: 'user-offers',
name: 'marketplace-user-offers',
component: UserParentOffersPage,
children: [
{
path: ':id',
name: 'marketplace-user-offer-id',
component: UserParentOffersPage,
},
],
},
{
path: 'supplies',
name: 'marketplace-supplies',
component: SuppliesListPage,
},
{
path: 'user-supplies',
name: 'marketplace-user-supplies',
component: UserSuppliesListPage,
},
],
},
//страница контактов
{
path: ':coopname/contacts',
name: 'contacts',
component: ContactsPage,
meta: {
is_desktop_menu: true,
title: 'Контакты',
icon: 'fa-solid fa-info',
roles: [],
},
},
{
path: '/something-bad',
name: 'somethingBad',
@@ -178,6 +30,36 @@ const baseRoutes = [
name: 'permissionDenied',
component: permissionDenied,
},
{
path: ':coopname/install',
name: 'install',
component: InstallCooperativePage,
children: [],
},
{
path: ':coopname/auth/signin',
name: 'signin',
component: SignInPage,
children: [],
},
{
path: ':coopname/auth/lost-key',
name: 'lostkey',
component: LostKeyPage,
children: [],
},
{
path: ':coopname/auth/reset-key',
name: 'resetkey',
component: ResetKeyPage,
children: [],
},
{
path: ':coopname/auth/signup',
name: 'signup',
component: SignUpPage,
children: [],
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
@@ -187,6 +69,109 @@ const baseRoutes = [
},
];
// const baseRoutes = [
// {
// path: '/',
// component: layout,
// children: [
// {
// path: '',
// name: 'index',
// component: index,
// },
// //страница кошелька пользователя
// {
// path: '/:coopname/home',
// name: 'home',
// component: UserHomePage,
// children: [],
// meta: {
// is_desktop_menu: true,
// title: 'Профиль',
// icon: 'fa-solid fa-id-card',
// roles: [],
// },
// },
// //страница управления кооперативом
// {
// path: ':coopname/settings',
// name: 'soviet',
// component: CoopSettingsPage,
// meta: {
// is_desktop_menu: true,
// title: 'Настройки',
// icon: 'fa-solid fa-cog',
// roles: ['chairman', 'member'],
// },
// },
// {
// path: ':coopname/agenda',
// name: 'agenda',
// component: decisions,
// meta: {
// is_desktop_menu: true,
// title: 'Повестка',
// icon: 'fa-solid fa-check-to-slot',
// roles: ['chairman', 'member'],
// },
// },
// {
// path: ':coopname/participants',
// name: 'participants',
// component: participants,
// meta: {
// is_desktop_menu: true,
// title: 'Пайщики',
// icon: 'fa-solid fa-users',
// roles: ['chairman', 'member'],
// },
// },
// {
// path: ':coopname/documents',
// name: 'documents',
// component: documents,
// meta: {
// is_desktop_menu: true,
// title: 'Документы',
// icon: 'fa-solid fa-file-invoice',
// roles: ['chairman', 'member'],
// },
// },
// {
// path: ':coopname/auth/signin',
// name: 'signin',
// component: SignInPage,
// children: [],
// },
// {
// path: ':coopname/auth/signup',
// name: 'signup',
// component: SignUpPage,
// children: [],
// },
// {
// path: '/something-bad',
// name: 'somethingBad',
// component: blank,
// },
// {
// path: '/permission-denied',
// name: 'permissionDenied',
// component: permissionDenied,
// },
// {
// path: '/:pathMatch(.*)*',
// name: 'NotFound',
// component: blank,
// },
// ],
// },
// ];
const rs = baseRoutes;
export const routes = rs as RouteRecordRaw[];
@@ -1,39 +1,39 @@
<template lang="pug">
div.menu-container
div.menu-items
div(v-for="route in menuRoutes" :key="route.path")
q-btn(
v-ripple
flat
class="cursor-pointer btn-menu"
:class="headerClass(route)"
@click="open(route)"
)
q-icon(:name="route.meta.icon").btn-icon.q-pt-xs
span.btn-font {{ t(route.meta.title) }}
q-btn(
v-for="route in menuRoutes" :key="route.path"
v-ripple
flat
class="cursor-pointer btn-menu"
:class="headerClass(route)"
@click="open(route)"
)
q-icon(:name="route.meta.icon").btn-icon.q-pt-xs
span.btn-font {{ route.meta.title }}
div.control-buttons
div
q-btn(flat @click="$q.dark.toggle()").btn-menu
q-icon(:name="isDark ? 'brightness_7' : 'brightness_3'").btn-icon
span.btn-font {{ isDark ? 'светлая' : 'тёмная' }}
div
q-btn(v-ripple flat class="cursor-pointer btn-menu" @click="logout")
q-icon( color="red" name="logout").btn-icon.q-pt-xs
div.btn-font Выход
q-btn(flat @click="$q.dark.toggle()" ).btn-menu
q-icon(:name="isDark ? 'brightness_7' : 'brightness_3'").q-pt-xs.btn-icon
span.btn-font {{ isDark ? 'светлая' : 'тёмная' }}
q-btn(v-ripple flat class="cursor-pointer btn-menu" @click="logout")
q-icon( color="red" name="logout").q-pt-xs.btn-icon
div.btn-font Выход
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { useQuasar } from 'quasar'
import { useMenuStore } from 'src/entities/Menu'
import type { IMenu } from 'src/entities/Menu'
import { COOPNAME } from 'src/shared/config'
import { useLogoutUser } from 'src/features/User/Logout/model'
import { useLogoutUser } from 'src/features/User/Logout'
import { FailAlert } from 'src/shared/api'
import { useCurrentUserStore } from 'src/entities/User'
import { type IRoute } from 'src/entities/Desktop/model/types'
import { useDesktopStore } from 'src/entities/Desktop/model'
const $q = useQuasar()
@@ -41,27 +41,24 @@ const isDark = computed(() => $q.dark.isActive)
const router = useRouter()
const route = useRoute()
const user = useCurrentUserStore()
const isRouteActive = (currentRoute: IMenu) => {
return route.name === currentRoute.name
const isRouteActive = (currentRoute: IRoute) => {
return route.matched.find(r => r.path === currentRoute.path) || route.name == currentRoute.name
}
const headerClass = (route: IMenu) => {
const headerClass = (route: IRoute) => {
const isActive = isRouteActive(route)
return isActive ? (isDark.value ? 'text-white bg-teal-8' : 'text-black bg-teal-2') : ''
return isActive ? (isDark.value ? 'text-white bg-teal' : 'text-white bg-teal') : ''
}
const currentUser = useCurrentUserStore()
const menuStore = useMenuStore()
const desktop = useDesktopStore()
const { t } = useI18n()
const open = (route: IRoute) => {
if (route.children)
router.push({ name: route.children[0].name, params: { coopname: COOPNAME } })
else router.push({ name: route.name, params: { coopname: COOPNAME } })
const menu = ref<IMenu[]>([])
menu.value = menuStore.getUserDesktopMenu(currentUser?.userAccount?.role)
const open = (route: IMenu) => {
router.push({ name: route.name, params: { coopname: COOPNAME } })
}
@@ -78,15 +75,23 @@ const logout = async () => {
}
}
const menuRoutes = computed(() => {
return menu.value
const userRole = user.userAccount?.role || 'user';
return desktop.firstLevel.filter(
(route) => route.meta.roles.includes(userRole) || route.meta.roles.length === 0
);
})
</script>
<style lang="scss" scoped>
.btn-menu {
height: 70px;
width: 70px;
height: 54px;
width: 65px;
}
.btn-icon {
@@ -99,13 +104,13 @@ const menuRoutes = computed(() => {
.logout-btn {
position: fixed;
bottom: 70px;
bottom: 54px;
width: 100%;
}
.menu-container {
display: flex;
flex-direction: column;
// flex-direction: column;
height: 100%;
}
@@ -1,47 +0,0 @@
<template lang="pug">
div(v-if="coop" flat)
div.row
div.col-md-4.q-pa-md
q-input(v-model="coop.username" flat readonly label="Имя аккаунта")
// q-input(flat readonly label="Верификация" )
// span(v-for="verification of coop.verifications") {{verification.verificator}}
// q-badge {{verification.is_verified}}
// q-input(flat readonly label="Хранилища данных" )
// q-btn(v-for="storage of coop.storages" flat) {{storage.storage_username}}
q-input(v-model="coop.initial" flat readonly label="Вступительный взнос")
q-input(v-model="coop.minimum" flat readonly label="Минимальный паевый взнос")
q-input(v-model="coop.coop_type" flat readonly label="Тип кооператива")
div.col-md-8.q-pa-md
q-input(v-model="coop.announce" flat label="Краткое описание").q-mb-lg
// q-input(flat label="Подробное описание" v-model="coop.description" type="textarea")
span(style="color: '#666666'; font-size: 12px;").text-grey Подробное описание
//- TiptapEditor(v-if="description" :v-model="description" @update:model-value="updateDescription")
q-btn(color="green" @click="save") сохранить
</template>
<script setup lang="ts">
import { onMounted, computed, ref } from 'vue'
import { useCooperativeStore } from 'src/entities/Cooperative';
import { COOPNAME } from 'src/shared/config';
const cooperativeStore = useCooperativeStore()
const description = ref<string | undefined>('')
onMounted(async () => {
await cooperativeStore.loadPublicCooperativeData(COOPNAME)
description.value = coop.value?.description
})
let coop = computed(() => cooperativeStore.publicCooperativeData)
const save = () => {
console.log('on save')
}
</script>
@@ -1,7 +1,5 @@
<template lang="pug">
div
//TODO валидация на члена совета
q-tabs( v-model="tab" dense switch-indicator inline-label outside-arrows mobile-arrows align="justify" active-bg-color="teal" active-color="white" indicator-color="secondary")
// q-tab(name="Дашборд" icon="mail" label="Дашборд")
//- q-tab(name="Повестка" label="Повестка")
@@ -9,16 +7,16 @@ div
//- q-tab(name="Пайщики" label="Пайщики")
//- q-tab(name="Документы" label="Документы")
//- q-tab(name="Администраторы" label="Администраторы")
q-tab(name="Администраторы" label="Администраторы")
//- q-tab(name="Члены совета" label="Члены совета")
//- q-tab(name="Кооператив" label="Кооператив")
q-tab(name="Члены совета" label="Члены совета")
q-tab(name="Кооператив" label="Кооператив")
//- // q-tab(name="Участки" icon="people" label="Участки")
// q-tab(name="Участки" icon="people" label="Участки")
//- // q-tab(name="Программы" label="Программы")
//- // q-tab(name="Собрания" icon="people" label="Собрания")
//- // q-tab(name="Фонды" label="Фонды")
// q-tab(name="Программы" label="Программы")
// q-tab(name="Собрания" icon="people" label="Собрания")
// q-tab(name="Фонды" label="Фонды")
q-tab-panels(
v-model="tab"
@@ -30,11 +28,8 @@ div
// p Дашборд
q-tab-panel(name="Кооператив")
cooperative
// boards.col-md-12.col-xs-12.q-pa-md
q-tab-panel(name="Члены совета")
members
//- cooperative
// boards.col-md-12.col-xs-12.q-pa-md
q-tab-panel(name="Участки")
p Действующие участки
@@ -45,13 +40,13 @@ div
staff
q-tab-panel(name="Повестка")
decisions
//- decisions
q-tab-panel(name="Пайщики")
participants
//- participants
q-tab-panel(name="Документы")
documents(:receiver="coopname")
//- documents(:receiver="coopname")
q-tab-panel(name="Программы")
div.text-h6 Программы
@@ -69,15 +64,8 @@ div
</template>
<script setup lang="ts">//
import { computed, ref } from 'vue'
import cooperative from './cooperative.vue'
import members from './members.vue'
import { ref } from 'vue'
import staff from './staff.vue'
import decisions from './decisions.vue'
import participants from './participants.vue'
import documents from './documents.vue'
import { COOPNAME } from 'src/shared/config'
const coopname = computed(() => COOPNAME)
const tab = ref('Повестка')
</script>
@@ -1,21 +0,0 @@
import index from './index.vue'
export default {
'menu': [{
'pageName': 'Совет',
'name': 'soviet',
'icon': 'fa-regular fa-circle',
'isMobile': true,
'protected': true,
'params': {
coopname: ''
}
}],
'routes': [
{
path: ':coopname/soviet',
name: 'soviet',
component: index,
}
]
}
@@ -1,211 +0,0 @@
<template lang="pug">
div.row
div(v-if="members").col-md-12
q-card(v-if="members.length > 0" flat)
q-table(
ref="tableRef" flat
bordered
:rows="members"
:columns="columns"
:table-colspan="9"
row-key="username"
:pagination="pagination"
virtual-scroll
:virtual-scroll-item-size="48"
:rows-per-page-options="[10]"
).full-height
template(#bottom)
q-btn(icon="add" flat @click="showAdd = true") добавить участника
template(#header="props")
q-tr(:props="props")
q-th(
v-for="col in props.cols"
:key="col.name"
:props="props"
) {{ col.label }}
q-th(auto-width)
template(#body="props")
q-tr(:key="`m_${props.row.username}`" :props="props")
//- q-td {{ props.row.position_title }}
q-td {{ props.row.username }}
q-td {{ props.row.data?.user_profile?.last_name }}
q-td {{ props.row.data?.user_profile?.first_name }}
q-td {{ props.row.data?.user_profile?.middle_name }}
q-td {{ props.row.data?.user_profile?.phone }}
q-td {{ props.row.data?.email }}
q-td {{ moment(props.row.data?.user_profile?.birthday).format('DD.MM.YY') }}
// q-td
// q-badge(v-for="(right, index) of props.row.rights" v-bind:key="index" ).q-pa-xs.q-ma-xs {{right.contract}}::{{right.action_name}}
q-td(auto-width)
q-btn(size="sm" color="primary" round dense icon="clear" @click="removeMember(props.row.username)")
q-dialog(v-model="showAdd" persistent :maximized="false" )
q-card
div()
q-bar.bg-primary.text-white
span Добавить сотрудника
q-space
q-btn(v-close-popup dense flat icon="close")
q-tooltip Close
div.q-pa-sm.row.justify-center
div.q-pa-md
span Внимание! Вы собиретесь добавить участика в совет кооператива.
div
q-input(v-model="persona.username" label="Имя аккаунта")
q-input(v-model="persona.position_title" label="Позиция")
div.q-mt-lg
q-btn(flat @click="showAdd = false") Назад
q-btn(color="primary" @click="addMember") Добавить
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useRoute } from 'vue-router';
import { Notify } from 'quasar';
import moment from 'moment-with-locales-es6'
import { readBlockchain, sendGET } from 'src/shared/api'
import { useSessionStore } from 'src/entities/Session';
import { useUpdateBoard } from 'src/features/Cooperative/UpdateBoard';
import { COOPNAME } from 'src/shared/config';
const route = useRoute();
const showAdd = ref(false)
const members = ref<any>([]);
let members_reserve: any = []
const persona = ref({
username: '',
position_title: '',
})
const loadMembers = async () => {
try {
members.value = await sendGET('/v1/coop/members', {
coopname: route.params.coopname
})
members_reserve = JSON.parse(JSON.stringify(members.value))
} catch (e: any) {
Notify.create({
message: e.message,
type: 'negative',
})
}
}
loadMembers()
const addMember = async () => {
const verified = await verify(persona.value.username)
if (!verified) {
Notify.create({
message: 'Имя аккаунта на найдено',
type: 'negative',
})
return
}
members.value.push({ username: persona.value.username, position_title: persona.value.position_title, position: 'member', is_voting: true });
updateBoard()
showAdd.value = false
};
const removeMember = (username: string) => {
members.value = members.value.filter((el: { username: string; }) => el.username != username)
updateBoard()
};
const verify = async (username: string) => {
try {
await readBlockchain.v1.chain.get_account(username);
return true;
} catch (e) {
return false;
}
};
const updateBoard = async () => {
try {
let members_for_send = members.value.map((el: { username: any; position_title: any; position: any; is_voting: any; }) => {
return {
username: el.username,
position_title: el.position_title,
position: el.position,
is_voting: el.is_voting
}
})
await useUpdateBoard().updateBoard({
coopname: COOPNAME,
chairman: useSessionStore().username,
board_id: 0,
members: members_for_send,
name: 'Совет',
description: 'Совет кооператива'
})
Notify.create({
message: 'Совет обновлен',
type: 'positive',
})
loadMembers()
} catch (e: any) {
members.value = members_reserve
Notify.create({
message: e.message,
type: 'negative',
})
}
}
const columns = [
// { name: 'position_title', align: 'left', label: 'Позиция', field: 'position_title', sortable: true },
{ name: 'username', align: 'left', label: 'Аккаунт', field: 'username', sortable: true },
{ name: 'last_name', align: 'left', label: 'Фамилия', field: 'last_name', sortable: true },
{ name: 'first_name', align: 'left', label: 'Имя', field: 'first_name', sortable: true },
{ name: 'middle_name', align: 'left', label: 'Отчество', field: 'middle_name', sortable: true },
{ name: 'phone', align: 'left', label: 'Телефон', field: 'phone', sortable: false },
{ name: 'email', align: 'left', label: 'Е-почта', field: 'email', sortable: false },
{ name: 'birthday', align: 'left', label: 'Дата рождения', field: 'birthday', sortable: true },
] as any
const tableRef = ref(null)
const pagination = ref({ rowsPerPage: 10 })
</script>
@@ -1,107 +0,0 @@
<template lang="pug">
div.row.justify-center.full-height
div.col-12.full-height
q-table(
ref="tableRef" v-model:expanded="expanded"
flat
bordered
:rows="participants.results"
:columns="columns"
:table-colspan="9"
row-key="username"
:pagination="pagination"
virtual-scroll
:virtual-scroll-item-size="48"
:rows-per-page-options="[10]"
:loading="onLoading"
:no-data-label="'У кооператива нет пайщиков'"
).full-height
template(#header="props")
q-tr(:props="props")
q-th(auto-width)
q-th(
v-for="col in props.cols"
:key="col.name"
:props="props"
) {{ col.label }}
template(#body="props")
q-tr(:key="`m_${props.row.username}`" :props="props")
q-td(auto-width)
// q-toggle(v-model="props.expand" checked-icon="fas fa-chevron-circle-left" unchecked-icon="fas fa-chevron-circle-right" )
q-btn(size="sm" color="primary" round dense :icon="props.expand ? 'remove' : 'add'" @click="props.expand = !props.expand")
q-td {{ props.row.username }}
q-td {{ props.row.private_data?.last_name }}
q-td {{ props.row.private_data?.first_name }}
q-td {{ props.row.private_data?.middle_name }}
q-td {{ props.row.private_data?.phone }}
q-td {{ props.row.private_data?.email }}
q-td {{ moment(props.row.private_data?.birthdate).format('DD.MM.YY') }}
q-td {{ moment(props.row.private_data._created_at).format('DD.MM.YY HH:mm:ss') }}
q-tr(v-show="props.expand" :key="`e_${props.row.username}`" :props="props" class="q-virtual-scroll--with-prev")
q-td(colspan="100%")
documents(v-if="props.expand" :receiver="props.row.username")
// div.text-left Здесь будет история подписанных документов: {{ props.row.username }}
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Notify } from 'quasar'
import documents from './documents.vue'
import { sendGET } from 'src/shared/api';
const participants = ref({ results: [] })
const onLoading = ref(false)
import moment from 'moment-with-locales-es6'
const loadParticipants = async () => {
try {
onLoading.value = true
participants.value = await sendGET('/v1/users', {
limit: 100
})
onLoading.value = false
} catch (e: any) {
onLoading.value = false
Notify.create({
message: e.message,
type: 'negative',
})
}
}
loadParticipants()
const columns = [
{ name: 'username', align: 'left', label: 'Аккаунт', field: 'username', sortable: true },
{ name: 'last_name', align: 'left', label: 'Фамилия', field: 'last_name', sortable: true },
{ name: 'first_name', align: 'left', label: 'Имя', field: 'first_name', sortable: true },
{ name: 'middle_name', align: 'left', label: 'Отчество', field: 'middle_name', sortable: true },
{ name: 'phone', align: 'left', label: 'Телефон', field: 'phone', sortable: false },
{ name: 'email', align: 'left', label: 'Е-почта', field: 'email', sortable: false },
{ name: 'birthday', align: 'left', label: 'Дата рождения', field: 'birthday', sortable: true },
{
name: 'created_at',
align: 'left',
label: 'Зарегистрирован',
field: 'created_at',
sortable: true,
},
] as any
const expanded = ref([])
const tableRef = ref(null)
const pagination = ref({ rowsPerPage: 10 })
</script>
@@ -0,0 +1 @@
export * as ChairmanDesktopModel from './model'
@@ -0,0 +1,63 @@
import { UserIdentityPage } from 'src/pages/User/IdentityPage';
import { UserPaymentMethodsPage } from 'src/pages/User/PaymentMethodsPage';
import { UserWalletPage } from 'src/pages/User/WalletPage';
import { markRaw } from 'vue';
export const manifest = {
'name': 'ChairmanDesktop',
'hash': 'hash2',
'authorizedHome': 'home',
'nonAuthorizedHome': 'signup',
'routes': [
{
meta: {
title: 'Пайщик',
icon: 'fa-solid fa-id-card',
roles: [],
},
path: '/:coopname/user',
name: 'home',
children: [{
meta: {
title: 'Удостоверение',
icon: '',
roles: [],
},
path: 'identity',
name: 'user-identity',
component: markRaw(UserIdentityPage),
children: [],
},{
meta: {
title: 'Кошелёк',
icon: '',
roles: [],
},
path: 'wallet',
name: 'user-wallet',
component: markRaw(UserWalletPage),
children: [],
},
{
meta: {
title: 'Реквизиты',
icon: '',
roles: [],
},
path: 'payment-methods',
name: 'user-payment-methods',
component: markRaw(UserPaymentMethodsPage),
children: [],
}
],
},
],
'config': {
'layout': 'default',
'theme': 'light'
},
'schemas': {
'layout': 'avj schema here',
'theme': 'avj schema here'
}
}
@@ -0,0 +1 @@
export * as MemberDesktopModel from './model'
@@ -0,0 +1,63 @@
import { UserIdentityPage } from 'src/pages/User/IdentityPage';
import { UserPaymentMethodsPage } from 'src/pages/User/PaymentMethodsPage';
import { UserWalletPage } from 'src/pages/User/WalletPage';
import { markRaw } from 'vue';
export const manifest = {
'name': 'ChairmanDesktop',
'hash': 'hash2',
'authorizedHome': 'home',
'nonAuthorizedHome': 'signup',
'routes': [
{
meta: {
title: 'Пайщик',
icon: 'fa-solid fa-id-card',
roles: [],
},
path: '/:coopname/user',
name: 'home',
children: [{
meta: {
title: 'Удостоверение',
icon: '',
roles: [],
},
path: 'identity',
name: 'user-identity',
component: markRaw(UserIdentityPage),
children: [],
},{
meta: {
title: 'Кошелёк',
icon: '',
roles: [],
},
path: 'wallet',
name: 'user-wallet',
component: markRaw(UserWalletPage),
children: [],
},
{
meta: {
title: 'Реквизиты',
icon: '',
roles: [],
},
path: 'payment-methods',
name: 'user-payment-methods',
component: markRaw(UserPaymentMethodsPage),
children: [],
}
],
},
],
'config': {
'layout': 'default',
'theme': 'light'
},
'schemas': {
'layout': 'avj schema here',
'theme': 'avj schema here'
}
}
@@ -0,0 +1 @@
export * as SetupDesktopModel from './model'

Some files were not shown because too many files have changed in this diff Show More