forked from baron/baron-sso
App 카드 로고 이미지 표시
This commit is contained in:
@@ -4602,6 +4602,48 @@ func (h *AuthHandler) ListLinkedRps(c *fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Consent session payload may omit metadata fields such as logo_url.
|
||||
// Rehydrate missing display fields from the full Hydra client object.
|
||||
for clientID, record := range records {
|
||||
if record == nil {
|
||||
continue
|
||||
}
|
||||
needsHydraLookup := record.Logo == "" || record.URL == "" || record.InitURL == ""
|
||||
if !needsHydraLookup {
|
||||
continue
|
||||
}
|
||||
|
||||
client, err := h.Hydra.GetClient(c.Context(), clientID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if record.Name == "" {
|
||||
name := strings.TrimSpace(client.ClientName)
|
||||
if name == "" {
|
||||
name = client.ClientID
|
||||
}
|
||||
record.Name = name
|
||||
}
|
||||
if record.Logo == "" {
|
||||
record.Logo = extractHydraClientLogo(client.Metadata)
|
||||
}
|
||||
if record.URL == "" {
|
||||
record.URL = resolveLinkedRPURL(
|
||||
client.ClientID,
|
||||
client.ClientURI,
|
||||
client.RedirectURIs,
|
||||
)
|
||||
}
|
||||
if record.InitURL == "" {
|
||||
record.InitURL = resolveLinkedRPInitURL(
|
||||
client.ClientID,
|
||||
record.Scopes,
|
||||
client.RedirectURIs,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// [New] DB에서 과거 동의 내역 가져와 병합 (비활성 RP 포함)
|
||||
if h.ConsentRepo != nil {
|
||||
for _, subject := range subjects {
|
||||
|
||||
@@ -165,3 +165,88 @@ func TestListLinkedRps_PriorityAndAggregation(t *testing.T) {
|
||||
assert.Equal(t, "1", parsedInitURL.Query().Get("auto"))
|
||||
assert.Equal(t, "/clients", parsedInitURL.Query().Get("returnTo"))
|
||||
}
|
||||
|
||||
func TestListLinkedRps_EnrichesLogoFromHydraClientWhenConsentSessionOmitsMetadata(t *testing.T) {
|
||||
transport := roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
switch r.URL.Host {
|
||||
case "kratos.test":
|
||||
if r.URL.Path == "/sessions/whoami" {
|
||||
return httpJSONAny(r, http.StatusOK, map[string]interface{}{
|
||||
"identity": map[string]interface{}{
|
||||
"id": "user-123",
|
||||
},
|
||||
}), nil
|
||||
}
|
||||
case "hydra.test":
|
||||
if r.URL.Path == "/oauth2/auth/sessions/consent" {
|
||||
return httpJSONAny(r, http.StatusOK, []map[string]interface{}{
|
||||
{
|
||||
"client": map[string]interface{}{
|
||||
"client_id": "gitea-client",
|
||||
"client_name": "Gitea",
|
||||
"redirect_uris": []string{
|
||||
"https://gitea.example.com/callback",
|
||||
},
|
||||
},
|
||||
"grant_scope": []string{"openid", "profile"},
|
||||
"handled_at": time.Now().Format(time.RFC3339),
|
||||
},
|
||||
}), nil
|
||||
}
|
||||
if r.URL.Path == "/clients/gitea-client" {
|
||||
return httpJSONAny(r, http.StatusOK, map[string]interface{}{
|
||||
"client_id": "gitea-client",
|
||||
"client_name": "Gitea",
|
||||
"redirect_uris": []string{
|
||||
"https://gitea.example.com/callback",
|
||||
},
|
||||
"metadata": map[string]interface{}{
|
||||
"logo_url": "https://cdn.example.com/gitea.svg",
|
||||
},
|
||||
}), nil
|
||||
}
|
||||
}
|
||||
return httpResponse(r, http.StatusNotFound, "not found"), nil
|
||||
})
|
||||
|
||||
client := &http.Client{Transport: transport}
|
||||
|
||||
origDefault := http.DefaultClient
|
||||
http.DefaultClient = client
|
||||
defer func() {
|
||||
http.DefaultClient = origDefault
|
||||
}()
|
||||
|
||||
h := &AuthHandler{
|
||||
Hydra: &service.HydraAdminService{
|
||||
AdminURL: "http://hydra.test",
|
||||
HTTPClient: client,
|
||||
},
|
||||
KratosAdmin: new(MockKratosAdminService),
|
||||
}
|
||||
|
||||
t.Setenv("KRATOS_PUBLIC_URL", "http://kratos.test")
|
||||
t.Setenv("KRATOS_ADMIN_URL", "http://kratos.test")
|
||||
t.Setenv("HYDRA_PUBLIC_URL", "https://sso.example.com/oidc")
|
||||
|
||||
app := newLinkedRpTestApp(h)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/user/rp/linked", nil)
|
||||
req.Header.Set("Cookie", "ory_kratos_session=valid")
|
||||
|
||||
resp, err := app.Test(req)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
||||
|
||||
var res struct {
|
||||
Items []struct {
|
||||
ID string `json:"id"`
|
||||
Logo string `json:"logo"`
|
||||
} `json:"items"`
|
||||
}
|
||||
json.NewDecoder(resp.Body).Decode(&res)
|
||||
|
||||
assert.Len(t, res.Items, 1)
|
||||
assert.Equal(t, "gitea-client", res.Items[0].ID)
|
||||
assert.Equal(t, "https://cdn.example.com/gitea.svg", res.Items[0].Logo)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math' as math;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -1384,6 +1385,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
_ActivityItem(
|
||||
clientId: rp.id,
|
||||
appName: name,
|
||||
logo: rp.logo.trim(),
|
||||
lastAuthAt: lastAuthLabel,
|
||||
status: statusCode,
|
||||
scopes: rp.scopes,
|
||||
@@ -1522,37 +1524,7 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.appName,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: _ink,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Text(
|
||||
item.status == 'active'
|
||||
? tr('ui.userfront.dashboard.activity.linked')
|
||||
: tr('ui.userfront.dashboard.status.revoked'),
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
_buildActivityCardHeader(item, statusColor),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
tr('ui.userfront.dashboard.last_auth_label'),
|
||||
@@ -1658,6 +1630,115 @@ class _DashboardScreenState extends ConsumerState<DashboardScreen> {
|
||||
return opaqueCard;
|
||||
}
|
||||
|
||||
Widget _buildActivityCardHeader(_ActivityItem item, Color statusColor) {
|
||||
final statusBadge = Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: statusColor,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Text(
|
||||
item.status == 'active'
|
||||
? tr('ui.userfront.dashboard.activity.linked')
|
||||
: tr('ui.userfront.dashboard.status.revoked'),
|
||||
style: const TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return SizedBox(
|
||||
height: 40,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (item.logo.isNotEmpty) ...[
|
||||
_buildActivityLogo(item.logo),
|
||||
const SizedBox(width: 10),
|
||||
],
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
item.appName,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: _ink,
|
||||
height: 1.25,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
statusBadge,
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActivityLogo(String logoUrl) {
|
||||
return SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
child: _buildActivityLogoImage(logoUrl),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActivityLogoImage(String logoUrl) {
|
||||
final isSvg = _isSvgLogoUrl(logoUrl);
|
||||
return isSvg
|
||||
? SvgPicture.network(
|
||||
logoUrl,
|
||||
fit: BoxFit.contain,
|
||||
placeholderBuilder: (context) => _buildActivityLogoLoading(),
|
||||
)
|
||||
: Image.network(
|
||||
logoUrl,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return _buildActivityLogoFallback();
|
||||
},
|
||||
loadingBuilder: (context, child, loadingProgress) {
|
||||
if (loadingProgress == null) {
|
||||
return child;
|
||||
}
|
||||
return _buildActivityLogoLoading();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
bool _isSvgLogoUrl(String logoUrl) {
|
||||
final normalized = logoUrl.trim().toLowerCase();
|
||||
if (normalized.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
final uri = Uri.tryParse(normalized);
|
||||
final path = uri?.path.toLowerCase() ?? normalized;
|
||||
return path.endsWith('.svg');
|
||||
}
|
||||
|
||||
Widget _buildActivityLogoLoading() {
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.grey[400],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActivityLogoFallback() {
|
||||
return Icon(Icons.apps_rounded, size: 20, color: Colors.grey[500]);
|
||||
}
|
||||
|
||||
Widget _buildAccessHistory(AuthTimelineState state, bool isWide) {
|
||||
final sessionsState = ref.watch(userSessionsProvider);
|
||||
if (state.isLoading && state.items.isEmpty) {
|
||||
@@ -2470,6 +2551,7 @@ enum _HistorySessionStatus { current, active, inactive }
|
||||
class _ActivityItem {
|
||||
final String clientId;
|
||||
final String appName;
|
||||
final String logo;
|
||||
final String lastAuthAt;
|
||||
final String status;
|
||||
final String? url;
|
||||
@@ -2482,6 +2564,7 @@ class _ActivityItem {
|
||||
_ActivityItem({
|
||||
required this.clientId,
|
||||
required this.appName,
|
||||
required this.logo,
|
||||
required this.lastAuthAt,
|
||||
required this.status,
|
||||
required this.scopes,
|
||||
|
||||
@@ -184,6 +184,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.2.0"
|
||||
flutter_svg:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_svg
|
||||
sha256: "1ded017b39c8e15c8948ea855070a5ff8ff8b3d5e83f3446e02d6bb12add7ad9"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.4"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@@ -388,6 +396,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_parsing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_parsing
|
||||
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -753,6 +769,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.5"
|
||||
vector_graphics:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics
|
||||
sha256: "81da85e9ca8885ade47f9685b953cb098970d11be4821ac765580a6607ea4373"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.21"
|
||||
vector_graphics_codec:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics_codec
|
||||
sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.13"
|
||||
vector_graphics_compiler:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics_compiler
|
||||
sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -825,6 +865,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.5.0"
|
||||
yaml:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -40,6 +40,7 @@ dependencies:
|
||||
go_router: ^17.0.1
|
||||
http: ^1.6.0
|
||||
flutter_dotenv: ^6.0.0
|
||||
flutter_svg: ^2.2.1
|
||||
url_launcher: ^6.3.2
|
||||
logging: ^1.2.0
|
||||
logger: ^2.0.0
|
||||
|
||||
Reference in New Issue
Block a user