firstly 0.0.6 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/esm/ROUTES.d.ts +11 -11
- package/esm/ROUTES.js +5 -5
- package/esm/auth/Adapter.js +4 -2
- package/esm/auth/AuthController.server.js +79 -57
- package/esm/auth/Entities.d.ts +1 -1
- package/esm/auth/Entities.js +3 -3
- package/esm/auth/RoleHelpers.js +8 -8
- package/esm/auth/client/Auth.js +1 -1
- package/esm/auth/index.js +2 -2
- package/esm/mail/index.js +1 -1
- package/esm/mail/templates/DefaultMail.svelte +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# firstly
|
|
2
2
|
|
|
3
|
+
## 0.0.7
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#25](https://github.com/jycouet/firstly/pull/25)
|
|
8
|
+
[`54f2f6a`](https://github.com/jycouet/firstly/commit/54f2f6a833c1977c3163e91ce3172fa8edc9da47)
|
|
9
|
+
Thanks [@jycouet](https://github.com/jycouet)! - adding e2e tests for accounts
|
|
10
|
+
|
|
11
|
+
- [#25](https://github.com/jycouet/firstly/pull/25)
|
|
12
|
+
[`943e9d0`](https://github.com/jycouet/firstly/commit/943e9d0b6d5d6a631dc78661d188a76f254d4632)
|
|
13
|
+
Thanks [@jycouet](https://github.com/jycouet)! - rename name to identifier in db
|
|
14
|
+
|
|
3
15
|
## 0.0.6
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/esm/ROUTES.d.ts
CHANGED
|
@@ -32,16 +32,16 @@ declare const AllObjs: {
|
|
|
32
32
|
repo?: (string | number);
|
|
33
33
|
}) => string;
|
|
34
34
|
"/": string;
|
|
35
|
+
"/auth": string;
|
|
35
36
|
"/mail": string;
|
|
36
|
-
"/
|
|
37
|
-
"/
|
|
38
|
-
"/
|
|
39
|
-
"/
|
|
40
|
-
"/ui": string;
|
|
37
|
+
"/ui/dialog": string;
|
|
38
|
+
"/ui/enum": string;
|
|
39
|
+
"/ui/fieldGroup": string;
|
|
40
|
+
"/ui/select": string;
|
|
41
41
|
};
|
|
42
42
|
type AllTypes = typeof AllObjs;
|
|
43
43
|
export type Routes = keyof AllTypes extends `${string}/${infer Route}` ? `/${Route}` : keyof AllTypes;
|
|
44
|
-
export declare const routes: ("/" | "/
|
|
44
|
+
export declare const routes: ("/" | "/auth" | "/mail" | "/ui/dialog" | "/ui/enum" | "/ui/fieldGroup" | "/ui/select" | "firstly_sign_in" | "remult_admin" | "github")[];
|
|
45
45
|
/**
|
|
46
46
|
* To be used like this:
|
|
47
47
|
* ```ts
|
|
@@ -70,12 +70,12 @@ export declare function route<T extends NonFunctionKeys<AllTypes>>(key: T): stri
|
|
|
70
70
|
export type KIT_ROUTES = {
|
|
71
71
|
PAGES: {
|
|
72
72
|
'/': never;
|
|
73
|
+
'/auth': never;
|
|
73
74
|
'/mail': never;
|
|
74
|
-
'/
|
|
75
|
-
'/
|
|
76
|
-
'/
|
|
77
|
-
'/
|
|
78
|
-
'/ui': never;
|
|
75
|
+
'/ui/dialog': never;
|
|
76
|
+
'/ui/enum': never;
|
|
77
|
+
'/ui/fieldGroup': never;
|
|
78
|
+
'/ui/select': never;
|
|
79
79
|
};
|
|
80
80
|
SERVERS: Record<string, never>;
|
|
81
81
|
ACTIONS: Record<string, never>;
|
package/esm/ROUTES.js
CHANGED
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
*/
|
|
10
10
|
const PAGES = {
|
|
11
11
|
"/": `/`,
|
|
12
|
+
"/auth": `/auth`,
|
|
12
13
|
"/mail": `/mail`,
|
|
13
|
-
"/
|
|
14
|
-
"/
|
|
15
|
-
"/
|
|
16
|
-
"/
|
|
17
|
-
"/ui": `/ui`
|
|
14
|
+
"/ui/dialog": `/ui/dialog`,
|
|
15
|
+
"/ui/enum": `/ui/enum`,
|
|
16
|
+
"/ui/fieldGroup": `/ui/fieldGroup`,
|
|
17
|
+
"/ui/select": `/ui/select`
|
|
18
18
|
};
|
|
19
19
|
/**
|
|
20
20
|
* SERVERS
|
package/esm/auth/Adapter.js
CHANGED
|
@@ -7,12 +7,14 @@ export class RemultLuciaAdapter {
|
|
|
7
7
|
if (session) {
|
|
8
8
|
const user = await remult.repo(oSafe.User).findId(session.userId);
|
|
9
9
|
if (user) {
|
|
10
|
+
const { identifier, ...userInfo } = user;
|
|
10
11
|
return [
|
|
11
12
|
{ ...session, attributes: {} },
|
|
12
13
|
{
|
|
13
|
-
...
|
|
14
|
+
...userInfo,
|
|
14
15
|
attributes: {
|
|
15
|
-
...
|
|
16
|
+
...userInfo,
|
|
17
|
+
name: identifier,
|
|
16
18
|
session: { id: session.id, expiresAt: session.expiresAt },
|
|
17
19
|
},
|
|
18
20
|
},
|
|
@@ -3,7 +3,7 @@ import { DEV } from 'esm-env';
|
|
|
3
3
|
import { generateId } from 'lucia';
|
|
4
4
|
import { createDate, TimeSpan } from 'oslo';
|
|
5
5
|
import { remult } from 'remult';
|
|
6
|
-
import { green, yellow } from '@kitql/helpers';
|
|
6
|
+
import { green, magenta, yellow } from '@kitql/helpers';
|
|
7
7
|
import { AUTH_OPTIONS, getSafeOptions, logAuth, lucia } from '.';
|
|
8
8
|
import { sendMail } from '../mail';
|
|
9
9
|
import { FFAuthProvider } from './Entities.js';
|
|
@@ -58,11 +58,11 @@ export class AuthControllerServer {
|
|
|
58
58
|
throw new Error(`${name} not found as demo account!`);
|
|
59
59
|
}
|
|
60
60
|
const oSafe = getSafeOptions();
|
|
61
|
-
let user = await remult.repo(oSafe.User).findFirst({ name });
|
|
61
|
+
let user = await remult.repo(oSafe.User).findFirst({ identifier: name });
|
|
62
62
|
if (!user) {
|
|
63
63
|
user = remult.repo(oSafe.User).create();
|
|
64
64
|
}
|
|
65
|
-
user.
|
|
65
|
+
user.identifier = name;
|
|
66
66
|
const r = mergeRoles(user.roles, account.roles);
|
|
67
67
|
user.roles = r.roles;
|
|
68
68
|
await remult.repo(oSafe.User).save(user);
|
|
@@ -75,22 +75,34 @@ export class AuthControllerServer {
|
|
|
75
75
|
*/
|
|
76
76
|
static async invite(email) {
|
|
77
77
|
const oSafe = getSafeOptions();
|
|
78
|
-
const
|
|
79
|
-
|
|
78
|
+
const existingAccount = await remult.repo(oSafe.Account).findOne({
|
|
79
|
+
where: {
|
|
80
|
+
providerUserId: email,
|
|
81
|
+
provider: FFAuthProvider.PASSWORD.id,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
if (existingAccount) {
|
|
80
85
|
// throw Error("Already invited !")
|
|
81
86
|
}
|
|
82
87
|
else {
|
|
83
|
-
const
|
|
84
|
-
|
|
88
|
+
const token = generateId(40);
|
|
89
|
+
await remult.repo(oSafe.Account).insert({
|
|
90
|
+
provider: FFAuthProvider.PASSWORD.id,
|
|
91
|
+
providerUserId: email,
|
|
92
|
+
// userId: user.id,
|
|
93
|
+
// hashPassword: await passwordHash(password),
|
|
94
|
+
token: oSafe.verifiedMethod === 'auto' ? undefined : token,
|
|
95
|
+
expiresAt: createDate(new TimeSpan(AUTH_OPTIONS.providers?.password?.verifyMailExpiresIn ?? 5 * 60, 's')),
|
|
96
|
+
lastVerifiedAt: undefined,
|
|
85
97
|
});
|
|
86
|
-
const url = `${remult.context.url.origin}`;
|
|
98
|
+
const url = `${remult.context.url.origin}${oSafe.firstlyData.props.ui?.paths.verify_email}?token=${token}`;
|
|
87
99
|
if (AUTH_OPTIONS?.invitationSend) {
|
|
88
100
|
await AUTH_OPTIONS?.invitationSend({ email, url });
|
|
89
|
-
logAuth.success(
|
|
101
|
+
logAuth.success(`${green('[custom]')}${magenta('[invitationSend]')} (${yellow(url)})`);
|
|
90
102
|
return 'Mail sent !';
|
|
91
103
|
}
|
|
92
104
|
else {
|
|
93
|
-
await sendMail('
|
|
105
|
+
await sendMail('invitationSend', {
|
|
94
106
|
to: email,
|
|
95
107
|
subject: 'Invitation',
|
|
96
108
|
templateProps: {
|
|
@@ -111,7 +123,7 @@ export class AuthControllerServer {
|
|
|
111
123
|
],
|
|
112
124
|
},
|
|
113
125
|
});
|
|
114
|
-
logAuth.success(
|
|
126
|
+
logAuth.success(`${magenta('[invitationSend]')} (${yellow(url)})`);
|
|
115
127
|
return 'Demo Mail sent !';
|
|
116
128
|
}
|
|
117
129
|
}
|
|
@@ -129,37 +141,49 @@ export class AuthControllerServer {
|
|
|
129
141
|
if (!oSafe.password_enabled) {
|
|
130
142
|
throw Error('Password is not enabled!');
|
|
131
143
|
}
|
|
132
|
-
const
|
|
133
|
-
|
|
144
|
+
const existingAccount = await remult.repo(oSafe.Account).findOne({
|
|
145
|
+
where: {
|
|
146
|
+
providerUserId: email,
|
|
147
|
+
provider: FFAuthProvider.PASSWORD.id,
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
if (existingAccount) {
|
|
134
151
|
throw Error("You can't signup twice !");
|
|
135
152
|
}
|
|
136
153
|
checkPassword(password);
|
|
137
|
-
const user = await remult.repo(oSafe.User).insert({
|
|
138
|
-
name: email,
|
|
139
|
-
});
|
|
140
154
|
const token = generateId(40);
|
|
141
|
-
await remult.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
:
|
|
150
|
-
|
|
155
|
+
await remult.dataProvider.transaction(async () => {
|
|
156
|
+
const user = await remult.repo(oSafe.User).insert({
|
|
157
|
+
identifier: email,
|
|
158
|
+
});
|
|
159
|
+
await remult.repo(oSafe.Account).insert({
|
|
160
|
+
provider: FFAuthProvider.PASSWORD.id,
|
|
161
|
+
providerUserId: email,
|
|
162
|
+
userId: user.id,
|
|
163
|
+
hashPassword: await passwordHash(password),
|
|
164
|
+
token: oSafe.verifiedMethod === 'auto' ? undefined : token,
|
|
165
|
+
expiresAt: oSafe.verifiedMethod === 'auto'
|
|
166
|
+
? undefined
|
|
167
|
+
: createDate(new TimeSpan(AUTH_OPTIONS.providers?.password?.verifyMailExpiresIn ?? 5 * 60, 's')),
|
|
168
|
+
lastVerifiedAt: oSafe.verifiedMethod === 'auto' ? new Date() : undefined,
|
|
169
|
+
});
|
|
151
170
|
});
|
|
152
171
|
if (oSafe.verifiedMethod === 'auto') {
|
|
153
|
-
await
|
|
172
|
+
const user = await remult.repo(oSafe.User).findFirst({
|
|
173
|
+
identifier: email,
|
|
174
|
+
});
|
|
175
|
+
if (user) {
|
|
176
|
+
await createSession(user.id);
|
|
177
|
+
}
|
|
154
178
|
}
|
|
155
179
|
else {
|
|
156
180
|
const url = `${remult.context.url.origin}${oSafe.firstlyData.props.ui?.paths.verify_email}?token=${token}`;
|
|
157
181
|
if (AUTH_OPTIONS.providers?.password?.verifyMailSend) {
|
|
158
182
|
await AUTH_OPTIONS.providers?.password.verifyMailSend({ email, url });
|
|
159
|
-
logAuth.success(
|
|
183
|
+
logAuth.success(`${green('[custom]')}${magenta('[verifyMailSend]')} (${yellow(url)})`);
|
|
160
184
|
}
|
|
161
185
|
else {
|
|
162
|
-
await sendMail('
|
|
186
|
+
await sendMail('verifyMailSend', {
|
|
163
187
|
to: email,
|
|
164
188
|
subject: 'Wecome',
|
|
165
189
|
templateProps: {
|
|
@@ -176,7 +200,7 @@ export class AuthControllerServer {
|
|
|
176
200
|
],
|
|
177
201
|
},
|
|
178
202
|
});
|
|
179
|
-
logAuth.success(
|
|
203
|
+
logAuth.success(`${magenta('[verifyMailSend]')} (${yellow(url)})`);
|
|
180
204
|
}
|
|
181
205
|
}
|
|
182
206
|
return 'ok';
|
|
@@ -190,14 +214,16 @@ export class AuthControllerServer {
|
|
|
190
214
|
if (!oSafe.password_enabled) {
|
|
191
215
|
throw Error('Password is not enabled!');
|
|
192
216
|
}
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
217
|
+
const existingAccount = await remult.repo(oSafe.Account).findOne({
|
|
218
|
+
where: {
|
|
219
|
+
providerUserId: email,
|
|
220
|
+
provider: FFAuthProvider.PASSWORD.id,
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
if (existingAccount) {
|
|
224
|
+
const validPassword = await passwordVerify(existingAccount?.hashPassword ?? '', password ?? '');
|
|
199
225
|
if (validPassword) {
|
|
200
|
-
await createSession(
|
|
226
|
+
await createSession(existingAccount.userId);
|
|
201
227
|
return 'ok';
|
|
202
228
|
}
|
|
203
229
|
throw Error('Incorrect username or password');
|
|
@@ -212,29 +238,25 @@ export class AuthControllerServer {
|
|
|
212
238
|
if (!oSafe.password_enabled) {
|
|
213
239
|
throw Error('Password is not enabled!');
|
|
214
240
|
}
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
authAccount.userId = u.id;
|
|
223
|
-
authAccount.provider = FFAuthProvider.PASSWORD.id;
|
|
224
|
-
authAccount.providerUserId = email;
|
|
225
|
-
}
|
|
241
|
+
const existingAccount = await remult.repo(oSafe.Account).findOne({
|
|
242
|
+
where: {
|
|
243
|
+
providerUserId: email,
|
|
244
|
+
provider: FFAuthProvider.PASSWORD.id,
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
if (existingAccount) {
|
|
226
248
|
const token = generateId(40);
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
await remult.repo(oSafe.Account).save(
|
|
249
|
+
existingAccount.token = token;
|
|
250
|
+
existingAccount.expiresAt = createDate(new TimeSpan(AUTH_OPTIONS.providers?.password?.resetPasswordExpiresIn ?? 5 * 60, 's'));
|
|
251
|
+
await remult.repo(oSafe.Account).save(existingAccount);
|
|
230
252
|
const url = `${remult.context.url.origin}${oSafe.firstlyData.props.ui?.paths.reset_password}?token=${token}`;
|
|
231
253
|
if (AUTH_OPTIONS.providers?.password?.resetPasswordSend) {
|
|
232
254
|
await AUTH_OPTIONS.providers?.password.resetPasswordSend({ email, url });
|
|
233
|
-
logAuth.success(
|
|
255
|
+
logAuth.success(`${green('[custom]')}${magenta('[resetPasswordSend]')} (${yellow(url)})`);
|
|
234
256
|
return 'Mail sent !';
|
|
235
257
|
}
|
|
236
258
|
else {
|
|
237
|
-
await sendMail('
|
|
259
|
+
await sendMail('resetPasswordSend', {
|
|
238
260
|
to: email,
|
|
239
261
|
subject: 'Reset your password',
|
|
240
262
|
templateProps: {
|
|
@@ -255,7 +277,7 @@ export class AuthControllerServer {
|
|
|
255
277
|
],
|
|
256
278
|
},
|
|
257
279
|
});
|
|
258
|
-
logAuth.success(
|
|
280
|
+
logAuth.success(`${magenta('[resetPasswordSend]')} (${yellow(url)})`);
|
|
259
281
|
return 'Demo Mail sent !';
|
|
260
282
|
}
|
|
261
283
|
}
|
|
@@ -308,11 +330,11 @@ export class AuthControllerServer {
|
|
|
308
330
|
const issuer = AUTH_OPTIONS.providers.otp.issuer ?? 'firstly';
|
|
309
331
|
const uri = createTOTPKeyURI(issuer, email, secret);
|
|
310
332
|
const oSafe = getSafeOptions();
|
|
311
|
-
let user = await remult.repo(oSafe.User).findFirst({
|
|
333
|
+
let user = await remult.repo(oSafe.User).findFirst({ identifier: email });
|
|
312
334
|
if (!user) {
|
|
313
335
|
user = remult.repo(oSafe.User).create();
|
|
314
336
|
}
|
|
315
|
-
user.
|
|
337
|
+
user.identifier = email;
|
|
316
338
|
user = await remult.repo(oSafe.User).save(user);
|
|
317
339
|
let account = await remult
|
|
318
340
|
.repo(oSafe.Account)
|
|
@@ -347,7 +369,7 @@ export class AuthControllerServer {
|
|
|
347
369
|
}
|
|
348
370
|
const account = accounts[0];
|
|
349
371
|
const user = await remult.repo(oSafe.User).findId(account.userId);
|
|
350
|
-
if (user?.
|
|
372
|
+
if (user?.identifier !== email) {
|
|
351
373
|
throw new Error('Invalid otp.');
|
|
352
374
|
}
|
|
353
375
|
const { decodeHex } = await import('oslo/encoding');
|
package/esm/auth/Entities.d.ts
CHANGED
package/esm/auth/Entities.js
CHANGED
|
@@ -14,7 +14,7 @@ let FFAuthUser = class FFAuthUser {
|
|
|
14
14
|
id;
|
|
15
15
|
createdAt;
|
|
16
16
|
updatedAt;
|
|
17
|
-
|
|
17
|
+
identifier;
|
|
18
18
|
roles = [];
|
|
19
19
|
accounts;
|
|
20
20
|
sessions;
|
|
@@ -33,12 +33,12 @@ __decorate([
|
|
|
33
33
|
validate: [
|
|
34
34
|
Validators.unique(),
|
|
35
35
|
(e) => {
|
|
36
|
-
if (e.
|
|
36
|
+
if (e.identifier.length < 2)
|
|
37
37
|
throw 'Must be at least 2 characters long';
|
|
38
38
|
},
|
|
39
39
|
],
|
|
40
40
|
})
|
|
41
|
-
], FFAuthUser.prototype, "
|
|
41
|
+
], FFAuthUser.prototype, "identifier", void 0);
|
|
42
42
|
__decorate([
|
|
43
43
|
Fields.object({
|
|
44
44
|
valueConverter: {
|
package/esm/auth/RoleHelpers.js
CHANGED
|
@@ -17,13 +17,13 @@ export const mergeRoles = (existing, newOnes) => {
|
|
|
17
17
|
return { roles: Array.from(result), changed };
|
|
18
18
|
};
|
|
19
19
|
export const initRoleFromEnv = async (log, userEntity, envValue, role) => {
|
|
20
|
-
const
|
|
21
|
-
for (let i = 0; i <
|
|
22
|
-
const
|
|
23
|
-
if (
|
|
24
|
-
let user = await repo(userEntity).findFirst({
|
|
20
|
+
const identifiers = envValue === undefined ? [] : (envValue ?? '').split(',').map((c) => c.trim());
|
|
21
|
+
for (let i = 0; i < identifiers.length; i++) {
|
|
22
|
+
const identifier = identifiers[i].trim();
|
|
23
|
+
if (identifier !== '') {
|
|
24
|
+
let user = await repo(userEntity).findFirst({ identifier });
|
|
25
25
|
if (!user) {
|
|
26
|
-
user = repo(userEntity).create({
|
|
26
|
+
user = repo(userEntity).create({ identifier, roles: [role] });
|
|
27
27
|
await repo(userEntity).save(user);
|
|
28
28
|
}
|
|
29
29
|
else {
|
|
@@ -34,8 +34,8 @@ export const initRoleFromEnv = async (log, userEntity, envValue, role) => {
|
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
-
if (
|
|
38
|
-
log.info(`${cyan(role)}: ${
|
|
37
|
+
if (identifiers.length > 0) {
|
|
38
|
+
log.info(`${cyan(role)}: ${identifiers.map((c) => green(c.trim())).join(', ')} added via ${yellow(`.env`)}.`);
|
|
39
39
|
}
|
|
40
40
|
else {
|
|
41
41
|
log.info(`${cyan(role)}: No users added via ${yellow(`.env`)}.`);
|
package/esm/auth/client/Auth.js
CHANGED
package/esm/auth/index.js
CHANGED
|
@@ -223,7 +223,7 @@ export const auth = (o) => {
|
|
|
223
223
|
for (let i = 0; i < info.nameOptions.length; i++) {
|
|
224
224
|
const existingUser = await remult
|
|
225
225
|
.repo(oSafe.User)
|
|
226
|
-
.findOne({ where: {
|
|
226
|
+
.findOne({ where: { identifier: info.nameOptions[i] } });
|
|
227
227
|
if (existingUser) {
|
|
228
228
|
// Don't do anything
|
|
229
229
|
}
|
|
@@ -236,7 +236,7 @@ export const auth = (o) => {
|
|
|
236
236
|
nameToUse = `${info.nameOptions[0]}-${info.providerUserId}`;
|
|
237
237
|
}
|
|
238
238
|
const user = remult.repo(oSafe.User).create();
|
|
239
|
-
user.
|
|
239
|
+
user.identifier = nameToUse;
|
|
240
240
|
account = remult.repo(oSafe.Account).create();
|
|
241
241
|
account.provider = keyState;
|
|
242
242
|
account.providerUserId = info.providerUserId;
|
package/esm/mail/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script>import { Button, Container, Head, Heading, Html, Preview, Section, Text } from "
|
|
1
|
+
<script>import { Button, Container, Head, Heading, Html, Preview, Section, Text } from "svelty-email";
|
|
2
2
|
export let previewText;
|
|
3
3
|
export let title;
|
|
4
4
|
export let sections = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "firstly",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Firstly, an opinionated Remult setup!",
|
|
6
6
|
"repository": {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"lucia": "^3.2.0",
|
|
32
32
|
"nodemailer": "^6.9.13",
|
|
33
33
|
"oslo": "^1.2.0",
|
|
34
|
-
"
|
|
34
|
+
"svelty-email": "^0.0.11",
|
|
35
35
|
"tailwind-merge": "^2.3.0",
|
|
36
36
|
"tailwindcss": "^3.4.3",
|
|
37
37
|
"vite": "^5.4.1",
|