@mframework/layer-auth 0.0.1
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/app/components/blocks/logoutButton.vue +14 -0
- package/app/composables/useAuth.ts +159 -0
- package/app/composables/useUser.ts +65 -0
- package/app/layouts/auth.vue +8 -0
- package/app/middleware/redirect.global.ts +7 -0
- package/app/middleware/seller.ts +7 -0
- package/app/middleware/session.ts +12 -0
- package/app/module.ts +19 -0
- package/app/pages/callback.vue +22 -0
- package/app/pages/confirm.vue +19 -0
- package/app/pages/forgot-password.vue +156 -0
- package/app/pages/i18n.json +111 -0
- package/app/pages/login.vue +138 -0
- package/app/pages/register.vue +177 -0
- package/app/pages/reset-password.vue +124 -0
- package/app/stores/user.ts +43 -0
- package/app/utils/plugins.ts +24 -0
- package/app/utils/providers/better-auth.ts +125 -0
- package/nuxt.config.ts +18 -0
- package/package.json +30 -0
- package/server/api/auth/[...all].ts +7 -0
- package/server/tsconfig.json +3 -0
- package/server/utils/auth.ts +3 -0
- package/server/utils/query.ts +28 -0
- package/server/utils/require-auth.ts +23 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<v-btn variant="flat" @click="signOut">Logout</v-btn>
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script setup>
|
|
6
|
+
const { logout } = useAuth()
|
|
7
|
+
const router = useRouter()
|
|
8
|
+
|
|
9
|
+
const signOut = async () => {
|
|
10
|
+
await logout()
|
|
11
|
+
await router.push('/login')
|
|
12
|
+
}
|
|
13
|
+
</script>
|
|
14
|
+
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Subscription
|
|
3
|
+
} from '@better-auth/stripe'
|
|
4
|
+
import type {
|
|
5
|
+
CustomerState
|
|
6
|
+
} from '@polar-sh/sdk/models/components/customerstate.js'
|
|
7
|
+
import type {
|
|
8
|
+
BetterAuthClientOptions,
|
|
9
|
+
InferSessionFromClient,
|
|
10
|
+
User
|
|
11
|
+
} from 'better-auth/client'
|
|
12
|
+
import {
|
|
13
|
+
createAuthClient
|
|
14
|
+
} from 'better-auth/client'
|
|
15
|
+
import {
|
|
16
|
+
getAuthPlugins
|
|
17
|
+
} from '../utils/plugins'
|
|
18
|
+
|
|
19
|
+
export type UseAuthOptions = {
|
|
20
|
+
/** An existing auth client. If provided, it's used as-is. */
|
|
21
|
+
client ? : any
|
|
22
|
+
/** Base URL for the auth client if creating one. */
|
|
23
|
+
baseURL ? : string
|
|
24
|
+
/** Optional headers to send with requests. */
|
|
25
|
+
headers ? : Record < string,
|
|
26
|
+
string >
|
|
27
|
+
/** Payment provider: 'stripe' or 'polar'. If omitted, 'stripe' is assumed. */
|
|
28
|
+
payment ? : 'stripe' | 'polar'
|
|
29
|
+
/** Optional app-level reload/redirect function (framework-specific). */
|
|
30
|
+
reload ? : (opts: {
|
|
31
|
+
path ? : string
|
|
32
|
+
}) => Promise < void >
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function useAuth(options: UseAuthOptions = {}) {
|
|
36
|
+
const {
|
|
37
|
+
client: providedClient,
|
|
38
|
+
baseURL,
|
|
39
|
+
headers,
|
|
40
|
+
payment = 'stripe',
|
|
41
|
+
reload
|
|
42
|
+
} = options
|
|
43
|
+
|
|
44
|
+
const client = providedClient || createAuthClient({
|
|
45
|
+
baseURL: baseURL || undefined,
|
|
46
|
+
fetchOptions: {
|
|
47
|
+
headers
|
|
48
|
+
},
|
|
49
|
+
socialProviders: {
|
|
50
|
+
github: {
|
|
51
|
+
clientId: process.env.GITHUB_CLIENT_ID!,
|
|
52
|
+
clientSecret: process.env.GITHUB_CLIENT_SECRET!
|
|
53
|
+
},
|
|
54
|
+
microsoft: {
|
|
55
|
+
clientId: process.env.MICROSOFT_CLIENT_ID!,
|
|
56
|
+
clientSecret: process.env.MICROSOFT_CLIENT_SECRET!
|
|
57
|
+
},
|
|
58
|
+
twitter: {
|
|
59
|
+
clientId: process.env.TWITTER_CLIENT_ID!,
|
|
60
|
+
clientSecret: process.env.TWITTER_CLIENT_SECRET!
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
plugins: getAuthPlugins({
|
|
64
|
+
subscription: true
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
let session: InferSessionFromClient < BetterAuthClientOptions > | null = null
|
|
69
|
+
let user: User | null = null
|
|
70
|
+
let subscriptions: Subscription[] = []
|
|
71
|
+
let polarState: CustomerState | null = null
|
|
72
|
+
let sessionFetching = false
|
|
73
|
+
|
|
74
|
+
const fetchSession = async () => {
|
|
75
|
+
if (sessionFetching) return
|
|
76
|
+
sessionFetching = true
|
|
77
|
+
const {
|
|
78
|
+
data
|
|
79
|
+
} = await client.getSession()
|
|
80
|
+
session = data?.session || null
|
|
81
|
+
|
|
82
|
+
const userDefaults = {
|
|
83
|
+
image: null,
|
|
84
|
+
role: null,
|
|
85
|
+
banReason: null,
|
|
86
|
+
banned: null,
|
|
87
|
+
banExpires: null,
|
|
88
|
+
stripeCustomerId: null
|
|
89
|
+
}
|
|
90
|
+
user = data?.user ? Object.assign({}, userDefaults, data.user) : null
|
|
91
|
+
subscriptions = []
|
|
92
|
+
if (user) {
|
|
93
|
+
if (payment == 'stripe') {
|
|
94
|
+
const {
|
|
95
|
+
data: subscriptionData
|
|
96
|
+
} = await client.subscription.list()
|
|
97
|
+
subscriptions = subscriptionData || []
|
|
98
|
+
} else if (payment == 'polar') {
|
|
99
|
+
const {
|
|
100
|
+
data: customerState
|
|
101
|
+
} = await client.customer.state()
|
|
102
|
+
polarState = customerState
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
sessionFetching = false
|
|
106
|
+
return data
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (client?.$store?.listen) {
|
|
110
|
+
client.$store.listen('$sessionSignal', async (signal: any) => {
|
|
111
|
+
if (!signal) return
|
|
112
|
+
await fetchSession()
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
session,
|
|
118
|
+
user,
|
|
119
|
+
subscription: client.subscription,
|
|
120
|
+
subscriptions,
|
|
121
|
+
loggedIn: () => !!session,
|
|
122
|
+
activeStripeSubscription: () => {
|
|
123
|
+
return subscriptions.find((sub: {
|
|
124
|
+
status: string
|
|
125
|
+
}) => sub.status === 'active' || sub.status === 'trialing')
|
|
126
|
+
},
|
|
127
|
+
activePolarSubscriptions: () => {
|
|
128
|
+
return polarState?.activeSubscriptions
|
|
129
|
+
},
|
|
130
|
+
signIn: client.signIn,
|
|
131
|
+
signUp: client.signUp,
|
|
132
|
+
forgetPassword: client.forgetPassword,
|
|
133
|
+
resetPassword: client.resetPassword,
|
|
134
|
+
sendVerificationEmail: client.sendVerificationEmail,
|
|
135
|
+
errorCodes: client.$ERROR_CODES,
|
|
136
|
+
async signOut({
|
|
137
|
+
redirectTo
|
|
138
|
+
}: {
|
|
139
|
+
redirectTo ? : string
|
|
140
|
+
} = {}) {
|
|
141
|
+
await client.signOut({
|
|
142
|
+
fetchOptions: {
|
|
143
|
+
onSuccess: async () => {
|
|
144
|
+
session = null
|
|
145
|
+
user = null
|
|
146
|
+
if (redirectTo && typeof reload === 'function') {
|
|
147
|
+
await reload({
|
|
148
|
+
path: redirectTo.toString()
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
})
|
|
154
|
+
},
|
|
155
|
+
fetchSession,
|
|
156
|
+
payment,
|
|
157
|
+
client
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { ref } from 'vue';
|
|
2
|
+
|
|
3
|
+
// `base-app` generated Prisma types may not exist in this workspace during isolated
|
|
4
|
+
// builds. Use a local minimal `User` alias to keep the layer buildable.
|
|
5
|
+
type User = any
|
|
6
|
+
|
|
7
|
+
const isEditOpen = ref(false);
|
|
8
|
+
const isCreateOpen = ref(false);
|
|
9
|
+
const isDeleteOpen = ref(false);
|
|
10
|
+
const isLoading = ref(false);
|
|
11
|
+
const userToEdit = ref<User | null>(null);
|
|
12
|
+
|
|
13
|
+
export const useUser = () => {
|
|
14
|
+
|
|
15
|
+
const closeOtherModals = () => {
|
|
16
|
+
userToEdit.value = null;
|
|
17
|
+
isEditOpen.value = false;
|
|
18
|
+
isDeleteOpen.value = false;
|
|
19
|
+
isCreateOpen.value = false;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
const openEditUser = (user: User) => {
|
|
24
|
+
closeOtherModals();
|
|
25
|
+
userToEdit.value = user;
|
|
26
|
+
isEditOpen.value = true;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const closeEditUser = () => {
|
|
30
|
+
isEditOpen.value = false;
|
|
31
|
+
userToEdit.value = null;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const openDeleteUser = () => {
|
|
35
|
+
closeOtherModals();
|
|
36
|
+
isDeleteOpen.value = true;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const closeDeleteUser = () => {
|
|
40
|
+
isDeleteOpen.value = false;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const openAddUser = () => {
|
|
44
|
+
closeOtherModals();
|
|
45
|
+
isCreateOpen.value = true;
|
|
46
|
+
};
|
|
47
|
+
const closeCreateUser = () => {
|
|
48
|
+
closeOtherModals();
|
|
49
|
+
isCreateOpen.value = false;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
isEditOpen,
|
|
54
|
+
isDeleteOpen,
|
|
55
|
+
isLoading,
|
|
56
|
+
userToEdit,
|
|
57
|
+
openEditUser,
|
|
58
|
+
closeEditUser,
|
|
59
|
+
openDeleteUser,
|
|
60
|
+
closeDeleteUser,
|
|
61
|
+
openAddUser,
|
|
62
|
+
isCreateOpen,
|
|
63
|
+
closeCreateUser
|
|
64
|
+
};
|
|
65
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineNuxtRouteMiddleware, useCookie } from "nuxt/app"
|
|
2
|
+
|
|
3
|
+
export default defineNuxtRouteMiddleware(() => {
|
|
4
|
+
const session = useCookie('session')
|
|
5
|
+
|
|
6
|
+
if (!session.value) {
|
|
7
|
+
session.value = {
|
|
8
|
+
id: crypto.randomUUID(),
|
|
9
|
+
date_created: new Date().toISOString()
|
|
10
|
+
} as any
|
|
11
|
+
}
|
|
12
|
+
})
|
package/app/module.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineNuxtModule, addPlugin, createResolver } from '@nuxt/kit'
|
|
2
|
+
|
|
3
|
+
// Module options TypeScript interface definition
|
|
4
|
+
export interface ModuleOptions {}
|
|
5
|
+
|
|
6
|
+
export default defineNuxtModule<ModuleOptions>({
|
|
7
|
+
meta: {
|
|
8
|
+
name: 'meeovi-auth',
|
|
9
|
+
configKey: 'meeoviAuth',
|
|
10
|
+
},
|
|
11
|
+
// Default configuration options of the Nuxt module
|
|
12
|
+
defaults: {},
|
|
13
|
+
setup(_options, _nuxt) {
|
|
14
|
+
const resolver = createResolver(import.meta.url)
|
|
15
|
+
|
|
16
|
+
// Do not add the extension since the `.ts` will be transpiled to `.mjs` after `npm run prepack`
|
|
17
|
+
addPlugin(resolver.resolve('./runtime/plugin'))
|
|
18
|
+
},
|
|
19
|
+
})
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>Processing login...</div>
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script setup>
|
|
6
|
+
const router = useRouter()
|
|
7
|
+
const store = useUserStore()
|
|
8
|
+
|
|
9
|
+
onMounted(async () => {
|
|
10
|
+
try {
|
|
11
|
+
// No server session to refresh; rely on local store state
|
|
12
|
+
if (store.user) {
|
|
13
|
+
await router.push('/')
|
|
14
|
+
} else {
|
|
15
|
+
await router.push('/login')
|
|
16
|
+
}
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.error('Callback navigation failed:', error)
|
|
19
|
+
await router.push('/login')
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
</script>
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div>Waiting for login...</div>
|
|
3
|
+
</template>
|
|
4
|
+
|
|
5
|
+
<script setup>
|
|
6
|
+
import { navigateTo } from '#app'
|
|
7
|
+
import { watch } from 'vue'
|
|
8
|
+
import { useUserStore } from '../stores/user'
|
|
9
|
+
|
|
10
|
+
const store = useUserStore()
|
|
11
|
+
|
|
12
|
+
watch(() => store.user, (u) => {
|
|
13
|
+
if (u) return navigateTo('/')
|
|
14
|
+
}, { immediate: true })
|
|
15
|
+
|
|
16
|
+
setTimeout(() => {
|
|
17
|
+
if (!store.user) navigateTo('/login')
|
|
18
|
+
}, 3000)
|
|
19
|
+
</script>
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="authPage">
|
|
3
|
+
<section data-bs-version="5.1" class="authForm">
|
|
4
|
+
<NuxtImg loading="lazy" src="~/assets/images/logo512alpha-128x128.png" alt="Meeovi Logo" class="authLogo" />
|
|
5
|
+
<h1 class="mbr-section-title mbr-fonts-style display-1">Forgot Password</h1>
|
|
6
|
+
|
|
7
|
+
<div class="mbr-section-btn">
|
|
8
|
+
<div class="request-reset-form">
|
|
9
|
+
<p>Enter your email address to receive a password reset link.</p>
|
|
10
|
+
<form class="mbr-section-btn" :schema="schema" :state="state" @submit="onSubmit">
|
|
11
|
+
<v-text-field v-model="state.email" type="email" label="Email" required></v-text-field>
|
|
12
|
+
<div class="mb-3">
|
|
13
|
+
<div ref="turnstileRef"></div>
|
|
14
|
+
</div>
|
|
15
|
+
<v-btn class="mt-2 btn btn-primary display-4" type="submit" block :loading="loading" :disabled="loading || !turnstileToken">
|
|
16
|
+
Send Reset Link
|
|
17
|
+
</v-btn>
|
|
18
|
+
</form>
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
<v-alert v-if="message" :type="messageType" class="mt-4" variant="tonal" closable>
|
|
22
|
+
{{ message }}
|
|
23
|
+
</v-alert>
|
|
24
|
+
|
|
25
|
+
<div class="mt-4 text-center">
|
|
26
|
+
<p>Remember your password?
|
|
27
|
+
<NuxtLink to="/login">Sign In</NuxtLink>
|
|
28
|
+
</p>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
</section>
|
|
32
|
+
</div>
|
|
33
|
+
</template>
|
|
34
|
+
|
|
35
|
+
<script setup>
|
|
36
|
+
import { useHead } from 'nuxt/app'
|
|
37
|
+
import { definePageMeta, useLocalePath, useAuth } from '#imports'
|
|
38
|
+
import { z } from 'zod'
|
|
39
|
+
import { reactive, ref } from 'vue'
|
|
40
|
+
|
|
41
|
+
definePageMeta({
|
|
42
|
+
auth: {
|
|
43
|
+
only: 'guest'
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
useHead({
|
|
48
|
+
title: 'Forgot Password'
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
const auth = useAuth()
|
|
52
|
+
const toast = useToast()
|
|
53
|
+
const localePath = useLocalePath()
|
|
54
|
+
|
|
55
|
+
const schema = z.object({
|
|
56
|
+
email: z.email(('forgotPassword.errors.invalidEmail'))
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const state = reactive<Partial<Schema>>({
|
|
60
|
+
email: undefined
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const loading = ref(false)
|
|
64
|
+
|
|
65
|
+
// expose message and messageType used by the template v-alert
|
|
66
|
+
const message = ref<string | null>(null)
|
|
67
|
+
const messageType = ref<'info' | 'success' | 'error' | 'warning' | undefined>(undefined)
|
|
68
|
+
|
|
69
|
+
// Turnstile placeholders referenced in the template
|
|
70
|
+
const turnstileRef = ref<HTMLElement | null>(null)
|
|
71
|
+
const turnstileToken = ref<string | null>(null)
|
|
72
|
+
|
|
73
|
+
async function onSubmit(event) {
|
|
74
|
+
event.preventDefault()
|
|
75
|
+
if (loading.value)
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
loading.value = true
|
|
79
|
+
// clear previous messages
|
|
80
|
+
message.value = null
|
|
81
|
+
messageType.value = undefined
|
|
82
|
+
|
|
83
|
+
const { error } = await auth.forgetPassword({
|
|
84
|
+
email: state.email,
|
|
85
|
+
redirectTo: localePath('/reset-password')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
if (error) {
|
|
89
|
+
const text = error.message || error.statusText
|
|
90
|
+
// update v-alert bindings
|
|
91
|
+
message.value = text
|
|
92
|
+
messageType.value = 'error'
|
|
93
|
+
toast.show({
|
|
94
|
+
title: text,
|
|
95
|
+
color: 'error'
|
|
96
|
+
})
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
const successText = ('forgotPassword.success')
|
|
100
|
+
// update v-alert bindings
|
|
101
|
+
message.value = successText
|
|
102
|
+
messageType.value = 'success'
|
|
103
|
+
toast.show({
|
|
104
|
+
title: successText,
|
|
105
|
+
color: 'success'
|
|
106
|
+
})
|
|
107
|
+
}
|
|
108
|
+
loading.value = false
|
|
109
|
+
}
|
|
110
|
+
</script>
|
|
111
|
+
|
|
112
|
+
<style scoped>
|
|
113
|
+
.authPage {
|
|
114
|
+
min-height: 100vh;
|
|
115
|
+
display: flex;
|
|
116
|
+
align-items: center;
|
|
117
|
+
justify-content: center;
|
|
118
|
+
background-color: var(--v-background);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.authForm {
|
|
122
|
+
width: 100%;
|
|
123
|
+
max-width: 400px;
|
|
124
|
+
margin: 2rem auto;
|
|
125
|
+
padding: 2rem;
|
|
126
|
+
background: var(--v-surface-variant);
|
|
127
|
+
border-radius: 8px;
|
|
128
|
+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.authLogo {
|
|
132
|
+
display: block;
|
|
133
|
+
margin: 0 auto 2rem;
|
|
134
|
+
max-width: 128px;
|
|
135
|
+
height: auto;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.mbr-section-title {
|
|
139
|
+
text-align: center;
|
|
140
|
+
margin-bottom: 2rem;
|
|
141
|
+
font-size: 2rem;
|
|
142
|
+
font-weight: 600;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.v-alert {
|
|
146
|
+
margin-top: 1rem;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
.text-center {
|
|
150
|
+
text-align: center;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.mt-4 {
|
|
154
|
+
margin-top: 1rem;
|
|
155
|
+
}
|
|
156
|
+
</style>
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
{
|
|
2
|
+
"en": {
|
|
3
|
+
"signIn": {
|
|
4
|
+
"welcome": "Welcome to {name}",
|
|
5
|
+
"email": "Email",
|
|
6
|
+
"password": "Password",
|
|
7
|
+
"emailPlaceholder": "Email Address",
|
|
8
|
+
"passwordPlaceholder": "Password",
|
|
9
|
+
"rememberMe": "Remember me",
|
|
10
|
+
"forgotPassword": "Forgot your password?",
|
|
11
|
+
"signIn": "Sign In",
|
|
12
|
+
"noAccount": "Don't have an account?",
|
|
13
|
+
"createAccount": "Create today!",
|
|
14
|
+
"or": "Or",
|
|
15
|
+
"signInSuccess": "Sign in Success",
|
|
16
|
+
"errors": {
|
|
17
|
+
"invalidEmail": "Invalid email address",
|
|
18
|
+
"passwordLength": "Password must be at least {min} characters"
|
|
19
|
+
},
|
|
20
|
+
"emailNotVerified": "Email Not Verified",
|
|
21
|
+
"emailNotVerifiedDesc": "Please verify your email address to continue.",
|
|
22
|
+
"sendEmail": "Send Verification Email",
|
|
23
|
+
"sendEmailSuccess": "Verification email has been sent",
|
|
24
|
+
"sendEmailError": "Failed to send verification email"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"zh-CN": {
|
|
28
|
+
"signIn": {
|
|
29
|
+
"welcome": "欢迎使用 {name}",
|
|
30
|
+
"email": "邮箱",
|
|
31
|
+
"password": "密码",
|
|
32
|
+
"emailPlaceholder": "邮箱地址",
|
|
33
|
+
"passwordPlaceholder": "密码",
|
|
34
|
+
"rememberMe": "记住我",
|
|
35
|
+
"forgotPassword": "忘记密码?",
|
|
36
|
+
"signIn": "登录",
|
|
37
|
+
"noAccount": "还没有账号?",
|
|
38
|
+
"createAccount": "立即注册!",
|
|
39
|
+
"or": "或者",
|
|
40
|
+
"signInSuccess": "登录成功",
|
|
41
|
+
"errors": {
|
|
42
|
+
"invalidEmail": "无效的邮箱地址",
|
|
43
|
+
"passwordLength": "密码长度至少为{min}个字符"
|
|
44
|
+
},
|
|
45
|
+
"emailNotVerified": "邮箱未验证",
|
|
46
|
+
"emailNotVerifiedDesc": "请验证您的邮箱地址以继续。",
|
|
47
|
+
"resendEmail": "重新发送验证邮件",
|
|
48
|
+
"resendEmailSuccess": "验证邮件已发送",
|
|
49
|
+
"resendEmailError": "发送验证邮件失败",
|
|
50
|
+
"sendEmail": "发送验证邮件",
|
|
51
|
+
"sendEmailSuccess": "验证邮件已发送",
|
|
52
|
+
"sendEmailError": "发送验证邮件失败"
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"ja": {
|
|
56
|
+
"signIn": {
|
|
57
|
+
"welcome": "{name}へようこそ",
|
|
58
|
+
"email": "メールアドレス",
|
|
59
|
+
"password": "パスワード",
|
|
60
|
+
"emailPlaceholder": "メールアドレス",
|
|
61
|
+
"passwordPlaceholder": "パスワード",
|
|
62
|
+
"rememberMe": "ログイン状態を保持",
|
|
63
|
+
"forgotPassword": "パスワードをお忘れですか?",
|
|
64
|
+
"signIn": "ログイン",
|
|
65
|
+
"noAccount": "アカウントをお持ちでない方",
|
|
66
|
+
"createAccount": "新規登録!",
|
|
67
|
+
"or": "または",
|
|
68
|
+
"signInSuccess": "ログインに成功しました",
|
|
69
|
+
"errors": {
|
|
70
|
+
"invalidEmail": "無効なメールアドレス",
|
|
71
|
+
"passwordLength": "パスワードは{min}文字以上である必要があります"
|
|
72
|
+
},
|
|
73
|
+
"emailNotVerified": "メールアドレス未確認",
|
|
74
|
+
"emailNotVerifiedDesc": "続行するにはメールアドレスを確認してください。",
|
|
75
|
+
"resendEmail": "確認メールを再送信",
|
|
76
|
+
"resendEmailSuccess": "確認メールを送信しました",
|
|
77
|
+
"resendEmailError": "確認メールの送信に失敗しました",
|
|
78
|
+
"sendEmail": "認証メールを送信",
|
|
79
|
+
"sendEmailSuccess": "認証メールを送信しました",
|
|
80
|
+
"sendEmailError": "認証メールの送信に失敗しました"
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"fr": {
|
|
84
|
+
"signIn": {
|
|
85
|
+
"welcome": "Bienvenue sur {name}",
|
|
86
|
+
"email": "E-mail",
|
|
87
|
+
"password": "Mot de passe",
|
|
88
|
+
"emailPlaceholder": "Adresse e-mail",
|
|
89
|
+
"passwordPlaceholder": "Mot de passe",
|
|
90
|
+
"rememberMe": "Se souvenir de moi",
|
|
91
|
+
"forgotPassword": "Mot de passe oublié ?",
|
|
92
|
+
"signIn": "Se connecter",
|
|
93
|
+
"noAccount": "Vous n'avez pas de compte ?",
|
|
94
|
+
"createAccount": "Créez-en un !",
|
|
95
|
+
"or": "Ou",
|
|
96
|
+
"signInSuccess": "Connexion réussie",
|
|
97
|
+
"errors": {
|
|
98
|
+
"invalidEmail": "Adresse e-mail invalide",
|
|
99
|
+
"passwordLength": "Le mot de passe doit contenir au moins {min} caractères"
|
|
100
|
+
},
|
|
101
|
+
"emailNotVerified": "Email non vérifié",
|
|
102
|
+
"emailNotVerifiedDesc": "Veuillez vérifier votre adresse e-mail pour continuer.",
|
|
103
|
+
"resendEmail": "Renvoyer l'e-mail de vérification",
|
|
104
|
+
"resendEmailSuccess": "L'e-mail de vérification a été envoyé",
|
|
105
|
+
"resendEmailError": "Échec de l'envoi de l'e-mail de vérification",
|
|
106
|
+
"sendEmail": "Envoyer l'e-mail de vérification",
|
|
107
|
+
"sendEmailSuccess": "L'e-mail de vérification a été envoyé",
|
|
108
|
+
"sendEmailError": "Échec de l'envoi de l'e-mail de vérification"
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|