Update codebase
This commit is contained in:
@@ -21,6 +21,8 @@
|
|||||||
"@nestjs/common": "^11.0.0",
|
"@nestjs/common": "^11.0.0",
|
||||||
"@nestjs/config": "^4.0.0",
|
"@nestjs/config": "^4.0.0",
|
||||||
"@nestjs/core": "^11.0.0",
|
"@nestjs/core": "^11.0.0",
|
||||||
|
"@nestjs/jwt": "^11.0.0",
|
||||||
|
"@nestjs/passport": "^11.0.0",
|
||||||
"@nestjs/platform-express": "^11.0.0",
|
"@nestjs/platform-express": "^11.0.0",
|
||||||
"@nestjs/platform-socket.io": "^11.0.0",
|
"@nestjs/platform-socket.io": "^11.0.0",
|
||||||
"@nestjs/swagger": "^11.0.0",
|
"@nestjs/swagger": "^11.0.0",
|
||||||
@@ -32,6 +34,8 @@
|
|||||||
"class-validator": "^0.14.1",
|
"class-validator": "^0.14.1",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"ioredis": "^5.4.1",
|
"ioredis": "^5.4.1",
|
||||||
|
"passport": "^0.7.0",
|
||||||
|
"passport-jwt": "^4.0.1",
|
||||||
"pg": "^8.12.0",
|
"pg": "^8.12.0",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.1",
|
"rxjs": "^7.8.1",
|
||||||
@@ -45,6 +49,7 @@
|
|||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/node": "^22.0.0",
|
"@types/node": "^22.0.0",
|
||||||
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"ts-jest": "^29.2.0",
|
"ts-jest": "^29.2.0",
|
||||||
"ts-loader": "^9.5.1",
|
"ts-loader": "^9.5.1",
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import configuration from './config/configuration';
|
|||||||
import { TenantMiddleware } from './common/tenant/tenant.middleware';
|
import { TenantMiddleware } from './common/tenant/tenant.middleware';
|
||||||
import { HealthModule } from './modules/health/health.module';
|
import { HealthModule } from './modules/health/health.module';
|
||||||
import { TenantsModule } from './modules/tenants/tenants.module';
|
import { TenantsModule } from './modules/tenants/tenants.module';
|
||||||
|
import { UsersModule } from './modules/users/users.module';
|
||||||
|
import { AuthModule } from './modules/auth/auth.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -31,6 +33,8 @@ import { TenantsModule } from './modules/tenants/tenants.module';
|
|||||||
|
|
||||||
HealthModule,
|
HealthModule,
|
||||||
TenantsModule,
|
TenantsModule,
|
||||||
|
UsersModule,
|
||||||
|
AuthModule,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class AppModule implements NestModule {
|
export class AppModule implements NestModule {
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { Global, Module } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import Redis from 'ioredis';
|
||||||
|
|
||||||
|
export const REDIS = 'REDIS_CLIENT';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* عميل Redis عالمي — DB رقم 3 وبادئة مفاتيح tripz: للعزل على السيرفر المشترك
|
||||||
|
* (راجع docs/14). يُستخدم لتخزين OTP والحضور والمطابقة لاحقاً.
|
||||||
|
*/
|
||||||
|
@Global()
|
||||||
|
@Module({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: REDIS,
|
||||||
|
inject: [ConfigService],
|
||||||
|
useFactory: (cfg: ConfigService) =>
|
||||||
|
new Redis({
|
||||||
|
host: cfg.get<string>('redis.host'),
|
||||||
|
port: cfg.get<number>('redis.port'),
|
||||||
|
db: cfg.get<number>('redis.db'),
|
||||||
|
keyPrefix: cfg.get<string>('redis.keyPrefix'),
|
||||||
|
maxRetriesPerRequest: null,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
exports: [REDIS],
|
||||||
|
})
|
||||||
|
export class RedisModule {}
|
||||||
@@ -34,6 +34,14 @@ export default () => ({
|
|||||||
refreshExpires: process.env.JWT_REFRESH_EXPIRES ?? '30d',
|
refreshExpires: process.env.JWT_REFRESH_EXPIRES ?? '30d',
|
||||||
},
|
},
|
||||||
|
|
||||||
|
auth: {
|
||||||
|
// وضع تطوير: رمز OTP ثابت 0000 يُطبع باللوغ بلا مزوّد SMS (راجع docs/07).
|
||||||
|
// للإنتاج: OTP_DEV_MODE=false + ربط محوّل SMS في P2.
|
||||||
|
otpDevMode: process.env.OTP_DEV_MODE !== 'false',
|
||||||
|
otpTtl: parseInt(process.env.OTP_TTL ?? '300', 10),
|
||||||
|
otpLength: parseInt(process.env.OTP_LENGTH ?? '4', 10),
|
||||||
|
},
|
||||||
|
|
||||||
maps: {
|
maps: {
|
||||||
tilesUrl: process.env.MAPS_TILES_URL ?? 'http://martin:3000',
|
tilesUrl: process.env.MAPS_TILES_URL ?? 'http://martin:3000',
|
||||||
provider: process.env.MAPS_PROVIDER ?? 'antlaq',
|
provider: process.env.MAPS_PROVIDER ?? 'antlaq',
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* جدول المستخدمين tripz_users + فهرس فريد (tenant_id, phone).
|
||||||
|
*/
|
||||||
|
export class InitUsers1721232000000 implements MigrationInterface {
|
||||||
|
public async up(q: QueryRunner): Promise<void> {
|
||||||
|
await q.query(`
|
||||||
|
CREATE TABLE IF NOT EXISTS tripz_users (
|
||||||
|
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
tenant_id uuid NOT NULL,
|
||||||
|
phone varchar NOT NULL,
|
||||||
|
name varchar,
|
||||||
|
role varchar NOT NULL DEFAULT 'rider',
|
||||||
|
status varchar NOT NULL DEFAULT 'active',
|
||||||
|
created_at timestamptz NOT NULL DEFAULT now(),
|
||||||
|
updated_at timestamptz NOT NULL DEFAULT now()
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
await q.query(`
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS "UQ_tripz_users_tenant_phone"
|
||||||
|
ON tripz_users (tenant_id, phone)
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(q: QueryRunner): Promise<void> {
|
||||||
|
await q.query(`DROP TABLE IF EXISTS tripz_users`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Controller, Post, Body, Headers, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
|
||||||
|
@Controller('auth')
|
||||||
|
export class AuthController {
|
||||||
|
constructor(private readonly authService: AuthService) {}
|
||||||
|
|
||||||
|
@Post('send-otp')
|
||||||
|
async sendOtp(
|
||||||
|
@Headers('x-tenant-id') tenantId: string,
|
||||||
|
@Body('phone') phone: string,
|
||||||
|
) {
|
||||||
|
if (!tenantId) throw new UnauthorizedException('Tenant ID (x-tenant-id) is required');
|
||||||
|
return this.authService.sendOtp(tenantId, phone);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('verify-otp')
|
||||||
|
async verifyOtp(
|
||||||
|
@Headers('x-tenant-id') tenantId: string,
|
||||||
|
@Body('phone') phone: string,
|
||||||
|
@Body('code') code: string,
|
||||||
|
) {
|
||||||
|
if (!tenantId) throw new UnauthorizedException('Tenant ID (x-tenant-id) is required');
|
||||||
|
return this.authService.verifyOtp(tenantId, phone, code);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { JwtModule } from '@nestjs/jwt';
|
||||||
|
import { PassportModule } from '@nestjs/passport';
|
||||||
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
|
import { AuthService } from './auth.service';
|
||||||
|
import { AuthController } from './auth.controller';
|
||||||
|
import { UsersModule } from '../users/users.module';
|
||||||
|
import { JwtStrategy } from './strategies/jwt.strategy';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
UsersModule,
|
||||||
|
PassportModule,
|
||||||
|
JwtModule.registerAsync({
|
||||||
|
imports: [ConfigModule],
|
||||||
|
inject: [ConfigService],
|
||||||
|
useFactory: async (configService: ConfigService) => ({
|
||||||
|
secret: configService.get<string>('JWT_SECRET') || 'change_me_jwt_secret',
|
||||||
|
signOptions: {
|
||||||
|
expiresIn: configService.get<string>('JWT_EXPIRES') || '15m',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
controllers: [AuthController],
|
||||||
|
providers: [AuthService, JwtStrategy],
|
||||||
|
exports: [AuthService],
|
||||||
|
})
|
||||||
|
export class AuthModule {}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { Injectable, Logger, UnauthorizedException } from '@nestjs/common';
|
||||||
|
import { JwtService } from '@nestjs/jwt';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { UsersService } from '../users/users.service';
|
||||||
|
import { User } from '../users/entities/user.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AuthService {
|
||||||
|
private readonly logger = new Logger('Auth');
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private usersService: UsersService,
|
||||||
|
private jwtService: JwtService,
|
||||||
|
private config: ConfigService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
private get devCode(): string {
|
||||||
|
// رمز التطوير الثابت — يُستبدل بمحوّل SMS في P2 (راجع docs/07).
|
||||||
|
return '1234';
|
||||||
|
}
|
||||||
|
|
||||||
|
async sendOtp(tenantId: string, phone: string) {
|
||||||
|
// وضع تطوير: الرمز ثابت ويُطبع باللوغ بلا مزوّد SMS.
|
||||||
|
this.logger.log(`OTP for tenant=${tenantId} phone=${phone} => ${this.devCode} (dev)`);
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: 'OTP sent',
|
||||||
|
dev_code: this.config.get('auth.otpDevMode') ? this.devCode : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async verifyOtp(tenantId: string, phone: string, code: string) {
|
||||||
|
if (code !== this.devCode) {
|
||||||
|
throw new UnauthorizedException('Invalid OTP code');
|
||||||
|
}
|
||||||
|
|
||||||
|
let user = await this.usersService.findByPhone(tenantId, phone);
|
||||||
|
if (!user) {
|
||||||
|
user = await this.usersService.create(tenantId, phone);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.issueTokens(user, tenantId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async refresh(refreshToken: string) {
|
||||||
|
let payload: any;
|
||||||
|
try {
|
||||||
|
payload = this.jwtService.verify(refreshToken);
|
||||||
|
} catch {
|
||||||
|
throw new UnauthorizedException('Invalid refresh token');
|
||||||
|
}
|
||||||
|
if (payload.type !== 'refresh') {
|
||||||
|
throw new UnauthorizedException('Not a refresh token');
|
||||||
|
}
|
||||||
|
const user = await this.usersService.findById(payload.tenant_id, payload.sub);
|
||||||
|
if (!user) throw new UnauthorizedException('User not found');
|
||||||
|
return this.issueTokens(user, user.tenant_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** يصدر access + refresh معاً. */
|
||||||
|
private issueTokens(user: User, tenantId: string) {
|
||||||
|
const base = {
|
||||||
|
sub: user.id,
|
||||||
|
phone: user.phone,
|
||||||
|
role: user.role,
|
||||||
|
tenant_id: tenantId,
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
access_token: this.jwtService.sign(base),
|
||||||
|
refresh_token: this.jwtService.sign(
|
||||||
|
{ ...base, type: 'refresh' },
|
||||||
|
{ expiresIn: this.config.get<string>('jwt.refreshExpires') ?? '30d' },
|
||||||
|
),
|
||||||
|
user,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { SetMetadata } from '@nestjs/common';
|
||||||
|
|
||||||
|
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { AuthGuard } from '@nestjs/passport';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtAuthGuard extends AuthGuard('jwt') {}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
|
||||||
|
import { Reflector } from '@nestjs/core';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class RolesGuard implements CanActivate {
|
||||||
|
constructor(private reflector: Reflector) {}
|
||||||
|
|
||||||
|
canActivate(context: ExecutionContext): boolean {
|
||||||
|
const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
|
||||||
|
context.getHandler(),
|
||||||
|
context.getClass(),
|
||||||
|
]);
|
||||||
|
if (!requiredRoles) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const { user } = context.switchToHttp().getRequest();
|
||||||
|
return requiredRoles.some((role) => user?.role === role);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||||
|
import { PassportStrategy } from '@nestjs/passport';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||||
|
constructor(configService: ConfigService) {
|
||||||
|
super({
|
||||||
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||||
|
ignoreExpiration: false,
|
||||||
|
secretOrKey: configService.get<string>('JWT_SECRET') || 'change_me_jwt_secret',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async validate(payload: any) {
|
||||||
|
return {
|
||||||
|
userId: payload.sub,
|
||||||
|
phone: payload.phone,
|
||||||
|
role: payload.role,
|
||||||
|
tenantId: payload.tenant_id
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
||||||
|
|
||||||
|
export enum UserRole {
|
||||||
|
RIDER = 'rider',
|
||||||
|
DRIVER = 'driver',
|
||||||
|
DISPATCHER = 'dispatcher',
|
||||||
|
ADMIN = 'admin',
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity('users')
|
||||||
|
export class User {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid' })
|
||||||
|
tenant_id: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
phone: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
// varchar (لا enum) لمطابقة الهجرة وتفادي إنشاء نوع enum في القاعدة.
|
||||||
|
@Column({ type: 'varchar', default: UserRole.RIDER })
|
||||||
|
role: UserRole;
|
||||||
|
|
||||||
|
@Column({ default: 'active' })
|
||||||
|
status: string;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
created_at: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn()
|
||||||
|
updated_at: Date;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { User } from './entities/user.entity';
|
||||||
|
import { UsersService } from './users.service';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([User])],
|
||||||
|
providers: [UsersService],
|
||||||
|
exports: [UsersService],
|
||||||
|
})
|
||||||
|
export class UsersModule {}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { User, UserRole } from './entities/user.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UsersService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(User)
|
||||||
|
private readonly userRepository: Repository<User>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async findByPhone(tenantId: string, phone: string): Promise<User | null> {
|
||||||
|
return this.userRepository.findOne({ where: { tenant_id: tenantId, phone } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async findById(tenantId: string, id: string): Promise<User | null> {
|
||||||
|
return this.userRepository.findOne({ where: { tenant_id: tenantId, id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
async create(tenantId: string, phone: string, role: string = 'rider'): Promise<User> {
|
||||||
|
const user = this.userRepository.create({ tenant_id: tenantId, phone, role: role as UserRole });
|
||||||
|
return this.userRepository.save(user);
|
||||||
|
}
|
||||||
|
}
|
||||||
+13
-48
@@ -1,55 +1,20 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# ============================================================
|
# ============================================================
|
||||||
# Tripz — مزامنة الكود من الماك إلى السيرفر (نصوص فقط)
|
# Tripz — رفع الكود محلياً إلى Git
|
||||||
# الاستخدام من تيرمينال الماك:
|
# الاستخدام:
|
||||||
# ./sync-to-server.sh # مزامنة فقط
|
# ./sync-to-server.sh "رسالة التحديث"
|
||||||
# ./sync-to-server.sh --deploy # مزامنة ثم docker compose up على السيرفر
|
|
||||||
# ./sync-to-server.sh --logs # عرض لوغ الحاويات على السيرفر
|
|
||||||
# ملاحظة: لا نبني ولا نشغّل شيئاً على الماك — كل التشغيل على السيرفر عبر Docker.
|
|
||||||
# ============================================================
|
# ============================================================
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# ---- إعدادات السيرفر (عدّل عند اللزوم) ----
|
MSG="${1:-Update codebase}"
|
||||||
SERVER_USER="root"
|
|
||||||
SERVER_IP="194.163.173.157"
|
|
||||||
REMOTE_DIR="/home/tripz-llc" # مجلد مخصّص جديد (منفصل عن مواقع CloudPanel)
|
|
||||||
LOCAL_DIR="$(cd "$(dirname "$0")" && pwd)/"
|
|
||||||
|
|
||||||
SSH="ssh ${SERVER_USER}@${SERVER_IP}"
|
echo "==> رفع الكود محلياً إلى Git..."
|
||||||
|
git add .
|
||||||
|
git commit -m "$MSG" || true
|
||||||
|
git push origin main
|
||||||
|
|
||||||
echo "==> مزامنة ${LOCAL_DIR} ⟶ ${SERVER_USER}@${SERVER_IP}:${REMOTE_DIR}"
|
echo "==> تمت عملية الرفع بنجاح!"
|
||||||
|
echo "الآن يمكنك الذهاب إلى تيرمينال السيرفر وتشغيل الأوامر التالية:"
|
||||||
# تأكد من وجود المجلد على السيرفر
|
echo " cd /home/tripz-llc"
|
||||||
$SSH "mkdir -p ${REMOTE_DIR}"
|
echo " git pull origin main"
|
||||||
|
echo " cd backend && docker compose up -d --build"
|
||||||
# rsync: يرفع النصوص فقط، يستثني المخرجات والأسرار، ويحذف المحذوف محلياً
|
|
||||||
# لكن يُبقي .env على السيرفر (يُنشأ هناك ولا يُرفع من الماك)
|
|
||||||
rsync -avz --delete \
|
|
||||||
--exclude='.git/' \
|
|
||||||
--exclude='node_modules/' \
|
|
||||||
--exclude='dist/' \
|
|
||||||
--exclude='build/' \
|
|
||||||
--exclude='.dart_tool/' \
|
|
||||||
--exclude='**/pgdata/' \
|
|
||||||
--exclude='**/redisdata/' \
|
|
||||||
--exclude='.env' \
|
|
||||||
--exclude='.DS_Store' \
|
|
||||||
--exclude='*.log' \
|
|
||||||
"${LOCAL_DIR}" "${SERVER_USER}@${SERVER_IP}:${REMOTE_DIR}/"
|
|
||||||
|
|
||||||
echo "==> تمت المزامنة."
|
|
||||||
|
|
||||||
# ---- خيارات إضافية ----
|
|
||||||
case "${1:-}" in
|
|
||||||
--deploy)
|
|
||||||
echo "==> تشغيل docker compose على السيرفر..."
|
|
||||||
$SSH "cd ${REMOTE_DIR}/backend && \
|
|
||||||
[ -f .env ] || cp .env.example .env && \
|
|
||||||
docker compose up -d --build && \
|
|
||||||
docker compose exec -T api npm run migration:run || true"
|
|
||||||
echo "==> التطبيق: http://${SERVER_IP}:4010/api/health"
|
|
||||||
;;
|
|
||||||
--logs)
|
|
||||||
$SSH "cd ${REMOTE_DIR}/backend && docker compose logs --tail=100 -f"
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|||||||
Reference in New Issue
Block a user