@@ -18,15 +18,27 @@ public class RawCredentials
1818}
1919
2020[ JsonSerializable ( typeof ( RawCredentials ) ) ]
21- public partial class RawCredentialsJsonContext : JsonSerializerContext
22- {
23- }
21+ public partial class RawCredentialsJsonContext : JsonSerializerContext ;
2422
2523public interface ICredentialManager
2624{
2725 public event EventHandler < CredentialModel > CredentialsChanged ;
2826
29- public CredentialModel GetCredentials ( ) ;
27+ /// <summary>
28+ /// Returns cached credentials or an invalid credential model if none are cached. It's preferable to use
29+ /// LoadCredentials if you are operating in an async context.
30+ /// </summary>
31+ public CredentialModel GetCachedCredentials ( ) ;
32+
33+ /// <summary>
34+ /// Get any sign-in URL. The returned value is not parsed to check if it's a valid URI.
35+ /// </summary>
36+ public string ? GetSignInUri ( ) ;
37+
38+ /// <summary>
39+ /// Returns cached credentials or loads/verifies them from storage if not cached.
40+ /// </summary>
41+ public Task < CredentialModel > LoadCredentials ( CancellationToken ct = default ) ;
3042
3143 public Task SetCredentials ( string coderUrl , string apiToken , CancellationToken ct = default ) ;
3244
@@ -37,30 +49,65 @@ public class CredentialManager : ICredentialManager
3749{
3850 private const string CredentialsTargetName = "Coder.Desktop.App.Credentials" ;
3951
40- private readonly RaiiSemaphoreSlim _lock = new ( 1 , 1 ) ;
52+ private readonly RaiiSemaphoreSlim _loadLock = new ( 1 , 1 ) ;
53+ private readonly RaiiSemaphoreSlim _stateLock = new ( 1 , 1 ) ;
4154 private CredentialModel ? _latestCredentials ;
4255
4356 public event EventHandler < CredentialModel > ? CredentialsChanged ;
4457
45- public CredentialModel GetCredentials ( )
58+ public CredentialModel GetCachedCredentials ( )
4659 {
47- using var _ = _lock . Lock ( ) ;
60+ using var _ = _stateLock . Lock ( ) ;
4861 if ( _latestCredentials != null ) return _latestCredentials . Clone ( ) ;
4962
50- var rawCredentials = ReadCredentials ( ) ;
51- if ( rawCredentials is null )
52- _latestCredentials = new CredentialModel
63+ return new CredentialModel
64+ {
65+ State = CredentialState . Unknown ,
66+ } ;
67+ }
68+
69+ public string ? GetSignInUri ( )
70+ {
71+ try
72+ {
73+ var raw = ReadCredentials ( ) ;
74+ if ( raw is not null && ! string . IsNullOrWhiteSpace ( raw . CoderUrl ) ) return raw . CoderUrl ;
75+ }
76+ catch
77+ {
78+ // ignored
79+ }
80+
81+ return null ;
82+ }
83+
84+ public async Task < CredentialModel > LoadCredentials ( CancellationToken ct = default )
85+ {
86+ using var _ = await _loadLock . LockAsync ( ct ) ;
87+ using ( await _stateLock . LockAsync ( ct ) )
88+ {
89+ if ( _latestCredentials != null ) return _latestCredentials . Clone ( ) ;
90+ }
91+
92+ CredentialModel model ;
93+ try
94+ {
95+ var raw = ReadCredentials ( ) ;
96+ model = await PopulateModel ( raw , ct ) ;
97+ }
98+ catch ( Exception e )
99+ {
100+ // We don't need to clear the credentials here, the app will think
101+ // they're unset and any subsequent SetCredentials call after the
102+ // user signs in again will overwrite the old invalid ones.
103+ model = new CredentialModel
53104 {
54105 State = CredentialState . Invalid ,
55106 } ;
56- else
57- _latestCredentials = new CredentialModel
58- {
59- State = CredentialState . Valid ,
60- CoderUrl = rawCredentials . CoderUrl ,
61- ApiToken = rawCredentials . ApiToken ,
62- } ;
63- return _latestCredentials . Clone ( ) ;
107+ }
108+
109+ UpdateState ( model . Clone ( ) ) ;
110+ return model . Clone ( ) ;
64111 }
65112
66113 public async Task SetCredentials ( string coderUrl , string apiToken , CancellationToken ct = default )
@@ -73,37 +120,15 @@ public async Task SetCredentials(string coderUrl, string apiToken, CancellationT
73120 if ( uri . PathAndQuery != "/" ) throw new ArgumentException ( "Coder URL must be the root URL" , nameof ( coderUrl ) ) ;
74121 if ( string . IsNullOrWhiteSpace ( apiToken ) ) throw new ArgumentException ( "API token is required" , nameof ( apiToken ) ) ;
75122 apiToken = apiToken . Trim ( ) ;
76- if ( apiToken . Length != 33 )
77- throw new ArgumentOutOfRangeException ( nameof ( apiToken ) , "API token must be 33 characters long" ) ;
78123
79- try
80- {
81- var cts = CancellationTokenSource . CreateLinkedTokenSource ( ct ) ;
82- cts . CancelAfter ( TimeSpan . FromSeconds ( 15 ) ) ;
83- var sdkClient = new CoderApiClient ( uri ) ;
84- sdkClient . SetSessionToken ( apiToken ) ;
85- // TODO: we should probably perform a version check here too,
86- // rather than letting the service do it on Start
87- _ = await sdkClient . GetBuildInfo ( cts . Token ) ;
88- _ = await sdkClient . GetUser ( User . Me , cts . Token ) ;
89- }
90- catch ( Exception e )
91- {
92- throw new InvalidOperationException ( "Could not connect to or verify Coder server" , e ) ;
93- }
94-
95- WriteCredentials ( new RawCredentials
124+ var raw = new RawCredentials
96125 {
97126 CoderUrl = coderUrl ,
98127 ApiToken = apiToken ,
99- } ) ;
100-
101- UpdateState ( new CredentialModel
102- {
103- State = CredentialState . Valid ,
104- CoderUrl = coderUrl ,
105- ApiToken = apiToken ,
106- } ) ;
128+ } ;
129+ var model = await PopulateModel ( raw , ct ) ;
130+ WriteCredentials ( raw ) ;
131+ UpdateState ( model ) ;
107132 }
108133
109134 public void ClearCredentials ( )
@@ -112,14 +137,47 @@ public void ClearCredentials()
112137 UpdateState ( new CredentialModel
113138 {
114139 State = CredentialState . Invalid ,
115- CoderUrl = null ,
116- ApiToken = null ,
117140 } ) ;
118141 }
119142
143+ private async Task < CredentialModel > PopulateModel ( RawCredentials ? credentials , CancellationToken ct = default )
144+ {
145+ if ( credentials is null || string . IsNullOrWhiteSpace ( credentials . CoderUrl ) ||
146+ string . IsNullOrWhiteSpace ( credentials . ApiToken ) )
147+ return new CredentialModel
148+ {
149+ State = CredentialState . Invalid ,
150+ } ;
151+
152+ BuildInfo buildInfo ;
153+ User me ;
154+ try
155+ {
156+ var cts = CancellationTokenSource . CreateLinkedTokenSource ( ct ) ;
157+ cts . CancelAfter ( TimeSpan . FromSeconds ( 15 ) ) ;
158+ var sdkClient = new CoderApiClient ( credentials . CoderUrl ) ;
159+ sdkClient . SetSessionToken ( credentials . ApiToken ) ;
160+ buildInfo = await sdkClient . GetBuildInfo ( cts . Token ) ;
161+ me = await sdkClient . GetUser ( User . Me , cts . Token ) ;
162+ }
163+ catch ( Exception e )
164+ {
165+ throw new InvalidOperationException ( "Could not connect to or verify Coder server" , e ) ;
166+ }
167+
168+ ServerVersionUtilities . ParseAndValidateServerVersion ( buildInfo . Version ) ;
169+ return new CredentialModel
170+ {
171+ State = CredentialState . Valid ,
172+ CoderUrl = credentials . CoderUrl ,
173+ ApiToken = credentials . ApiToken ,
174+ Username = me . Username ,
175+ } ;
176+ }
177+
120178 private void UpdateState ( CredentialModel newModel )
121179 {
122- using ( _lock . Lock ( ) )
180+ using ( _stateLock . Lock ( ) )
123181 {
124182 _latestCredentials = newModel . Clone ( ) ;
125183 }
0 commit comments