initialising for ci-cd pipeline
CI/CD - ERP Core / scripts (pull_request) Failing after 12m21s
CI/CD - ERP Core / e2e-typecheck (pull_request) Failing after 13m13s
CI/CD - ERP Core / frontend (pull_request) Failing after 14m5s
CI/CD - ERP Core / backend (pull_request) Failing after 14m57s
CI/CD - ERP Core / detect (pull_request) Failing after 13m42s
CI/CD - ERP Core / Build & Deploy (pull_request) Failing after 12m47s
CI/CD - ERP Core / ci-ok (pull_request) Failing after 14m58s
CI/CD - ERP Core / scripts (pull_request) Failing after 12m21s
CI/CD - ERP Core / e2e-typecheck (pull_request) Failing after 13m13s
CI/CD - ERP Core / frontend (pull_request) Failing after 14m5s
CI/CD - ERP Core / backend (pull_request) Failing after 14m57s
CI/CD - ERP Core / detect (pull_request) Failing after 13m42s
CI/CD - ERP Core / Build & Deploy (pull_request) Failing after 12m47s
CI/CD - ERP Core / ci-ok (pull_request) Failing after 14m58s
This commit is contained in:
@@ -11,6 +11,8 @@ internal sealed class TestCurrentUser : ICurrentUser
|
||||
public string UserId => "system";
|
||||
public int AuditUserId => User.SystemUserId;
|
||||
public bool IsAuthenticated => false;
|
||||
public string? RoleCode => null;
|
||||
public bool IsAdmin => false;
|
||||
}
|
||||
|
||||
internal sealed class ThrowingUnitOfWork : ERPCore.Infra.UoW.IUnitOfWork
|
||||
|
||||
@@ -5,6 +5,8 @@ VisualStudioVersion = 17.0.31903.59
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ERPCore", "ERPCore\ERPCore.csproj", "{B38203ED-80EA-41C6-8B7D-B664451299E3}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ERPCore.Tests", "ERPCore.Tests\ERPCore.Tests.csproj", "{E451472F-550F-40A0-AE82-E24ECD6C647A}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -27,6 +29,18 @@ Global
|
||||
{B38203ED-80EA-41C6-8B7D-B664451299E3}.Release|x64.Build.0 = Release|Any CPU
|
||||
{B38203ED-80EA-41C6-8B7D-B664451299E3}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{B38203ED-80EA-41C6-8B7D-B664451299E3}.Release|x86.Build.0 = Release|Any CPU
|
||||
{E451472F-550F-40A0-AE82-E24ECD6C647A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{E451472F-550F-40A0-AE82-E24ECD6C647A}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{E451472F-550F-40A0-AE82-E24ECD6C647A}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{E451472F-550F-40A0-AE82-E24ECD6C647A}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{E451472F-550F-40A0-AE82-E24ECD6C647A}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{E451472F-550F-40A0-AE82-E24ECD6C647A}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{E451472F-550F-40A0-AE82-E24ECD6C647A}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{E451472F-550F-40A0-AE82-E24ECD6C647A}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{E451472F-550F-40A0-AE82-E24ECD6C647A}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{E451472F-550F-40A0-AE82-E24ECD6C647A}.Release|x64.Build.0 = Release|Any CPU
|
||||
{E451472F-550F-40A0-AE82-E24ECD6C647A}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{E451472F-550F-40A0-AE82-E24ECD6C647A}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
@@ -18,6 +18,11 @@ public sealed class GeneralLedgerClient(HttpClient http, IConfiguration configur
|
||||
public async Task<GeneralLedgerResponse> SendAsync(
|
||||
HttpMethod method, string path, string? queryString, string? contentType, Stream? body, CancellationToken ct)
|
||||
{
|
||||
// The base appsettings.json ships the key blank; name it here rather than forward GL's 401
|
||||
// for an empty X-Api-Key (deployed environments already refuse to start without it).
|
||||
if (string.IsNullOrWhiteSpace(_apiKey))
|
||||
throw new DomainException(ErrorCodes.GlServiceUnavailable, "GeneralLedgerService:ApiKey is not configured.", 503);
|
||||
|
||||
var relativeUri = path.TrimStart('/') + queryString;
|
||||
using var request = new HttpRequestMessage(method, relativeUri);
|
||||
request.Headers.TryAddWithoutValidation("X-Api-Key", _apiKey);
|
||||
|
||||
@@ -39,17 +39,29 @@ public static class DataSeeder
|
||||
("PRD-REWORK-LOSS", "Production Rework Loss", ReasonContext.Production),
|
||||
];
|
||||
|
||||
public static async Task SeedAsync(ErpDbContext db, CancellationToken ct = default)
|
||||
/// <param name="includeDemoData">
|
||||
/// Also seed demo rows — sample warehouses/UOMs/category/items, an on-hand stock layer,
|
||||
/// customers, bundle and draft sales documents (<c>Seed:DemoData</c>). On for Development
|
||||
/// and the test deployment (the E2E suite relies on MAIN/SHOP, PCS/BOX, General Goods);
|
||||
/// never for release, which gets reference data only (docs/40-DEPLOYMENT.md).
|
||||
/// </param>
|
||||
public static async Task SeedAsync(ErpDbContext db, bool includeDemoData, CancellationToken ct = default)
|
||||
{
|
||||
// Reference data — every environment.
|
||||
var dirty = await SeedReasonCodesAsync(db, ct);
|
||||
dirty |= await SeedItemTypesAsync(db, ct);
|
||||
// dirty |= await SeedCompanyProfileAsync(db, ct);
|
||||
dirty |= await SeedProductConfigAsync(db, ct);
|
||||
dirty |= await SeedBranchConfigAsync(db, ct);
|
||||
dirty |= await SeedNotificationConfigAsync(db, ct);
|
||||
dirty |= await SeedSalesMastersAsync(db, ct);
|
||||
dirty |= await SeedSalesStockAsync(db, ct);
|
||||
dirty |= await SeedSalesAsync(db, ct);
|
||||
dirty |= await SeedSalesSequencesAsync(db, ct);
|
||||
|
||||
if (includeDemoData)
|
||||
{
|
||||
dirty |= await SeedSalesMastersAsync(db, ct);
|
||||
dirty |= await SeedSalesStockAsync(db, ct);
|
||||
dirty |= await SeedSalesAsync(db, ct);
|
||||
}
|
||||
|
||||
if (dirty) await db.SaveChangesAsync(ct);
|
||||
}
|
||||
@@ -344,9 +356,9 @@ public static class DataSeeder
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seeds the minimum sales bootstrap data needed for UI/backend development:
|
||||
/// a couple of customer rows, current-year document counters, and a few draft
|
||||
/// invoice/slip samples when the required master data already exists.
|
||||
/// Seeds the demo sales data used for UI/backend development: a couple of customer
|
||||
/// rows and a few draft invoice/slip samples when the required master data already
|
||||
/// exists. (Current-year document counters are reference data, seeded in SeedAsync.)
|
||||
/// This intentionally never clears or rewrites any existing rows.
|
||||
/// </summary>
|
||||
private static async Task<bool> SeedSalesAsync(ErpDbContext db, CancellationToken ct)
|
||||
@@ -354,7 +366,6 @@ public static class DataSeeder
|
||||
var dirty = false;
|
||||
|
||||
dirty |= await SeedSalesCustomersAsync(db, ct);
|
||||
dirty |= await SeedSalesSequencesAsync(db, ct);
|
||||
dirty |= await SeedSampleSalesDocsAsync(db, ct);
|
||||
|
||||
try
|
||||
|
||||
@@ -15,6 +15,7 @@ using ERPCore.Services.Production;
|
||||
using ERPCore.Services.Stock;
|
||||
using ERPCore.System.Errors;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.HttpOverrides;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using Microsoft.OpenApi;
|
||||
@@ -23,10 +24,32 @@ using Serilog;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Serilog (file sink) — reads levels from configuration, plus a rolling daily file.
|
||||
// Fail fast on missing deployment config. Outside Development these arrive as environment
|
||||
// variables (docs/40-DEPLOYMENT.md §5); a blank value would otherwise surface later as an
|
||||
// unrelated crash (e.g. `new Uri("")` for AuthHex:BaseUrl) instead of naming the key.
|
||||
if (!builder.Environment.IsDevelopment())
|
||||
{
|
||||
string[] requiredKeys =
|
||||
[
|
||||
"ConnectionStrings:DefaultConnection",
|
||||
"AuthHex:BaseUrl",
|
||||
"GeneralLedgerService:BaseUrl",
|
||||
"GeneralLedgerService:ApiKey",
|
||||
"Auth:RsaPublicKeyXml",
|
||||
];
|
||||
var missing = requiredKeys.Where(k => string.IsNullOrWhiteSpace(builder.Configuration[k])).ToArray();
|
||||
if (missing.Length > 0)
|
||||
throw new InvalidOperationException(
|
||||
$"Missing required configuration for environment '{builder.Environment.EnvironmentName}': " +
|
||||
$"{string.Join(", ", missing)}. Set them as environment variables (':' becomes '__', " +
|
||||
"e.g. ConnectionStrings__DefaultConnection).");
|
||||
}
|
||||
|
||||
// Serilog (file sink) — reads levels from configuration, plus a rolling daily file. The path is
|
||||
// configurable so deployed instances log outside their release folder (LogFile:Path).
|
||||
builder.Host.UseSerilog((ctx, cfg) => cfg
|
||||
.ReadFrom.Configuration(ctx.Configuration)
|
||||
.WriteTo.File("logs/erpcore-.log", rollingInterval: RollingInterval.Day));
|
||||
.WriteTo.File(ctx.Configuration["LogFile:Path"] ?? "logs/erpcore-.log", rollingInterval: RollingInterval.Day));
|
||||
|
||||
// Controllers + JSON: serialize enums as their string names (docs/11 §8, camelCase).
|
||||
builder.Services.AddControllers()
|
||||
@@ -49,8 +72,10 @@ builder.Services.AddErpJwtAuth(builder.Configuration);
|
||||
// AuthController proxy → AuthHex (docs/11 §2.0)
|
||||
builder.Services.AddHttpClient<IAuthHexClient, AuthHexClient>(c =>
|
||||
{
|
||||
var baseUrl = builder.Configuration["AuthHex:BaseUrl"]
|
||||
?? throw new InvalidOperationException("AuthHex:BaseUrl is not configured.");
|
||||
// Blank counts as missing: the base appsettings.json ships these keys empty on purpose.
|
||||
var baseUrl = builder.Configuration["AuthHex:BaseUrl"];
|
||||
if (string.IsNullOrWhiteSpace(baseUrl))
|
||||
throw new InvalidOperationException("AuthHex:BaseUrl is not configured (set it in appsettings.Development.json locally).");
|
||||
c.BaseAddress = new Uri(baseUrl);
|
||||
});
|
||||
builder.Services.AddScoped<IAuthUserService, AuthUserService>();
|
||||
@@ -60,8 +85,9 @@ builder.Services.AddScoped<IAuthAltService, AuthAltService>();
|
||||
// General Ledger service proxy → external GL microservice (docs/12-GENERAL-LEDGER-INTEGRATION.md)
|
||||
builder.Services.AddHttpClient<IGeneralLedgerClient, GeneralLedgerClient>(c =>
|
||||
{
|
||||
var baseUrl = builder.Configuration["GeneralLedgerService:BaseUrl"]
|
||||
?? throw new InvalidOperationException("GeneralLedgerService:BaseUrl is not configured.");
|
||||
var baseUrl = builder.Configuration["GeneralLedgerService:BaseUrl"];
|
||||
if (string.IsNullOrWhiteSpace(baseUrl))
|
||||
throw new InvalidOperationException("GeneralLedgerService:BaseUrl is not configured (set it in appsettings.Development.json locally).");
|
||||
c.BaseAddress = new Uri(baseUrl);
|
||||
});
|
||||
builder.Services.AddScoped<IGeneralLedgerService, GeneralLedgerService>();
|
||||
@@ -205,7 +231,10 @@ using (var scope = app.Services.CreateScope())
|
||||
await db.Database.MigrateAsync();
|
||||
try
|
||||
{
|
||||
await DataSeeder.SeedAsync(db);
|
||||
// Demo rows (sample catalog, stock, customers, sales docs) default on only in Development;
|
||||
// the test deployment opts in with Seed__DemoData=true, release never does.
|
||||
var includeDemoData = app.Configuration.GetValue("Seed:DemoData", app.Environment.IsDevelopment());
|
||||
await DataSeeder.SeedAsync(db, includeDemoData);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -213,9 +242,18 @@ using (var scope = app.Services.CreateScope())
|
||||
}
|
||||
}
|
||||
|
||||
// Behind the co-located Apache reverse proxy (docs/40-DEPLOYMENT.md): take the client IP and
|
||||
// scheme from X-Forwarded-*. Only loopback proxies are trusted by default, which is exactly it.
|
||||
app.UseForwardedHeaders(new ForwardedHeadersOptions
|
||||
{
|
||||
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
|
||||
});
|
||||
|
||||
app.UseSerilogRequestLogging();
|
||||
|
||||
if (app.Environment.IsDevelopment())
|
||||
// Off in Production only. The test deployment (Staging) keeps it; it is still not public there
|
||||
// because Apache proxies only /api/ — reach it through an SSH tunnel to the backend port.
|
||||
if (!app.Environment.IsProduction())
|
||||
{
|
||||
app.UseSwagger();
|
||||
app.UseSwaggerUI();
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Host=127.0.0.1;Port=5432;Database=ERPCore;Username=postgres;Password=CHANGE_ME"
|
||||
},
|
||||
"AuthHex": {
|
||||
"BaseUrl": "http://localhost:5011"
|
||||
},
|
||||
"GeneralLedgerService": {
|
||||
"BaseUrl": "https://localhost:7024/api/v1/",
|
||||
"ApiKey": "ask-the-team-for-the-dev-key"
|
||||
},
|
||||
"Seed": {
|
||||
"DemoData": true
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
}
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"DefaultConnection": "Host=localhost;Port=5432;Database=ERPCore;Username=root;Password=root"
|
||||
"DefaultConnection": ""
|
||||
},
|
||||
"Auth": {
|
||||
"Issuer": "AuthHex",
|
||||
@@ -16,15 +16,18 @@
|
||||
"RequiredRoleCode": ""
|
||||
},
|
||||
"AuthHex": {
|
||||
"BaseUrl": "http://localhost:5602"
|
||||
"BaseUrl": ""
|
||||
},
|
||||
"FileStorage": {
|
||||
"RootPath": "App_Data/hr-documents",
|
||||
"MaxSizeBytes": 10485760
|
||||
},
|
||||
"LogFile": {
|
||||
"Path": "logs/erpcore-.log"
|
||||
},
|
||||
"GeneralLedgerService": {
|
||||
"BaseUrl": "https://localhost:7024/api/v1/",
|
||||
"ApiKey": "1A5FE0E389C029B1FBCD2A94650CEBF0656083BF06FF48E5E11987040ABA262D"
|
||||
"BaseUrl": "",
|
||||
"ApiKey": ""
|
||||
},
|
||||
"Grn": {
|
||||
"GlInventoryAccountCode": "1100",
|
||||
|
||||
@@ -4,6 +4,34 @@ Legend: `[ ]` not started · `[~]` in progress · `[x]` done
|
||||
Spec: `docs/10-BACKEND-PHASE1.md` (model + rules) · `docs/11-BACKEND-PHASE1.md` (API)
|
||||
Convention: `docs/01-DOC-GUIDE.md §6`. Update this file in the **same commit** as the code. When ticking `[x]`, append a short note + any deviation.
|
||||
|
||||
## Deployment readiness + CI/CD (2026-09-14) — `docs/40-DEPLOYMENT.md`
|
||||
|
||||
- [x] **Secrets out of git.** `appsettings.Development.json` untracked (template: `appsettings.Development.json.example`); `appsettings.Production.json` deleted (its `Jwt:SigningKey` was unused); base `appsettings.json` now blanks the connection string, `AuthHex:BaseUrl` and `GeneralLedgerService:BaseUrl/ApiKey`; `Backend/smoke/smoke_common.py` has no default password (`ERP_SMOKE_PASSWORD`). Values committed earlier still need rotating (`docs/02-SECURITY.md` AR-06).
|
||||
- [x] **Fail-fast config check** (`Program.cs`): outside Development, startup throws one `InvalidOperationException` naming every blank required key. Verified: `ASPNETCORE_ENVIRONMENT=Production` with no env vars → lists the four blank keys and exits. In Development (where that check is skipped) the `AuthHex`/`GeneralLedgerService` HTTP-client registrations now treat a **blank** `BaseUrl` as missing — they only caught null, so the newly blank base values would have surfaced as an unnamed `UriFormatException` — and `GeneralLedgerClient` returns `GL_SERVICE_UNAVAILABLE` naming `GeneralLedgerService:ApiKey` instead of forwarding GL's 401 for an empty `X-Api-Key`.
|
||||
- [x] **`LogFile:Path`** drives the Serilog file sink (default unchanged); **Swagger** is now off only in Production (test/Staging keeps it; Apache does not proxy it); **`UseForwardedHeaders`** for X-Forwarded-For/Proto from the co-located Apache.
|
||||
- [x] **Seeder split** — `DataSeeder.SeedAsync(db, includeDemoData)`: reference data (reason codes, item types, product/branch/notification config, sales document counters) always; demo data (MAIN/SHOP warehouses, PCS/BOX UOMs, General Goods, items, stock layers, customers, bundles, sample docs) only with `Seed:DemoData` — default on in Development, `Seed__DemoData=true` on test, never on release. **Deviation from the plan:** no `Seed` key in base `appsettings.json`, because it would override the Development default.
|
||||
- [x] **`ERPCore.Tests` added to `ERPCore.sln`.** It was outside the solution and had **stopped compiling** (`TestCurrentUser` lacked `ICurrentUser.RoleCode`/`IsAdmin`, added in `f664961`) — fixed, so CI can run it.
|
||||
- [x] `dotnet ef migrations has-pending-model-changes` verified with a blank connection string (what CI has) → "No changes have been made to the model since the last migration."
|
||||
- Pipeline assets: `.gitea/workflows/{ci,deploy}.yml`, `deploy/scripts/*.sh`, `deploy/{systemd,apache,env}/`. `detect.sh` exercised locally against a scratch state dir (first deploy / no-op / frontend-only / forced backend / rollback / non-deployable branch — all as designed); `migration-scan.sh`'s awk ignores `Down()` and flags real `Up()` drops (e.g. `20260831060800_AddPurchaseInvoices`); shellcheck clean at `--severity=warning`. **Not yet exercised:** anything needing Docker or the VPS (container builds, systemd, Apache, the Gitea runner).
|
||||
- [x] **2026-09-15 — switched to a production-only, Fitness-style deployment** (only `erp.hexdive.com` exists; `docs/40-DEPLOYMENT.md` "Active").
|
||||
- A PR merged into `production` runs one job:
|
||||
- detect which side changed (tree hashes, `base.sha` vs merge commit);
|
||||
- host `dotnet publish`;
|
||||
- `pg_dump` when migrations changed (30-day retention);
|
||||
- guard that uploads and logs are outside the publish folder;
|
||||
- stop `erpcore.service`, wipe and copy `/home/erp/core/publish`, `chown erp`, start;
|
||||
- `deploy/scripts/wait-healthy.sh`, which fails fast on a crash and allows up to 300 s for migrations.
|
||||
- No auto-rollback.
|
||||
- The two-environment pipeline, test Apache vhost, `erpcore@.service` and the hash/release/E2E/prune scripts moved to `deploy/on-hold/` (README explains restoring); the `test` branch in `common.sh` is commented out.
|
||||
- Verified locally: both workflows are valid YAML; shellcheck is clean on the active scripts and on every inline `run:` block; the detect step gives the right flags on real merges (`8835ba9` frontend-only, `ee9fa40` backend+migrations, `8555702` neither, `eb36e0a` both), on a manual run and when the base commit is unavailable (both sides); the guard step rejects paths inside the publish folder (including the folder itself and the log path) and accepts `..`-escapes, sibling prefixes and quoted values.
|
||||
- **Not exercised:** the real Gitea run, `erpcore.service` under `/home` (SELinux), `pg_dump` via `runuser`.
|
||||
- [x] **2026-09-15 — `ci.yml` + `deploy.yml` merged into one `.gitea/workflows/ci-cd.yml`.**
|
||||
- Check jobs run on PR open / update / reopen into `Dev` or `production`; `Build & Deploy` runs only when a PR is merged into `production` (the job checks `pull_request.base.ref` itself, so a merge into `Dev` deploys nothing) or on a manual run.
|
||||
- One concurrency group for the file, never cancelled: a PR push can't interrupt a deploy, but superseded check runs now queue.
|
||||
- The deploy's env vars moved from workflow level to the deploy job, so they don't leak into the check jobs.
|
||||
- Verified locally: valid YAML, and every job `if` evaluated against six event shapes (PR opened into Dev, updated into production, merged into Dev, merged into production, closed unmerged, manual run) gives the intended jobs.
|
||||
- Once branch protection is set up, the required check appears as `CI/CD - ERP Core / ci-ok`.
|
||||
|
||||
## Per-variant content size (2026-08-11) — follow-up to the UOM re-model below
|
||||
|
||||
The UOM re-model put `ContentQty`/`ContentUnit` on `Item` correctly, but the item **create page
|
||||
|
||||
@@ -38,7 +38,8 @@ DEFAULTS = {
|
||||
"api": "http://localhost:5224/api/v1",
|
||||
"auth": "http://localhost:5602",
|
||||
"user": "admin@gmail.com",
|
||||
"password": "Naveen@99",
|
||||
# Deliberately no default: credentials never live in the repo (docs/40-DEPLOYMENT.md §5).
|
||||
"password": None,
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +47,10 @@ def parse_args(description: str) -> argparse.Namespace:
|
||||
p = argparse.ArgumentParser(description=description)
|
||||
for key, default in DEFAULTS.items():
|
||||
p.add_argument(f"--{key}", default=os.environ.get(f"ERP_SMOKE_{key.upper()}", default))
|
||||
return p.parse_args()
|
||||
args = p.parse_args()
|
||||
if not args.password:
|
||||
p.error("no password given: set ERP_SMOKE_PASSWORD or pass --password")
|
||||
return args
|
||||
|
||||
|
||||
class Response:
|
||||
|
||||
Reference in New Issue
Block a user