@@ -21,6 +21,99 @@ function serialize(clientIdHex, token) {
2121 } ;
2222}
2323
24+ /**
25+ * Sorts authorized clients by last_access_time, client_name, created_time, and scope.
26+ */
27+ function sortAuthorizedClients ( clients ) {
28+ return clients . sort ( function ( a , b ) {
29+ if ( b . last_access_time > a . last_access_time ) {
30+ return 1 ;
31+ }
32+ if ( b . last_access_time < a . last_access_time ) {
33+ return - 1 ;
34+ }
35+ if ( a . client_name > b . client_name ) {
36+ return 1 ;
37+ }
38+ if ( a . client_name < b . client_name ) {
39+ return - 1 ;
40+ }
41+ if ( a . created_time > b . created_time ) {
42+ return 1 ;
43+ }
44+ if ( a . created_time < b . created_time ) {
45+ return - 1 ;
46+ }
47+ // To help provide a deterministic result order to simplify testing, also sort of scope values.
48+ if ( a . scope > b . scope ) {
49+ return 1 ;
50+ }
51+ if ( a . scope < b . scope ) {
52+ return - 1 ;
53+ }
54+ return 0 ;
55+ } ) ;
56+ }
57+
58+ /**
59+ * Processes access tokens into unified records by clientId.
60+ * Merges multiple access tokens for the same client.
61+ * Filters out clients already seen in refresh tokens and canGrant clients.
62+ * @param {Array } accessTokens
63+ * @param {Set } seenClientIds - ClientIds to exclude
64+ * @returns {Array } Array of serialized records
65+ */
66+ function processAccessTokens ( accessTokens , seenClientIds ) {
67+ const accessTokenRecordsByClientId = new Map ( ) ;
68+
69+ for ( const token of accessTokens ) {
70+ const clientId = token . clientId . toString ( 'hex' ) ;
71+ if ( ! seenClientIds . has ( clientId ) && ! token . clientCanGrant ) {
72+ let record = accessTokenRecordsByClientId . get ( clientId ) ;
73+ if ( typeof record === 'undefined' ) {
74+ record = {
75+ clientId,
76+ clientName : token . clientName ,
77+ createdAt : token . createdAt ,
78+ lastUsedAt : token . createdAt ,
79+ scope : ScopeSet . fromArray ( [ ] ) ,
80+ } ;
81+ accessTokenRecordsByClientId . set ( clientId , record ) ;
82+ }
83+ // Merge details of all access tokens into a single record.
84+ record . scope . add ( token . scope ) ;
85+ if ( token . createdAt < record . createdAt ) {
86+ record . createdAt = token . createdAt ;
87+ }
88+ if ( record . lastUsedAt < token . createdAt ) {
89+ record . lastUsedAt = token . createdAt ;
90+ }
91+ }
92+ }
93+
94+ return Array . from ( accessTokenRecordsByClientId . values ( ) ) . map ( ( record ) =>
95+ serialize ( record . clientId , record )
96+ ) ;
97+ }
98+
99+ /**
100+ * Processes refresh tokens into serialized records.
101+ * @param {Array } refreshTokens
102+ * @returns {Object } { records: Array, seenClientIds: Set }
103+ */
104+ function processRefreshTokens ( refreshTokens ) {
105+ const records = [ ] ;
106+ const seenClientIds = new Set ( ) ;
107+
108+ for ( const token of refreshTokens ) {
109+ const clientId = token . clientId . toString ( 'hex' ) ;
110+ records . push ( serialize ( clientId , token ) ) ;
111+ seenClientIds . add ( clientId ) ;
112+ }
113+
114+ return { records, seenClientIds } ;
115+ }
116+
24117module . exports = {
25118 async destroy ( clientId , uid , refreshTokenId ) {
26119 await oauthDB . ready ( ) ;
@@ -34,90 +127,47 @@ module.exports = {
34127 await oauthDB . deleteClientAuthorization ( clientId , uid ) ;
35128 }
36129 } ,
130+ /**
131+ * Fetches all authorized clients for a given user ID,
132+ * @param {* } uid
133+ * @returns
134+ */
37135 async list ( uid ) {
38136 await oauthDB . ready ( ) ;
39- const authorizedClients = [ ] ;
40-
137+ // get both refresh and access tokens in parallel
41138 const [ refreshTokens , accessTokens ] = await Promise . all ( [
42139 oauthDB . getRefreshTokensByUid ( uid ) ,
43140 oauthDB . getAccessTokensByUid ( uid ) ,
44141 ] ) ;
45142
46- // First, enumerate all the refresh tokens.
47- // Each of these is a separate instance of an authorized client
48- // and should be displayed to the user as such. Nice and simple!
49- const seenClientIds = new Set ( ) ;
50- for ( const token of refreshTokens ) {
51- const clientId = token . clientId . toString ( 'hex' ) ;
52- authorizedClients . push ( serialize ( clientId , token ) ) ;
53- seenClientIds . add ( clientId ) ;
54- }
143+ const { records : refreshRecords , seenClientIds } =
144+ processRefreshTokens ( refreshTokens ) ;
55145
56- // Next, enumerate all the access tokens. In the interests of giving the user a
57- // complete-yet-comprehensible list of all the things attached to their account,
58- // we want to:
59- //
60- // 1. Show a single unified record for any client that is not using refresh tokens.
61- // 2. Avoid showing access tokens for `canGrant` clients; such clients will always
62- // hold some other sort of token, and we don't want them to appear in the list twice.
63- const accessTokenRecordsByClientId = new Map ( ) ;
64- for ( const token of accessTokens ) {
65- const clientId = token . clientId . toString ( 'hex' ) ;
66- if ( ! seenClientIds . has ( clientId ) && ! token . clientCanGrant ) {
67- let record = accessTokenRecordsByClientId . get ( clientId ) ;
68- if ( typeof record === 'undefined' ) {
69- record = {
70- clientId,
71- clientName : token . clientName ,
72- createdAt : token . createdAt ,
73- lastUsedAt : token . createdAt ,
74- scope : ScopeSet . fromArray ( [ ] ) ,
75- } ;
76- accessTokenRecordsByClientId . set ( clientId , record ) ;
77- }
78- // Merge details of all access tokens into a single record.
79- record . scope . add ( token . scope ) ;
80- if ( token . createdAt < record . createdAt ) {
81- record . createdAt = token . createdAt ;
82- }
83- if ( record . lastUsedAt < token . createdAt ) {
84- record . lastUsedAt = token . createdAt ;
85- }
86- }
87- }
88- for ( const [ clientId , record ] of accessTokenRecordsByClientId . entries ( ) ) {
89- authorizedClients . push ( serialize ( clientId , record ) ) ;
90- }
146+ const accessRecords = processAccessTokens ( accessTokens , seenClientIds ) ;
91147
92- // Sort the final list first by last_access_time, then by client_name, then by created_time.
93- authorizedClients . sort ( function ( a , b ) {
94- if ( b . last_access_time > a . last_access_time ) {
95- return 1 ;
96- }
97- if ( b . last_access_time < a . last_access_time ) {
98- return - 1 ;
99- }
100- if ( a . client_name > b . client_name ) {
101- return 1 ;
102- }
103- if ( a . client_name < b . client_name ) {
104- return - 1 ;
105- }
106- if ( a . created_time > b . created_time ) {
107- return 1 ;
108- }
109- if ( a . created_time < b . created_time ) {
110- return - 1 ;
111- }
112- // To help provide a deterministic result order to simplify testing, also sort of scope values.
113- if ( a . scope > b . scope ) {
114- return 1 ;
115- }
116- if ( a . scope < b . scope ) {
117- return - 1 ;
118- }
119- return 0 ;
120- } ) ;
121- return authorizedClients ;
148+ const authorizedClients = [ ...refreshRecords , ...accessRecords ] ;
149+ return sortAuthorizedClients ( authorizedClients ) ;
150+ } ,
151+
152+ /**
153+ * Fetches a list of unique OAuth clients authorized by a user.
154+ * Each clientId appears only once. The DB layer handles uniqueness
155+ * and selects the token with the most recent lastAccessTime.
156+ */
157+ async listUnique ( uid ) {
158+ await oauthDB . ready ( ) ;
159+
160+ const [ refreshTokens , accessTokens ] = await Promise . all ( [
161+ oauthDB . getUniqueRefreshTokensByUid ( uid ) ,
162+ oauthDB . getAccessTokensByUid ( uid ) ,
163+ ] ) ;
164+
165+ const { records : refreshRecords , seenClientIds } =
166+ processRefreshTokens ( refreshTokens ) ;
167+
168+ const accessRecords = processAccessTokens ( accessTokens , seenClientIds ) ;
169+
170+ const authorizedClients = [ ...refreshRecords , ...accessRecords ] ;
171+ return sortAuthorizedClients ( authorizedClients ) ;
122172 } ,
123173} ;
0 commit comments