feat: username/password authentication for admin and merchant panels
CI/CD / CI · API (dotnet build + test) (push) Successful in 49s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 42s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m8s
CI/CD / CI · Admin Web (tsc) (push) Successful in 37s
CI/CD / CI · Website (tsc) (push) Successful in 46s
CI/CD / CI · Koja (tsc) (push) Successful in 49s
CI/CD / Deploy · all services (push) Has been cancelled
CI/CD / CI · API (dotnet build + test) (push) Successful in 49s
CI/CD / CI · Admin API (dotnet build) (push) Successful in 42s
CI/CD / CI · Dashboard (tsc) (push) Successful in 1m8s
CI/CD / CI · Admin Web (tsc) (push) Successful in 37s
CI/CD / CI · Website (tsc) (push) Successful in 46s
CI/CD / CI · Koja (tsc) (push) Successful in 49s
CI/CD / Deploy · all services (push) Has been cancelled
- Add PasswordHasher utility (PBKDF2/SHA-256, 100k iterations)
- Add Username + PasswordHash fields to Employee and SystemAdmin entities
- EF migration: AddPasswordLogin (nullable columns on both tables)
- Meezi.API: POST /api/auth/login (employee password login, CHOOSE_CAFE support)
- Meezi.API: PUT/DELETE /api/cafes/{id}/employees/{id}/credentials (Owner/Manager only)
- Meezi.Admin.API: POST /api/admin/auth/login + PUT /api/admin/auth/password
- Dashboard login page: OTP / Password tabs
- Admin login page: OTP / Password tabs
- HR screen: new Credentials tab for setting employee username/password
- PlatformDataSeeder: ensure system admin + integration settings in production
- Trial countdown banner: updated deadline to 1 Tir 1405 (Jun 22)
- i18n: fa/en/ar updated for all new UI strings
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+3299
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,58 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Meezi.Infrastructure.Data.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class AddPasswordLogin : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "PasswordHash",
|
||||
table: "SystemAdmins",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Username",
|
||||
table: "SystemAdmins",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "PasswordHash",
|
||||
table: "Employees",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Username",
|
||||
table: "Employees",
|
||||
type: "text",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PasswordHash",
|
||||
table: "SystemAdmins");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Username",
|
||||
table: "SystemAdmins");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "PasswordHash",
|
||||
table: "Employees");
|
||||
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Username",
|
||||
table: "Employees");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -929,6 +929,9 @@ namespace Meezi.Infrastructure.Data.Migrations
|
||||
b.Property<string>("NationalId")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Phone")
|
||||
.IsRequired()
|
||||
.HasColumnType("text");
|
||||
@@ -939,6 +942,9 @@ namespace Meezi.Infrastructure.Data.Migrations
|
||||
b.Property<int>("Role")
|
||||
.HasColumnType("integer");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BranchId");
|
||||
@@ -2119,11 +2125,17 @@ namespace Meezi.Infrastructure.Data.Migrations
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("character varying(200)");
|
||||
|
||||
b.Property<string>("PasswordHash")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.Property<string>("Phone")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("character varying(20)");
|
||||
|
||||
b.Property<string>("Username")
|
||||
.HasColumnType("text");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Phone")
|
||||
|
||||
@@ -3,7 +3,9 @@ using Meezi.Core.Constants;
|
||||
using Meezi.Core.Entities;
|
||||
using Meezi.Core.Enums;
|
||||
using Meezi.Core.Platform;
|
||||
using Meezi.Core.Utilities;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -17,18 +19,25 @@ public static class PlatformDataSeeder
|
||||
public static async Task SeedAsync(IServiceProvider services)
|
||||
{
|
||||
var env = services.GetRequiredService<IHostEnvironment>();
|
||||
if (!env.IsDevelopment())
|
||||
return;
|
||||
|
||||
var logger = services.GetRequiredService<ILoggerFactory>().CreateLogger("PlatformDataSeeder");
|
||||
await using var scope = services.CreateAsyncScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
var config = scope.ServiceProvider.GetRequiredService<IConfiguration>();
|
||||
|
||||
await EnsureCatalogUpgradesAsync(db, logger);
|
||||
// Production-safe: ensure the platform owner's system-admin account exists
|
||||
// on every boot (ALL environments) so the admin panel is reachable on a
|
||||
// fresh deploy. Idempotent. Phone is overridable via "Seed:SystemAdminPhone".
|
||||
await EnsureOwnerAdminAsync(db, config, logger);
|
||||
|
||||
if (!env.IsDevelopment())
|
||||
{
|
||||
// Production: also ensure integration settings (Kavenegar enabled/template,
|
||||
// etc.) exist so the admin Integrations page is populated. Idempotent.
|
||||
await EnsureIntegrationSettingsAsync(db, logger);
|
||||
return;
|
||||
}
|
||||
|
||||
await EnsureCatalogUpgradesAsync(db, logger);
|
||||
await SeedSystemAdminAsync(db, logger);
|
||||
await SeedPlansAsync(db, logger);
|
||||
await SeedFeaturesAsync(db, logger);
|
||||
@@ -36,6 +45,49 @@ public static class PlatformDataSeeder
|
||||
await EnsureIntegrationSettingsAsync(db, logger);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensures the platform owner's system-admin account exists in EVERY environment
|
||||
/// (including production), so the admin panel is reachable on a fresh deploy.
|
||||
/// The phone is configurable via "Seed:SystemAdminPhone" (env Seed__SystemAdminPhone)
|
||||
/// and defaults to the platform owner's number. Idempotent — never duplicates.
|
||||
/// </summary>
|
||||
private static async Task EnsureOwnerAdminAsync(AppDbContext db, IConfiguration config, ILogger logger)
|
||||
{
|
||||
const string DefaultOwnerPhone = "09190345606";
|
||||
var configured = config["Seed:SystemAdminPhone"];
|
||||
var phone = PhoneNormalizer.Normalize(
|
||||
string.IsNullOrWhiteSpace(configured) ? DefaultOwnerPhone : configured);
|
||||
|
||||
if (!PhoneNormalizer.IsValidIranMobile(phone))
|
||||
{
|
||||
logger.LogWarning("Owner system-admin seed skipped — invalid phone '{Phone}'", phone);
|
||||
return;
|
||||
}
|
||||
|
||||
if (await db.SystemAdmins.AnyAsync(a => a.Phone == phone))
|
||||
return;
|
||||
|
||||
db.SystemAdmins.Add(new SystemAdmin
|
||||
{
|
||||
Id = "sysadmin_owner",
|
||||
Name = "مدیر سامانه",
|
||||
Phone = phone,
|
||||
IsActive = true
|
||||
});
|
||||
|
||||
try
|
||||
{
|
||||
await db.SaveChangesAsync();
|
||||
logger.LogInformation("Seeded owner system admin with phone {Phone}", phone);
|
||||
}
|
||||
catch (DbUpdateException)
|
||||
{
|
||||
// api + admin-api boot concurrently against the same DB; another instance
|
||||
// already inserted this admin. Safe to ignore.
|
||||
logger.LogInformation("Owner system admin already seeded by another instance");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Idempotent plan/feature upgrades for all environments (including production).</summary>
|
||||
public static async Task EnsureCatalogUpgradesAsync(IServiceProvider services)
|
||||
{
|
||||
@@ -126,7 +178,7 @@ public static class PlatformDataSeeder
|
||||
S("payment.vandar.enabled", "false", "payment", "فعال وندار"),
|
||||
S("payment.vandar.sandbox", "true", "payment", "حالت تست وندار"),
|
||||
S("integrations.kavenegar.enabled", "true", "integrations", "فعال کاوهنگار"),
|
||||
S("integrations.kavenegar.otpTemplate", "verify", "integrations", "قالب OTP"),
|
||||
S("integrations.kavenegar.otpTemplate", "meeziotp", "integrations", "قالب OTP"),
|
||||
S("integrations.openai.enabled", "false", "integrations", "فعال OpenAI"),
|
||||
S("integrations.openai.model", "gpt-4o-mini", "integrations", "مدل OpenAI"),
|
||||
S("integrations.openai.coffeeAdvisor.enabled", "true", "integrations", "مشاور قهوه"),
|
||||
@@ -296,7 +348,7 @@ public static class PlatformDataSeeder
|
||||
S("payment.vandar.enabled", "false", "payment", "فعال وندار"),
|
||||
S("payment.vandar.sandbox", "true", "payment", "حالت تست وندار"),
|
||||
S("integrations.kavenegar.enabled", "true", "integrations", "فعال کاوهنگار"),
|
||||
S("integrations.kavenegar.otpTemplate", "verify", "integrations", "قالب OTP"),
|
||||
S("integrations.kavenegar.otpTemplate", "meeziotp", "integrations", "قالب OTP"),
|
||||
S("integrations.openai.enabled", "false", "integrations", "فعال OpenAI"),
|
||||
S("integrations.openai.model", "gpt-4o-mini", "integrations", "مدل OpenAI"),
|
||||
S("integrations.openai.coffeeAdvisor.enabled", "true", "integrations", "مشاور قهوه"),
|
||||
|
||||
Reference in New Issue
Block a user