@@ -18,15 +18,27 @@ public class RawCredentials
18
18
}
19
19
20
20
[ JsonSerializable ( typeof ( RawCredentials ) ) ]
21
- public partial class RawCredentialsJsonContext : JsonSerializerContext
22
- {
23
- }
21
+ public partial class RawCredentialsJsonContext : JsonSerializerContext ;
24
22
25
23
public interface ICredentialManager
26
24
{
27
25
public event EventHandler < CredentialModel > CredentialsChanged ;
28
26
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 ) ;
30
42
31
43
public Task SetCredentials ( string coderUrl , string apiToken , CancellationToken ct = default ) ;
32
44
@@ -37,30 +49,65 @@ public class CredentialManager : ICredentialManager
37
49
{
38
50
private const string CredentialsTargetName = "Coder.Desktop.App.Credentials" ;
39
51
40
- private readonly RaiiSemaphoreSlim _lock = new ( 1 , 1 ) ;
52
+ private readonly RaiiSemaphoreSlim _loadLock = new ( 1 , 1 ) ;
53
+ private readonly RaiiSemaphoreSlim _stateLock = new ( 1 , 1 ) ;
41
54
private CredentialModel ? _latestCredentials ;
42
55
43
56
public event EventHandler < CredentialModel > ? CredentialsChanged ;
44
57
45
- public CredentialModel GetCredentials ( )
58
+ public CredentialModel GetCachedCredentials ( )
46
59
{
47
- using var _ = _lock . Lock ( ) ;
60
+ using var _ = _stateLock . Lock ( ) ;
48
61
if ( _latestCredentials != null ) return _latestCredentials . Clone ( ) ;
49
62
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
53
104
{
54
105
State = CredentialState . Invalid ,
55
106
} ;
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 ( ) ;
64
111
}
65
112
66
113
public async Task SetCredentials ( string coderUrl , string apiToken , CancellationToken ct = default )
@@ -73,37 +120,15 @@ public async Task SetCredentials(string coderUrl, string apiToken, CancellationT
73
120
if ( uri . PathAndQuery != "/" ) throw new ArgumentException ( "Coder URL must be the root URL" , nameof ( coderUrl ) ) ;
74
121
if ( string . IsNullOrWhiteSpace ( apiToken ) ) throw new ArgumentException ( "API token is required" , nameof ( apiToken ) ) ;
75
122
apiToken = apiToken . Trim ( ) ;
76
- if ( apiToken . Length != 33 )
77
- throw new ArgumentOutOfRangeException ( nameof ( apiToken ) , "API token must be 33 characters long" ) ;
78
123
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
96
125
{
97
126
CoderUrl = coderUrl ,
98
127
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 ) ;
107
132
}
108
133
109
134
public void ClearCredentials ( )
@@ -112,14 +137,47 @@ public void ClearCredentials()
112
137
UpdateState ( new CredentialModel
113
138
{
114
139
State = CredentialState . Invalid ,
115
- CoderUrl = null ,
116
- ApiToken = null ,
117
140
} ) ;
118
141
}
119
142
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
+
120
178
private void UpdateState ( CredentialModel newModel )
121
179
{
122
- using ( _lock . Lock ( ) )
180
+ using ( _stateLock . Lock ( ) )
123
181
{
124
182
_latestCredentials = newModel . Clone ( ) ;
125
183
}
0 commit comments