Compare commits

...

19 Commits

Author SHA1 Message Date
bac19b0bb2 Update deploy-dev.yml for branch-specific deployment and service update improvements
Some checks failed
Deploy Dev (Branch Trigger) / build-and-deploy (push) Failing after 1m39s
2026-02-12 10:56:26 +03:30
1b4a380cc9 Update deploy-dev.yml to change runner configuration from self-hosted to linux_amd64
Some checks failed
Deploy Dev (Fixed) / build-and-deploy (push) Failing after 9s
2026-02-12 10:53:44 +03:30
b16261928c Update deploy-dev.yml to use generic PAT for Docker login
Some checks failed
Deploy Dev (Fixed) / build-and-deploy (push) Has been cancelled
2026-02-12 10:13:32 +03:30
cfceb2877f Update deploy-dev.yml for improved deployment process and DNS fix 2026-02-12 10:02:00 +03:30
5a244ed35e change deploy-dev.yml
Some checks failed
Deploy Dev (Branch Trigger) / build-and-deploy (push) Failing after 3m50s
2026-02-10 16:37:48 +03:30
42008d3c4d complete docker v1 2026-02-10 15:20:36 +03:30
387682aedb Refactor FaceEmbeddingService to use configuration for API base URL and update CORS policy to read origins from configuration 2026-02-07 17:13:36 +03:30
577acfd0ae change program.cs logger and docker file 2026-02-01 14:04:56 +03:30
04cb584ae3 Add .gitea/workflows/dad-mehr-gitea-deploy.yml
Some checks failed
Build and Deploy (.NET) / build-push (push) Failing after 2m28s
Build and Deploy (.NET) / deploy (push) Has been skipped
2026-01-31 16:35:58 +03:30
f6cddff59d Delete .gitea/workflows/upload-dad-mehr.yml 2026-01-31 16:34:55 +03:30
7b09cc53c3 ش 2026-01-31 16:34:12 +03:30
a7d3ff5298 comment http redirection 2026-01-31 15:57:34 +03:30
8ecbbf6975 update Docker configuration and Visual Studio solution for improved compatibility and build process 2026-01-31 15:29:48 +03:30
3720288bed update Dockerfile and project files for improved build process and remove documentation generation 2026-01-28 19:42:32 +03:30
4f400ccef0 add InsuranceList directory to Docker setup 2026-01-27 20:14:01 +03:30
d777fad96b add Docker bind mounts configuration and setup documentation 2026-01-27 18:42:58 +03:30
fb7b04596c run Dockerfile and docker-compose.yml in development 2026-01-27 17:10:06 +03:30
76d2c0e3c4 add local host certs 2026-01-27 14:56:35 +03:30
a745dfff86 add Dockerfile 2026-01-27 14:55:15 +03:30
26 changed files with 1690 additions and 2436 deletions

View File

@@ -0,0 +1,64 @@
name: Deploy Dev (Branch Trigger)
on:
push:
branches:
- Feature/general/docker
env:
IMAGE_NAME: gozareshgir-api
# مسیری که فایل docker-compose.yml مخصوص تست در سرور قرار دارد
SERVER_PATH: ~/apps/test-dev/backend-api
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
# 1. لاگین به داکر هاب/رجیستری شخصی
- name: Login to Docker Registry
uses: docker/login-action@v3
with:
registry: ${{ secrets.DOCKER_REGISTRY }}
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
# 2. بیلد و پوش کردن ایمیج با تگ :dev
- name: Build and Push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ secrets.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:dev
# 3. اتصال به سرور و آپدیت سرویس
- name: Update Service on Test Server
uses: appleboy/ssh-action@v1.0.3
env:
DOCKER_REGISTRY: ${{ secrets.DOCKER_REGISTRY }}
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
APP_VERSION: dev # ورژن تست همیشه dev است
with:
host: ${{ secrets.SSH_HOST_TEST }}
username: ${{ secrets.SSH_USERNAME_TEST }}
key: ${{ secrets.SSH_KEY_TEST }}
port: 22
envs: DOCKER_REGISTRY,DOCKER_USERNAME,DOCKER_PASSWORD,APP_VERSION
script: |
cd ${{ env.SERVER_PATH }}
# لاگین مجدد در سرور برای اطمینان
echo "$DOCKER_PASSWORD" | docker login $DOCKER_REGISTRY -u $DOCKER_USERNAME --password-stdin
# اکسپورت کردن ورژن برای اینکه فایل داکر-کمپوز سرور آن را بشناسد
export APP_VERSION=$APP_VERSION
# دانلود ایمیج جدید و آپدیت کانتینر
docker compose pull
docker compose up -d --remove-orphans
# پاک کردن ایمیج‌های قدیمی برای پر نشدن فضای سرور
docker image prune -f

18
.gitignore vendored
View File

@@ -1,3 +1,21 @@
.env*
.env
certs/*.pfx
certs/*.pem
certs/*.key
certs/*.crt
Storage/
Logs/
*.user
*.suo
bin/
obj/
certs/*.pfx
certs/*.pem
certs/*.key
certs/*.crt
Storage/
Logs/
## Ignore Visual Studio temporary files, build results, and ## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons. ## files generated by popular Visual Studio add-ons.
## ##

View File

@@ -9,6 +9,7 @@ using _0_Framework.Application;
using _0_Framework.Application.FaceEmbedding; using _0_Framework.Application.FaceEmbedding;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Http; using Microsoft.Extensions.Http;
using Microsoft.Extensions.Configuration;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace _0_Framework.Infrastructure; namespace _0_Framework.Infrastructure;
@@ -24,12 +25,12 @@ public class FaceEmbeddingService : IFaceEmbeddingService
private readonly string _apiBaseUrl; private readonly string _apiBaseUrl;
public FaceEmbeddingService(IHttpClientFactory httpClientFactory, ILogger<FaceEmbeddingService> logger, public FaceEmbeddingService(IHttpClientFactory httpClientFactory, ILogger<FaceEmbeddingService> logger,
IFaceEmbeddingNotificationService notificationService = null) IConfiguration configuration, IFaceEmbeddingNotificationService notificationService = null)
{ {
_httpClientFactory = httpClientFactory; _httpClientFactory = httpClientFactory;
_logger = logger; _logger = logger;
_notificationService = notificationService; _notificationService = notificationService;
_apiBaseUrl = "http://localhost:8000"; _apiBaseUrl = configuration["FaceEmbeddingApi:BaseUrl"] ?? "http://localhost:8000";
} }
public async Task<OperationResult> GenerateEmbeddingsAsync(long employeeId, long workshopId, public async Task<OperationResult> GenerateEmbeddingsAsync(long employeeId, long workshopId,

View File

@@ -1,624 +0,0 @@
# راهنمای اتصال اپلیکیشن Android به SignalR برای Face Embedding
## 1. افزودن کتابخانه SignalR به پروژه Android
در فایل `build.gradle` (Module: app) خود، dependency زیر را اضافه کنید:
```gradle
dependencies {
// SignalR for Android
implementation 'com.microsoft.signalr:signalr:7.0.0'
// اگر از Kotlin استفاده می‌کنید:
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.1'
// برای JSON پردازش:
implementation 'com.google.code.gson:gson:2.10.1'
}
```
## 2. اضافه کردن Permission در AndroidManifest.xml
```xml
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
```
## 3. کد Java/Kotlin برای اتصال به SignalR
### نسخه Java:
```java
import com.microsoft.signalr.HubConnection;
import com.microsoft.signalr.HubConnectionBuilder;
import com.microsoft.signalr.HubConnectionState;
import com.google.gson.JsonObject;
import android.util.Log;
public class FaceEmbeddingSignalRClient {
private static final String TAG = "FaceEmbeddingHub";
private HubConnection hubConnection;
private String serverUrl = "http://YOUR_SERVER_IP:PORT/trackingFaceEmbeddingHub"; // آدرس سرور خود را وارد کنید
private long workshopId;
public FaceEmbeddingSignalRClient(long workshopId) {
this.workshopId = workshopId;
initializeSignalR();
}
private void initializeSignalR() {
// ایجاد اتصال SignalR
hubConnection = HubConnectionBuilder
.create(serverUrl)
.build();
// دریافت رویداد ایجاد Embedding
hubConnection.on("EmbeddingCreated", (data) -> {
JsonObject jsonData = (JsonObject) data;
long employeeId = jsonData.get("employeeId").getAsLong();
String employeeFullName = jsonData.get("employeeFullName").getAsString();
String timestamp = jsonData.get("timestamp").getAsString();
Log.d(TAG, "Embedding Created - Employee: " + employeeFullName + " (ID: " + employeeId + ")");
// اینجا می‌توانید داده‌های جدید را از سرور بگیرید یا UI را بروزرسانی کنید
onEmbeddingCreated(employeeId, employeeFullName, timestamp);
}, JsonObject.class);
// دریافت رویداد حذف Embedding
hubConnection.on("EmbeddingDeleted", (data) -> {
JsonObject jsonData = (JsonObject) data;
long employeeId = jsonData.get("employeeId").getAsLong();
String timestamp = jsonData.get("timestamp").getAsString();
Log.d(TAG, "Embedding Deleted - Employee ID: " + employeeId);
onEmbeddingDeleted(employeeId, timestamp);
}, JsonObject.class);
// دریافت رویداد بهبود Embedding
hubConnection.on("EmbeddingRefined", (data) -> {
JsonObject jsonData = (JsonObject) data;
long employeeId = jsonData.get("employeeId").getAsLong();
String timestamp = jsonData.get("timestamp").getAsString();
Log.d(TAG, "Embedding Refined - Employee ID: " + employeeId);
onEmbeddingRefined(employeeId, timestamp);
}, JsonObject.class);
}
public void connect() {
if (hubConnection.getConnectionState() == HubConnectionState.DISCONNECTED) {
hubConnection.start()
.doOnComplete(() -> {
Log.d(TAG, "Connected to SignalR Hub");
joinWorkshopGroup();
})
.doOnError(error -> {
Log.e(TAG, "Error connecting to SignalR: " + error.getMessage());
})
.subscribe();
}
}
private void joinWorkshopGroup() {
// عضویت در گروه مخصوص این کارگاه
hubConnection.send("JoinWorkshopGroup", workshopId);
Log.d(TAG, "Joined workshop group: " + workshopId);
}
public void disconnect() {
if (hubConnection.getConnectionState() == HubConnectionState.CONNECTED) {
// خروج از گروه
hubConnection.send("LeaveWorkshopGroup", workshopId);
hubConnection.stop();
Log.d(TAG, "Disconnected from SignalR Hub");
}
}
// این متدها را در Activity/Fragment خود override کنید
protected void onEmbeddingCreated(long employeeId, String employeeFullName, String timestamp) {
// اینجا UI را بروزرسانی کنید یا داده جدید را بگیرید
}
protected void onEmbeddingDeleted(long employeeId, String timestamp) {
// اینجا UI را بروزرسانی کنید
}
protected void onEmbeddingRefined(long employeeId, String timestamp) {
// اینجا UI را بروزرسانی کنید
}
}
```
### نسخه Kotlin:
```kotlin
import com.microsoft.signalr.HubConnection
import com.microsoft.signalr.HubConnectionBuilder
import com.microsoft.signalr.HubConnectionState
import com.google.gson.JsonObject
import android.util.Log
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class FaceEmbeddingSignalRClient(private val workshopId: Long) {
companion object {
private const val TAG = "FaceEmbeddingHub"
}
private lateinit var hubConnection: HubConnection
private val serverUrl = "http://YOUR_SERVER_IP:PORT/trackingFaceEmbeddingHub" // آدرس سرور خود را وارد کنید
init {
initializeSignalR()
}
private fun initializeSignalR() {
hubConnection = HubConnectionBuilder
.create(serverUrl)
.build()
// دریافت رویداد ایجاد Embedding
hubConnection.on("EmbeddingCreated", { data: JsonObject ->
val employeeId = data.get("employeeId").asLong
val employeeFullName = data.get("employeeFullName").asString
val timestamp = data.get("timestamp").asString
Log.d(TAG, "Embedding Created - Employee: $employeeFullName (ID: $employeeId)")
onEmbeddingCreated(employeeId, employeeFullName, timestamp)
}, JsonObject::class.java)
// دریافت رویداد حذف Embedding
hubConnection.on("EmbeddingDeleted", { data: JsonObject ->
val employeeId = data.get("employeeId").asLong
val timestamp = data.get("timestamp").asString
Log.d(TAG, "Embedding Deleted - Employee ID: $employeeId")
onEmbeddingDeleted(employeeId, timestamp)
}, JsonObject::class.java)
// دریافت رویداد بهبود Embedding
hubConnection.on("EmbeddingRefined", { data: JsonObject ->
val employeeId = data.get("employeeId").asLong
val timestamp = data.get("timestamp").asString
Log.d(TAG, "Embedding Refined - Employee ID: $employeeId")
onEmbeddingRefined(employeeId, timestamp)
}, JsonObject::class.java)
}
fun connect() {
if (hubConnection.connectionState == HubConnectionState.DISCONNECTED) {
CoroutineScope(Dispatchers.IO).launch {
try {
hubConnection.start().blockingAwait()
Log.d(TAG, "Connected to SignalR Hub")
joinWorkshopGroup()
} catch (e: Exception) {
Log.e(TAG, "Error connecting to SignalR: ${e.message}")
}
}
}
}
private fun joinWorkshopGroup() {
hubConnection.send("JoinWorkshopGroup", workshopId)
Log.d(TAG, "Joined workshop group: $workshopId")
}
fun disconnect() {
if (hubConnection.connectionState == HubConnectionState.CONNECTED) {
hubConnection.send("LeaveWorkshopGroup", workshopId)
hubConnection.stop()
Log.d(TAG, "Disconnected from SignalR Hub")
}
}
// این متدها را override کنید
open fun onEmbeddingCreated(employeeId: Long, employeeFullName: String, timestamp: String) {
// اینجا UI را بروزرسانی کنید یا داده جدید را بگیرید
}
open fun onEmbeddingDeleted(employeeId: Long, timestamp: String) {
// اینجا UI را بروزرسانی کنید
}
open fun onEmbeddingRefined(employeeId: Long, timestamp: String) {
// اینجا UI را بروزرسانی کنید
}
}
```
## 4. استفاده در Activity یا Fragment
### مثال با Login و دریافت WorkshopId
#### Java:
```java
public class LoginActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
Button btnLogin = findViewById(R.id.btnLogin);
btnLogin.setOnClickListener(v -> performLogin());
}
private void performLogin() {
// فراخوانی API لاگین
// فرض کنید response شامل workshopId است
// مثال ساده (باید از Retrofit یا کتابخانه مشابه استفاده کنید):
// LoginResponse response = apiService.login(username, password);
// long workshopId = response.getWorkshopId();
long workshopId = 123; // این را از response دریافت کنید
// ذخیره workshopId
SharedPreferences prefs = getSharedPreferences("AppPrefs", MODE_PRIVATE);
prefs.edit().putLong("workshopId", workshopId).apply();
// رفتن به صفحه اصلی
Intent intent = new Intent(this, MainActivity.class);
startActivity(intent);
finish();
}
}
public class MainActivity extends AppCompatActivity {
private FaceEmbeddingSignalRClient signalRClient;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// دریافت workshopId از SharedPreferences
SharedPreferences prefs = getSharedPreferences("AppPrefs", MODE_PRIVATE);
long workshopId = prefs.getLong("workshopId", 0);
if (workshopId == 0) {
// اگر workshopId وجود نداره، برگرد به صفحه لاگین
Intent intent = new Intent(this, LoginActivity.class);
startActivity(intent);
finish();
return;
}
// ایجاد و اتصال SignalR
signalRClient = new FaceEmbeddingSignalRClient(workshopId) {
@Override
protected void onEmbeddingCreated(long employeeId, String employeeFullName, String timestamp) {
runOnUiThread(() -> {
// بروزرسانی UI
Toast.makeText(MainActivity.this,
"Embedding ایجاد شد برای: " + employeeFullName,
Toast.LENGTH_SHORT).show();
// دریافت داده‌های جدید از API
refreshEmployeeList();
});
}
@Override
protected void onEmbeddingDeleted(long employeeId, String timestamp) {
runOnUiThread(() -> {
// بروزرسانی UI
refreshEmployeeList();
});
}
@Override
protected void onEmbeddingRefined(long employeeId, String timestamp) {
runOnUiThread(() -> {
// بروزرسانی UI
refreshEmployeeList();
});
}
};
signalRClient.connect();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (signalRClient != null) {
signalRClient.disconnect();
}
}
private void refreshEmployeeList() {
// دریافت لیست جدید کارمندان از API
}
}
```
#### Kotlin:
```kotlin
class LoginActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
val btnLogin = findViewById<Button>(R.id.btnLogin)
btnLogin.setOnClickListener { performLogin() }
}
private fun performLogin() {
// فراخوانی API لاگین
// فرض کنید response شامل workshopId است
// مثال ساده (باید از Retrofit یا کتابخانه مشابه استفاده کنید):
// val response = apiService.login(username, password)
// val workshopId = response.workshopId
val workshopId = 123L // این را از response دریافت کنید
// ذخیره workshopId
val prefs = getSharedPreferences("AppPrefs", Context.MODE_PRIVATE)
prefs.edit().putLong("workshopId", workshopId).apply()
// رفتن به صفحه اصلی
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
finish()
}
}
class MainActivity : AppCompatActivity() {
private lateinit var signalRClient: FaceEmbeddingSignalRClient
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// دریافت workshopId از SharedPreferences
val prefs = getSharedPreferences("AppPrefs", Context.MODE_PRIVATE)
val workshopId = prefs.getLong("workshopId", 0L)
if (workshopId == 0L) {
// اگر workshopId وجود نداره، برگرد به صفحه لاگین
val intent = Intent(this, LoginActivity::class.java)
startActivity(intent)
finish()
return
}
// ایجاد و اتصال SignalR
signalRClient = object : FaceEmbeddingSignalRClient(workshopId) {
override fun onEmbeddingCreated(employeeId: Long, employeeFullName: String, timestamp: String) {
runOnUiThread {
// بروزرسانی UI
Toast.makeText(this@MainActivity,
"Embedding ایجاد شد برای: $employeeFullName",
Toast.LENGTH_SHORT).show()
// دریافت داده‌های جدید از API
refreshEmployeeList()
}
}
override fun onEmbeddingDeleted(employeeId: Long, timestamp: String) {
runOnUiThread {
// بروزرسانی UI
refreshEmployeeList()
}
}
override fun onEmbeddingRefined(employeeId: Long, timestamp: String) {
runOnUiThread {
// بروزرسانی UI
refreshEmployeeList()
}
}
}
signalRClient.connect()
}
override fun onDestroy() {
super.onDestroy()
signalRClient.disconnect()
}
private fun refreshEmployeeList() {
// دریافت لیست جدید کارمندان از API
}
}
```
### مثال ساده بدون Login:
اگر workshopId را از قبل می‌دانید:
#### Java:
```java
public class MainActivity extends AppCompatActivity {
private FaceEmbeddingSignalRClient signalRClient;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
long workshopId = 123; // شناسه کارگاه خود را وارد کنید
signalRClient = new FaceEmbeddingSignalRClient(workshopId) {
@Override
protected void onEmbeddingCreated(long employeeId, String employeeFullName, String timestamp) {
runOnUiThread(() -> {
// بروزرسانی UI
Toast.makeText(MainActivity.this,
"Embedding ایجاد شد برای: " + employeeFullName,
Toast.LENGTH_SHORT).show();
// دریافت داده‌های جدید از API
refreshEmployeeList();
});
}
@Override
protected void onEmbeddingDeleted(long employeeId, String timestamp) {
runOnUiThread(() -> {
// بروزرسانی UI
refreshEmployeeList();
});
}
};
signalRClient.connect();
}
@Override
protected void onDestroy() {
super.onDestroy();
if (signalRClient != null) {
signalRClient.disconnect();
}
}
private void refreshEmployeeList() {
// دریافت لیست جدید کارمندان از API
}
}
```
#### Kotlin:
```kotlin
class MainActivity : AppCompatActivity() {
private lateinit var signalRClient: FaceEmbeddingSignalRClient
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val workshopId = 123L // شناسه کارگاه خود را وارد کنید
signalRClient = object : FaceEmbeddingSignalRClient(workshopId) {
override fun onEmbeddingCreated(employeeId: Long, employeeFullName: String, timestamp: String) {
runOnUiThread {
// بروزرسانی UI
Toast.makeText(this@MainActivity,
"Embedding ایجاد شد برای: $employeeFullName",
Toast.LENGTH_SHORT).show()
// دریافت داده‌های جدید از API
refreshEmployeeList()
}
}
override fun onEmbeddingDeleted(employeeId: Long, timestamp: String) {
runOnUiThread {
// بروزرسانی UI
refreshEmployeeList()
}
}
}
signalRClient.connect()
}
override fun onDestroy() {
super.onDestroy()
signalRClient.disconnect()
}
private fun refreshEmployeeList() {
// دریافت لیست جدید کارمندان از API
}
}
```
## 5. نکات مهم
### آدرس سرور
- اگر روی شبیه‌ساز اندروید تست می‌کنید و سرور روی localhost اجرا می‌شود، از آدرس `http://10.0.2.2:PORT` استفاده کنید
- اگر روی دستگاه فیزیکی تست می‌کنید، از آدرس IP شبکه محلی سرور استفاده کنید (مثل `http://192.168.1.100:PORT`)
- PORT پیش‌فرض معمولاً 5000 یا 5001 است (بسته به کانفیگ پروژه شما)
### دریافت WorkshopId از Login
بعد از login موفق، workshopId را از سرور دریافت کنید و در SharedPreferences یا یک Singleton ذخیره کنید:
```java
// بعد از login موفق
SharedPreferences prefs = getSharedPreferences("AppPrefs", MODE_PRIVATE);
prefs.edit().putLong("workshopId", workshopId).apply();
// استفاده در Activity
long workshopId = prefs.getLong("workshopId", 0);
```
یا در Kotlin:
```kotlin
// بعد از login موفق
val prefs = getSharedPreferences("AppPrefs", Context.MODE_PRIVATE)
prefs.edit().putLong("workshopId", workshopId).apply()
// استفاده در Activity
val workshopId = prefs.getLong("workshopId", 0L)
```
### مدیریت اتصال
برای reconnection خودکار:
```java
hubConnection.onClosed(exception -> {
Log.e(TAG, "Connection closed. Attempting to reconnect...");
new Handler().postDelayed(() -> connect(), 5000); // تلاش مجدد بعد از 5 ثانیه
});
```
### Thread Safety
همیشه UI updates را در main thread انجام دهید:
```java
runOnUiThread(() -> {
// UI updates here
});
```
## 6. تست اتصال
برای تست می‌توانید:
1. اپلیکیشن را اجرا کنید
2. از طریق Postman یا Swagger یک Embedding ایجاد کنید
3. باید در Logcat پیام "Embedding Created" را ببینید
## 7. خطایابی (Debugging)
برای دیدن جزئیات بیشتر:
```java
hubConnection = HubConnectionBuilder
.create(serverUrl)
.withHttpConnectionOptions(options -> {
options.setLogging(LogLevel.TRACE);
})
.build();
```
---
## خلاصه Endpoints
| نوع رویداد | متد SignalR | پارامترهای دریافتی |
|-----------|-------------|---------------------|
| ایجاد Embedding | `EmbeddingCreated` | workshopId, employeeId, employeeFullName, timestamp |
| حذف Embedding | `EmbeddingDeleted` | workshopId, employeeId, timestamp |
| بهبود Embedding | `EmbeddingRefined` | workshopId, employeeId, timestamp |
| متد ارسالی | پارامتر | توضیحات |
|-----------|---------|---------|
| `JoinWorkshopGroup` | workshopId | عضویت در گروه کارگاه |
| `LeaveWorkshopGroup` | workshopId | خروج از گروه کارگاه |

View File

@@ -1,175 +0,0 @@
# سیستم گزارش خرابی (Bug Report System)
## نمای کلی
این سیستم برای جمع‌آوری، ذخیره و مدیریت گزارش‌های خرابی از تطبیق موبایلی طراحی شده است.
## ساختار فایل‌ها
### Domain Layer
- `AccountManagement.Domain/BugReportAgg/`
- `BugReport.cs` - موجودیت اصلی
- `BugReportLog.cs` - لاگ‌های گزارش
- `BugReportScreenshot.cs` - تصاویر ضمیمه شده
### Application Contracts
- `AccountManagement.Application.Contracts/BugReport/`
- `IBugReportApplication.cs` - اینترفیس سرویس
- `CreateBugReportCommand.cs` - درخواست ایجاد
- `EditBugReportCommand.cs` - درخواست ویرایش
- `BugReportViewModel.cs` - نمایش لیست
- `BugReportDetailViewModel.cs` - نمایش جزئیات
- `IBugReportRepository.cs` - اینترفیس Repository
### Application Service
- `AccountManagement.Application/BugReportApplication.cs` - پیاده‌سازی سرویس
### Infrastructure
- `AccountMangement.Infrastructure.EFCore/`
- `Mappings/BugReportMapping.cs`
- `Mappings/BugReportLogMapping.cs`
- `Mappings/BugReportScreenshotMapping.cs`
- `Repository/BugReportRepository.cs`
### API Controller
- `ServiceHost/Controllers/BugReportController.cs`
### Admin Pages
- `ServiceHost/Areas/AdminNew/Pages/BugReport/`
- `BugReportPageModel.cs` - base model
- `Index.cshtml.cs / Index.cshtml` - لیست گزارش‌ها
- `Details.cshtml.cs / Details.cshtml` - جزئیات کامل
- `Edit.cshtml.cs / Edit.cshtml` - ویرایش وضعیت/اولویت
- `Delete.cshtml.cs / Delete.cshtml` - حذف
## روش استفاده
### 1. ثبت گزارش از موبایل
```csharp
POST /api/bugreport/submit
{
"title": "برنامه هنگام ورود خراب می‌شود",
"description": "هنگام وارد کردن نام کاربری، برنامه کرش می‌کند",
"userEmail": "user@example.com",
"deviceModel": "Samsung Galaxy S21",
"osVersion": "Android 12",
"platform": "Android",
"manufacturer": "Samsung",
"deviceId": "device-unique-id",
"screenResolution": "1440x3200",
"memoryInMB": 8000,
"storageInMB": 256000,
"batteryLevel": 75,
"isCharging": false,
"networkType": "4G",
"appVersion": "1.0.0",
"buildNumber": "100",
"packageName": "com.example.app",
"installTime": "2024-01-01T10:00:00Z",
"lastUpdateTime": "2024-12-01T14:30:00Z",
"flavor": "production",
"type": 1, // Crash = 1
"priority": 2, // High = 2
"stackTrace": "...",
"logs": ["log1", "log2"],
"screenshots": ["base64-encoded-image-1"]
}
```
### 2. دسترسی به Admin Panel
```
https://yourdomain.com/AdminNew/BugReport
```
**صفحات موجود:**
- **Index** - لیست تمام گزارش‌ها با فیلترها
- **Details** - نمایش جزئیات کامل شامل:
- معلومات کاربر و گزارش
- معلومات دستگاه
- معلومات برنامه
- لاگ‌ها
- تصاویر
- Stack Trace
- **Edit** - تغییر وضعیت و اولویت
- **Delete** - حذف گزارش
### 3. درخواست‌های API
#### دریافت لیست
```
GET /api/bugreport/list?type=1&priority=2&status=1&searchTerm=crash&pageNumber=1&pageSize=10
```
#### دریافت جزئیات
```
GET /api/bugreport/{id}
```
#### ویرایش
```
PUT /api/bugreport/{id}
{
"id": 1,
"priority": 2,
"status": 3
}
```
#### حذف
```
DELETE /api/bugreport/{id}
```
## انواع (Enums)
### BugReportType
- `1` - Crash (کرش)
- `2` - UI (مشکل رابط)
- `3` - Performance (عملکرد)
- `4` - Feature (فیچر)
- `5` - Network (شبکه)
- `6` - Camera (دوربین)
- `7` - FaceRecognition (تشخیص چهره)
- `8` - Database (دیتابیس)
- `9` - Login (ورود)
- `10` - Other (سایر)
### BugPriority
- `1` - Critical (بحرانی)
- `2` - High (بالا)
- `3` - Medium (متوسط)
- `4` - Low (پایین)
### BugReportStatus
- `1` - Open (باز)
- `2` - InProgress (در حال بررسی)
- `3` - Fixed (رفع شده)
- `4` - Closed (بسته شده)
- `5` - Reopened (مجدداً باز)
## Migration
برای اعمال تغییرات دیتابیس:
```powershell
Add-Migration AddBugReportTables
Update-Database
```
## نکات مهم
1. **تصاویر**: تصاویر به صورت Base64 encoded ذخیره می‌شوند
2. **لاگ‌ها**: تمام لاگ‌ها به صورت جدا ذخیره می‌شوند
3. **وضعیت پیش‌فرض**: وقتی گزارش ثبت می‌شود، وضعیت آن "Open" است
4. **تاریخ**: تاریخ ایجاد و بروزرسانی خودکار ثبت می‌شود
## Security
- API endpoints از `authentication` محافظت می‌شوند
- Admin pages تنها برای کاربرانی با دسترسی AdminArea قابل دسترس هستند
- حذف و ویرایش نیاز به تأیید دارد

View File

@@ -1,314 +0,0 @@
# خلاصه تغییرات سیستم گزارش خرابی
## 📝 فایل‌های اضافه شده (23 فایل)
### 1⃣ Domain Layer (3 فایل)
```
✓ AccountManagement.Domain/BugReportAgg/
├── BugReport.cs
├── BugReportLog.cs
└── BugReportScreenshot.cs
```
### 2⃣ Application Contracts (6 فایل)
```
✓ AccountManagement.Application.Contracts/BugReport/
├── IBugReportRepository.cs
├── IBugReportApplication.cs
├── CreateBugReportCommand.cs
├── EditBugReportCommand.cs
├── BugReportViewModel.cs
└── BugReportDetailViewModel.cs
```
### 3⃣ Application Service (1 فایل)
```
✓ AccountManagement.Application/
└── BugReportApplication.cs
```
### 4⃣ Infrastructure EFCore (4 فایل)
```
✓ AccountMangement.Infrastructure.EFCore/
├── Mappings/
│ ├── BugReportMapping.cs
│ ├── BugReportLogMapping.cs
│ └── BugReportScreenshotMapping.cs
└── Repository/
└── BugReportRepository.cs
```
### 5⃣ API Controller (1 فایل)
```
✓ ServiceHost/Controllers/
└── BugReportController.cs
```
### 6⃣ Admin Pages (8 فایل)
```
✓ ServiceHost/Areas/AdminNew/Pages/BugReport/
├── BugReportPageModel.cs
├── Index.cshtml.cs
├── Index.cshtml
├── Details.cshtml.cs
├── Details.cshtml
├── Edit.cshtml.cs
├── Edit.cshtml
├── Delete.cshtml.cs
└── Delete.cshtml
```
### 7⃣ Documentation (2 فایل)
```
✓ BUG_REPORT_SYSTEM.md
✓ FLUTTER_BUG_REPORT_EXAMPLE.dart
```
---
## ✏️ فایل‌های اصلاح شده (2 فایل)
### 1. AccountManagement.Configuration/AccountManagementBootstrapper.cs
**تغییر:** اضافه کردن using برای BugReport
```csharp
using AccountManagement.Application.Contracts.BugReport;
```
**تغییر:** رجیستریشن سرویس‌ها
```csharp
services.AddTransient<IBugReportApplication, BugReportApplication>();
services.AddTransient<IBugReportRepository, BugReportRepository>();
```
### 2. AccountMangement.Infrastructure.EFCore/AccountContext.cs
**تغییر:** اضافه کردن using
```csharp
using AccountManagement.Domain.BugReportAgg;
```
**تغییر:** اضافه کردن DbSets
```csharp
#region BugReport
public DbSet<BugReport> BugReports { get; set; }
public DbSet<BugReportLog> BugReportLogs { get; set; }
public DbSet<BugReportScreenshot> BugReportScreenshots { get; set; }
#endregion
```
---
## 🔧 موارد مورد نیاز قبل از استفاده
### 1. Database Migration
```powershell
# در Package Manager Console
cd AccountMangement.Infrastructure.EFCore
Add-Migration AddBugReportSystem
Update-Database
```
### 2. الگوی Enum برای Flutter
```dart
enum BugReportType {
crash, // 1
ui, // 2
performance, // 3
feature, // 4
network, // 5
camera, // 6
faceRecognition, // 7
database, // 8
login, // 9
other, // 10
}
enum BugPriority {
critical, // 1
high, // 2
medium, // 3
low, // 4
}
```
---
## 🚀 نقاط ورود
### API Endpoints
```
POST /api/bugreport/submit - ثبت گزارش جدید
GET /api/bugreport/list - دریافت لیست
GET /api/bugreport/{id} - دریافت جزئیات
PUT /api/bugreport/{id} - ویرایش وضعیت/اولویت
DELETE /api/bugreport/{id} - حذف گزارش
```
### Admin Pages
```
/AdminNew/BugReport - لیست گزارش‌ها
/AdminNew/BugReport/Details/{id} - جزئیات کامل
/AdminNew/BugReport/Edit/{id} - ویرایش
/AdminNew/BugReport/Delete/{id} - حذف
```
---
## 📊 Database Schema
### BugReports جدول
```sql
- id (bigint, PK)
- Title (nvarchar(200))
- Description (ntext)
- UserEmail (nvarchar(150))
- AccountId (bigint, nullable)
- DeviceModel (nvarchar(100))
- OsVersion (nvarchar(50))
- Platform (nvarchar(50))
- Manufacturer (nvarchar(100))
- DeviceId (nvarchar(200))
- ScreenResolution (nvarchar(50))
- MemoryInMB (int)
- StorageInMB (int)
- BatteryLevel (int)
- IsCharging (bit)
- NetworkType (nvarchar(50))
- AppVersion (nvarchar(50))
- BuildNumber (nvarchar(50))
- PackageName (nvarchar(150))
- InstallTime (datetime2)
- LastUpdateTime (datetime2)
- Flavor (nvarchar(50))
- Type (int)
- Priority (int)
- Status (int)
- StackTrace (ntext, nullable)
- CreationDate (datetime2)
- UpdateDate (datetime2, nullable)
```
### BugReportLogs جدول
```sql
- id (bigint, PK)
- BugReportId (bigint, FK)
- Message (ntext)
- Timestamp (datetime2)
```
### BugReportScreenshots جدول
```sql
- id (bigint, PK)
- BugReportId (bigint, FK)
- Base64Data (ntext)
- FileName (nvarchar(255))
- UploadDate (datetime2)
```
---
## ✨ مثال درخواست API
```json
POST /api/bugreport/submit
Content-Type: application/json
{
"title": "برنامه هنگام ورود خراب می‌شود",
"description": "هنگام فشار دادن دکمه ورود، برنامه کرش می‌کند",
"userEmail": "user@example.com",
"accountId": 123,
"deviceModel": "Samsung Galaxy S21",
"osVersion": "Android 12",
"platform": "Android",
"manufacturer": "Samsung",
"deviceId": "device-12345",
"screenResolution": "1440x3200",
"memoryInMB": 8000,
"storageInMB": 256000,
"batteryLevel": 75,
"isCharging": false,
"networkType": "4G",
"appVersion": "1.0.0",
"buildNumber": "100",
"packageName": "com.example.app",
"installTime": "2024-01-01T10:00:00Z",
"lastUpdateTime": "2024-12-07T14:30:00Z",
"flavor": "production",
"type": 1,
"priority": 2,
"stackTrace": "...",
"logs": ["log line 1", "log line 2"],
"screenshots": ["base64-string"]
}
```
---
## 🔐 Security Features
- ✅ Authorization برای Admin Pages (AdminAreaPermission required)
- ✅ API Authentication
- ✅ XSS Protection (Html.Raw محدود)
- ✅ CSRF Protection (ASP.NET Core default)
- ✅ Input Validation
- ✅ Safe Delete with Confirmation
---
## 📚 Documentation Files
1. **BUG_REPORT_SYSTEM.md** - راهنمای کامل سیستم
2. **FLUTTER_BUG_REPORT_EXAMPLE.dart** - مثال پیاده‌سازی Flutter
3. **CHANGELOG.md** (این فایل) - خلاصه تغییرات
---
## ✅ Checklist پیاده‌سازی
- [x] Domain Models
- [x] Database Mappings
- [x] Repository Pattern
- [x] Application Services
- [x] API Endpoints
- [x] Admin UI Pages
- [x] Dependency Injection
- [x] Error Handling
- [x] Documentation
- [x] Flutter Example
- [ ] Database Migration (باید دستی اجرا شود)
- [ ] Testing
---
## 🎯 مراحل بعدی
1. **اجرای Migration:**
```powershell
Add-Migration AddBugReportSystem
Update-Database
```
2. **تست API:**
- استفاده از Postman/Thunder Client
- تست تمام endpoints
3. **تست Admin Panel:**
- دسترسی به /AdminNew/BugReport
- تست فیلترها و جستجو
- تست ویرایش و حذف
4. **Integration Flutter:**
- کپی کردن `FLUTTER_BUG_REPORT_EXAMPLE.dart`
- سازگار کردن با پروژه Flutter
- تست ثبت گزارش‌ها
---
## 📞 پشتیبانی
برای هر سوال یا مشکل:
1. بررسی کنید `BUG_REPORT_SYSTEM.md`
2. بررسی کنید logs و error messages
3. مطمئن شوید Migration اجرا شده است

248
CONFIGURATION_SUMMARY.md Normal file
View File

@@ -0,0 +1,248 @@
# ✅ Docker Bind Mounts Configuration - Summary
## What Was Changed
### 1. docker-compose.yml
**Before:**
```yaml
volumes:
- ./ServiceHost/certs:/app/certs:ro
- app_storage:/app/Storage # ❌ Docker volume
- app_logs:/app/Logs # ❌ Docker volume
volumes:
app_storage:
driver: local
app_logs:
driver: local
```
**After:**
```yaml
volumes:
# ✅ Bind mounts for production-critical data on Windows host
- ./ServiceHost/certs:/app/certs:ro
- D:/AppData/Faces:/app/Faces
- D:/AppData/Storage:/app/Storage
- D:/AppData/Logs:/app/Logs
# ✅ No volumes section needed
```
### 2. New Files Created
- `DOCKER_BIND_MOUNTS_SETUP.md` - Complete documentation
- `setup-bind-mounts.ps1` - Automated setup script
- `QUICK_REFERENCE.md` - Quick command reference
## Path Mapping
| Container (Linux paths) | Windows Host (forward slash) | Actual Windows Path |
|-------------------------|------------------------------|---------------------|
| `/app/Faces` | `D:/AppData/Faces` | `D:\AppData\Faces` |
| `/app/Storage` | `D:/AppData/Storage` | `D:\AppData\Storage`|
| `/app/Logs` | `D:/AppData/Logs` | `D:\AppData\Logs` |
**Note:** Docker Compose on Windows accepts both `D:/` and `D:\` but prefers forward slashes.
## Application Code Compatibility
Your application uses:
```csharp
Path.Combine(env.ContentRootPath, "Faces"); // → /app/Faces
Path.Combine(env.ContentRootPath, "Storage"); // → /app/Storage
```
Where `env.ContentRootPath` = `/app` in the container.
**No code changes required!** The bind mounts map directly to these paths.
## Setup Instructions
### Option 1: Automated Setup (Recommended)
```powershell
# Navigate to project directory
cd D:\GozareshgirOrginal\OriginalGozareshgir
# Run setup script with permissions
.\setup-bind-mounts.ps1 -GrantFullPermissions
# Start the application
docker-compose up -d
```
### Option 2: Manual Setup
```powershell
# 1. Create directories
New-Item -ItemType Directory -Force -Path "D:\AppData\Faces"
New-Item -ItemType Directory -Force -Path "D:\AppData\Storage"
New-Item -ItemType Directory -Force -Path "D:\AppData\Logs"
# 2. Grant permissions
icacls "D:\AppData\Faces" /grant Everyone:F /T
icacls "D:\AppData\Storage" /grant Everyone:F /T
icacls "D:\AppData\Logs" /grant Everyone:F /T
# 3. Start the application
docker-compose up -d
```
## Verification Checklist
After starting the container:
1. **Check if directories are mounted:**
```powershell
docker exec gozareshgir-servicehost ls -la /app
```
Should show: `Faces/`, `Storage/`, `Logs/`
2. **Test write access from container:**
```powershell
docker exec gozareshgir-servicehost sh -c "echo 'test' > /app/Storage/test.txt"
Get-Content D:\AppData\Storage\test.txt # Should display: test
Remove-Item D:\AppData\Storage\test.txt
```
3. **Test write access from host:**
```powershell
"test from host" | Out-File "D:\AppData\Storage\host-test.txt"
docker exec gozareshgir-servicehost cat /app/Storage/host-test.txt
Remove-Item D:\AppData\Storage\host-test.txt
```
4. **Check application logs:**
```powershell
docker logs gozareshgir-servicehost --tail 50
# Or directly on host:
Get-Content D:\AppData\Logs\gozareshgir_log.txt -Tail 50
```
## Data Persistence Guarantees
✅ **Files persist through:**
- `docker-compose down`
- `docker-compose restart`
- Container removal (`docker rm`)
- Image rebuilds (`docker-compose build`)
- Server reboots (with `restart: unless-stopped`)
✅ **Direct access:**
- Files can be accessed from Windows Explorer at `D:\AppData\*`
- Can be backed up using Windows Backup, robocopy, or any backup software
- Can be edited directly on the host (changes visible in container immediately)
⚠️ **Data does NOT survive:**
- Deleting the host directories (`D:\AppData\*`)
- Formatting the D: drive
- Without regular backups, hardware failures
## Production Checklist
Before deploying to production:
- [ ] Run `setup-bind-mounts.ps1 -GrantFullPermissions`
- [ ] Verify disk space on D: drive (at least 50 GB recommended)
- [ ] Set up scheduled backups (see `DOCKER_BIND_MOUNTS_SETUP.md`)
- [ ] Replace `Everyone` with specific service account for permissions
- [ ] Enable NTFS encryption for sensitive data (optional)
- [ ] Test container restart: `docker-compose restart`
- [ ] Test data persistence: Create a test file, restart container, verify file exists
- [ ] Configure monitoring for disk space usage
## Security Recommendations
1. **Restrict permissions** (production):
```powershell
# Replace Everyone with specific account
icacls "D:\AppData\Faces" /grant "DOMAIN\ServiceAccount:(OI)(CI)F" /T
icacls "D:\AppData\Storage" /grant "DOMAIN\ServiceAccount:(OI)(CI)F" /T
icacls "D:\AppData\Logs" /grant "DOMAIN\ServiceAccount:(OI)(CI)F" /T
```
2. **Enable encryption** for sensitive data:
```powershell
cipher /e "D:\AppData\Faces"
cipher /e "D:\AppData\Storage"
```
3. **Set up audit logging:**
```powershell
auditpol /set /subcategory:"File System" /success:enable /failure:enable
```
## Backup Strategy
### Scheduled Backup (Recommended)
```powershell
# Create daily backup at 2 AM
$action = New-ScheduledTaskAction -Execute "robocopy" -Argument '"D:\AppData" "D:\Backups\AppData" /MIR /Z /LOG:"D:\Backups\backup.log"'
$trigger = New-ScheduledTaskTrigger -Daily -At 2am
Register-ScheduledTask -Action $action -Trigger $trigger -TaskName "GozareshgirBackup" -Description "Daily backup of Gozareshgir data"
```
### Manual Backup
```powershell
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
robocopy "D:\AppData" "D:\Backups\AppData_$timestamp" /MIR /Z
```
## Troubleshooting
### Issue: Container starts but files not appearing
**Solution:**
```powershell
# Check mount points
docker inspect gozareshgir-servicehost --format='{{json .Mounts}}' | ConvertFrom-Json
# Verify directories exist
Test-Path D:\AppData\Faces
Test-Path D:\AppData\Storage
Test-Path D:\AppData\Logs
```
### Issue: Permission denied errors
**Solution:**
```powershell
# Re-grant permissions
icacls "D:\AppData\Faces" /grant Everyone:F /T
icacls "D:\AppData\Storage" /grant Everyone:F /T
icacls "D:\AppData\Logs" /grant Everyone:F /T
```
### Issue: Out of disk space
**Solution:**
```powershell
# Check disk usage
Get-ChildItem D:\AppData -Recurse | Measure-Object -Property Length -Sum
# Clean old log files (example: older than 30 days)
Get-ChildItem D:\AppData\Logs -Recurse -File | Where-Object {$_.LastWriteTime -lt (Get-Date).AddDays(-30)} | Remove-Item
```
## Support & Documentation
- **Full Documentation:** `DOCKER_BIND_MOUNTS_SETUP.md`
- **Quick Reference:** `QUICK_REFERENCE.md`
- **Setup Script:** `setup-bind-mounts.ps1`
## Migration from Docker Volumes (If applicable)
If you previously used Docker volumes, migrate the data:
```powershell
# 1. Stop the container
docker-compose down
# 2. Copy data from old volumes to host
docker run --rm -v old_volume_name:/source -v D:/AppData/Storage:/dest alpine cp -av /source/. /dest/
# 3. Start with new bind mounts
docker-compose up -d
```
---
**Configuration Date:** January 2026
**Tested On:** Windows Server 2019/2022 with Docker Desktop
**Status:** ✅ Production Ready

View File

@@ -21,8 +21,9 @@
<ProjectReference Include="..\_0_Framework\_0_Framework_b.csproj" /> <ProjectReference Include="..\_0_Framework\_0_Framework_b.csproj" />
</ItemGroup> </ItemGroup>
<Target Name="CopyDocs" AfterTargets="Build"> <Target Name="CopyDocs" AfterTargets="Build">
<Copy SourceFiles="$(OutputPath)CompanyManagment.App.Contracts.xml" DestinationFolder="../ServiceHost\bin\Debug\net8.0\" /> <Copy SourceFiles="$(TargetDir)CompanyManagment.App.Contracts.xml"
</Target> DestinationFolder="../ServiceHost\bin\$(Configuration)\net10.0\"
Condition="Exists('$(TargetDir)CompanyManagment.App.Contracts.xml')" />
</Target>
</Project> </Project>

View File

@@ -79,13 +79,12 @@ public interface IInstitutionContractApplication
/// <returns>لیست قراردادها برای چاپ</returns> /// <returns>لیست قراردادها برای چاپ</returns>
List<InstitutionContractViewModel> PrintAll(List<long> id); List<InstitutionContractViewModel> PrintAll(List<long> id);
[Obsolete("استفاده نشود، از متد غیرهمزمان استفاده شود")]
/// <summary> /// <summary>
/// چاپ یک قرارداد /// چاپ یک قرارداد
/// </summary> /// </summary>
/// <param name="id">شناسه قرارداد</param> /// <param name="id">شناسه قرارداد</param>
/// <returns>اطلاعات قرارداد برای چاپ</returns> /// <returns>اطلاعات قرارداد برای چاپ</returns>
[Obsolete("استفاده نشود، از متد غیرهمزمان استفاده شود")]
InstitutionContractViewModel PrintOne(long id); InstitutionContractViewModel PrintOne(long id);
/// <summary> /// <summary>

View File

@@ -1,297 +0,0 @@
# 📋 Delivery Checklist - سیستم گزارش خرابی
## ✅ تمام فایل‌ها ایجاد شده‌اند
### Domain Models (3/3)
- [x] BugReport.cs - اصلی
- [x] BugReportLog.cs - لاگ‌ها
- [x] BugReportScreenshot.cs - عکس‌ها
### Application Contracts (6/6)
- [x] IBugReportApplication.cs - اینترفیس
- [x] IBugReportRepository.cs - Repository interface
- [x] CreateBugReportCommand.cs - Create DTO
- [x] EditBugReportCommand.cs - Edit DTO
- [x] BugReportViewModel.cs - List view model
- [x] BugReportDetailViewModel.cs - Detail view model
### Application Service (1/1)
- [x] BugReportApplication.cs - Service implementation
### Infrastructure (4/4)
- [x] BugReportMapping.cs - EFCore mapping
- [x] BugReportLogMapping.cs - Log mapping
- [x] BugReportScreenshotMapping.cs - Screenshot mapping
- [x] BugReportRepository.cs - Repository implementation
### API (1/1)
- [x] BugReportController.cs - 5 endpoints
### Admin Pages (9/9)
- [x] BugReportPageModel.cs - Base page model
- [x] Index.cshtml.cs + Index.cshtml - List
- [x] Details.cshtml.cs + Details.cshtml - Details
- [x] Edit.cshtml.cs + Edit.cshtml - Edit
- [x] Delete.cshtml.cs + Delete.cshtml - Delete
### Configuration (1/1)
- [x] AccountManagementBootstrapper.cs - DI updated
### Infrastructure Context (1/1)
- [x] AccountContext.cs - DbSets updated
### Documentation (4/4)
- [x] BUG_REPORT_SYSTEM.md - کامل
- [x] FLUTTER_BUG_REPORT_EXAMPLE.dart - مثال
- [x] CHANGELOG.md - تغییرات
- [x] QUICK_START.md - شروع سریع
---
## 📊 خلاصه
| موضوع | تعداد | وضعیت |
|------|------|------|
| Domain Models | 3 | ✅ کامل |
| DTOs/Commands | 4 | ✅ کامل |
| ViewModels | 2 | ✅ کامل |
| Application Service | 1 | ✅ کامل |
| Infrastructure Mapping | 3 | ✅ کامل |
| Repository | 1 | ✅ کامل |
| API Endpoints | 5 | ✅ کامل |
| Admin Pages | 4 | ✅ کامل |
| Documentation | 4 | ✅ کامل |
| **کل** | **28** | **✅ کامل** |
---
## 🎯 API Endpoints
### ✅ 5 Endpoints
```
1. POST /api/bugreport/submit - ثبت
2. GET /api/bugreport/list - لیست
3. GET /api/bugreport/{id} - جزئیات
4. PUT /api/bugreport/{id} - ویرایش
5. DELETE /api/bugreport/{id} - حذف
```
---
## 🖥️ Admin Pages
### ✅ 4 Pages
```
1. Index - لیست با فیلترها
2. Details - جزئیات کامل
3. Edit - ویرایش وضعیت
4. Delete - حذف
```
---
## 🗄️ Database
### ✅ 3 Tables
```
1. BugReports - گزارش‌های اصلی
2. BugReportLogs - لاگ‌های گزارش
3. BugReportScreenshots - عکس‌های گزارش
```
---
## 🔧 Configuration
### ✅ Dependency Injection
```csharp
services.AddTransient<IBugReportApplication, BugReportApplication>();
services.AddTransient<IBugReportRepository, BugReportRepository>();
```
### ✅ DbContext
```csharp
public DbSet<BugReport> BugReports { get; set; }
public DbSet<BugReportLog> BugReportLogs { get; set; }
public DbSet<BugReportScreenshot> BugReportScreenshots { get; set; }
```
---
## 📚 Documentation
### ✅ 4 نوع Documentation
1. **BUG_REPORT_SYSTEM.md**
- نمای کلی
- ساختار فایل‌ها
- روش استفاده
- Enums
- Security
2. **FLUTTER_BUG_REPORT_EXAMPLE.dart**
- مثال Dart
- BugReportRequest class
- BugReportService class
- AppErrorHandler class
- Setup example
3. **CHANGELOG.md**
- لیست تمام فایل‌های ایجاد شده
- فایل‌های اصلاح شده
- Database schema
- Endpoints
- Security features
4. **QUICK_START.md**
- 9 مراحل
- Setup اولیه
- تست API
- Admin panel
- Flutter integration
- مشکل‌شناسی
- مثال عملی
---
## ✨ Features
### ✅ جمع‌آوری اطلاعات
- معلومات دستگاه (مدل، OS، حافظه، باتری، شبکه)
- معلومات برنامه (نسخه، بیلد، پکیج)
- لاگ‌های برنامه
- عکس‌های صفحه (Base64)
- Stack Trace
### ✅ مدیریت
- ثبت خودکار
- فیلترینگ (نوع، اولویت، وضعیت)
- جستجو
- Pagination
### ✅ Admin Panel
- لیست کامل
- جزئیات پر اطلاعات
- تغییر وضعیت و اولویت
- حذف محفوظ
- نمایش عکس‌ها
- نمایش لاگ‌ها
---
## 🔐 Security
- ✅ Authorization (AdminAreaPermission required)
- ✅ Authentication
- ✅ Input Validation
- ✅ XSS Protection
- ✅ CSRF Protection
- ✅ Safe Delete
---
## 🚀 Ready to Deploy
### Pre-Deployment Checklist
- [x] تمام کد نوشته شده و تست شده
- [x] Documentation کامل شده
- [x] Error handling اضافه شده
- [x] Security measures اضافه شده
- [x] Examples و tutorials آماده شده
### Deployment Steps
1. ✅ Add-Migration AddBugReportSystem
2. ✅ Update-Database
3. ✅ Build project
4. ✅ Deploy to server
5. ✅ Test all endpoints
6. ✅ Test admin pages
7. ✅ Integrate with Flutter
---
## 📞 Support Documentation
### سوالات متداول پاسخ شده:
- ✅ چگونه ثبت کنیم؟
- ✅ چگونه لیست ببینیم؟
- ✅ چگونه مشاهده کنیم؟
- ✅ چگونه ویرایش کنیم؟
- ✅ چگونه حذف کنیم؟
- ✅ چگونه Flutter integrate کنیم؟
- ✅ مشکل‌شناسی چگونه؟
---
## 📦 Deliverables
### Code Files (25)
- 3 Domain Models
- 6 Contracts
- 1 Application Service
- 4 Infrastructure
- 1 API Controller
- 9 Admin Pages
- 1 Updated Bootstrapper
- 1 Updated Context
### Documentation (4)
- BUG_REPORT_SYSTEM.md
- FLUTTER_BUG_REPORT_EXAMPLE.dart
- CHANGELOG.md
- QUICK_START.md
---
## 🎉 نتیجه نهایی
**سیستم گزارش خرابی (Bug Report System) کامل شده است**
**وضعیت:** آماده برای استفاده
**Testing:** Ready
**Documentation:** Complete
**Security:** Implemented
**Flutter Integration:** Example provided
---
## ✅ تأیید
- [x] کد quality: ✅ بالا
- [x] Documentation: ✅ کامل
- [x] Security: ✅ محفوظ
- [x] Performance: ✅ بهینه
- [x] User Experience: ✅ خوب
---
## 🎯 Next Step
**اجرای Database Migration:**
```powershell
Add-Migration AddBugReportSystem
Update-Database
```
**سپس:**
- ✅ API را تست کنید
- ✅ Admin Panel را بررسی کنید
- ✅ Flutter integration را انجام دهید
- ✅ در production deploy کنید
---
**تاریخ:** 7 دسامبر 2024
**نسخه:** 1.0
**وضعیت:** ✅ تکمیل شده
🚀 **آماده برای استفاده!**

255
DOCKER_BIND_MOUNTS_SETUP.md Normal file
View File

@@ -0,0 +1,255 @@
# Docker Bind Mounts Setup for Windows Server
## Overview
This application uses **bind mounts** (not Docker volumes) to store business-critical files directly on the Windows host filesystem.
## Directory Structure
### Container Paths (inside Docker)
- `/app/Faces` - User face recognition data
- `/app/Storage` - Uploaded files and documents
- `/app/Logs` - Application logs
### Windows Host Paths
- `D:\AppData\Faces`
- `D:\AppData\Storage`
- `D:\AppData\Logs`
## Initial Setup
### 1. Create Host Directories
Before starting the container, create the required directories on the Windows host:
```powershell
# Create directories if they don't exist
New-Item -ItemType Directory -Force -Path "D:\AppData\Faces"
New-Item -ItemType Directory -Force -Path "D:\AppData\Storage"
New-Item -ItemType Directory -Force -Path "D:\AppData\Logs"
```
### 2. Set Permissions (Windows Server)
Grant full access to the directories for the Docker container:
```powershell
# Grant full control to Everyone (or specific user account)
icacls "D:\AppData\Faces" /grant Everyone:F /T
icacls "D:\AppData\Storage" /grant Everyone:F /T
icacls "D:\AppData\Logs" /grant Everyone:F /T
```
**Note:** For production, replace `Everyone` with a specific service account:
```powershell
# Example with specific user
icacls "D:\AppData\Faces" /grant "DOMAIN\ServiceAccount:(OI)(CI)F" /T
icacls "D:\AppData\Storage" /grant "DOMAIN\ServiceAccount:(OI)(CI)F" /T
icacls "D:\AppData\Logs" /grant "DOMAIN\ServiceAccount:(OI)(CI)F" /T
```
## Docker Compose Configuration
The `docker-compose.yml` is already configured with bind mounts:
```yaml
volumes:
- ./ServiceHost/certs:/app/certs:ro
- D:/AppData/Faces:/app/Faces
- D:/AppData/Storage:/app/Storage
- D:/AppData/Logs:/app/Logs
```
### Start the Application
```powershell
docker-compose up -d
```
## Alternative: Docker Run Command
If you prefer using `docker run` instead of docker-compose:
```powershell
docker run -d `
--name gozareshgir-servicehost `
-p 5003:80 `
-p 5004:443 `
-v "D:/AppData/Faces:/app/Faces" `
-v "D:/AppData/Storage:/app/Storage" `
-v "D:/AppData/Logs:/app/Logs" `
-v "${PWD}/ServiceHost/certs:/app/certs:ro" `
--env-file ./ServiceHost/.env `
--add-host=host.docker.internal:host-gateway `
--restart unless-stopped `
gozareshgir-servicehost:latest
```
## Verification
### 1. Check if directories are mounted correctly
```powershell
docker exec gozareshgir-servicehost ls -la /app
```
You should see:
```
drwxr-xr-x Faces
drwxr-xr-x Storage
drwxr-xr-x Logs
```
### 2. Test write access
```powershell
# Create a test file from within the container
docker exec gozareshgir-servicehost sh -c "echo 'test' > /app/Storage/test.txt"
# Verify it appears on the host
Get-Content "D:\AppData\Storage\test.txt"
# Clean up
Remove-Item "D:\AppData\Storage\test.txt"
```
### 3. Verify from the host side
```powershell
# Create a file on the host
"test from host" | Out-File -FilePath "D:\AppData\Storage\host-test.txt"
# Check if visible in container
docker exec gozareshgir-servicehost cat /app/Storage/host-test.txt
# Clean up
Remove-Item "D:\AppData\Storage\host-test.txt"
```
## Application Code Compatibility
The application uses:
```csharp
Path.Combine(env.ContentRootPath, "Faces");
Path.Combine(env.ContentRootPath, "Storage");
```
Where `env.ContentRootPath` = `/app` in the container.
**No code changes required** - the bind mounts map exactly to these paths.
## Data Persistence & Safety
**Benefits of Bind Mounts:**
- Files persist on host even if container is removed
- Direct backup from Windows Server (e.g., Windows Backup, robocopy)
- Can be accessed by other applications/services on the host
- No Docker volume management needed
- Easy to migrate to a different server
**Safety:**
- Data survives `docker-compose down`
- Data survives `docker rm`
- Data survives container rebuilds
- Can be included in host backup solutions
⚠️ **Important:**
- Do NOT delete the host directories (`D:\AppData\*`)
- Ensure adequate disk space on D: drive
- Regular backups of `D:\AppData\` recommended
## Backup Strategy
### Manual Backup
```powershell
# Create a timestamped backup
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
robocopy "D:\AppData" "D:\Backups\AppData_$timestamp" /MIR /Z /LOG:"D:\Backups\backup_$timestamp.log"
```
### Scheduled Backup (Task Scheduler)
```powershell
# Create a scheduled task for daily backups
$action = New-ScheduledTaskAction -Execute "robocopy" -Argument '"D:\AppData" "D:\Backups\AppData" /MIR /Z'
$trigger = New-ScheduledTaskTrigger -Daily -At 2am
Register-ScheduledTask -Action $action -Trigger $trigger -TaskName "GozareshgirBackup" -Description "Daily backup of application data"
```
## Troubleshooting
### Issue: Permission Denied
```powershell
# Fix permissions
icacls "D:\AppData\Faces" /grant Everyone:F /T
icacls "D:\AppData\Storage" /grant Everyone:F /T
icacls "D:\AppData\Logs" /grant Everyone:F /T
```
### Issue: Directory Not Found
```powershell
# Ensure directories exist
Test-Path "D:\AppData\Faces"
Test-Path "D:\AppData\Storage"
Test-Path "D:\AppData\Logs"
# Create if missing
New-Item -ItemType Directory -Force -Path "D:\AppData\Faces"
New-Item -ItemType Directory -Force -Path "D:\AppData\Storage"
New-Item -ItemType Directory -Force -Path "D:\AppData\Logs"
```
### Issue: Files Not Appearing
1. Check container logs:
```powershell
docker logs gozareshgir-servicehost
```
2. Verify mount points:
```powershell
docker inspect gozareshgir-servicehost --format='{{json .Mounts}}' | ConvertFrom-Json
```
3. Test write access (see Verification section above)
## Migration Notes
### Moving to a Different Server
1. Stop the container:
```powershell
docker-compose down
```
2. Copy the data:
```powershell
robocopy "D:\AppData" "\\NewServer\D$\AppData" /MIR /Z
```
3. On the new server, ensure directories exist and have correct permissions
4. Start the container on the new server:
```powershell
docker-compose up -d
```
## Performance Considerations
- **Bind mounts on Windows** have good performance for most workloads
- For high-frequency writes, consider using SSD storage for `D:\AppData`
- Monitor disk space regularly:
```powershell
Get-PSDrive D | Select-Object Used,Free
```
## Security Best Practices
1. **Restrict permissions** to specific service accounts (not Everyone)
2. **Enable NTFS encryption** for sensitive data:
```powershell
cipher /e "D:\AppData\Faces"
cipher /e "D:\AppData\Storage"
```
3. **Regular backups** with retention policy
4. **Firewall rules** to restrict access to the host
5. **Audit logging** for file access:
```powershell
auditpol /set /subcategory:"File System" /success:enable /failure:enable
```
---
**Last Updated:** January 2026
**Tested On:** Windows Server 2019/2022 with Docker Desktop or Docker Engine

View File

@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00 Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17 # Visual Studio Version 18
VisualStudioVersion = 17.1.32210.238 VisualStudioVersion = 18.2.11415.280 d18.0
MinimumVisualStudioVersion = 10.0.40219.1 MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Company", "Company", "{FAF16FCC-F7E6-4F0B-AF35-95368A4A0736}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Company", "Company", "{FAF16FCC-F7E6-4F0B-AF35-95368A4A0736}"
EndProject EndProject
@@ -237,6 +237,10 @@ Global
{08B234B6-783B-44E9-9961-4F97EAD16308}.Debug|Any CPU.Build.0 = Debug|Any CPU {08B234B6-783B-44E9-9961-4F97EAD16308}.Debug|Any CPU.Build.0 = Debug|Any CPU
{08B234B6-783B-44E9-9961-4F97EAD16308}.Release|Any CPU.ActiveCfg = Release|Any CPU {08B234B6-783B-44E9-9961-4F97EAD16308}.Release|Any CPU.ActiveCfg = Release|Any CPU
{08B234B6-783B-44E9-9961-4F97EAD16308}.Release|Any CPU.Build.0 = Release|Any CPU {08B234B6-783B-44E9-9961-4F97EAD16308}.Release|Any CPU.Build.0 = Release|Any CPU
{81DDED9D-158B-E303-5F62-77A2896D2A5A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{81DDED9D-158B-E303-5F62-77A2896D2A5A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{81DDED9D-158B-E303-5F62-77A2896D2A5A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{81DDED9D-158B-E303-5F62-77A2896D2A5A}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

107
Dockerfile Normal file
View File

@@ -0,0 +1,107 @@
# Multi-stage build for ASP.NET Core 10
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
# Copy solution and project files
COPY ["DadmehrGostar.sln", "DadmehrGostar.sln"]
COPY ["ServiceHost/ServiceHost.csproj", "ServiceHost/"]
COPY ["0_Framework/0_Framework.csproj", "0_Framework/"]
COPY ["_0_Framework/_0_Framework_b.csproj", "_0_Framework/"]
COPY ["AccountManagement.Application/AccountManagement.Application.csproj", "AccountManagement.Application/"]
COPY ["AccountManagement.Application.Contracts/AccountManagement.Application.Contracts.csproj", "AccountManagement.Application.Contracts/"]
COPY ["AccountManagement.Configuration/AccountManagement.Configuration.csproj", "AccountManagement.Configuration/"]
COPY ["AccountManagement.Domain/AccountManagement.Domain.csproj", "AccountManagement.Domain/"]
COPY ["AccountMangement.Infrastructure.EFCore/AccountMangement.Infrastructure.EFCore.csproj", "AccountMangement.Infrastructure.EFCore/"]
COPY ["BackgroundInstitutionContract/BackgroundInstitutionContract.Task/BackgroundInstitutionContract.Task.csproj", "BackgroundInstitutionContract/BackgroundInstitutionContract.Task/"]
COPY ["Company.Domain/Company.Domain.csproj", "Company.Domain/"]
COPY ["CompanyManagement.Infrastructure.Excel/CompanyManagement.Infrastructure.Excel.csproj", "CompanyManagement.Infrastructure.Excel/"]
COPY ["CompanyManagement.Infrastructure.Mongo/CompanyManagement.Infrastructure.Mongo.csproj", "CompanyManagement.Infrastructure.Mongo/"]
COPY ["CompanyManagment.App.Contracts/CompanyManagment.App.Contracts.csproj", "CompanyManagment.App.Contracts/"]
COPY ["CompanyManagment.Application/CompanyManagment.Application.csproj", "CompanyManagment.Application/"]
COPY ["CompanyManagment.EFCore/CompanyManagment.EFCore.csproj", "CompanyManagment.EFCore/"]
COPY ["PersonalContractingParty.Config/PersonalContractingParty.Config.csproj", "PersonalContractingParty.Config/"]
COPY ["ProgramManager/src/Application/GozareshgirProgramManager.Application/GozareshgirProgramManager.Application.csproj", "ProgramManager/src/Application/GozareshgirProgramManager.Application/"]
COPY ["ProgramManager/src/Domain/GozareshgirProgramManager.Domain/GozareshgirProgramManager.Domain.csproj", "ProgramManager/src/Domain/GozareshgirProgramManager.Domain/"]
COPY ["ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/GozareshgirProgramManager.Infrastructure.csproj", "ProgramManager/src/Infrastructure/GozareshgirProgramManager.Infrastructure/"]
COPY ["Query/Query.csproj", "Query/"]
COPY ["Query.Bootstrapper/Query.Bootstrapper.csproj", "Query.Bootstrapper/"]
COPY ["Shared.Contracts/Shared.Contracts.csproj", "Shared.Contracts/"]
COPY ["WorkFlow/Application/WorkFlow.Application/WorkFlow.Application.csproj", "WorkFlow/Application/WorkFlow.Application/"]
COPY ["WorkFlow/Application/WorkFlow.Application.Contracts/WorkFlow.Application.Contracts.csproj", "WorkFlow/Application/WorkFlow.Application.Contracts/"]
COPY ["WorkFlow/Domain/WorkFlow.Domain/WorkFlow.Domain.csproj", "WorkFlow/Domain/WorkFlow.Domain/"]
COPY ["WorkFlow/Infrastructure/WorkFlow.Infrastructure.ACL/WorkFlow.Infrastructure.ACL.csproj", "WorkFlow/Infrastructure/WorkFlow.Infrastructure.ACL/"]
COPY ["WorkFlow/Infrastructure/WorkFlow.Infrastructure.Config/WorkFlow.Infrastructure.Config.csproj", "WorkFlow/Infrastructure/WorkFlow.Infrastructure.Config/"]
COPY ["WorkFlow/Infrastructure/WorkFlow.Infrastructure.EfCore/WorkFlow.Infrastructure.EfCore.csproj", "WorkFlow/Infrastructure/WorkFlow.Infrastructure.EfCore/"]
COPY ["BackgroundJobs/BackgroundJobs.Task/BackgroundJobs.Task.csproj", "BackgroundJobs/BackgroundJobs.Task/"]
COPY ["backService/backService.csproj", "backService/"]
# Restore all projects
RUN dotnet restore "ServiceHost/ServiceHost.csproj"
# Copy source code
COPY . .
# Build the ServiceHost project
WORKDIR /src/ServiceHost
RUN dotnet build "ServiceHost.csproj" -c Release -o /app/build
# Publish stage
FROM build AS publish
RUN dotnet publish "ServiceHost.csproj" -c Release -o /app/publish /p:UseAppHost=false
# Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
WORKDIR /app
# Install tzdata and set timezone
# tzdata for timzone
RUN apt-get update && apt-get install -y tzdata && \
cp /usr/share/zoneinfo/Asia/Tehran /etc/localtime && \
echo "Asia/Tehran" > /etc/timezone
# timezone env with default
ENV TZ='Asia/Tehran'
# Install curl for health checks
#RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
# Copy published app
COPY --from=publish /app/publish .
# -------------------------------------------------------------------
# ✅ روش اصلاح شده و امن برای هندل کردن فایل‌های دارای فاصله (Space)
# -------------------------------------------------------------------
RUN echo '#!/bin/bash' > /rename_script.sh && \
echo 'find /app/wwwroot -depth -name "*[A-Z]*" -print0 | while IFS= read -r -d "" file; do' >> /rename_script.sh && \
echo ' dir=$(dirname "$file")' >> /rename_script.sh && \
echo ' base=$(basename "$file")' >> /rename_script.sh && \
echo ' lower=$(echo "$base" | tr "[:upper:]" "[:lower:]")' >> /rename_script.sh && \
echo ' if [ "$base" != "$lower" ]; then' >> /rename_script.sh && \
echo ' mv -f "$file" "$dir/$lower"' >> /rename_script.sh && \
echo ' fi' >> /rename_script.sh && \
echo 'done' >> /rename_script.sh && \
chmod +x /rename_script.sh && \
/rename_script.sh && \
rm /rename_script.sh
# Create directories for certificates, storage, faces, and logs
# Note: Bind-mounted directories will override these, but we create them for consistency
RUN mkdir -p /app/certs /app/Faces /app/Storage /app/Logs app/InsuranceList && \
chmod 777 /app/Faces /app/Storage /app/Logs app/InsuranceList && \
chmod 755 /app/certs
# Expose ports
EXPOSE 80 443
# Health check - check both HTTP and HTTPS
#HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
# CMD curl -f http://localhost:80/health || curl -f -k https://localhost:443/health || exit 1
# Set entry point
ENTRYPOINT ["dotnet", "ServiceHost.dll"]

View File

@@ -1,214 +0,0 @@
/// مثال استفاده از Bug Report در Flutter
/// ابتدا مدل‌های Dart را برای تطابق با API ایجاد کنید:
class BugReportRequest {
final String title;
final String description;
final String userEmail;
final int? accountId;
final String deviceModel;
final String osVersion;
final String platform;
final String manufacturer;
final String deviceId;
final String screenResolution;
final int memoryInMB;
final int storageInMB;
final int batteryLevel;
final bool isCharging;
final String networkType;
final String appVersion;
final String buildNumber;
final String packageName;
final DateTime installTime;
final DateTime lastUpdateTime;
final String flavor;
final int type; // BugReportType enum value
final int priority; // BugPriority enum value
final String? stackTrace;
final List<String>? logs;
final List<String>? screenshots; // Base64 encoded
BugReportRequest({
required this.title,
required this.description,
required this.userEmail,
this.accountId,
required this.deviceModel,
required this.osVersion,
required this.platform,
required this.manufacturer,
required this.deviceId,
required this.screenResolution,
required this.memoryInMB,
required this.storageInMB,
required this.batteryLevel,
required this.isCharging,
required this.networkType,
required this.appVersion,
required this.buildNumber,
required this.packageName,
required this.installTime,
required this.lastUpdateTime,
required this.flavor,
required this.type,
required this.priority,
this.stackTrace,
this.logs,
this.screenshots,
});
Map<String, dynamic> toJson() {
return {
'title': title,
'description': description,
'userEmail': userEmail,
'accountId': accountId,
'deviceModel': deviceModel,
'osVersion': osVersion,
'platform': platform,
'manufacturer': manufacturer,
'deviceId': deviceId,
'screenResolution': screenResolution,
'memoryInMB': memoryInMB,
'storageInMB': storageInMB,
'batteryLevel': batteryLevel,
'isCharging': isCharging,
'networkType': networkType,
'appVersion': appVersion,
'buildNumber': buildNumber,
'packageName': packageName,
'installTime': installTime.toIso8601String(),
'lastUpdateTime': lastUpdateTime.toIso8601String(),
'flavor': flavor,
'type': type,
'priority': priority,
'stackTrace': stackTrace,
'logs': logs,
'screenshots': screenshots,
};
}
}
/// سرویس برای ارسال Bug Report:
class BugReportService {
final Dio dio;
BugReportService(this.dio);
Future<bool> submitBugReport(BugReportRequest report) async {
try {
final response = await dio.post(
'/api/bugreport/submit',
data: report.toJson(),
options: Options(
validateStatus: (status) => status! < 500,
headers: {
'Content-Type': 'application/json',
},
),
);
return response.statusCode == 200;
} catch (e) {
print('Error submitting bug report: $e');
return false;
}
}
}
/// استفاده در یک Error Handler:
class AppErrorHandler {
final BugReportService bugReportService;
final DeviceInfoService deviceInfoService;
AppErrorHandler(this.bugReportService, this.deviceInfoService);
Future<void> handleError(
FlutterErrorDetails details, {
String? userEmail,
int? accountId,
String? bugTitle,
int bugType = 1, // Crash
int bugPriority = 1, // Critical
}) async {
try {
final deviceInfo = await deviceInfoService.getDeviceInfo();
final report = BugReportRequest(
title: bugTitle ?? 'برنامه کرش کرد',
description: details.exceptionAsString(),
userEmail: userEmail ?? 'unknown@example.com',
accountId: accountId,
deviceModel: deviceInfo['model'],
osVersion: deviceInfo['osVersion'],
platform: deviceInfo['platform'],
manufacturer: deviceInfo['manufacturer'],
deviceId: deviceInfo['deviceId'],
screenResolution: deviceInfo['screenResolution'],
memoryInMB: deviceInfo['memoryInMB'],
storageInMB: deviceInfo['storageInMB'],
batteryLevel: deviceInfo['batteryLevel'],
isCharging: deviceInfo['isCharging'],
networkType: deviceInfo['networkType'],
appVersion: deviceInfo['appVersion'],
buildNumber: deviceInfo['buildNumber'],
packageName: deviceInfo['packageName'],
installTime: deviceInfo['installTime'],
lastUpdateTime: deviceInfo['lastUpdateTime'],
flavor: deviceInfo['flavor'],
type: bugType,
priority: bugPriority,
stackTrace: details.stack.toString(),
logs: await _collectLogs(),
screenshots: await _captureScreenshots(),
);
await bugReportService.submitBugReport(report);
} catch (e) {
print('Error handling bug report: $e');
}
}
Future<List<String>> _collectLogs() async {
// جمع‌آوری لاگ‌های برنامه
return [];
}
Future<List<String>> _captureScreenshots() async {
// گرفتن عکس‌های صفحه به صورت Base64
return [];
}
}
/// مثال استفاده:
void setupErrorHandling() {
final bugReportService = BugReportService(dio);
final errorHandler = AppErrorHandler(bugReportService, deviceInfoService);
FlutterError.onError = (FlutterErrorDetails details) {
errorHandler.handleError(
details,
userEmail: getCurrentUserEmail(),
accountId: getCurrentAccountId(),
bugTitle: 'خطای نامشخص',
bugType: 1, // Crash
bugPriority: 1, // Critical
);
};
PlatformDispatcher.instance.onError = (error, stack) {
errorHandler.handleError(
FlutterErrorDetails(
exception: error,
stack: stack,
context: ErrorDescription('Platform error'),
),
);
return true;
};
}

142
QUICK_REFERENCE.md Normal file
View File

@@ -0,0 +1,142 @@
# 🚀 Quick Reference - Docker Bind Mounts
## Setup (First Time Only)
```powershell
# Run the setup script
.\setup-bind-mounts.ps1 -GrantFullPermissions
# Or manually create directories
New-Item -ItemType Directory -Force -Path "D:\AppData\Faces"
New-Item -ItemType Directory -Force -Path "D:\AppData\Storage"
New-Item -ItemType Directory -Force -Path "D:\AppData\Logs"
# Grant permissions
icacls "D:\AppData\Faces" /grant Everyone:F /T
icacls "D:\AppData\Storage" /grant Everyone:F /T
icacls "D:\AppData\Logs" /grant Everyone:F /T
```
## Daily Operations
### Start Container
```powershell
docker-compose up -d
```
### Stop Container
```powershell
docker-compose down
```
### View Logs
```powershell
docker-compose logs -f
# Or check the host directory
Get-Content D:\AppData\Logs\gozareshgir_log.txt -Tail 50 -Wait
```
### Restart Container
```powershell
docker-compose restart
```
### Rebuild & Restart
```powershell
docker-compose down
docker-compose build --no-cache
docker-compose up -d
```
## Verification Commands
### Check if directories are mounted
```powershell
docker exec gozareshgir-servicehost ls -la /app
```
### Test write access from container
```powershell
docker exec gozareshgir-servicehost sh -c "echo 'test' > /app/Storage/test.txt"
Get-Content D:\AppData\Storage\test.txt
Remove-Item D:\AppData\Storage\test.txt
```
### View mount details
```powershell
docker inspect gozareshgir-servicehost --format='{{json .Mounts}}' | ConvertFrom-Json | Format-List
```
## Troubleshooting
### Permission Issues
```powershell
# Fix permissions
icacls "D:\AppData\Faces" /grant Everyone:F /T
icacls "D:\AppData\Storage" /grant Everyone:F /T
icacls "D:\AppData\Logs" /grant Everyone:F /T
```
### Check Disk Space
```powershell
Get-PSDrive D | Select-Object Used,Free,@{Name="FreeGB";Expression={[math]::Round($_.Free/1GB,2)}}
```
### View Container Logs
```powershell
docker logs gozareshgir-servicehost --tail 100 -f
```
## Backup
### Manual Backup
```powershell
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
robocopy "D:\AppData" "D:\Backups\AppData_$timestamp" /MIR /Z
```
### Quick Backup (no mirroring)
```powershell
robocopy "D:\AppData" "D:\Backups\AppData" /E /Z
```
## Path Mapping Reference
| Container Path | Windows Host Path | Purpose |
|-----------------|-----------------------|----------------------------|
| `/app/Faces` | `D:\AppData\Faces` | Face recognition data |
| `/app/Storage` | `D:\AppData\Storage` | Uploaded files/documents |
| `/app/Logs` | `D:\AppData\Logs` | Application logs |
| `/app/certs` | `.\ServiceHost\certs` | SSL certificates (readonly)|
## Important Notes
**Data is persistent** - survives container removal and rebuilds
**Direct access** - files can be accessed directly from Windows Explorer
**Real-time sync** - changes in container appear on host immediately
⚠️ **Do not delete** - `D:\AppData\*` directories contain production data
⚠️ **Backup regularly** - set up scheduled backups for business continuity
## Docker Run Alternative
If not using docker-compose:
```powershell
docker run -d `
--name gozareshgir-servicehost `
-p 5003:80 `
-p 5004:443 `
-v "D:/AppData/Faces:/app/Faces" `
-v "D:/AppData/Storage:/app/Storage" `
-v "D:/AppData/Logs:/app/Logs" `
-v "${PWD}/ServiceHost/certs:/app/certs:ro" `
--env-file ./ServiceHost/.env `
--add-host=host.docker.internal:host-gateway `
--restart unless-stopped `
gozareshgir-servicehost:latest
```
---
📖 **Full documentation:** `DOCKER_BIND_MOUNTS_SETUP.md`

View File

@@ -1,4 +1,5 @@
using System.Reflection; using System.Net;
using System.Reflection;
using _0_Framework.Application.Sms; using _0_Framework.Application.Sms;
using _0_Framework.Application; using _0_Framework.Application;
using AccountManagement.Configuration; using AccountManagement.Configuration;
@@ -31,516 +32,361 @@ using GozareshgirProgramManager.Application.Interfaces;
using GozareshgirProgramManager.Application.Modules.Users.Commands.CreateUser; using GozareshgirProgramManager.Application.Modules.Users.Commands.CreateUser;
using GozareshgirProgramManager.Infrastructure; using GozareshgirProgramManager.Infrastructure;
using GozareshgirProgramManager.Infrastructure.Persistence.Seed; using GozareshgirProgramManager.Infrastructure.Persistence.Seed;
using Microsoft.OpenApi; using Microsoft.AspNetCore.HttpOverrides;
using Serilog; using Serilog;
using Serilog.Events; using Serilog.Events;
using ServiceHost.Hubs.ProgramManager; using ServiceHost.Hubs.ProgramManager;
using ServiceHost.Notifications.ProgramManager; using ServiceHost.Notifications.ProgramManager;
using ServiceHost.Conventions; using ServiceHost.Conventions;
using ServiceHost.Filters; using ServiceHost.Filters;
using Microsoft.Extensions.FileProviders;
using Microsoft.OpenApi; // Corrected using for PhysicalFileProvider
// ====================================================================
// ✅ BEST PRACTICE: Use two-stage Serilog initialization to log startup errors.
// ====================================================================
var builder = WebApplication.CreateBuilder(args); // Use Docker-compatible log path
var logDirectory = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == Environments.Development
builder.WebHost.ConfigureKestrel(serverOptions => { serverOptions.Limits.MaxRequestBodySize = long.MaxValue; }); ? @"C:\LogsGozareshgir\\"
: "/app/Logs";
builder.Services.AddRazorPages()
.AddRazorRuntimeCompilation();
//Register Services
//test
#region Register Services
builder.Services.AddHttpContextAccessor();
builder.Services.AddHttpClient("holidayApi", c => c.BaseAddress = new System.Uri("https://api.github.com"));
var connectionString = builder.Configuration.GetConnectionString("MesbahDb");
var connectionStringTestDb = builder.Configuration.GetConnectionString("TestDb");
#region Serilog
var logDirectory = @"C:\Logs\Gozareshgir\";
if (!Directory.Exists(logDirectory)) if (!Directory.Exists(logDirectory))
{ {
Directory.CreateDirectory(logDirectory); Directory.CreateDirectory(logDirectory);
} }
// Bootstrap logger: Catches errors during host configuration
Log.Logger = new LoggerConfiguration() Log.Logger = new LoggerConfiguration()
//NO EF Core log .MinimumLevel.Information()
.MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning) .WriteTo.Console()
.CreateBootstrapLogger();
//NO DbCommand log Log.Information("Starting web host...");
.MinimumLevel.Override("Microsoft.EntityFrameworkCore.Database.Command", LogEventLevel.Warning)
//NO Microsoft Public log try
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
//.MinimumLevel.Information()
.WriteTo.File(
path: Path.Combine(logDirectory, "gozareshgir_log.txt"),
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 30,
shared: true,
outputTemplate:
"{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level}] {Message}{NewLine}{Exception}"
).CreateLogger();
#endregion
builder.Services.AddProgramManagerApplication();
builder.Services.AddProgramManagerInfrastructure(builder.Configuration);
builder.Services.AddValidatorsFromAssemblyContaining<CreateUserCommandValidators>();
builder.Services.AddScoped<IDataSeeder, DataSeeder>();
builder.Services.AddScoped<IBoardNotificationPublisher, SignalRBoardNotificationPublisher>();
#region MongoDb
var mongoConnectionSection = builder.Configuration.GetSection("MongoDb");
var mongoDbSettings = mongoConnectionSection.Get<MongoDbConfig>();
var mongoClient = new MongoClient(mongoDbSettings.ConnectionString);
var mongoDatabase = mongoClient.GetDatabase(mongoDbSettings.DatabaseName);
builder.Services.AddSingleton<IMongoDatabase>(mongoDatabase);
#endregion
builder.Services.AddSingleton<IActionResultExecutor<JsonResult>, CustomJsonResultExecutor>();
PersonalBootstrapper.Configure(builder.Services, connectionString);
TestDbBootStrapper.Configure(builder.Services, connectionStringTestDb);
AccountManagementBootstrapper.Configure(builder.Services, connectionString);
WorkFlowBootstrapper.Configure(builder.Services, connectionString);
QueryBootstrapper.Configure(builder.Services);
builder.Services.AddSingleton<IPasswordHasher, PasswordHasher>();
builder.Services.AddTransient<IFileUploader, FileUploader>();
builder.Services.AddTransient<IAuthHelper, AuthHelper>();
builder.Services.AddTransient<IGoogleRecaptcha, GoogleRecaptcha>();
builder.Services.AddTransient<ISmsService, SmsService>();
builder.Services.AddTransient<IUidService, UidService>();
builder.Services.AddTransient<IFaceEmbeddingNotificationService, FaceEmbeddingNotificationService>();
//services.AddSingleton<IWorkingTest, WorkingTest>();
//services.AddHostedService<JobWorker>();
#region Mahan
builder.Services.AddTransient<Tester>();
builder.Services.Configure<AppSettingConfiguration>(builder.Configuration);
#endregion
builder.Services.Configure<FormOptions>(options =>
{ {
options.ValueCountLimit = int.MaxValue; var builder = WebApplication.CreateBuilder(args);
options.KeyLengthLimit = int.MaxValue;
options.ValueLengthLimit = int.MaxValue;
options.MultipartBodyLengthLimit = long.MaxValue;
options.MemoryBufferThreshold = int.MaxValue;
options.MultipartHeadersLengthLimit = int.MaxValue;
});
builder.Services.Configure<CookiePolicyOptions>(options => // ====================================================================
{ // ✅ STANDARD SERILOG CONFIGURATION FOR PRODUCTION
options.CheckConsentNeeded = context => true; // ====================================================================
//options.MinimumSameSitePolicy = SameSiteMode.Strict; builder.Host.UseSerilog((context, services, configuration) => configuration
}); .ReadFrom.Configuration(context.Configuration) // Optional: Allows config from appsettings.json
var domain = builder.Configuration["Domain"]; .ReadFrom.Services(services)
.Enrich.FromLogContext()
builder.Services.ConfigureApplicationCookie(options => .MinimumLevel.Information() // Default minimum level for your application's own logs
{ .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) // Suppress noisy Microsoft logs
//options.Cookie.Name = "GozarAuth"; .MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information) // ✅ KEEP THIS: Shows "Now listening on..."
options.Cookie.HttpOnly = true; .MinimumLevel.Override("Microsoft.EntityFrameworkCore.Database.Command", LogEventLevel.Warning) // Suppresses EF query logs
options.Cookie.SameSite = SameSiteMode.None; // مهم ✅ .WriteTo.Console(outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
options.Cookie.SecurePolicy = CookieSecurePolicy.Always; // فقط روی HTTPS کار می‌کنه ✅ .WriteTo.File(
options.Cookie.Domain = domain; // دامنه مشترک بین پدر و ساب‌دامین‌ها ✅
});
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, o =>
{
o.LoginPath = new PathString("/");
o.LogoutPath = new PathString("/index");
o.AccessDeniedPath = new PathString("/AccessDenied");
o.ExpireTimeSpan = TimeSpan.FromHours(10);
o.SlidingExpiration = true;
});
//services.AddAuthorization(options =>
// options.AddPolicy("AdminArea", builder =>builder.RequireRole(Roles.role)));
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminArea",
builder => builder.RequireClaim("AccountId"));
options.AddPolicy("AdminArea",
builder => builder.RequireClaim("AdminAreaPermission", new List<string> { "true" }));
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("ClientArea",
builder => builder.RequireClaim("AccountId"));
options.AddPolicy("ClientArea",
builder => builder.RequireClaim("ClientAriaPermission", new List<string> { "true" }));
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("CameraArea",
builder => builder.RequireClaim("AccountId"));
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminNewArea",
builder => builder.RequireClaim("AccountId"));
options.AddPolicy("AdminNewArea",
builder => builder.RequireClaim("AdminAreaPermission", new List<string> { "true" }));
});
//services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
// .AddCookie(option =>
// {
// option.LoginPath = "/Index";
// option.LogoutPath = "/Index";
// option.ExpireTimeSpan = TimeSpan.FromDays(1);
// });
builder.Services.AddControllers(options =>
{
options.Conventions.Add(new ParameterBindingConvention());
options.Filters.Add(new OperationResultFilter());
})
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
//builder.Services.AddControllers(
//options=> {
// options.Filters.Add(new ApiJsonEnumFilter());
//});
builder.Services.AddRazorPages(options =>
options.Conventions.AuthorizeAreaFolder("Admin", "/", "AdminArea"));
builder.Services.AddRazorPages(options =>
options.Conventions.AuthorizeAreaFolder("Client", "/", "ClientArea"))
.AddMvcOptions(options => options.Filters.Add<SecurityPageFilter>());
builder.Services.AddRazorPages(options =>
options.Conventions.AuthorizeAreaFolder("Camera", "/", "CameraArea"));
builder.Services.AddRazorPages(options =>
options.Conventions.AuthorizeAreaFolder("AdminNew", "/", "AdminNewArea"));
builder.Services.AddMvc();
builder.Services.AddSignalR();
#endregion
#region PWA
//old
//builder.Services.AddProgressiveWebApp();
//new
//builder.Services.AddProgressiveWebApp(new PwaOptions
//{
// RegisterServiceWorker = true,
// RegisterWebmanifest = true,
// Strategy = ServiceWorkerStrategy.NetworkFirst,
//});
#endregion
#region Swagger
builder.Services.AddSwaggerGen(options =>
{
options.UseInlineDefinitionsForEnums();
options.CustomSchemaIds(type => type.FullName);
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
options.IncludeXmlComments(xmlPath);
// Get XML comments from the class library
var classLibraryXmlFile = "CompanyManagment.App.Contracts.xml";
var classLibraryXmlPath = Path.Combine(AppContext.BaseDirectory, classLibraryXmlFile);
options.IncludeXmlComments(classLibraryXmlPath);
options.SwaggerDoc("General", new OpenApiInfo { Title = "API - General", Version = "v1" });
options.SwaggerDoc("Admin", new OpenApiInfo { Title = "API - Admin", Version = "v1" });
options.SwaggerDoc("Client", new OpenApiInfo { Title = "API - Client", Version = "v1" });
options.SwaggerDoc("Camera", new OpenApiInfo { Title = "API - Camera", Version = "v1" });
options.SwaggerDoc("ProgramManager", new OpenApiInfo { Title = "API - ProgramManager", Version = "v1" });
options.DocInclusionPredicate((docName, apiDesc) =>
string.Equals(docName, apiDesc.GroupName, StringComparison.OrdinalIgnoreCase));
// اضافه کردن پشتیبانی از JWT در Swagger
// options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
// {
// Name = "Authorization",
// Type = SecuritySchemeType.ApiKey,
// Scheme = "Bearer",
// BearerFormat = "JWT",
// In = ParameterLocation.Header,
// Description = "لطفاً 'Bearer [space] token' را وارد کنید."
// });
//
// options.AddSecurityRequirement(new OpenApiSecurityRequirement
// {
// {
// new Microsoft.OpenApi.Models.OpenApiSecurityScheme
// {
// Reference = new Microsoft.OpenApi.Models.OpenApiReference
// {
// Type = Microsoft.OpenApi.Models.ReferenceType.SecurityScheme,
// Id = "Bearer"
// }
// },
// Array.Empty<string>()
// }
// });
options.EnableAnnotations();
});
#endregion
#region CORS
builder.Services.AddCors(options =>
{
options.AddPolicy("AllowSpecificOrigins", policy =>
{
policy.WithOrigins(
"http://localhost:3000",
"http://localhost:4000",
"http://localhost:4001",
"http://localhost:4002",
"http://localhost:3001",
"https://gozareshgir.ir",
"https://dad-mehr.ir",
"https://admin.dad-mehr.ir",
"https://client.dad-mehr.ir",
"https://admin.gozareshgir.ir",
"https://client.gozareshgir.ir",
"https://admin.dadmehrg.ir",
"https://client.dadmehrg.ir",
"http://localhost:3300"
)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
//builder.Services.AddCors(options =>
//{
// options.AddPolicy("AllowAny", policy =>
// {
// policy.AllowAnyOrigin()
// .AllowAnyHeader()
// .AllowAnyMethod();
// });
// options.AddPolicy("AllowSpecificOrigins", policy =>
// {
// policy.WithOrigins("http://localhost:3000", "http://localhost:3001", "https://gozareshgir.ir", "https://dad-mehr.ir")
// .AllowAnyHeader()
// .AllowAnyMethod()
// .AllowCredentials();
// });
//});
#endregion
builder.Services.AddExceptionHandler<CustomExceptionHandler>();
var sepehrTerminalId = builder.Configuration.GetValue<long>("SepehrGateWayTerminalId");
builder.Services.AddParbad().ConfigureGateways(gateways =>
{
gateways.AddSepehr().WithAccounts(accounts =>
{
accounts.AddInMemory(account =>
{
account.TerminalId = sepehrTerminalId;
account.Name="Sepehr Account";
});
});
}).ConfigureHttpContext(httpContext=>httpContext.UseDefaultAspNetCore())
.ConfigureStorage(storage =>
{
storage.UseMemoryCache();
});
if (builder.Environment.IsDevelopment())
{
builder.Host.UseSerilog((context, services, configuration) =>
{
var logConfig = configuration
.ReadFrom.Configuration(context.Configuration)
.ReadFrom.Services(services)
.Enrich.FromLogContext();
logConfig.WriteTo.File(
path: Path.Combine(logDirectory, "gozareshgir_log.txt"), path: Path.Combine(logDirectory, "gozareshgir_log.txt"),
rollingInterval: RollingInterval.Day, rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 30, retainedFileCountLimit: 30,
shared: true, shared: true,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level}] {Message}{NewLine}{Exception}" outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] [{SourceContext}] {Message:lj}{NewLine}{Exception}"
); ));
}, writeToProviders: true); // این باعث میشه کنسول پیش‌فرض هم کار کنه
} builder.WebHost.ConfigureKestrel(serverOptions => { serverOptions.Limits.MaxRequestBodySize = long.MaxValue; });
else
{
builder.Host.UseSerilog();
}
Log.Information("SERILOG STARTED SUCCESSFULLY"); builder.Services.AddRazorPages()
.AddRazorRuntimeCompilation();
var app = builder.Build(); #region Register Services
app.UseCors("AllowSpecificOrigins");
#region InternalProgarmManagerApi builder.Services.AddHttpContextAccessor();
builder.Services.AddHttpClient("holidayApi", c => c.BaseAddress = new Uri("https://api.github.com"));
var connectionString = builder.Configuration.GetConnectionString("MesbahDb");
var connectionStringTestDb = builder.Configuration.GetConnectionString("TestDb");
builder.Services.AddProgramManagerApplication();
builder.Services.AddProgramManagerInfrastructure(builder.Configuration);
builder.Services.AddValidatorsFromAssemblyContaining<CreateUserCommandValidators>();
builder.Services.AddScoped<IDataSeeder, DataSeeder>();
builder.Services.AddScoped<IBoardNotificationPublisher, SignalRBoardNotificationPublisher>();
#region MongoDb
var mongoConnectionSection = builder.Configuration.GetSection("MongoDb");
var mongoDbSettings = mongoConnectionSection.Get<MongoDbConfig>();
var mongoClient = new MongoClient(mongoDbSettings.ConnectionString);
var mongoDatabase = mongoClient.GetDatabase(mongoDbSettings.DatabaseName);
builder.Services.AddSingleton<IMongoDatabase>(mongoDatabase);
#endregion
app.Use(async (context, next) => builder.Services.AddSingleton<IActionResultExecutor<JsonResult>, CustomJsonResultExecutor>();
{ PersonalBootstrapper.Configure(builder.Services, connectionString);
var host = context.Request.Host.Host?.ToLower() ?? ""; TestDbBootStrapper.Configure(builder.Services, connectionStringTestDb);
AccountManagementBootstrapper.Configure(builder.Services, connectionString);
WorkFlowBootstrapper.Configure(builder.Services, connectionString);
QueryBootstrapper.Configure(builder.Services);
string baseUrl; builder.Services.AddSingleton<IPasswordHasher, PasswordHasher>();
builder.Services.AddTransient<IFileUploader, FileUploader>();
builder.Services.AddTransient<IAuthHelper, AuthHelper>();
builder.Services.AddTransient<IGoogleRecaptcha, GoogleRecaptcha>();
builder.Services.AddTransient<ISmsService, SmsService>();
builder.Services.AddTransient<IUidService, UidService>();
builder.Services.AddTransient<IFaceEmbeddingNotificationService, FaceEmbeddingNotificationService>();
if (host.Contains("localhost")) #region Mahan
baseUrl = builder.Configuration["InternalApi:Local"]; builder.Services.AddTransient<Tester>();
else if (host.Contains("dadmehrg.ir")) builder.Services.Configure<AppSettingConfiguration>(builder.Configuration);
baseUrl = builder.Configuration["InternalApi:Dadmehrg"]; #endregion
else if (host.Contains("gozareshgir.ir"))
baseUrl = builder.Configuration["InternalApi:Gozareshgir"];
else
baseUrl = builder.Configuration["InternalApi:Local"]; // fallback
InternalApiCaller.SetBaseUrl(baseUrl); builder.Services.Configure<FormOptions>(options =>
await next.Invoke();
});
#endregion
#region Mahan
//app.UseStatusCodePagesWithRedirects("/error/{0}");
//the backend Tester
if (builder.Environment.IsDevelopment())
{
using var scope = app.Services.CreateScope();
var tester = scope.ServiceProvider.GetRequiredService<Tester>();
await tester.Test();
}
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(options =>
{ {
options.DocExpansion(DocExpansion.None); options.ValueCountLimit = int.MaxValue;
options.SwaggerEndpoint("/swagger/General/swagger.json", "API - General"); options.KeyLengthLimit = int.MaxValue;
options.SwaggerEndpoint("/swagger/Admin/swagger.json", "API - Admin"); options.ValueLengthLimit = int.MaxValue;
options.SwaggerEndpoint("/swagger/Client/swagger.json", "API - Client"); options.MultipartBodyLengthLimit = long.MaxValue;
options.SwaggerEndpoint("/swagger/Camera/swagger.json", "API - Camera"); options.MemoryBufferThreshold = int.MaxValue;
options.SwaggerEndpoint("/swagger/ProgramManager/swagger.json", "API - ProgramManager"); options.MultipartHeadersLengthLimit = int.MaxValue;
}); });
}
#endregion builder.Services.Configure<CookiePolicyOptions>(options =>
//Create Http Pipeline
#region Create Http Pipeline
if (builder.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
// The default HSTS value is 30 days. You may want to change this for pro
app.UseHsts();
}
app.UseExceptionHandler(options => { }); // این خط CustomExceptionHandler رو فعال می‌کنه
app.UseRouting();
app.UseWebSockets();
app.UseAuthentication();
app.UseAuthorization();
app.UseHttpsRedirection();
app.UseStaticFiles();
// Static files برای فایل‌های آپلود شده
var uploadsPath = builder.Configuration["FileStorage:LocalPath"] ?? Path.Combine(Directory.GetCurrentDirectory(), "Storage");
if (!Directory.Exists(uploadsPath))
{
Directory.CreateDirectory(uploadsPath);
}
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new Microsoft.Extensions.FileProviders.PhysicalFileProvider(uploadsPath),
RequestPath = "/storage",
OnPrepareResponse = ctx =>
{ {
// Cache برای فایل‌ها (30 روز) options.CheckConsentNeeded = context => true;
ctx.Context.Response.Headers.Append("Cache-Control", "public,max-age=2592000"); });
var domain = builder.Configuration["Domain"];
builder.Services.ConfigureApplicationCookie(options =>
{
options.Cookie.HttpOnly = true;
options.Cookie.SameSite = SameSiteMode.None;
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.Domain = domain;
});
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, o =>
{
o.LoginPath = new PathString("/");
o.LogoutPath = new PathString("/index");
o.AccessDeniedPath = new PathString("/AccessDenied");
o.ExpireTimeSpan = TimeSpan.FromHours(10);
o.SlidingExpiration = true;
});
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("AdminArea", builder => builder.RequireClaim("AccountId").RequireClaim("AdminAreaPermission", "true"));
options.AddPolicy("ClientArea", builder => builder.RequireClaim("AccountId").RequireClaim("ClientAriaPermission", "true"));
options.AddPolicy("CameraArea", builder => builder.RequireClaim("AccountId"));
options.AddPolicy("AdminNewArea", builder => builder.RequireClaim("AccountId").RequireClaim("AdminAreaPermission", "true"));
});
builder.Services.AddControllers(options =>
{
options.Conventions.Add(new ParameterBindingConvention());
options.Filters.Add(new OperationResultFilter());
})
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
builder.Services.AddRazorPages(options =>
options.Conventions.AuthorizeAreaFolder("Admin", "/", "AdminArea"));
builder.Services.AddRazorPages(options =>
options.Conventions.AuthorizeAreaFolder("Client", "/", "ClientArea"))
.AddMvcOptions(options => options.Filters.Add<SecurityPageFilter>());
builder.Services.AddRazorPages(options =>
options.Conventions.AuthorizeAreaFolder("Camera", "/", "CameraArea"));
builder.Services.AddRazorPages(options =>
options.Conventions.AuthorizeAreaFolder("AdminNew", "/", "AdminNewArea"));
builder.Services.AddMvc();
builder.Services.AddSignalR();
#endregion
#region Swagger
builder.Services.AddSwaggerGen(options =>
{
options.UseInlineDefinitionsForEnums();
options.CustomSchemaIds(type => type.FullName);
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
options.IncludeXmlComments(xmlPath);
var classLibraryXmlFile = "CompanyManagment.App.Contracts.xml";
var classLibraryXmlPath = Path.Combine(AppContext.BaseDirectory, classLibraryXmlFile);
options.IncludeXmlComments(classLibraryXmlPath);
options.SwaggerDoc("General", new OpenApiInfo { Title = "API - General", Version = "v1" });
options.SwaggerDoc("Admin", new OpenApiInfo { Title = "API - Admin", Version = "v1" });
options.SwaggerDoc("Client", new OpenApiInfo { Title = "API - Client", Version = "v1" });
options.SwaggerDoc("Camera", new OpenApiInfo { Title = "API - Camera", Version = "v1" });
options.SwaggerDoc("ProgramManager", new OpenApiInfo { Title = "API - ProgramManager", Version = "v1" });
options.DocInclusionPredicate((docName, apiDesc) =>
string.Equals(docName, apiDesc.GroupName, StringComparison.OrdinalIgnoreCase));
options.EnableAnnotations();
});
#endregion
#region CORS
builder.Services.AddCors(options =>
{
var corsOrigins = builder.Configuration.GetSection("CorsOrigins").Get<string[]>() ?? Array.Empty<string>();
options.AddPolicy("AllowSpecificOrigins", policy =>
{
policy.WithOrigins(corsOrigins)
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
#endregion
builder.Services.AddExceptionHandler<CustomExceptionHandler>();
var sepehrTerminalId = builder.Configuration.GetValue<long>("SepehrGateWayTerminalId");
builder.Services.AddParbad().ConfigureGateways(gateways =>
{
gateways.AddSepehr().WithAccounts(accounts =>
{
accounts.AddInMemory(account =>
{
account.TerminalId = sepehrTerminalId;
account.Name = "Sepehr Account";
});
});
}).ConfigureHttpContext(httpContext => httpContext.UseDefaultAspNetCore())
.ConfigureStorage(storage =>
{
storage.UseMemoryCache();
});
builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
var proxies = builder.Configuration["KNOWN_PROXIES"];
if (!string.IsNullOrWhiteSpace(proxies))
{
foreach (var proxy in proxies.Split(',', StringSplitOptions.RemoveEmptyEntries))
{
options.KnownProxies.Add(IPAddress.Parse(proxy.Trim()));
}
}
});
var app = builder.Build();
// ====================================================================
// ✅ HTTP PIPELINE CONFIGURATION
// ====================================================================
app.UseCors("AllowSpecificOrigins");
#region InternalProgarmManagerApi
app.Use(async (context, next) =>
{
var host = context.Request.Host.Host?.ToLower() ?? "";
string baseUrl = host switch
{
var h when h.Contains("localhost") => builder.Configuration["InternalApi:Local"],
var h when h.Contains("dadmehrg.ir") => builder.Configuration["InternalApi:Dadmehrg"],
var h when h.Contains("gozareshgir.ir") => builder.Configuration["InternalApi:Gozareshgir"],
_ => builder.Configuration["InternalApi:Local"]
};
InternalApiCaller.SetBaseUrl(baseUrl);
await next.Invoke();
});
#endregion
#region Mahan
if (builder.Environment.IsDevelopment())
{
using var scope = app.Services.CreateScope();
var tester = scope.ServiceProvider.GetRequiredService<Tester>();
await tester.Test();
} }
});
app.UseCookiePolicy(); if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(options =>
{
options.DocExpansion(DocExpansion.None);
options.SwaggerEndpoint("/swagger/General/swagger.json", "API - General");
options.SwaggerEndpoint("/swagger/Admin/swagger.json", "API - Admin");
options.SwaggerEndpoint("/swagger/Client/swagger.json", "API - Client");
options.SwaggerEndpoint("/swagger/Camera/swagger.json", "API - Camera");
options.SwaggerEndpoint("/swagger/ProgramManager/swagger.json", "API - ProgramManager");
});
}
#endregion
app.UseForwardedHeaders();
#region Mahan if (builder.Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseHsts();
}
//app.UseLoginHandlerMiddleware(); app.UseExceptionHandler(options => { });
//app.UseCheckTaskMiddleware(); app.Use(async (context, next) =>
app.UseMiddleware<RazorJsonEnumOverrideMiddleware>(); {
if (context.Request.Path.HasValue)
{
context.Request.Path = context.Request.Path.Value.ToLowerInvariant();
}
await next();
});
#endregion app.UseStaticFiles();
var uploadsPath = builder.Configuration["FileStorage:LocalPath"] ?? Path.Combine(Directory.GetCurrentDirectory(), "Storage");
if (!Directory.Exists(uploadsPath))
{
Directory.CreateDirectory(uploadsPath);
}
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = new PhysicalFileProvider(uploadsPath),
RequestPath = "/storage",
OnPrepareResponse = ctx =>
{
ctx.Context.Response.Headers.Append("Cache-Control", "public,max-age=2592000");
}
});
app.MapHub<CreateContractTarckingHub>("/trackingHub"); app.UseRouting();
app.MapHub<SendAccountMessage>("/trackingSmsHub"); app.UseWebSockets();
app.MapHub<HolidayApiHub>("/trackingHolidayHub"); app.UseCookiePolicy();
app.MapHub<CheckoutHub>("/trackingCheckoutHub"); app.UseAuthentication();
// app.MapHub<FaceEmbeddingHub>("/trackingFaceEmbeddingHub"); app.UseAuthorization();
app.MapHub<SendSmsHub>("/trackingSendSmsHub");
app.MapHub<ProjectBoardHub>("api/pm/board");
app.MapRazorPages();
app.MapControllers();
#endregion #region Mahan
app.UseMiddleware<RazorJsonEnumOverrideMiddleware>();
#endregion
app.Run(); app.MapHub<CreateContractTarckingHub>("/trackingHub");
app.MapHub<SendAccountMessage>("/trackingSmsHub");
app.MapHub<HolidayApiHub>("/trackingHolidayHub");
app.MapHub<CheckoutHub>("/trackingCheckoutHub");
app.MapHub<SendSmsHub>("/trackingSendSmsHub");
app.MapHub<ProjectBoardHub>("api/pm/board");
app.MapRazorPages();
app.MapControllers();
app.MapGet("/health", () => Results.Ok(new { status = "Healthy", timestamp = DateTime.UtcNow }));
app.Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "Host terminated unexpectedly");
}
finally
{
Log.CloseAndFlush();
}

View File

@@ -19,7 +19,7 @@
"sqlDebugging": true, "sqlDebugging": true,
"dotnetRunMessages": "true", "dotnetRunMessages": "true",
"nativeDebugging": true, "nativeDebugging": true,
"applicationUrl": "https://localhost:5004;http://localhost:5003;", "applicationUrl": "https://localhost:5004;http://localhost:5003;https://192.168.0.117:5006",
"jsWebView2Debugging": false, "jsWebView2Debugging": false,
"hotReloadEnabled": true "hotReloadEnabled": true
}, },
@@ -47,6 +47,28 @@
"applicationUrl": "https://localhost:5004;http://localhost:5003;", "applicationUrl": "https://localhost:5004;http://localhost:5003;",
"jsWebView2Debugging": false, "jsWebView2Debugging": false,
"hotReloadEnabled": true "hotReloadEnabled": true
},
"Docker": {
"commandName": "DockerCompose",
"commandLineArgs": "up",
"launchBrowser": true,
"launchUrl": "https://localhost:5004",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"dockerComposeProjectPath": "..\\docker-compose.yml",
"useSSL": true
},
"Container (Dockerfile)": {
"commandName": "Docker",
"launchBrowser": true,
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}",
"environmentVariables": {
"ASPNETCORE_HTTPS_PORTS": "8081",
"ASPNETCORE_HTTP_PORTS": "8080"
},
"publishAllPorts": true,
"useSSL": true
} }
}, },
"iisSettings": { "iisSettings": {

View File

@@ -5,12 +5,16 @@
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn> <NoWarn>$(NoWarn);1591</NoWarn>
<!-- Disable static web assets for Docker/Production builds -->
<DisableStaticWebAssets>true</DisableStaticWebAssets>
<!--<StartupObject>ServiceHost.Program</StartupObject>--> <!--<StartupObject>ServiceHost.Program</StartupObject>-->
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<RazorCompileOnBuild>true</RazorCompileOnBuild> <RazorCompileOnBuild>true</RazorCompileOnBuild>
<UserSecretsId>a6049acf-0286-4947-983a-761d06d65f36</UserSecretsId> <UserSecretsId>a6049acf-0286-4947-983a-761d06d65f36</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerComposeProjectPath>..\docker-compose.dcproj</DockerComposeProjectPath>
</PropertyGroup> </PropertyGroup>
<!--<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'"> <!--<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
@@ -91,6 +95,7 @@
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.23.0" />
<PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="10.0.0" /> <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="10.0.0" />
<PackageReference Include="MongoDB.Driver" Version="3.5.2" /> <PackageReference Include="MongoDB.Driver" Version="3.5.2" />
<PackageReference Include="Parbad.AspNetCore" Version="1.5.0" /> <PackageReference Include="Parbad.AspNetCore" Version="1.5.0" />

Binary file not shown.

267
VISUAL_GUIDE.md Normal file
View File

@@ -0,0 +1,267 @@
# 📊 Docker Bind Mounts - Visual Guide
## Architecture Diagram
```
┌─────────────────────────────────────────────────────────────────┐
│ Windows Server Host │
│ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ D:\AppData\ │ │
│ │ │ │
│ │ ┌─────────────┐ ┌──────────────┐ ┌────────────┐ │ │
│ │ │ Faces\ │ │ Storage\ │ │ Logs\ │ │ │
│ │ │ (Face DB) │ │ (Uploads) │ │ (App Logs) │ │ │
│ │ └──────┬──────┘ └──────┬───────┘ └─────┬──────┘ │ │
│ │ │ │ │ │ │
│ └─────────┼─────────────────┼─────────────────┼────────────┘ │
│ │ │ │ │
│ Bind │ Bind │ Bind │ │
│ Mount │ Mount │ Mount │ │
│ │ │ │ │
│ ┌─────────┼─────────────────┼─────────────────┼────────────┐ │
│ │ ↓ ↓ ↓ │ │
│ │ ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ │ │
│ │ ┃ Docker Container: gozareshgir-servicehost ┃ │ │
│ │ ┃ ┃ │ │
│ │ ┃ ┌─────────┐ ┌───────────┐ ┌──────────┐ ┃ │ │
│ │ ┃ │ /app/ │ │ /app/ │ │ /app/ │ ┃ │ │
│ │ ┃ │ Faces/ │ │ Storage/ │ │ Logs/ │ ┃ │ │
│ │ ┃ └─────────┘ └───────────┘ └──────────┘ ┃ │ │
│ │ ┃ ┃ │ │
│ │ ┃ ┌────────────────────────────────────────┐ ┃ │ │
│ │ ┃ │ ASP.NET Core Application │ ┃ │ │
│ │ ┃ │ - Reads/Writes to /app/Faces │ ┃ │ │
│ │ ┃ │ - Reads/Writes to /app/Storage │ ┃ │ │
│ │ ┃ │ - Writes logs to /app/Logs │ ┃ │ │
│ │ ┃ └────────────────────────────────────────┘ ┃ │ │
│ │ ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ │ │
│ │ │ │
│ │ Ports: 5003 (HTTP) → 80, 5004 (HTTPS) → 443 │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
```
## Data Flow Example
### Scenario 1: User uploads a file via the web application
```
User → HTTPS (5004) → Container (:443) → ASP.NET Core
Writes to /app/Storage/file.pdf
[Bind Mount - Real-time sync]
D:\AppData\Storage\file.pdf
```
**File persists on Windows host immediately**
### Scenario 2: Application logs an event
```
ASP.NET Core → Serilog → Writes to /app/Logs/gozareshgir_log.txt
[Bind Mount - Real-time sync]
D:\AppData\Logs\gozareshgir_log.txt
```
**Logs can be viewed directly from Windows host**
### Scenario 3: Administrator adds a face image manually
```
Admin → Copies file to D:\AppData\Faces\user123.jpg
[Bind Mount - Real-time sync]
/app/Faces/user123.jpg
ASP.NET Core sees file immediately
```
**No container restart needed**
## Container Lifecycle vs Data Persistence
```
┌────────────────────────────────────────────────────────────┐
│ Container Lifecycle │
├────────────────────────────────────────────────────────────┤
│ │
│ docker-compose up -d │
│ ↓ │
│ Container Running │
│ │ │
│ ├─ /app/Faces ←──────┐ │
│ ├─ /app/Storage ←──────┼─ Bind Mounts (always active) │
│ ├─ /app/Logs ←──────┘ │
│ │ │
│ docker-compose down │
│ ↓ │
│ Container Removed │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ ✅ DATA STILL EXISTS ON HOST │ │
│ │ D:\AppData\Faces\ │ │
│ │ D:\AppData\Storage\ │ │
│ │ D:\AppData\Logs\ │ │
│ └──────────────────────────────────────────┘ │
│ │
│ docker-compose up -d │
│ ↓ │
│ Container Running (new instance) │
│ │ │
│ ├─ /app/Faces ←──────┐ │
│ ├─ /app/Storage ←──────┼─ Bind Mounts (reconnected) │
│ ├─ /app/Logs ←──────┘ │
│ │ │
│ ✅ ALL PREVIOUS DATA IMMEDIATELY AVAILABLE │
│ │
└────────────────────────────────────────────────────────────┘
```
## Bind Mounts vs Docker Volumes
| Feature | Bind Mounts (Current) | Docker Volumes (Old) |
|----------------------------|-----------------------|----------------------|
| Location | `D:\AppData\*` | Hidden Docker storage|
| Direct access from host | ✅ Yes | ❌ Difficult |
| Windows Explorer | ✅ Yes | ❌ No |
| Backup with robocopy | ✅ Yes | ❌ Requires export |
| Visible path on host | ✅ Yes | ❌ Obscured |
| Production-safe | ✅ Yes | ⚠️ Less transparent |
| Performance on Windows | ✅ Good | ✅ Good |
| Migration to another server| ✅ Easy (copy folder) | ⚠️ Export/import |
## File Operations - Who Can Access?
```
┌───────────────────────────────────────────────────────────┐
│ File Access Matrix │
├──────────────────────┬────────────────┬───────────────────┤
│ │ Container │ Windows Host │
├──────────────────────┼────────────────┼───────────────────┤
│ Read files │ ✅ Yes │ ✅ Yes │
│ Write files │ ✅ Yes │ ✅ Yes │
│ Delete files │ ✅ Yes │ ✅ Yes │
│ Create directories │ ✅ Yes │ ✅ Yes │
│ Rename files │ ✅ Yes │ ✅ Yes │
│ Move files │ ✅ Yes │ ✅ Yes │
│ Real-time sync │ ✅ Instant │ ✅ Instant │
│ File locking │ ✅ Shared │ ✅ Shared │
└──────────────────────┴────────────────┴───────────────────┘
```
## Permissions Flow
```
Windows Host
D:\AppData\Faces (NTFS Permissions: Everyone - Full Control)
Bind Mount (Docker translates permissions)
/app/Faces (Container sees as writable)
ASP.NET Core (Can read/write as app user)
```
## Storage Usage Monitoring
```
┌─────────────────────────────────────────────────────┐
│ Recommended Monitoring │
├─────────────────────────────────────────────────────┤
│ │
│ Weekly: Check disk space │
│ Get-PSDrive D | Select Used, Free │
│ │
│ Monthly: Analyze folder sizes │
│ Get-ChildItem D:\AppData -Directory | │
│ ForEach { $_ | Add-Member -NotePropertyName │
│ Size -NotePropertyValue (Get-ChildItem $_. │
│ FullName -Recurse | Measure-Object -Property │
│ Length -Sum).Sum -PassThru } | Select Name, │
│ @{Name="SizeGB";Expression={[math]::Round( │
│ $_.Size/1GB,2)}} │
│ │
│ Quarterly: Review and archive old files │
│ Consider moving files older than 6 months │
│ │
└─────────────────────────────────────────────────────┘
```
## Quick Command Reference
### Check what's mounted
```powershell
docker inspect gozareshgir-servicehost --format='{{range .Mounts}}{{.Source}} → {{.Destination}}{{println}}{{end}}'
```
Expected output:
```
D:\AppData\Faces → /app/Faces
D:\AppData\Storage → /app/Storage
D:\AppData\Logs → /app/Logs
```
### Test bi-directional sync
```powershell
# From container to host
docker exec gozareshgir-servicehost sh -c "echo 'from container' > /app/Storage/test1.txt"
Get-Content D:\AppData\Storage\test1.txt
# From host to container
"from host" | Out-File D:\AppData\Storage\test2.txt
docker exec gozareshgir-servicehost cat /app/Storage/test2.txt
# Cleanup
Remove-Item D:\AppData\Storage\test*.txt
```
### Monitor real-time file activity
```powershell
# Watch for file changes in Storage directory
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path = "D:\AppData\Storage"
$watcher.IncludeSubdirectories = $true
$watcher.EnableRaisingEvents = $true
Register-ObjectEvent $watcher "Created" -Action {
Write-Host "File created: $($Event.SourceEventArgs.FullPath)" -ForegroundColor Green
}
```
## Troubleshooting Flowchart
```
Container not seeing files?
Check: Do directories exist on host?
├─ No → Run: setup-bind-mounts.ps1
└─ Yes → Continue
Check: Are bind mounts configured?
├─ No → Fix docker-compose.yml
└─ Yes → Continue
Check: Does container have write permissions?
├─ No → Run: icacls commands
└─ Yes → Continue
Check: Is container running?
├─ No → docker-compose up -d
└─ Yes → Check application logs
```
---
**For more details, see:**
- `CONFIGURATION_SUMMARY.md` - Complete setup guide
- `DOCKER_BIND_MOUNTS_SETUP.md` - Full documentation
- `QUICK_REFERENCE.md` - Command reference

43
docker-compose.yml Normal file
View File

@@ -0,0 +1,43 @@
version: '3.8'
services:
# ASP.NET Core Application with HTTPS Support
servicehost:
build:
context: .
dockerfile: Dockerfile
network: host
container_name: gozareshgir-api
image: gozareshgir-api
# ✅ Run as root to ensure write permissions to bind mounts
user: "0:0"
# ✅ All environment variables are now in ServiceHost/.env
env_file:
- .env
ports:
- "${HTTP_PORT:-5003}:80"
- "${HTTPS_PORT:-5004}:443"
volumes:
# ✅ Bind mounts for production-critical data on Windows host
- ./ServiceHost/certs:/app/certs:ro
- D:/AppData/Faces:/app/Faces
- D:/AppData/Storage:/app/Storage
- D:/AppData/Logs:/app/Logs
- D:/AppData/InsuranceList:/app/InsuranceList
networks:
- gozareshgir-network
extra_hosts:
- "host.docker.internal:host-gateway"
healthcheck:
test: ["CMD", "curl", "-f", "-k", "https://localhost:443/health", "||", "exit", "1"]
interval: 30s
timeout: 10s
start_period: 40s
retries: 3
restart: unless-stopped
networks:
gozareshgir-network:
driver: bridge

38
fix-permissions.ps1 Normal file
View File

@@ -0,0 +1,38 @@
# Fix Permissions for Docker Bind Mounts
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "Fixing Docker Bind Mount Permissions" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
$directories = @(
"D:\AppData\Faces",
"D:\AppData\Storage",
"D:\AppData\Logs"
)
Write-Host "[1/3] Ensuring directories exist..." -ForegroundColor Yellow
foreach ($dir in $directories) {
if (Test-Path $dir) {
Write-Host " OK: $dir" -ForegroundColor Green
} else {
New-Item -ItemType Directory -Force -Path $dir | Out-Null
Write-Host " Created: $dir" -ForegroundColor Green
}
}
Write-Host ""
Write-Host "[2/3] Setting permissions..." -ForegroundColor Yellow
foreach ($dir in $directories) {
Write-Host " Processing: $dir" -ForegroundColor Gray
icacls $dir /grant "Everyone:(OI)(CI)F" /T /Q | Out-Null
Write-Host " Done: $dir" -ForegroundColor Green
}
Write-Host ""
Write-Host "[3/3] Verifying write access..." -ForegroundColor Yellow
foreach ($dir in $directories) {
$testFile = Join-Path $dir "test-$(Get-Random).txt"
"test" | Out-File -FilePath $testFile
Remove-Item $testFile -Force
Write-Host " Writable: $dir" -ForegroundColor Green
}
Write-Host ""
Write-Host "Done! Now restart your container:" -ForegroundColor Cyan
Write-Host " docker-compose down" -ForegroundColor Gray
Write-Host " docker-compose up -d --build" -ForegroundColor Gray

7
nuget.config Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<packageSources>
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
</packageSources>
</configuration>

View File

@@ -1,184 +0,0 @@
# Plan: Add ApkType to AndroidApkVersion for WebView and FaceDetection separation
## Overview
The user wants to integrate the `ApkType` enum (WebView/FaceDetection) into the `AndroidApkVersion` entity to distinguish between the two different APK types. Currently, all APKs are treated as WebView. The system needs to be updated to support both types with separate handling.
## Steps
### 1. Update AndroidApkVersion.cs domain entity
**File:** `Company.Domain\AndroidApkVersionAgg\AndroidApkVersion.cs`
- Add `ApkType` property to the class
- Update constructor to accept `ApkType` parameter
- Modify `Title` property generation to include APK type information
- Update the constructor logic to handle both WebView and FaceDetection types
### 2. Update AndroidApkVersionMapping.cs EF configuration
**File:** `CompanyManagment.EFCore\Mapping\AndroidApkVersionMapping.cs`
- Add mapping configuration for `ApkType` property
- Use enum-to-string conversion (similar to existing `IsActive` mapping)
- Set appropriate column max length for the enum value
### 3. Create database migration
**Files:** Generated migration files in `CompanyManagment.EFCore\Migrations\`
- Generate new migration to add `ApkType` column to `AndroidApkVersions` table
- Set default value to `ApkType.WebView` for existing records
- Apply migration to update database schema
### 4. Update IAndroidApkVersionRepository.cs interface
**File:** `Company.Domain\AndroidApkVersionAgg\IAndroidApkVersionRepository.cs`
- Modify `GetActives()` to accept `ApkType` parameter for filtering
- Modify `GetLatestActiveVersionPath()` to accept `ApkType` parameter
- Add methods to handle type-specific queries
### 5. Update AndroidApkVersionRepository.cs implementation
**File:** `CompanyManagment.EFCore\Repository\AndroidApkVersionRepository.cs`
- Implement type-based filtering in `GetActives()` method
- Implement type-based filtering in `GetLatestActiveVersionPath()` method
- Add appropriate WHERE clauses to filter by `ApkType`
### 6. Update IAndroidApkVersionApplication.cs interface
**File:** `CompanyManagment.App.Contracts\AndroidApkVersion\IAndroidApkVersionApplication.cs`
- Add `ApkType` parameter to `CreateAndActive()` method
- Add `ApkType` parameter to `CreateAndDeActive()` method
- Add `ApkType` parameter to `GetLatestActiveVersionPath()` method
- Add `ApkType` parameter to `HasAndroidApkToDownload()` method
### 7. Update AndroidApkVersionApplication.cs implementation
**File:** `CompanyManagment.Application\AndroidApkVersionApplication.cs`
- Update `CreateAndActive()` method:
- Accept `ApkType` parameter
- Change storage path from hardcoded "GozreshgirWebView" to dynamic based on type
- Use "GozreshgirWebView" for `ApkType.WebView`
- Use "GozreshgirFaceDetection" for `ApkType.FaceDetection`
- Pass `ApkType` to repository methods when getting/deactivating existing APKs
- Pass `ApkType` to entity constructor
- Update `CreateAndDeActive()` method:
- Accept `ApkType` parameter
- Update storage path logic similar to `CreateAndActive()`
- Pass `ApkType` to entity constructor
- Update `GetLatestActiveVersionPath()` method:
- Accept `ApkType` parameter
- Pass type to repository method
- Update `HasAndroidApkToDownload()` method:
- Accept `ApkType` parameter
- Filter by type when checking for active APKs
### 8. Update AndroidApk.cs controller
**File:** `ServiceHost\Pages\Apk\AndroidApk.cs`
- Modify the download endpoint to accept `ApkType` parameter
- Options:
- Add query string parameter: `/Apk/Android?type=WebView` or `/Apk/Android?type=FaceDetection`
- Create separate routes: `/Apk/Android/WebView` and `/Apk/Android/FaceDetection`
- Pass the type parameter to `GetLatestActiveVersionPath()` method
- Maintain backward compatibility by defaulting to `ApkType.WebView` if no type specified
### 9. Update admin UI Index.cshtml.cs
**File:** `ServiceHost\Areas\AdminNew\Pages\Company\AndroidApk\Index.cshtml.cs`
- Add property to store selected `ApkType`
- Add `[BindProperty]` for ApkType selection
- Modify `OnPostUpload()` to pass selected `ApkType` to application method
- Create corresponding UI changes in Index.cshtml (if exists) to allow type selection
### 10. Update client-facing pages
**Files:**
- `ServiceHost\Pages\login\Index.cshtml.cs`
- `ServiceHost\Areas\Client\Pages\Index.cshtml.cs`
- Update calls to `HasAndroidApkToDownload()` to specify which APK type to check
- Consider showing different download buttons/links for WebView vs FaceDetection apps
- Update download links to include APK type parameter
## Migration Strategy
### Handling Existing Data
- All existing `AndroidApkVersion` records should be marked as `ApkType.WebView` by default
- Use migration to set default value
- No manual data update required if migration includes default value
### Database Schema Change
```sql
ALTER TABLE AndroidApkVersions
ADD ApkType NVARCHAR(20) NOT NULL DEFAULT 'WebView';
```
## UI Design Considerations
### Admin Upload Page
**Recommended approach:** Single form with radio buttons or dropdown
- Add radio buttons or dropdown to select APK type before upload
- Labels: "WebView Application" and "Face Detection Application"
- Group uploads by type in the list/table view
- Show type column in the APK list
### Client Download Pages
**Recommended approach:** Separate download buttons
- Show "Download Gozareshgir WebView" button (existing functionality)
- Show "Download Gozareshgir FaceDetection" button (new functionality)
- Only show buttons if corresponding APK type is available
- Use different icons or colors to distinguish between types
## Download URL Structure
**Recommended approach:** Single endpoint with query parameter
- Current: `/Apk/Android` (defaults to WebView for backward compatibility)
- New WebView: `/Apk/Android?type=WebView`
- New FaceDetection: `/Apk/Android?type=FaceDetection`
**Alternative approach:** Separate endpoints
- `/Apk/Android/WebView`
- `/Apk/Android/FaceDetection`
## Testing Checklist
1. ✅ Upload WebView APK successfully
2. ✅ Upload FaceDetection APK successfully
3. ✅ Both types can coexist in database
4. ✅ Activating WebView APK doesn't affect FaceDetection APK
5. ✅ Activating FaceDetection APK doesn't affect WebView APK
6. ✅ Download correct APK based on type parameter
7. ✅ Admin UI shows type information correctly
8. ✅ Client pages show correct download availability
9. ✅ Backward compatibility maintained (existing links still work)
10. ✅ Migration applies successfully to existing database
## File Summary
**Files to modify:**
1. `Company.Domain\AndroidApkVersionAgg\AndroidApkVersion.cs`
2. `CompanyManagment.EFCore\Mapping\AndroidApkVersionMapping.cs`
3. `Company.Domain\AndroidApkVersionAgg\IAndroidApkVersionRepository.cs`
4. `CompanyManagment.EFCore\Repository\AndroidApkVersionRepository.cs`
5. `CompanyManagment.App.Contracts\AndroidApkVersion\IAndroidApkVersionApplication.cs`
6. `CompanyManagment.Application\AndroidApkVersionApplication.cs`
7. `ServiceHost\Pages\Apk\AndroidApk.cs`
8. `ServiceHost\Areas\AdminNew\Pages\Company\AndroidApk\Index.cshtml.cs`
9. `ServiceHost\Pages\login\Index.cshtml.cs`
10. `ServiceHost\Areas\Client\Pages\Index.cshtml.cs`
**Files to create:**
1. New migration file (auto-generated)
2. Possibly `ServiceHost\Areas\AdminNew\Pages\Company\AndroidApk\Index.cshtml` (if doesn't exist)
## Notes
- The `ApkType` enum is already defined in `AndroidApkVersion.cs`
- Storage folders will be separate: `Storage/Apk/Android/GozreshgirWebView` and `Storage/Apk/Android/GozreshgirFaceDetection`
- Each APK type maintains its own active/inactive state independently
- Consider adding validation to ensure APK file matches selected type (optional enhancement)

View File

@@ -1,151 +0,0 @@
# Plan: Add IsForce Field to Android APK Version Management
## Overview
Add support for force update functionality to the Android APK version management system. This allows administrators to specify whether an APK update is mandatory (force update) or optional when uploading new versions. The system now supports two separate APK types: WebView and FaceDetection.
## Context
- The system manages Android APK versions for two different application types
- Previously, all updates were treated as optional
- Need to add ability to mark certain updates as mandatory
- Force update flag should be stored in database and returned via API
## Requirements
1. Add `IsForce` boolean field to the `AndroidApkVersion` entity
2. Allow administrators to specify force update status when uploading APK
3. Store force update status in database
4. Return force update status via API endpoint
5. Separate handling for WebView and FaceDetection APK types
## Implementation Steps
### 1. Domain Layer Updates
- ✅ Add `IsForce` property to `AndroidApkVersion` entity
- ✅ Update constructor to accept `isForce` parameter with default value of `false`
- ✅ File: `Company.Domain/AndroidApkVersionAgg/AndroidApkVersion.cs`
### 2. Database Mapping
- ✅ Add `IsForce` property mapping in `AndroidApkVersionMapping`
- ✅ File: `CompanyManagment.EFCore/Mapping/AndroidApkVersionMapping.cs`
### 3. Application Layer Updates
- ✅ Update `IAndroidApkVersionApplication` interface:
- Add `isForce` parameter to `CreateAndActive` method
- Add `isForce` parameter to `CreateAndDeActive` method
- Remove `isForceUpdate` parameter from `GetLatestActiveInfo` method
- ✅ File: `CompanyManagment.App.Contracts/AndroidApkVersion/IAndroidApkVersionApplication.cs`
### 4. Application Implementation
- ✅ Update `AndroidApkVersionApplication`:
- Pass `isForce` to `AndroidApkVersion` constructor in `CreateAndActive`
- Pass `isForce` to `AndroidApkVersion` constructor in `CreateAndDeActive`
- Update `GetLatestActiveInfo` to return `IsForce` from database entity instead of parameter
- ✅ File: `CompanyManagment.Application/AndroidApkVersionApplication.cs`
### 5. API Controller Updates
- ✅ Update `AndroidApkController`:
- Remove `force` parameter from `CheckUpdate` endpoint
- API now returns `IsForce` from database
- ✅ File: `ServiceHost/Areas/Admin/Controllers/AndroidApkController.cs`
### 6. Admin UI Updates
- ✅ Add `IsForce` property to `IndexModel`
- ✅ Add checkbox for force update in upload form
- ✅ Pass `IsForce` value to `CreateAndActive` method
- ✅ Files:
- `ServiceHost/Areas/AdminNew/Pages/Company/AndroidApk/Index.cshtml.cs`
- `ServiceHost/Areas/AdminNew/Pages/Company/AndroidApk/Index.cshtml`
### 7. Database Migration (To Be Done)
- ⚠️ **REQUIRED**: Create and run migration to add `IsForce` column to `AndroidApkVersions` table
- Command: `Add-Migration AddIsForceToAndroidApkVersion`
- Then: `Update-Database`
## API Endpoints
### Check for Updates
```http
GET /api/android-apk/check-update?type={ApkType}&currentVersionCode={int}
```
**Parameters:**
- `type`: Enum value - `WebView` or `FaceDetection`
- `currentVersionCode`: Current version code of installed app (integer)
**Response:**
```json
{
"latestVersionCode": 120,
"latestVersionName": "1.2.0",
"shouldUpdate": true,
"isForceUpdate": false,
"downloadUrl": "/Apk/Android?type=WebView",
"releaseNotes": "Bug fixes and improvements"
}
```
## APK Type Separation
The system now fully supports two separate APK types:
1. **WebView**: Original web-view based application
- Stored in: `Storage/Apk/Android/GozreshgirWebView/`
- Title format: `Gozareshgir-WebView-{version}-{date}`
2. **FaceDetection**: New face detection application
- Stored in: `Storage/Apk/Android/GozreshgirFaceDetection/`
- Title format: `Gozareshgir-FaceDetection-{version}-{date}`
Each APK type maintains its own:
- Version history
- Active version
- Force update settings
- Download endpoint
## Usage Examples
### Admin Upload with Force Update
1. Navigate to admin APK upload page
2. Select APK file
3. Choose APK type (WebView or FaceDetection)
4. Check "آپدیت اجباری (Force Update)" if update should be mandatory
5. Click Upload
### Client Check for Update (WebView)
```http
GET /api/android-apk/check-update?type=WebView&currentVersionCode=100
```
### Client Check for Update (FaceDetection)
```http
GET /api/android-apk/check-update?type=FaceDetection&currentVersionCode=50
```
## Testing Checklist
- [ ] Test uploading APK with force update enabled for WebView
- [ ] Test uploading APK with force update disabled for WebView
- [ ] Test uploading APK with force update enabled for FaceDetection
- [ ] Test uploading APK with force update disabled for FaceDetection
- [ ] Verify API returns correct `isForceUpdate` value for WebView
- [ ] Verify API returns correct `isForceUpdate` value for FaceDetection
- [ ] Verify only one active version exists per APK type
- [ ] Test migration creates `IsForce` column correctly
- [ ] Verify existing records default to `false` for `IsForce`
## Notes
- Default value for `IsForce` is `false` (optional update)
- When uploading new active APK, all previous active versions of same type are deactivated
- Each APK type is managed independently
- Force update flag is stored per version, not globally
- API returns force update status from the latest active version in database
## Files Modified
1. `Company.Domain/AndroidApkVersionAgg/AndroidApkVersion.cs`
2. `CompanyManagment.EFCore/Mapping/AndroidApkVersionMapping.cs`
3. `CompanyManagment.App.Contracts/AndroidApkVersion/IAndroidApkVersionApplication.cs`
4. `CompanyManagment.Application/AndroidApkVersionApplication.cs`
5. `ServiceHost/Areas/Admin/Controllers/AndroidApkController.cs`
6. `ServiceHost/Areas/AdminNew/Pages/Company/AndroidApk/Index.cshtml.cs`
7. `ServiceHost/Areas/AdminNew/Pages/Company/AndroidApk/Index.cshtml`
## Migration Required
⚠️ **Important**: Don't forget to create and run the database migration to add the `IsForce` column.

146
setup-bind-mounts.ps1 Normal file
View File

@@ -0,0 +1,146 @@
# ========================================
# Gozareshgir Docker Bind Mounts Setup Script
# ========================================
# This script prepares the Windows host for Docker bind mounts
# Run this BEFORE starting the Docker container
param(
[string]$BasePath = "D:\AppData",
[switch]$GrantFullPermissions,
[string]$ServiceAccount = "Everyone"
)
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "Gozareshgir Docker Setup Script" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
# Define directories
$directories = @(
"$BasePath\Faces",
"$BasePath\Storage",
"$BasePath\Logs"
)
# Step 1: Create directories
Write-Host "[1/3] Creating directories..." -ForegroundColor Yellow
foreach ($dir in $directories) {
if (Test-Path $dir) {
Write-Host " ✓ Already exists: $dir" -ForegroundColor Green
} else {
try {
New-Item -ItemType Directory -Force -Path $dir | Out-Null
Write-Host " ✓ Created: $dir" -ForegroundColor Green
} catch {
Write-Host " ✗ Failed to create: $dir" -ForegroundColor Red
Write-Host " Error: $_" -ForegroundColor Red
exit 1
}
}
}
Write-Host ""
# Step 2: Set permissions
Write-Host "[2/3] Setting permissions..." -ForegroundColor Yellow
if ($GrantFullPermissions) {
foreach ($dir in $directories) {
try {
# Grant full control
$acl = Get-Acl $dir
$permission = "$ServiceAccount", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow"
$accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule $permission
$acl.SetAccessRule($accessRule)
Set-Acl $dir $acl
Write-Host " ✓ Granted full control to '$ServiceAccount': $dir" -ForegroundColor Green
} catch {
Write-Host " ✗ Failed to set permissions: $dir" -ForegroundColor Red
Write-Host " Error: $_" -ForegroundColor Red
}
}
} else {
Write-Host " ⓘ Skipped (use -GrantFullPermissions to enable)" -ForegroundColor Gray
Write-Host " To grant permissions manually, run:" -ForegroundColor Gray
foreach ($dir in $directories) {
Write-Host " icacls `"$dir`" /grant Everyone:F /T" -ForegroundColor DarkGray
}
}
Write-Host ""
# Step 3: Verify setup
Write-Host "[3/3] Verifying setup..." -ForegroundColor Yellow
$allOk = $true
foreach ($dir in $directories) {
$exists = Test-Path $dir
$writable = $false
if ($exists) {
try {
$testFile = Join-Path $dir "test-write-$(Get-Random).txt"
"test" | Out-File -FilePath $testFile -ErrorAction Stop
Remove-Item $testFile -ErrorAction SilentlyContinue
$writable = $true
} catch {
$writable = $false
}
}
if ($exists -and $writable) {
Write-Host " ✓ OK: $dir" -ForegroundColor Green
} elseif ($exists -and -not $writable) {
Write-Host " ⚠ WARNING: $dir (exists but not writable)" -ForegroundColor Yellow
$allOk = $false
} else {
Write-Host " ✗ FAILED: $dir (does not exist)" -ForegroundColor Red
$allOk = $false
}
}
Write-Host ""
# Step 4: Check disk space
Write-Host "[Bonus] Checking disk space..." -ForegroundColor Yellow
try {
$drive = (Get-Item $BasePath).PSDrive
$driveInfo = Get-PSDrive $drive.Name
$freeGB = [math]::Round($driveInfo.Free / 1GB, 2)
$usedGB = [math]::Round($driveInfo.Used / 1GB, 2)
$totalGB = [math]::Round(($driveInfo.Used + $driveInfo.Free) / 1GB, 2)
Write-Host " Drive: $($drive.Name):" -ForegroundColor Cyan
Write-Host " Total: $totalGB GB" -ForegroundColor Gray
Write-Host " Used: $usedGB GB" -ForegroundColor Gray
Write-Host " Free: $freeGB GB" -ForegroundColor Gray
if ($freeGB -lt 10) {
Write-Host " ⚠ WARNING: Low disk space (less than 10 GB available)" -ForegroundColor Yellow
} else {
Write-Host " ✓ Sufficient disk space available" -ForegroundColor Green
}
} catch {
Write-Host " ⓘ Could not check disk space" -ForegroundColor Gray
}
Write-Host ""
# Summary
Write-Host "========================================" -ForegroundColor Cyan
if ($allOk) {
Write-Host "✓ Setup completed successfully!" -ForegroundColor Green
Write-Host ""
Write-Host "Next steps:" -ForegroundColor Cyan
Write-Host " 1. Start the Docker container:" -ForegroundColor White
Write-Host " docker-compose up -d" -ForegroundColor Gray
Write-Host " 2. Verify the mounts:" -ForegroundColor White
Write-Host " docker exec gozareshgir-servicehost ls -la /app" -ForegroundColor Gray
} else {
Write-Host "⚠ Setup completed with warnings!" -ForegroundColor Yellow
Write-Host "Please review the issues above before starting Docker." -ForegroundColor Yellow
}
Write-Host "========================================" -ForegroundColor Cyan
Write-Host ""
# Display docker-compose command
Write-Host "To start the container, run:" -ForegroundColor Cyan
Write-Host " cd $PSScriptRoot" -ForegroundColor Gray
Write-Host " docker-compose up -d" -ForegroundColor Green
Write-Host ""