update project

This commit is contained in:
ZOMBIIIIIII
2026-04-14 13:30:26 +03:00
parent a81e29807c
commit 37146f7375
65 changed files with 3782 additions and 629 deletions

View File

@@ -12,7 +12,7 @@ const config: Knex.Config = {
port: parseInt(process.env.DB_PORT || '5432'),
user: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || 'postgres',
database: process.env.DB_NAME || 'cryptowallet_v2',
database: process.env.DB_NAME || 'cryptowallet',
},
migrations: {
directory: path.resolve(__dirname, 'migrations'),

View File

@@ -3,21 +3,12 @@ import type { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
await knex.schema.createTable('users', (t) => {
t.string('id', 26).primary();
t.string('email', 255).notNullable().unique();
t.string('password_hash', 255).notNullable();
t.string('last_name', 128).nullable();
t.string('first_name', 128).nullable();
t.string('middle_name', 128).nullable();
t.date('birth_date').nullable();
t.string('crypto_wallet', 255).nullable();
t.string('phone', 16).nullable();
t.string('bik', 9).nullable();
t.string('account_number', 20).nullable();
t.string('card_number', 19).nullable();
t.string('inn', 12).nullable();
t.boolean('kyc_verified').notNullable().defaultTo(false);
t.timestamp('kyc_verified_at', { useTz: true }).nullable();
t.boolean('is_deleted').notNullable().defaultTo(false);
t.string('username', 64).notNullable().unique();
t.text('password_hash').notNullable();
t.text('pin_hash').notNullable();
t.text('encrypted_vault').notNullable();
t.string('vault_salt', 128).notNullable();
t.boolean('mnemonic_shown').notNullable().defaultTo(false);
t.timestamp('created_at', { useTz: true }).notNullable().defaultTo(knex.fn.now());
t.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(knex.fn.now());
});

View File

@@ -3,22 +3,16 @@ import type { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
await knex.schema.createTable('sessions', (t) => {
t.string('id', 26).primary();
t.string('sid', 26).notNullable().unique();
t.string('user_id', 26).notNullable().references('id').inTable('users').onDelete('CASCADE');
t.string('device_id', 26).nullable();
t.string('user_agent', 500).nullable();
t.string('first_ip', 64).nullable();
t.string('last_ip', 64).nullable();
t.timestamp('last_seen_at', { useTz: true }).nullable();
t.timestamp('revoked_at', { useTz: true }).nullable();
t.string('refresh_jti_hash', 255).nullable();
t.timestamp('refresh_expires_at', { useTz: true }).nullable();
t.text('refresh_token_hash').notNullable();
t.string('user_agent').nullable();
t.specificType('ip_address', 'inet').nullable();
t.timestamp('expires_at', { useTz: true }).notNullable();
t.timestamp('created_at', { useTz: true }).notNullable().defaultTo(knex.fn.now());
t.timestamp('updated_at', { useTz: true }).notNullable().defaultTo(knex.fn.now());
});
await knex.schema.raw('CREATE INDEX idx_sessions_user_id ON sessions(user_id)');
await knex.schema.raw('CREATE INDEX idx_sessions_sid ON sessions(sid)');
await knex.schema.raw('CREATE INDEX idx_sessions_expires ON sessions(expires_at)');
}
export async function down(knex: Knex): Promise<void> {

View File

@@ -0,0 +1,18 @@
import type { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
await knex.schema.createTable('login_attempts', (t) => {
t.string('id', 26).primary();
t.string('username', 64).notNullable();
t.specificType('ip_address', 'inet').notNullable();
t.boolean('success').notNullable().defaultTo(false);
t.timestamp('created_at', { useTz: true }).notNullable().defaultTo(knex.fn.now());
});
await knex.schema.raw('CREATE INDEX idx_login_attempts_username_created ON login_attempts(username, created_at)');
await knex.schema.raw('CREATE INDEX idx_login_attempts_ip_created ON login_attempts(ip_address, created_at)');
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists('login_attempts');
}

View File

@@ -0,0 +1,33 @@
import type { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
await knex.schema.alterTable('users', (t) => {
t.dropColumn('username');
t.dropColumn('password_hash');
t.dropColumn('pin_hash');
t.string('bitok_user_id', 26).notNullable().unique();
t.string('email', 255).nullable();
t.boolean('kyc_verified').notNullable().defaultTo(false);
t.string('kyc_level', 20).nullable();
t.boolean('deleted').notNullable().defaultTo(false);
t.index(['bitok_user_id'], 'idx_users_bitok_user_id');
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.alterTable('users', (t) => {
t.dropIndex(['bitok_user_id'], 'idx_users_bitok_user_id');
t.dropColumn('bitok_user_id');
t.dropColumn('email');
t.dropColumn('kyc_verified');
t.dropColumn('kyc_level');
t.dropColumn('deleted');
t.string('username', 64).notNullable().unique();
t.text('password_hash').notNullable();
t.text('pin_hash').notNullable();
});
}

View File

@@ -0,0 +1,35 @@
import type { Knex } from 'knex';
export async function up(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists('sessions');
await knex.schema.dropTableIfExists('login_attempts');
await knex.schema.createTable('processed_events', (t) => {
t.string('event_id', 26).primary();
t.string('event_type', 64).notNullable();
t.string('payload_hash', 64).notNullable();
t.timestamp('processed_at', { useTz: true }).notNullable().defaultTo(knex.fn.now());
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists('processed_events');
await knex.schema.createTable('sessions', (t) => {
t.string('id', 26).primary();
t.string('user_id', 26).notNullable().references('id').inTable('users').onDelete('CASCADE');
t.text('refresh_token_hash').notNullable();
t.string('user_agent').nullable();
t.specificType('ip_address', 'inet').nullable();
t.timestamp('expires_at', { useTz: true }).notNullable();
t.timestamp('created_at', { useTz: true }).notNullable().defaultTo(knex.fn.now());
});
await knex.schema.createTable('login_attempts', (t) => {
t.string('id', 26).primary();
t.string('username', 64).notNullable();
t.specificType('ip_address', 'inet').notNullable();
t.boolean('success').notNullable().defaultTo(false);
t.timestamp('created_at', { useTz: true }).notNullable().defaultTo(knex.fn.now());
});
}

View File

@@ -0,0 +1,49 @@
import dotenv from 'dotenv';
import path from 'path';
import knex from 'knex';
// Load .env from repo root (works when running from apps/api)
dotenv.config({ path: path.resolve(__dirname, '../../../../.env') });
dotenv.config({ path: path.resolve(process.cwd(), '.env') });
const dbName = process.env.DB_NAME || 'cryptowallet_devphase3';
if (!/^[a-zA-Z0-9_]+$/.test(dbName)) {
console.error('[DB Reset] Invalid DB_NAME');
process.exit(1);
}
const baseConnection = {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432'),
user: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || 'postgres',
};
async function reset() {
const admin = knex({
client: 'pg',
connection: { ...baseConnection, database: 'postgres' },
});
try {
await admin.raw(
`SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = ? AND pid <> pg_backend_pid()`,
[dbName]
);
} catch {
// Ignore if no connections
}
const safeName = dbName.replace(/"/g, '""');
await admin.raw(`DROP DATABASE IF EXISTS "${safeName}"`);
await admin.raw(`CREATE DATABASE "${safeName}"`);
await admin.destroy();
console.log('[DB Reset] Database dropped and recreated:', dbName);
}
reset().catch((err: unknown) => {
console.error('[DB Reset] Failed:', err instanceof Error ? err.message : String(err));
if (err instanceof Error && err.stack) console.error(err.stack);
process.exit(1);
});