Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
I
IMI-Apps
Project overview
Project overview
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Trendy Dwi Anugrah
IMI-Apps
Commits
da29d648
Commit
da29d648
authored
Oct 28, 2026
by
serverKhansia
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix(auth): repair PostgreSQL column types and preserve login server errors
parent
7d0e284d
Changes
4
Hide whitespace changes
Inline
Side-by-side
Showing
4 changed files
with
158 additions
and
0 deletions
+158
-0
backend/apps/common/management/commands/repair_postgres_column_types.py
...ommon/management/commands/repair_postgres_column_types.py
+60
-0
docs/login-repair-2026-10-28.md
docs/login-repair-2026-10-28.md
+45
-0
mobile/lib/core/services/api_service.dart
mobile/lib/core/services/api_service.dart
+8
-0
mobile/test/login_server_error_test.dart
mobile/test/login_server_error_test.dart
+45
-0
No files found.
backend/apps/common/management/commands/repair_postgres_column_types.py
0 → 100644
View file @
da29d648
"""Restore typed columns imported as text without changing stored identifiers."""
from
django.apps
import
apps
from
django.core.management.base
import
BaseCommand
,
CommandError
from
django.db
import
connection
,
transaction
class
Command
(
BaseCommand
):
help
=
"Validate text columns against Django types; use --apply after backing up."
def
add_arguments
(
self
,
parser
):
parser
.
add_argument
(
"--apply"
,
action
=
"store_true"
)
def
handle
(
self
,
*
args
,
**
options
):
if
connection
.
vendor
!=
"postgresql"
:
raise
CommandError
(
"This repair is only for PostgreSQL."
)
quote
=
connection
.
ops
.
quote_name
changes
=
{}
with
connection
.
cursor
()
as
cursor
:
cursor
.
execute
(
"SELECT table_name, column_name, udt_name "
"FROM information_schema.columns WHERE table_schema = current_schema()"
)
actual
=
{(
t
,
c
):
kind
for
t
,
c
,
kind
in
cursor
.
fetchall
()}
for
model
in
apps
.
get_models
(
include_auto_created
=
True
):
for
field
in
model
.
_meta
.
local_fields
:
key
=
(
model
.
_meta
.
db_table
,
field
.
column
)
target
=
field
.
db_type
(
connection
)
if
actual
.
get
(
key
)
!=
"text"
or
not
target
:
continue
if
target
in
{
"bigint"
,
"integer"
,
"smallint"
,
"boolean"
,
"date"
,
"timestamp with time zone"
,
"time"
,
"double precision"
,
"uuid"
,
"jsonb"
,
}
or
target
.
startswith
(
"numeric("
):
changes
[
key
]
=
(
target
,
field
.
null
)
# Validate every cast first. Any invalid value aborts the entire repair.
with
transaction
.
atomic
(),
connection
.
cursor
()
as
cursor
:
cursor
.
execute
(
"SET LOCAL lock_timeout = '5s'"
)
for
(
table
,
column
),
(
target
,
nullable
)
in
changes
.
items
():
name
=
quote
(
column
)
value
=
f
"NULLIF({name}, '')"
if
nullable
else
name
cursor
.
execute
(
f
"SELECT COUNT(({value})::{target}) FROM {quote(table)}"
)
self
.
stdout
.
write
(
f
"Validated {len(changes)} column conversions."
)
if
options
[
"apply"
]:
tables
=
{}
for
(
table
,
column
),
(
target
,
nullable
)
in
changes
.
items
():
name
=
quote
(
column
)
value
=
f
"NULLIF({name}, '')"
if
nullable
else
name
tables
.
setdefault
(
table
,
[])
.
append
(
f
"ALTER COLUMN {name} TYPE {target} USING ({value})::{target}"
)
for
table
,
alterations
in
tables
.
items
():
cursor
.
execute
(
f
"ALTER TABLE {quote(table)} "
+
", "
.
join
(
alterations
)
)
self
.
stdout
.
write
(
self
.
style
.
SUCCESS
(
"Column types repaired atomically."
))
else
:
self
.
stdout
.
write
(
"Validation only; no schema changes. Back up before --apply."
)
docs/login-repair-2026-10-28.md
0 → 100644
View file @
da29d648
# Perbaikan login, 28 Oktober 2026
## Penyebab
-
HTTP 503 publik:
`imi-api.service`
berhenti normal pukul 12:55 WIB;
Gunicorn pada port 8010 tidak tersedia. Log tidak mengidentifikasi pemicu stop.
-
HTTP 500 sebelum layanan berhenti berhasil direproduksi pada PostgreSQL:
`accounts_member.user_id`
bertipe text, sementara ID user bertipe bigint.
Query relasi gagal dengan
`operator does not exist: text = integer`
.
-
Pemeriksaan model menemukan 389 kolom text yang seharusnya berupa angka,
boolean, tanggal/waktu, JSON, atau UUID.
## Tindakan
1.
Backup custom-format PostgreSQL diverifikasi dengan
`pg_restore --list`
:
`/tmp/imi-login-repair-backup/20261028-133038.dump`
(192516 byte, mode 0600).
Backup berisi data pengguna; jangan commit atau publikasikan. Pindahkan ke
penyimpanan backup privat yang persisten sebelum pembersihan
`/tmp`
.
2.
Command
`repair_postgres_column_types`
memvalidasi cast terhadap model Django.
Default hanya validasi;
`--apply`
menerapkan perubahan dalam satu transaksi,
dengan lock timeout 5 detik. Nilai kosong pada kolom nullable menjadi NULL.
Command hanya memperbaiki tipe, bukan membuat ulang constraint/index/sequence.
3.
Simulasi konversi dan login dijalankan dalam transaksi yang di-rollback.
4.
Setelah simulasi lulus, 389 konversi diterapkan dan
`imi-api.service`
dinyalakan.
Pemeriksaan ulang menemukan 0 konversi tersisa.
5.
Mobile mempertahankan status HTTP 5xx saat login, termasuk respons HTML dari
proxy, alih-alih melaporkannya sebagai status 0/kesalahan koneksi.
## Verifikasi
-
Django system check dan pip check lulus.
-
24 tes akun lulus pada SQLite in-memory dengan OTP development aktif.
-
Simulasi PostgreSQL: login admin/member dan profil 200; katalog event/partner,
notifikasi dan tiket member 200; password salah 401. Tiket untuk admin tanpa
keanggotaan menghasilkan 403 sesuai permission.
-
Uji HTTPS publik dengan verifikasi sertifikat: GET login 405; POST login
admin/member dan profil 200; event, partner, notifikasi, tiket member 200;
password salah 401. Token tidak dicetak atau disimpan oleh skrip pengujian.
-
Tes regresi mobile ditambahkan pada
`mobile/test/login_server_error_test.dart`
.
Flutter/Dart SDK tidak tersedia pada server ini, sehingga tes mobile dan build
APK belum dijalankan. Perubahan mobile memerlukan build/distribusi berikutnya.
Migrasi seed SPKLU yang belum diterapkan tidak diperlukan untuk perbaikan login
dan tidak dijalankan. Konfigurasi endpoint mobile serta file web-admin yang sudah
berubah sebelum pekerjaan ini tidak ditimpa.
mobile/lib/core/services/api_service.dart
View file @
da29d648
...
@@ -101,6 +101,14 @@ class ApiService {
...
@@ -101,6 +101,14 @@ class ApiService {
}),
}),
);
);
if
(
res
.
statusCode
>=
500
)
{
return
ApiResponse
(
success:
false
,
message:
'Server backend mengalami gangguan (HTTP
${res.statusCode}
). Silakan coba lagi nanti.'
,
statusCode:
res
.
statusCode
,
);
}
final
data
=
_decodeJsonSafely
(
res
);
final
data
=
_decodeJsonSafely
(
res
);
if
(
res
.
statusCode
==
200
)
{
if
(
res
.
statusCode
==
200
)
{
if
(
autoSaveTokens
&&
if
(
autoSaveTokens
&&
...
...
mobile/test/login_server_error_test.dart
0 → 100644
View file @
da29d648
import
'package:flutter_test/flutter_test.dart'
;
import
'package:http/http.dart'
as
http
;
import
'package:http/testing.dart'
;
import
'package:shared_preferences/shared_preferences.dart'
;
import
'package:imi_jabar_app/core/services/api_service.dart'
;
import
'package:imi_jabar_app/core/services/storage_service.dart'
;
void
main
(
)
{
TestWidgetsFlutterBinding
.
ensureInitialized
();
setUp
(()
async
{
SharedPreferences
.
setMockInitialValues
({});
await
StorageService
.
init
();
await
StorageService
.
clearAuth
();
});
tearDown
(
ApiService
.
resetClient
);
for
(
final
status
in
[
500
,
503
])
{
test
(
'Login preserves HTTP
$status
for an HTML server error'
,
()
async
{
ApiService
.
client
=
MockClient
((
_
)
async
=>
http
.
Response
(
'<html>Service unavailable</html>'
,
status
,
headers:
{
'content-type'
:
'text/html'
},
));
final
result
=
await
ApiService
.
login
(
identifier:
'test'
,
password:
'test'
,
);
expect
(
result
.
success
,
isFalse
);
expect
(
result
.
statusCode
,
status
);
expect
(
result
.
message
,
contains
(
'HTTP
$status
'
));
expect
(
StorageService
.
getAccessToken
(),
isNull
);
});
}
test
(
'Login distinguishes a network failure from an HTTP response'
,
()
async
{
ApiService
.
client
=
MockClient
((
_
)
async
{
throw
http
.
ClientException
(
'Connection refused'
);
});
final
result
=
await
ApiService
.
login
(
identifier:
'test'
,
password:
'test'
);
expect
(
result
.
success
,
isFalse
);
expect
(
result
.
statusCode
,
0
);
});
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment