@stacksjs/defaults 0.74.60 → 0.74.61
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/ai/skills/stacks-auth/SKILL.md +22 -2
- package/app/Models/FailedJob.ts +5 -0
- package/app/Models/Job.ts +5 -0
- package/functions/auth.ts +90 -35
- package/ide/vscode/package.json +1 -1
- package/package.json +2 -2
- package/resources/components/Dashboard/Auth/Login.stx +1 -1
- package/resources/components/Dashboard/Auth/LoginDashboard.stx +45 -5
- package/resources/components/Dashboard/Auth/TwoFactorLogin.stx +63 -0
- package/types/dashboard.ts +9 -1
|
@@ -352,14 +352,34 @@ await authUser.authorize('edit-post', post) // throws if denied
|
|
|
352
352
|
providers: { users: { driver: 'database', table: 'users' } },
|
|
353
353
|
username: 'email', // AUTH_USERNAME_FIELD env
|
|
354
354
|
password: 'password', // AUTH_PASSWORD_FIELD env
|
|
355
|
-
tokenExpiry:
|
|
356
|
-
|
|
355
|
+
tokenExpiry: 60 * 60 * 1000, // milliseconds, 1 hour
|
|
356
|
+
refreshTokenExpiry: 30 * 24 * 60 * 60 * 1000, // milliseconds
|
|
357
|
+
browserSession: {
|
|
358
|
+
baselineLifetime: 7 * 24 * 60 * 60 * 1000, // absolute milliseconds
|
|
359
|
+
rememberedLifetime: 30 * 24 * 60 * 60 * 1000,
|
|
360
|
+
withRefreshToken: false, // fixed browser lifetime, no unused refresh token
|
|
361
|
+
logoutRedirect: '/login?logged_out=1', // local path for HTML logout only
|
|
362
|
+
},
|
|
363
|
+
tokenRotation: 24, // hours
|
|
357
364
|
defaultAbilities: ['*'],
|
|
358
365
|
defaultTokenName: 'auth-token',
|
|
359
366
|
passwordReset: { expire: 60, throttle: 60 }
|
|
360
367
|
}
|
|
361
368
|
```
|
|
362
369
|
|
|
370
|
+
`browserSession` applies to credentials issued by the default login,
|
|
371
|
+
registration, and completed two-factor actions. Dedicated personal access
|
|
372
|
+
token and OAuth issuance remain unchanged. The default login form sends
|
|
373
|
+
`remember`; registration uses the baseline tier unless a custom client sends
|
|
374
|
+
that field. A two-factor challenge preserves the choice without minting a
|
|
375
|
+
session until verification succeeds. Cookie Max-Age comes from the lifetime
|
|
376
|
+
returned by token issuance, so it cannot outlive the token. Cookie-authenticated
|
|
377
|
+
writes use the CSRF flow and same-origin credentials.
|
|
378
|
+
|
|
379
|
+
When migrating an app that copied framework auth actions, remove only the
|
|
380
|
+
equivalent login, registration, two-factor, logout, and cookie-helper overrides.
|
|
381
|
+
Retain application-specific onboarding and event hooks.
|
|
382
|
+
|
|
363
383
|
### config/hashing.ts
|
|
364
384
|
```typescript
|
|
365
385
|
{
|
package/app/Models/FailedJob.ts
CHANGED
|
@@ -36,8 +36,12 @@ export default defineModel({
|
|
|
36
36
|
factory: () => 'default',
|
|
37
37
|
},
|
|
38
38
|
|
|
39
|
+
// Both `text`: a payload is a serialized job envelope and an exception
|
|
40
|
+
// carries a stack trace, and neither fits the default varchar(255) that
|
|
41
|
+
// Postgres enforces.
|
|
39
42
|
payload: {
|
|
40
43
|
fillable: true,
|
|
44
|
+
type: 'text',
|
|
41
45
|
validation: {
|
|
42
46
|
rule: schema.string().required(),
|
|
43
47
|
},
|
|
@@ -46,6 +50,7 @@ export default defineModel({
|
|
|
46
50
|
|
|
47
51
|
exception: {
|
|
48
52
|
fillable: true,
|
|
53
|
+
type: 'text',
|
|
49
54
|
validation: {
|
|
50
55
|
rule: schema.string().required(),
|
|
51
56
|
},
|
package/app/Models/Job.ts
CHANGED
|
@@ -28,6 +28,11 @@ export default defineModel({
|
|
|
28
28
|
|
|
29
29
|
payload: {
|
|
30
30
|
fillable: true,
|
|
31
|
+
// `text`, not the default varchar(255): a job's serialized envelope is
|
|
32
|
+
// far longer, and on Postgres every dispatch failed with "value too long
|
|
33
|
+
// for type character varying(255)". SQLite ignores the length, which is
|
|
34
|
+
// how it went unnoticed.
|
|
35
|
+
type: 'text',
|
|
31
36
|
validation: {
|
|
32
37
|
rule: schema.string().required(),
|
|
33
38
|
},
|
package/functions/auth.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolveApiBaseUrl } from './api-url'
|
|
2
2
|
import type { Ref } from '@stacksjs/stx'
|
|
3
|
-
import type { AuthUser, LoginError, LoginResponse, MeResponse, RegisterCredentials, RegisterError, RegisterResponse, ResponseError, UserData } from '../types/dashboard'
|
|
3
|
+
import type { AuthUser, LoginError, LoginResponse, LoginResult, MeResponse, RegisterCredentials, RegisterError, RegisterResponse, ResponseError, TwoFactorLoginChallenge, UserData } from '../types/dashboard'
|
|
4
4
|
import { withCsrfHeader } from '@stacksjs/browser/composables/csrf'
|
|
5
5
|
import { useStorage } from '@stacksjs/browser/composables/useStorage'
|
|
6
6
|
import { ref } from '@stacksjs/stx'
|
|
@@ -28,30 +28,72 @@ const baseUrl = resolveApiBaseUrl('')
|
|
|
28
28
|
// Create singleton state
|
|
29
29
|
const isAuthenticated = ref(false)
|
|
30
30
|
|
|
31
|
+
export function isTwoFactorChallenge(data: unknown): data is TwoFactorLoginChallenge {
|
|
32
|
+
if (!data || typeof data !== 'object')
|
|
33
|
+
return false
|
|
34
|
+
const candidate = data as Record<string, unknown>
|
|
35
|
+
return candidate.requires_two_factor === true && typeof candidate.challenge_token === 'string' && candidate.challenge_token.length > 0
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function isLoginResponse(data: unknown): data is LoginResponse {
|
|
39
|
+
if (!data || typeof data !== 'object')
|
|
40
|
+
return false
|
|
41
|
+
const candidate = data as Record<string, unknown>
|
|
42
|
+
return typeof candidate.token === 'string'
|
|
43
|
+
&& candidate.token.length > 0
|
|
44
|
+
&& typeof candidate.user === 'object'
|
|
45
|
+
&& candidate.user !== null
|
|
46
|
+
&& (candidate.refresh_token === undefined
|
|
47
|
+
|| (typeof candidate.refresh_token === 'string' && candidate.refresh_token.length > 0))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Normalize a requested post-auth destination and reject cross-origin forms. */
|
|
51
|
+
export function safeAuthRedirect(value: unknown): string {
|
|
52
|
+
if (typeof value !== 'string')
|
|
53
|
+
return '/'
|
|
54
|
+
const candidate = value.trim()
|
|
55
|
+
if (!candidate.startsWith('/') || candidate.startsWith('//'))
|
|
56
|
+
return '/'
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const base = new URL('https://stacks.invalid')
|
|
60
|
+
const resolved = new URL(candidate, base)
|
|
61
|
+
if (resolved.origin !== base.origin)
|
|
62
|
+
return '/'
|
|
63
|
+
const target = `${resolved.pathname}${resolved.search}${resolved.hash}`
|
|
64
|
+
return target.startsWith('/') && !target.startsWith('//') ? target : '/'
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return '/'
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
31
71
|
export interface AuthComposable {
|
|
32
72
|
isAuthenticated: Ref<boolean>
|
|
33
73
|
user: { value: UserData | null }
|
|
34
|
-
login: (user: AuthUser) => Promise<
|
|
74
|
+
login: (user: AuthUser) => Promise<LoginResult | LoginError>
|
|
75
|
+
verifyTwoFactorLogin: (challengeToken: string, code: string) => Promise<LoginResponse | LoginError>
|
|
35
76
|
register: (user: RegisterCredentials) => Promise<RegisterResponse | RegisterError>
|
|
36
77
|
fetchAuthUser: () => Promise<UserData | null>
|
|
37
78
|
checkAuthentication: () => Promise<boolean>
|
|
38
|
-
logout: () => void
|
|
79
|
+
logout: () => Promise<void>
|
|
39
80
|
getToken: () => string | null
|
|
40
81
|
token: { value: string | null }
|
|
41
82
|
}
|
|
42
83
|
|
|
43
84
|
export function useAuth(): AuthComposable {
|
|
85
|
+
function storeLogin(data: LoginResponse): void {
|
|
86
|
+
token.value = data.token
|
|
87
|
+
user.value = data.user
|
|
88
|
+
isAuthenticated.value = true
|
|
89
|
+
}
|
|
90
|
+
|
|
44
91
|
async function fetchAuthUser(): Promise<UserData | null> {
|
|
45
92
|
try {
|
|
46
|
-
if (!token.value) {
|
|
47
|
-
isAuthenticated.value = false
|
|
48
|
-
user.value = null
|
|
49
|
-
return null
|
|
50
|
-
}
|
|
51
|
-
|
|
52
93
|
const response = await fetch(`${baseUrl}/me`, {
|
|
94
|
+
credentials: 'same-origin',
|
|
53
95
|
headers: {
|
|
54
|
-
Authorization: `Bearer ${token.value}
|
|
96
|
+
...(token.value ? { Authorization: `Bearer ${token.value}` } : {}),
|
|
55
97
|
Accept: 'application/json',
|
|
56
98
|
},
|
|
57
99
|
})
|
|
@@ -122,7 +164,7 @@ export function useAuth(): AuthComposable {
|
|
|
122
164
|
return 'token' in data && 'user' in data
|
|
123
165
|
}
|
|
124
166
|
|
|
125
|
-
async function login(credentials: AuthUser): Promise<
|
|
167
|
+
async function login(credentials: AuthUser): Promise<LoginResult | LoginError> {
|
|
126
168
|
const url = `${baseUrl}/login`
|
|
127
169
|
const response = await fetch(url, {
|
|
128
170
|
method: 'POST',
|
|
@@ -132,37 +174,49 @@ export function useAuth(): AuthComposable {
|
|
|
132
174
|
}),
|
|
133
175
|
body: JSON.stringify(credentials),
|
|
134
176
|
})
|
|
135
|
-
const data = await response.json() as
|
|
177
|
+
const data = await response.json() as LoginResult | LoginError
|
|
136
178
|
|
|
137
|
-
if (!response.ok ||
|
|
179
|
+
if (!response.ok || isTwoFactorChallenge(data))
|
|
138
180
|
return data
|
|
139
181
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
182
|
+
if (!isLoginResponse(data))
|
|
183
|
+
return data
|
|
184
|
+
|
|
185
|
+
storeLogin(data)
|
|
186
|
+
return data
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function verifyTwoFactorLogin(challengeToken: string, code: string): Promise<LoginResponse | LoginError> {
|
|
190
|
+
const response = await fetch(`${baseUrl}/verify-two-factor-login`, {
|
|
191
|
+
method: 'POST',
|
|
192
|
+
credentials: 'same-origin',
|
|
193
|
+
headers: withCsrfHeader({ 'Content-Type': 'application/json' }),
|
|
194
|
+
body: JSON.stringify({ challenge_token: challengeToken, code }),
|
|
195
|
+
})
|
|
196
|
+
const data = await response.json() as LoginResponse | LoginError
|
|
197
|
+
if (!response.ok || !isLoginResponse(data))
|
|
198
|
+
return data
|
|
199
|
+
|
|
200
|
+
storeLogin(data)
|
|
143
201
|
return data
|
|
144
202
|
}
|
|
145
203
|
|
|
146
204
|
async function logout() {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
token.value = ''
|
|
163
|
-
user.value = null
|
|
164
|
-
isAuthenticated.value = false
|
|
165
|
-
}
|
|
205
|
+
const currentToken = token.value
|
|
206
|
+
const response = await fetch(`${baseUrl}/logout`, {
|
|
207
|
+
method: 'POST',
|
|
208
|
+
credentials: 'same-origin',
|
|
209
|
+
headers: withCsrfHeader({
|
|
210
|
+
...(currentToken ? { Authorization: `Bearer ${currentToken}` } : {}),
|
|
211
|
+
Accept: 'application/json',
|
|
212
|
+
}),
|
|
213
|
+
})
|
|
214
|
+
if (!response.ok)
|
|
215
|
+
throw new Error(`Logout failed with status ${response.status}`)
|
|
216
|
+
|
|
217
|
+
token.value = ''
|
|
218
|
+
user.value = null
|
|
219
|
+
isAuthenticated.value = false
|
|
166
220
|
}
|
|
167
221
|
|
|
168
222
|
return {
|
|
@@ -172,6 +226,7 @@ export function useAuth(): AuthComposable {
|
|
|
172
226
|
getToken: () => token.value,
|
|
173
227
|
register,
|
|
174
228
|
login,
|
|
229
|
+
verifyTwoFactorLogin,
|
|
175
230
|
logout,
|
|
176
231
|
fetchAuthUser,
|
|
177
232
|
checkAuthentication,
|
package/ide/vscode/package.json
CHANGED
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/defaults",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.74.
|
|
5
|
+
"version": "0.74.61",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/stacksjs/stacks.git",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"dependencies": {
|
|
56
56
|
"@iconify-json/f7": "^1.2.2",
|
|
57
57
|
"@iconify-json/hugeicons": "^1.2.27",
|
|
58
|
-
"@stacksjs/mobile": "^0.74.
|
|
58
|
+
"@stacksjs/mobile": "^0.74.61",
|
|
59
59
|
"@stacksjs/sanitizer": "^0.2.113",
|
|
60
60
|
"ts-qr-codes": "^0.1.8"
|
|
61
61
|
}
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
<script client>
|
|
2
2
|
import type { AuthUser } from '../../../../types/dashboard'
|
|
3
|
-
import { describeAuthError, useAuth } from '../../../../functions/auth'
|
|
3
|
+
import { describeAuthError, isLoginResponse, isTwoFactorChallenge, safeAuthRedirect, useAuth } from '../../../../functions/auth'
|
|
4
4
|
|
|
5
|
-
const { login } = useAuth()
|
|
5
|
+
const { login, verifyTwoFactorLogin } = useAuth()
|
|
6
6
|
const route = useRoute()
|
|
7
7
|
|
|
8
8
|
const isLoading = state(false)
|
|
9
9
|
const error = state('')
|
|
10
|
+
const challengeToken = state('')
|
|
10
11
|
|
|
11
12
|
function safeRedirect(): string {
|
|
12
|
-
|
|
13
|
-
return redirect.startsWith('/') && !redirect.startsWith('//') ? redirect : '/'
|
|
13
|
+
return safeAuthRedirect(route.query.redirect)
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
async function handleLogin(credentials: AuthUser): Promise<void> {
|
|
@@ -20,7 +20,12 @@ async function handleLogin(credentials: AuthUser): Promise<void> {
|
|
|
20
20
|
try {
|
|
21
21
|
const result = await login(credentials)
|
|
22
22
|
|
|
23
|
-
if (
|
|
23
|
+
if (isTwoFactorChallenge(result)) {
|
|
24
|
+
challengeToken.set(result.challenge_token)
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!isLoginResponse(result)) {
|
|
24
29
|
error.set(describeAuthError(result, 'Invalid credentials'))
|
|
25
30
|
return
|
|
26
31
|
}
|
|
@@ -34,10 +39,38 @@ async function handleLogin(credentials: AuthUser): Promise<void> {
|
|
|
34
39
|
isLoading.set(false)
|
|
35
40
|
}
|
|
36
41
|
}
|
|
42
|
+
|
|
43
|
+
async function handleTwoFactor(code: string): Promise<void> {
|
|
44
|
+
isLoading.set(true)
|
|
45
|
+
error.set('')
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const result = await verifyTwoFactorLogin(challengeToken(), code)
|
|
49
|
+
if (!isLoginResponse(result)) {
|
|
50
|
+
challengeToken.set('')
|
|
51
|
+
error.set(describeAuthError(result, 'Invalid or expired login attempt. Please sign in again.'))
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
navigate(safeRedirect())
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
error.set(err instanceof Error ? err.message : 'Unable to verify the authentication code')
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
isLoading.set(false)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function restartLogin(): void {
|
|
66
|
+
challengeToken.set('')
|
|
67
|
+
error.set('')
|
|
68
|
+
}
|
|
37
69
|
</script>
|
|
38
70
|
|
|
39
71
|
<template>
|
|
40
72
|
<Login
|
|
73
|
+
:if="!challengeToken()"
|
|
41
74
|
@submit="handleLogin"
|
|
42
75
|
:isLoading="isLoading()"
|
|
43
76
|
:error="error()"
|
|
@@ -47,4 +80,11 @@ async function handleLogin(credentials: AuthUser): Promise<void> {
|
|
|
47
80
|
sign-in inside the card. -->
|
|
48
81
|
<template #social><slot name="social" /></template>
|
|
49
82
|
</Login>
|
|
83
|
+
<TwoFactorLogin
|
|
84
|
+
:if="challengeToken()"
|
|
85
|
+
@submit="handleTwoFactor"
|
|
86
|
+
@restart="restartLogin"
|
|
87
|
+
:isLoading="isLoading()"
|
|
88
|
+
:error="error()"
|
|
89
|
+
/>
|
|
50
90
|
</template>
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
@import('../UI/Input', '../UI/Button')
|
|
2
|
+
|
|
3
|
+
<script client>
|
|
4
|
+
const emit = defineEmits()
|
|
5
|
+
const isLoading = useReactiveProp<boolean>('isLoading', false)
|
|
6
|
+
const error = useReactiveProp<string>('error', '')
|
|
7
|
+
const code = state('')
|
|
8
|
+
const codeError = derived(() => code() && !/^\d{6}$/.test(code()) ? 'Enter the 6-digit code from your authenticator app' : '')
|
|
9
|
+
|
|
10
|
+
function submit(): void {
|
|
11
|
+
if (!/^\d{6}$/.test(code()))
|
|
12
|
+
return
|
|
13
|
+
emit('submit', code())
|
|
14
|
+
}
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<div class="flex items-center justify-center px-4 py-12 min-h-screen">
|
|
18
|
+
<div class="max-w-sm w-full">
|
|
19
|
+
<div class="mb-8 text-center">
|
|
20
|
+
<div class="inline-flex items-center justify-center mb-4 h-12 w-12 bg-gradient-to-br from-blue-500 to-blue-600 rounded-xl shadow-blue-500/25 shadow-lg">
|
|
21
|
+
<span class="h-7 w-7 text-white i-hugeicons-shield-key"></span>
|
|
22
|
+
</div>
|
|
23
|
+
<h1 class="font-semibold text-2xl text-neutral-900 dark:text-white">Two-factor authentication</h1>
|
|
24
|
+
<p class="mt-2 text-neutral-500 text-sm dark:text-neutral-400">Enter the code from your authenticator app.</p>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div class="p-6 bg-white dark:bg-neutral-900 border border-neutral-200/60 rounded-2xl dark:border-neutral-800 shadow-black/5 shadow-xl dark:shadow-black/20">
|
|
28
|
+
<div :if="error()" role="alert" class="mb-4 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 rounded-lg dark:border-red-800">
|
|
29
|
+
<p class="text-red-600 text-sm dark:text-red-400">{{ error() }}</p>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<form @submit.prevent="submit">
|
|
33
|
+
<Input
|
|
34
|
+
type="text"
|
|
35
|
+
name="code"
|
|
36
|
+
label="Authentication code"
|
|
37
|
+
placeholder="123456"
|
|
38
|
+
autocomplete="one-time-code"
|
|
39
|
+
v-model:value="code"
|
|
40
|
+
:error="codeError()"
|
|
41
|
+
required
|
|
42
|
+
:disabled="isLoading()"
|
|
43
|
+
/>
|
|
44
|
+
|
|
45
|
+
<div class="grid gap-3 mt-6">
|
|
46
|
+
<Button
|
|
47
|
+
type="submit"
|
|
48
|
+
variant="primary"
|
|
49
|
+
size="lg"
|
|
50
|
+
fullWidth
|
|
51
|
+
:loading="isLoading()"
|
|
52
|
+
:disabled="isLoading() || !/^\d{6}$/.test(code())"
|
|
53
|
+
>
|
|
54
|
+
Verify and sign in
|
|
55
|
+
</Button>
|
|
56
|
+
<Button type="button" variant="ghost" size="lg" fullWidth :disabled="isLoading()" @click="emit('restart')">
|
|
57
|
+
Back to sign in
|
|
58
|
+
</Button>
|
|
59
|
+
</div>
|
|
60
|
+
</form>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
package/types/dashboard.ts
CHANGED
|
@@ -38,7 +38,7 @@ export interface RegisterResponse {
|
|
|
38
38
|
// is kept alongside `access_token` for backward compatibility.
|
|
39
39
|
export interface LoginResponse {
|
|
40
40
|
access_token: string
|
|
41
|
-
refresh_token
|
|
41
|
+
refresh_token?: string
|
|
42
42
|
token_type: string
|
|
43
43
|
expires_in: number
|
|
44
44
|
token: string
|
|
@@ -49,6 +49,13 @@ export interface LoginResponse {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
export interface TwoFactorLoginChallenge {
|
|
53
|
+
requires_two_factor: true
|
|
54
|
+
challenge_token: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type LoginResult = LoginResponse | TwoFactorLoginChallenge
|
|
58
|
+
|
|
52
59
|
export interface Response<T> {
|
|
53
60
|
errors: ResponseError
|
|
54
61
|
data: T
|
|
@@ -57,6 +64,7 @@ export interface Response<T> {
|
|
|
57
64
|
export interface AuthUser {
|
|
58
65
|
email: string
|
|
59
66
|
password: string
|
|
67
|
+
remember?: boolean
|
|
60
68
|
}
|
|
61
69
|
|
|
62
70
|
export interface RegisterCredentials extends AuthUser {
|