Skip to content

Commit 18f60bb

Browse files
Fix login.
1 parent f416318 commit 18f60bb

File tree

9 files changed

+85
-62
lines changed

9 files changed

+85
-62
lines changed

.github/workflows/dev.yml

-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ jobs:
3131
uses: docker/[email protected]
3232
with:
3333
load: true
34-
build-args: "NOTIFO__RUNTIME__VERSION=1.0.0-dev-${{ env.BUILD_NUMBER }}"
3534
cache-from: type=gha
3635
cache-to: type=gha,mode=max
3736
tags: notifo-local

backend/src/Notifo.Domain/Identity/IUser.cs

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ public interface IUser
1919

2020
object Identity { get; }
2121

22+
bool HasLoginOrPassword { get; }
23+
2224
IReadOnlySet<string> Roles { get; }
2325

2426
IReadOnlyList<Claim> Claims { get; }

backend/src/Notifo.Identity/DefaultUserService.cs

+5-4
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,6 @@ public async Task<IUser> CreateAsync(string email, UserValues? values = null, bo
171171
Guard.NotNullOrEmpty(email);
172172

173173
var user = userFactory.Create(email);
174-
175174
try
176175
{
177176
var isFirst = !userManager.Users.Any();
@@ -391,11 +390,13 @@ private Task<IUser[]> ResolveAsync(IEnumerable<IdentityUser> users)
391390

392391
private async Task<IUser> ResolveAsync(IdentityUser user)
393392
{
394-
var (claims, roles) = await AsyncHelper.WhenAll(
393+
var (claims, roles, logins, hasPassword) = await AsyncHelper.WhenAll(
395394
userManager.GetClaimsAsync(user),
396-
userManager.GetRolesAsync(user));
395+
userManager.GetRolesAsync(user),
396+
userManager.GetLoginsAsync(user),
397+
userManager.HasPasswordAsync(user));
397398

398-
return new UserWithClaims(user, claims.ToList(), roles.ToHashSet());
399+
return new UserWithClaims(user, claims.ToList(), roles.ToHashSet(), logins.Any() || hasPassword);
399400
}
400401

401402
private async Task<IUser?> ResolveOptionalAsync(IdentityUser? user)

backend/src/Notifo.Identity/UserWithClaims.cs

+11-19
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,15 @@
1212

1313
namespace Notifo.Identity;
1414

15-
internal sealed class UserWithClaims : IUser
15+
internal sealed class UserWithClaims(
16+
IdentityUser user,
17+
IReadOnlyList<Claim> claims,
18+
IReadOnlySet<string> roles,
19+
bool hasLoginOrPassword) : IUser
1620
{
17-
private readonly IdentityUser snapshot;
21+
private readonly IdentityUser snapshot = SimpleMapper.Map(user, new IdentityUser());
1822

19-
public IdentityUser Identity { get; }
23+
public IdentityUser Identity { get; } = user;
2024

2125
public string Id
2226
{
@@ -33,23 +37,11 @@ public bool IsLocked
3337
get => snapshot.LockoutEnd > DateTimeOffset.UtcNow;
3438
}
3539

36-
public IReadOnlyList<Claim> Claims { get; }
40+
public bool HasLoginOrPassword { get; } = hasLoginOrPassword;
3741

38-
public IReadOnlySet<string> Roles { get; }
42+
public IReadOnlyList<Claim> Claims { get; } = claims;
3943

40-
object IUser.Identity => Identity;
41-
42-
public UserWithClaims(IdentityUser user, IReadOnlyList<Claim> claims, IReadOnlySet<string> roles)
43-
{
44-
Identity = user;
45-
46-
// Clone the user so that we capture the previous values, even when the user is updated.
47-
snapshot = SimpleMapper.Map(user, new IdentityUser());
44+
public IReadOnlySet<string> Roles { get; } = roles;
4845

49-
// Claims are immutable so we do not need a copy of them.
50-
Claims = claims;
51-
52-
// Roles are immutable so we do not need a copy of them.
53-
Roles = roles;
54-
}
46+
object IUser.Identity => Identity;
5547
}

backend/src/Notifo.Infrastructure/Tasks/AsyncHelper.cs

+9
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,15 @@ public static class AsyncHelper
2424

2525
#pragma warning disable MA0042 // Do not use blocking calls in an async method
2626
return (task1.Result, task2.Result, task3.Result);
27+
#pragma warning restore MA0042 // Do not use blocking calls in an async method
28+
}
29+
30+
public static async Task<(T1, T2, T3, T4)> WhenAll<T1, T2, T3, T4>(Task<T1> task1, Task<T2> task2, Task<T3> task3, Task<T4> task4)
31+
{
32+
await Task.WhenAll(task1, task2, task3, task4);
33+
34+
#pragma warning disable MA0042 // Do not use blocking calls in an async method
35+
return (task1.Result, task2.Result, task3.Result, task4.Result);
2736
#pragma warning restore MA0042 // Do not use blocking calls in an async method
2837
}
2938
}

backend/src/Notifo/Areas/Account/Pages/ExternalLogin.cshtml.cs

+10-2
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,16 @@ public async Task<IActionResult> OnPostConfirmation()
158158
IUser user;
159159
try
160160
{
161-
user = await UserService.CreateAsync(email, ct: HttpContext.RequestAborted);
161+
var byEmail = await UserService.FindByEmailAsync(email, HttpContext.RequestAborted);
162+
// If the user has no login it has probably been created when he was invited to an app and we can assign it.
163+
if (byEmail?.HasLoginOrPassword == false)
164+
{
165+
user = byEmail;
166+
}
167+
else
168+
{
169+
user = await UserService.CreateAsync(email, ct: HttpContext.RequestAborted);
170+
}
162171

163172
await UserService.AddLoginAsync(user.Id, loginInfo, HttpContext.RequestAborted);
164173
}
@@ -169,7 +178,6 @@ public async Task<IActionResult> OnPostConfirmation()
169178
}
170179

171180
await SignInManager.SignInAsync((IdentityUser)user.Identity, false);
172-
173181
return RedirectTo(ReturnUrl);
174182
}
175183

frontend/src/app/pages/apps/AppDialog.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export const AppDialog = (props: AppDialogProps) => {
6969
<FormAlert text={texts.apps.createInfo} />
7070

7171
<fieldset className='mt-3' disabled={createRunning}>
72-
<Forms.Text name='name' vertical
72+
<Forms.Text name='name' vertical autoFocus
7373
label={texts.common.name} />
7474
</fieldset>
7575

frontend/src/app/shared/components/Forms.tsx

+32-29
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ export interface FormEditorProps {
2020
// The label.
2121
label?: string;
2222

23+
// Indicates that the input should be focused automatically.
24+
autoFocus?: boolean;
25+
2326
// The optional class name.
2427
className?: string;
2528

@@ -169,67 +172,67 @@ export module Forms {
169172
);
170173
};
171174

172-
export const Text = ({ placeholder, ...other }: FormEditorProps) => {
175+
export const Text = ({ autoFocus, placeholder, ...other }: FormEditorProps) => {
173176
return (
174177
<Forms.Row {...other}>
175-
<InputText name={other.name} placeholder={placeholder} />
178+
<InputText name={other.name} placeholder={placeholder} autoFocus={autoFocus} />
176179
</Forms.Row>
177180
);
178181
};
179182

180-
export const Phone = ({ placeholder, ...other }: FormEditorProps) => {
183+
export const Phone = ({ autoFocus, placeholder, ...other }: FormEditorProps) => {
181184
return (
182185
<Forms.Row {...other}>
183-
<InputSpecial type='tel' name={other.name} placeholder={placeholder} />
186+
<InputSpecial type='tel' name={other.name} placeholder={placeholder} autoFocus={autoFocus} />
184187
</Forms.Row>
185188
);
186-
}
189+
};
187190

188-
export const Url = ({ placeholder, ...other }: FormEditorProps) => {
191+
export const Url = ({ autoFocus, placeholder, ...other }: FormEditorProps) => {
189192
return (
190193
<Forms.Row {...other}>
191-
<InputSpecial type='url' name={other.name} placeholder={placeholder} />
194+
<InputSpecial type='url' name={other.name} placeholder={placeholder} autoFocus={autoFocus} />
192195
</Forms.Row>
193196
);
194197
};
195198

196-
export const Email = ({ placeholder, ...other }: FormEditorProps) => {
199+
export const Email = ({ autoFocus, placeholder, ...other }: FormEditorProps) => {
197200
return (
198201
<Forms.Row {...other}>
199-
<InputSpecial type='email' name={other.name} placeholder={placeholder} />
202+
<InputSpecial type='email' name={other.name} placeholder={placeholder} autoFocus={autoFocus} />
200203
</Forms.Row>
201204
);
202205
};
203206

204-
export const Time = ({ placeholder, ...other }: FormEditorProps) => {
207+
export const Time = ({ autoFocus, placeholder, ...other }: FormEditorProps) => {
205208
return (
206209
<Forms.Row {...other}>
207-
<InputSpecial type='time' name={other.name} placeholder={placeholder} />
210+
<InputSpecial type='time' name={other.name} placeholder={placeholder} autoFocus={autoFocus} />
208211
</Forms.Row>
209212
);
210213
};
211214

212-
export const Date = ({ placeholder, ...other }: FormEditorProps) => {
215+
export const Date = ({ autoFocus, placeholder, ...other }: FormEditorProps) => {
213216
return (
214217
<Forms.Row {...other}>
215-
<InputSpecial type='date' name={other.name} placeholder={placeholder} />
218+
<InputSpecial type='date' name={other.name} placeholder={placeholder} autoFocus={autoFocus} />
216219
</Forms.Row>
217220
);
218221
};
219222

220-
export const Textarea = ({ placeholder, ...other }: FormEditorProps) => {
223+
export const Textarea = ({ autoFocus, placeholder, ...other }: FormEditorProps) => {
221224
return (
222225
<Forms.Row {...other}>
223-
<InputTextarea name={other.name} placeholder={placeholder} />
226+
<InputTextarea name={other.name} placeholder={placeholder} autoFocus={autoFocus} />
224227
</Forms.Row>
225228
);
226229
};
227230

228-
export const Number = ({ max, min, placeholder, step, unit, ...other }: FormEditorProps & { unit?: string; min?: number; max?: number; step?: number }) => {
231+
export const Number = ({ autoFocus, max, min, placeholder, step, unit, ...other }: FormEditorProps & { unit?: string; min?: number; max?: number; step?: number }) => {
229232
return (
230233
<Forms.Row {...other}>
231234
<InputGroup>
232-
<InputNumber name={other.name} placeholder={placeholder} max={max} min={min} step={step} />
235+
<InputNumber name={other.name} placeholder={placeholder} max={max} min={min} step={step} autoFocus={autoFocus} />
233236

234237
{unit &&
235238
<InputGroupAddon addonType='prepend'>
@@ -241,10 +244,10 @@ export module Forms {
241244
);
242245
};
243246

244-
export const Password = (props: FormEditorProps) => {
247+
export const Password = ({ autoFocus, placeholder, ...other }: FormEditorProps) => {
245248
return (
246-
<Forms.Row {...props}>
247-
<InputPassword name={props.name} placeholder={props.placeholder} />
249+
<Forms.Row {...other}>
250+
<InputPassword name={other.name} placeholder={placeholder} autoFocus={autoFocus} />
248251
</Forms.Row>
249252
);
250253
};
@@ -282,7 +285,7 @@ const FormDescription = ({ hints }: { hints?: string }) => {
282285
);
283286
};
284287

285-
const InputText = ({ name, picker, placeholder }: FormEditorProps) => {
288+
const InputText = ({ autoFocus, name, picker, placeholder }: FormEditorProps) => {
286289
const { field, fieldState, formState } = useController({ name });
287290

288291
const doAddPick = useEventCallback((pick: string) => {
@@ -295,14 +298,14 @@ const InputText = ({ name, picker, placeholder }: FormEditorProps) => {
295298
<Picker {...picker} onPick={doAddPick} value={field.value} />
296299
}
297300

298-
<Input type='text' id={name} {...field} invalid={isInvalid(fieldState, formState)}
301+
<Input type='text' id={name} {...field} invalid={isInvalid(fieldState, formState)} autoFocus={autoFocus}
299302
placeholder={placeholder}
300303
/>
301304
</div>
302305
);
303306
};
304307

305-
const InputTextarea = ({ name, picker, placeholder }: FormEditorProps) => {
308+
const InputTextarea = ({ autoFocus, name, picker, placeholder }: FormEditorProps) => {
306309
const { field, fieldState, formState } = useController({ name });
307310

308311
const doAddPick = useEventCallback((pick: string) => {
@@ -315,31 +318,31 @@ const InputTextarea = ({ name, picker, placeholder }: FormEditorProps) => {
315318
<Picker {...picker} onPick={doAddPick} value={field.value} />
316319
}
317320

318-
<Input type='textarea' id={name} {...field} invalid={isInvalid(fieldState, formState)}
321+
<Input type='textarea' id={name} {...field} invalid={isInvalid(fieldState, formState)} autoFocus={autoFocus}
319322
placeholder={placeholder}
320323
/>
321324
</div>
322325
);
323326
};
324327

325-
const InputNumber = ({ name, max, min, placeholder, step }: FormEditorProps & { min?: number; max?: number; step?: number }) => {
328+
const InputNumber = ({ autoFocus, name, max, min, placeholder, step }: FormEditorProps & { min?: number; max?: number; step?: number }) => {
326329
const { field, fieldState, formState } = useController({ name });
327330

328331
return (
329332
<>
330-
<Input type='number' id={name} {...field} invalid={isInvalid(fieldState, formState)}
333+
<Input type='number' id={name} {...field} invalid={isInvalid(fieldState, formState)} autoFocus={autoFocus}
331334
max={max} min={min} step={step} placeholder={placeholder}
332335
/>
333336
</>
334337
);
335338
};
336339

337-
const InputSpecial = ({ name, placeholder, type }: FormEditorProps & { type: InputType }) => {
340+
const InputSpecial = ({ autoFocus, name, placeholder, type }: FormEditorProps & { type: InputType }) => {
338341
const { field, fieldState, formState } = useController({ name });
339342

340343
return (
341344
<>
342-
<Input type={type} id={name} {...field} invalid={isInvalid(fieldState, formState)}
345+
<Input type={type} id={name} {...field} invalid={isInvalid(fieldState, formState)} autoFocus={autoFocus}
343346
placeholder={placeholder}
344347
/>
345348
</>
@@ -350,7 +353,7 @@ const InputPassword = ({ name, placeholder }: FormEditorProps) => {
350353

351354
return (
352355
<>
353-
<PasswordInput id={name} {...field} invalid={isInvalid(fieldState, formState)}
356+
<PasswordInput id={name} {...field} invalid={isInvalid(fieldState, formState)} autoFocus={autoFocus}
354357
placeholder={placeholder}
355358
/>
356359
</>

frontend/vite.build.mjs

+15-6
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,20 @@ const dirName = fileURLToPath(new URL('.', import.meta.url));
1616

1717
const inputs = {
1818
// The actual management app.
19-
['app']: path.resolve(dirName, 'index.html'),
19+
['app']: {
20+
entry: path.resolve(dirName, 'index.html'),
21+
format: undefined,
22+
},
2023
// Build the worker separately to prevent exports.
21-
['notifo-sdk']: path.resolve(dirName, 'src/sdk/sdk.ts'),
24+
['notifo-sdk']: {
25+
entry: path.resolve(dirName, 'src/sdk/sdk.ts'),
26+
format: 'iife',
27+
},
2228
// Build the worker separately so that it does not get any file.
23-
['notifo-sdk-worker']: path.resolve(dirName, 'src/sdk/sdk-worker.ts'),
29+
['notifo-sdk-worker']: {
30+
entry: path.resolve(dirName, 'src/sdk/sdk-worker.ts'),
31+
format: 'iife',
32+
},
2433
};
2534

2635
defaultConfig.plugins.push(
@@ -40,18 +49,18 @@ defaultConfig.plugins.push(
4049
async function buildPackages() {
4150
await rimraf('./build');
4251

43-
for (const [chunk, source] of Object.entries(inputs)) {
52+
for (const [chunk, config] of Object.entries(inputs)) {
4453
// https://vitejs.dev/config/
4554
await build({
4655
publicDir: false,
4756
build: {
4857
outDir: 'build',
4958
rollupOptions: {
5059
input: {
51-
[chunk]: source,
60+
[chunk]: config.entry,
5261
},
5362
output: {
54-
format: 'iife',
63+
format: config.format,
5564

5665
entryFileNames: chunk => {
5766
return `${chunk.name}.js`;

0 commit comments

Comments
 (0)