Stabilize auth flow and profile images

This commit is contained in:
Codex
2026-07-20 13:38:39 +09:00
parent 57caca8dc8
commit 5d3eee7a16
128 changed files with 28860 additions and 1468 deletions
@@ -101,19 +101,24 @@ class PhoneLoginResponse {
required this.token,
required this.expiresAt,
required this.user,
this.orgContextCredential,
});
final String status;
final String token;
final DateTime expiresAt;
final LoginUser user;
final OrgContextCredential? orgContextCredential;
factory PhoneLoginResponse.fromJson(Map<String, dynamic> json) {
return PhoneLoginResponse(
status: json['status'] as String? ?? '',
token: json['token'] as String? ?? '',
token: json['token'] as String? ?? json['accessToken'] as String? ?? '',
expiresAt: DateTime.parse(json['expiresAt'] as String),
user: LoginUser.fromJson(json['user'] as Map<String, dynamic>? ?? {}),
orgContextCredential: OrgContextCredential.fromJsonOrNull(
json['orgContextCredential'] ?? json['org_context'],
),
);
}
@@ -123,10 +128,176 @@ class PhoneLoginResponse {
'token': token,
'expiresAt': expiresAt.toUtc().toIso8601String(),
'user': user.toJson(),
if (orgContextCredential != null)
'orgContextCredential': orgContextCredential!.toJson(),
};
}
}
class OrgContextCredential {
const OrgContextCredential({
required this.baseUrl,
required this.tenantSlug,
required this.keyId,
required this.keySecret,
this.expiresAt,
});
final String baseUrl;
final String tenantSlug;
final String keyId;
final String keySecret;
final DateTime? expiresAt;
bool get isUsable {
return baseUrl.trim().isNotEmpty &&
tenantSlug.trim().isNotEmpty &&
keyId.trim().isNotEmpty &&
keySecret.trim().isNotEmpty &&
(expiresAt == null || expiresAt!.isAfter(DateTime.now().toUtc()));
}
factory OrgContextCredential.fromJson(Map<String, dynamic> json) {
return OrgContextCredential(
baseUrl: _readString(json, const ['baseUrl', 'base_url']),
tenantSlug: _readString(json, const ['tenantSlug', 'tenant_slug']),
keyId: _readString(json, const ['keyId', 'key_id']),
keySecret: _readString(json, const ['keySecret', 'key_secret']),
expiresAt: _readDate(json['expiresAt'] ?? json['expires_at']),
);
}
static OrgContextCredential? fromJsonOrNull(Object? value) {
if (value is! Map) {
return null;
}
final credential = OrgContextCredential.fromJson(
Map<String, dynamic>.from(value),
);
return credential.isUsable ? credential : null;
}
Map<String, dynamic> toJson() {
return {
'baseUrl': baseUrl,
'tenantSlug': tenantSlug,
'keyId': keyId,
'keySecret': keySecret,
if (expiresAt != null) 'expiresAt': expiresAt!.toUtc().toIso8601String(),
};
}
static String _readString(Map<String, dynamic> json, List<String> keys) {
for (final key in keys) {
final value = json[key];
if (value is String && value.trim().isNotEmpty) {
return value.trim();
}
}
return '';
}
static DateTime? _readDate(Object? value) {
if (value is! String || value.trim().isEmpty) {
return null;
}
return DateTime.tryParse(value)?.toUtc();
}
}
class PhoneLoginLinkInitRequest {
const PhoneLoginLinkInitRequest({
required this.phoneNumber,
required this.device,
});
final String phoneNumber;
final LoginDeviceInfo device;
Map<String, dynamic> toJson() {
return {'phoneNumber': phoneNumber, 'device': device.toJson()};
}
}
class PhoneLoginLinkInitResponse {
const PhoneLoginLinkInitResponse({
required this.status,
required this.pendingRef,
required this.expiresIn,
required this.interval,
required this.resendAfter,
this.provider,
});
final String status;
final String pendingRef;
final int expiresIn;
final int interval;
final int resendAfter;
final String? provider;
factory PhoneLoginLinkInitResponse.fromJson(Map<String, dynamic> json) {
return PhoneLoginLinkInitResponse(
status: json['status'] as String? ?? '',
pendingRef: json['pendingRef'] as String? ?? '',
expiresIn: json['expiresIn'] as int? ?? 180,
interval: json['interval'] as int? ?? json['pollInterval'] as int? ?? 3,
resendAfter: json['resendAfter'] as int? ?? 30,
provider: json['provider'] as String?,
);
}
}
class PhoneLoginLinkPollRequest {
const PhoneLoginLinkPollRequest({required this.pendingRef});
final String pendingRef;
Map<String, dynamic> toJson() {
return {'pendingRef': pendingRef};
}
}
class PhoneLoginLinkPollResponse {
const PhoneLoginLinkPollResponse({
required this.status,
this.code,
this.interval,
this.session,
});
final String status;
final String? code;
final int? interval;
final PhoneLoginResponse? session;
bool get isPending =>
status == 'pending' ||
code == 'authorization_pending' ||
code == 'slow_down';
bool get isExpired => code == 'expired_token';
factory PhoneLoginLinkPollResponse.fromJson(Map<String, dynamic> json) {
PhoneLoginResponse? session;
if (json['session'] is Map<String, dynamic>) {
session = PhoneLoginResponse.fromJson(
json['session'] as Map<String, dynamic>,
);
} else if ((json['token'] != null || json['accessToken'] != null) &&
json['expiresAt'] != null) {
session = PhoneLoginResponse.fromJson(json);
}
return PhoneLoginLinkPollResponse(
status: json['status'] as String? ?? '',
code: json['code'] as String?,
interval: json['interval'] as int? ?? json['pollInterval'] as int?,
session: session,
);
}
}
class UserPermissions {
const UserPermissions({
required this.directory,