mirror of
https://github.com/logseq/logseq.git
synced 2026-02-01 14:43:56 +00:00
feat: new authenticate UI (#12172)
* enhance(ui): login form * enhance(ui): add localization support with translate and locale management * enhance(ui): WIP implement new authentication forms with context management * enhance(ui): add password visibility toggle to input row * enhance(ui): adjust padding for password visibility toggle * enhance(i18n): implement internationalization support for authentication UI * enhance(ui): implement sign in and sign up functionality with loading state * enhance(ui): add session management and error handling in login form * enhance(ui): add confirm code form and enhance authentication flow * enhance(ui): improve sign-in flow and confirm code handling * enhance(ui): add warning variant to alerts and improve error handling * enhance(ui): implement countdown timer for code resend functionality * enhance(ui): implement countdown timer for password reset and enhance login flow * enhance(ui): export authentication and enhance UI components * enhance(ui): integrate new login component and refresh token handling * chore: clear amplify related codes * enhance(i18n): normalize language codes and update locale handling * enhance(auth): add multilingual support for signup and password reset flows * enhance(ui): update login styles to inherit text color * enhance(ui): adjust input color variables for improved accessibility * enhance(auth): add password policy validation and tips in multiple languages * enhance(ui): improve localization handling and update alert styles * enhance(mobile): enhance login modal styling and accessibility * fix(ui): update password validation regex for special characters * enhance(ui): add padding to card header in login dialog --------- Co-authored-by: Tienson Qin <tiensonqin@gmail.com>
This commit is contained in:
@@ -79,7 +79,6 @@ const common = {
|
|||||||
'node_modules/marked/marked.min.js',
|
'node_modules/marked/marked.min.js',
|
||||||
'node_modules/@highlightjs/cdn-assets/highlight.min.js',
|
'node_modules/@highlightjs/cdn-assets/highlight.min.js',
|
||||||
'node_modules/@isomorphic-git/lightning-fs/dist/lightning-fs.min.js',
|
'node_modules/@isomorphic-git/lightning-fs/dist/lightning-fs.min.js',
|
||||||
'packages/amplify/dist/amplify.js',
|
|
||||||
'packages/ui/dist/ui/ui.js',
|
'packages/ui/dist/ui/ui.js',
|
||||||
'node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3.wasm',
|
'node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3.wasm',
|
||||||
'node_modules/react/umd/react.production.min.js',
|
'node_modules/react/umd/react.production.min.js',
|
||||||
@@ -130,7 +129,6 @@ const common = {
|
|||||||
'node_modules/prop-types/prop-types.min.js',
|
'node_modules/prop-types/prop-types.min.js',
|
||||||
'node_modules/interactjs/dist/interact.min.js',
|
'node_modules/interactjs/dist/interact.min.js',
|
||||||
'node_modules/photoswipe/dist/umd/*.js',
|
'node_modules/photoswipe/dist/umd/*.js',
|
||||||
'packages/amplify/dist/amplify.js',
|
|
||||||
'packages/ui/dist/ui/ui.js',
|
'packages/ui/dist/ui/ui.js',
|
||||||
'node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3.wasm',
|
'node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3.wasm',
|
||||||
]).pipe(gulp.dest(path.join(outputPath, 'mobile', 'js'))),
|
]).pipe(gulp.dest(path.join(outputPath, 'mobile', 'js'))),
|
||||||
|
|||||||
@@ -105,7 +105,7 @@
|
|||||||
"tldraw:build": "yarn --cwd packages/tldraw install",
|
"tldraw:build": "yarn --cwd packages/tldraw install",
|
||||||
"amplify:build": "yarn --cwd packages/amplify install",
|
"amplify:build": "yarn --cwd packages/amplify install",
|
||||||
"ui:build": "yarn --cwd packages/ui install",
|
"ui:build": "yarn --cwd packages/ui install",
|
||||||
"postinstall": "yarn tldraw:build && yarn amplify:build && yarn ui:build"
|
"postinstall": "yarn tldraw:build && yarn ui:build"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor-community/safe-area": "7.0.0-alpha.1",
|
"@capacitor-community/safe-area": "7.0.0-alpha.1",
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ const alertVariants = cva(
|
|||||||
default: 'bg-background text-foreground',
|
default: 'bg-background text-foreground',
|
||||||
destructive:
|
destructive:
|
||||||
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
|
'border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive',
|
||||||
|
warning:
|
||||||
|
'border-yellow-600 text-yellow-600 dark:text-yellow-500/70 [&>svg]:text-yellow-600',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
|||||||
@@ -9,6 +9,11 @@
|
|||||||
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
|
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
|
||||||
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
|
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
|
||||||
<script src="./index.tsx" type="module"></script>
|
<script src="./index.tsx" type="module"></script>
|
||||||
|
<style>
|
||||||
|
a {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@@ -2,22 +2,63 @@ import '../src/index.css'
|
|||||||
import { setupGlobals } from '../src/ui'
|
import { setupGlobals } from '../src/ui'
|
||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import * as ReactDOM from 'react-dom'
|
import * as ReactDOM from 'react-dom'
|
||||||
|
import { init, t } from '../src/amplify/core'
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { Button } from '@/components/ui/button'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import { LoginForm, ResetPasswordForm, SignupForm, ConfirmWithCodeForm } from '../src/amplify/ui'
|
||||||
|
import { AuthFormRootContext } from '../src/amplify/core'
|
||||||
|
|
||||||
// bootstrap
|
// bootstrap
|
||||||
setupGlobals()
|
setupGlobals()
|
||||||
|
init()
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
const [errors, setErrors] = React.useState<string | null>(null)
|
||||||
|
const [currentTab, setCurrentTab] = React.useState<'login' | 'reset' | 'signup' | 'confirm-code' | any>('login')
|
||||||
|
const onSessionCallback = React.useCallback((session: any) => {
|
||||||
|
console.log('==>>session callback:', session)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setErrors(null)
|
||||||
|
}, [currentTab])
|
||||||
|
|
||||||
|
let content = null
|
||||||
|
// support passing object with type field
|
||||||
|
let _currentTab = currentTab?.type ? currentTab.type : currentTab
|
||||||
|
let _currentTabProps = currentTab?.props || {}
|
||||||
|
|
||||||
|
switch (_currentTab) {
|
||||||
|
case 'login':
|
||||||
|
content = <LoginForm/>
|
||||||
|
break
|
||||||
|
case 'reset':
|
||||||
|
content = <ResetPasswordForm/>
|
||||||
|
break
|
||||||
|
case 'signup':
|
||||||
|
content = <SignupForm/>
|
||||||
|
break
|
||||||
|
case 'confirm-code':
|
||||||
|
content = <ConfirmWithCodeForm {..._currentTabProps}/>
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className={'p-8'}>
|
<main className={'h-screen flex flex-col justify-center items-center gap-4'}>
|
||||||
<h1 className={'text-red-500 mb-8'}>
|
<AuthFormRootContext.Provider value={{
|
||||||
Hello, Logseq UI :)
|
errors, setErrors, setCurrentTab,
|
||||||
</h1>
|
onSessionCallback
|
||||||
<Button asChild>
|
}}>
|
||||||
<a href={'https://google.com'} target={'_blank'}>go to google.com</a>
|
<Card className={'sm:w-96'}>
|
||||||
</Button>
|
<CardHeader>
|
||||||
|
<CardTitle className={'capitalize'}>{t(_currentTab)?.replace('-', ' ')}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{content}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</AuthFormRootContext.Provider>
|
||||||
</main>
|
</main>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
"@radix-ui/react-toggle-group": "^1.1.7",
|
"@radix-ui/react-toggle-group": "^1.1.7",
|
||||||
"@radix-ui/react-tooltip": "^1.2.4",
|
"@radix-ui/react-tooltip": "^1.2.4",
|
||||||
"@silk-hq/components": "^0.9.10",
|
"@silk-hq/components": "^0.9.10",
|
||||||
|
"aws-amplify": "^6.15.6",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.0.0",
|
"clsx": "^2.0.0",
|
||||||
"cmdk": "^0.2.0",
|
"cmdk": "^0.2.0",
|
||||||
@@ -70,6 +71,7 @@
|
|||||||
"@types/prop-types": "^15",
|
"@types/prop-types": "^15",
|
||||||
"@types/react": "17",
|
"@types/react": "17",
|
||||||
"@types/react-dom": "17",
|
"@types/react-dom": "17",
|
||||||
|
"buffer": "^5.5.0",
|
||||||
"parcel": "2.8.3",
|
"parcel": "2.8.3",
|
||||||
"postcss": "^8.4.31",
|
"postcss": "^8.4.31",
|
||||||
"postcss-loader": "^7.3.3",
|
"postcss-loader": "^7.3.3",
|
||||||
|
|||||||
26
packages/ui/src/amplify/core.ts
Normal file
26
packages/ui/src/amplify/core.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { Amplify } from 'aws-amplify'
|
||||||
|
import { createContext, useContext } from 'react'
|
||||||
|
import { translate, setNSDicts, setLocale } from '../i18n'
|
||||||
|
|
||||||
|
export const AuthFormRootContext = createContext<any>(null)
|
||||||
|
export const useAuthFormState = () => {
|
||||||
|
return useContext(AuthFormRootContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function t(key: string, ...args: any) {
|
||||||
|
return translate('amplify', key, ...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function init({ lang, authCognito }: any) {
|
||||||
|
// Load default language
|
||||||
|
setNSDicts('amplify', require('./lang').default)
|
||||||
|
if (lang) setLocale(lang)
|
||||||
|
Amplify.configure({
|
||||||
|
Auth: {
|
||||||
|
Cognito: {
|
||||||
|
...authCognito,
|
||||||
|
loginWith: { email: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
8
packages/ui/src/amplify/index.ts
Normal file
8
packages/ui/src/amplify/index.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import * as Auth from 'aws-amplify/auth'
|
||||||
|
import { init } from './core'
|
||||||
|
import { LSAuthenticator } from './ui'
|
||||||
|
|
||||||
|
export {
|
||||||
|
init, Auth,
|
||||||
|
LSAuthenticator
|
||||||
|
}
|
||||||
114
packages/ui/src/amplify/lang.ts
Normal file
114
packages/ui/src/amplify/lang.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
export default {
|
||||||
|
'en': {
|
||||||
|
'signup': 'Sign Up',
|
||||||
|
'reset-password': 'Reset Password',
|
||||||
|
'confirm-code': 'Confirm Code',
|
||||||
|
'CODE_ON_THE_WAY_TIP': 'Your code is on the way. To log in, enter the code we sent you. It may take a minute to arrive.',
|
||||||
|
'PW_POLICY_TIP': '1. at least 8 characters.\n' +
|
||||||
|
'2. must have lowercase characters.\n' +
|
||||||
|
'3. must have uppercase characters.\n' +
|
||||||
|
'4. must have symbol characters.',
|
||||||
|
},
|
||||||
|
'zh-cn': {
|
||||||
|
'login': '登录',
|
||||||
|
'signup': '注册',
|
||||||
|
'reset-password': '重置密码',
|
||||||
|
'confirm-code': '确认验证码',
|
||||||
|
'PW_POLICY_TIP': '1. 密码长度至少8个字符\n' +
|
||||||
|
'2. 密码必须包含小写字母\n' +
|
||||||
|
'3. 密码必须包含大写字母\n' +
|
||||||
|
'4. 密码必须包含特殊字符',
|
||||||
|
'CODE_ON_THE_WAY_TIP': '验证码已发送。请输入我们发送给您的验证码以登录。可能需要一分钟才能收到。',
|
||||||
|
'Sign in to your account': '登录到您的账户',
|
||||||
|
'Email': '电子邮箱',
|
||||||
|
'Password': '密码',
|
||||||
|
'Sign in': '登录',
|
||||||
|
'Confirm': '确认',
|
||||||
|
'Don\'t have an account?': '还没有账户?',
|
||||||
|
'Sign up': '注册',
|
||||||
|
'or': '或 ',
|
||||||
|
'Forgot your password?': '忘记密码?',
|
||||||
|
'Create account': '创建您的账户',
|
||||||
|
'Username': '用户名',
|
||||||
|
'Confirm Password': '确认密码',
|
||||||
|
'New Password': '新密码',
|
||||||
|
'By signing up, you agree to our': '注册即表示您同意我们的 ',
|
||||||
|
'Terms of Service': '服务条款',
|
||||||
|
'Privacy Policy': '隐私政策',
|
||||||
|
'Already have an account?': '已经有账户?',
|
||||||
|
'Reset password': '重置您的密码',
|
||||||
|
'Enter the code sent to your email': '输入发送到您邮箱的验证码',
|
||||||
|
'Send code': '发送验证码',
|
||||||
|
'Resend code': '重新发送验证码',
|
||||||
|
'Back to login': '返回登录',
|
||||||
|
'Enter your email': '请输入您的电子邮箱'
|
||||||
|
},
|
||||||
|
'zh-hant': {
|
||||||
|
'login': '登入',
|
||||||
|
'signup': '註冊',
|
||||||
|
'reset-password': '重置密碼',
|
||||||
|
'confirm-code': '確認驗證碼',
|
||||||
|
'CODE_ON_THE_WAY_TIP': '驗證碼已發送。請輸入我們發送給您的驗證碼以登入。可能需要一分鐘才能收到。',
|
||||||
|
'PW_POLICY_TIP': '1. 密碼長度至少8個字符\n' +
|
||||||
|
'2. 密碼必須包含小寫字母\n' +
|
||||||
|
'3. 密碼必須包含大寫字母\n' +
|
||||||
|
'4. 密碼必須包含特殊字符',
|
||||||
|
'Sign in to your account': '登入到您的帳戶',
|
||||||
|
'Email': '電子郵箱',
|
||||||
|
'Password': '密碼',
|
||||||
|
'Sign in': '登入',
|
||||||
|
'Confirm': '確認',
|
||||||
|
'Don\'t have an account?': '還沒有帳戶?',
|
||||||
|
'Sign up': '註冊',
|
||||||
|
'or': '或 ',
|
||||||
|
'Forgot your password?': '忘記密碼?',
|
||||||
|
'Create account': '創建您的帳戶',
|
||||||
|
'Username': '用戶名',
|
||||||
|
'Confirm Password': '確認密碼',
|
||||||
|
'New Password': '新密碼',
|
||||||
|
'By signing up, you agree to our': '註冊即表示您同意我們的 ',
|
||||||
|
'Terms of Service': '服務條款',
|
||||||
|
'Privacy Policy': '隱私政策',
|
||||||
|
'Already have an account?': '已經有帳戶?',
|
||||||
|
'Reset password': '重置您的密碼',
|
||||||
|
'Enter the code sent to your email': '輸入發送到您郵箱的驗證碼',
|
||||||
|
'Send code': '發送驗證碼',
|
||||||
|
'Resend code': '重新發送驗證碼',
|
||||||
|
'Back to login': '返回登入',
|
||||||
|
'Enter your email': '請輸入您的電子郵箱'
|
||||||
|
},
|
||||||
|
'ja': {
|
||||||
|
'login': 'ログイン',
|
||||||
|
'signup': 'サインアップ',
|
||||||
|
'reset-password': 'パスワードをリセットする',
|
||||||
|
'confirm-code': 'コードを確認する',
|
||||||
|
'CODE_ON_THE_WAY_TIP': 'コードが送信されました。ログインするには、送信したコードを入力してください。届くまでに1分ほどかかる場合があります。',
|
||||||
|
'PW_POLICY_TIP': '1. パスワードは8文字以上であること。\n' +
|
||||||
|
'2. パスワードには小文字を含める必要があります。\n' +
|
||||||
|
'3. パスワードには大文字を含める必要があります。\n' +
|
||||||
|
'4. パスワードには記号を含める必要があります。',
|
||||||
|
'Sign in to your account': 'アカウントにサインイン',
|
||||||
|
'Email': 'メール',
|
||||||
|
'Password': 'パスワード',
|
||||||
|
'Sign in': 'サインイン',
|
||||||
|
'Confirm': '確認',
|
||||||
|
'Don\'t have an account?': 'アカウントをお持ちでないですか?',
|
||||||
|
'Sign up': 'サインアップ',
|
||||||
|
'or': 'または ',
|
||||||
|
'Forgot your password?': 'パスワードをお忘れですか?',
|
||||||
|
'Create account': 'アカウントを作成する',
|
||||||
|
'Username': 'ユーザー名',
|
||||||
|
'New Password': '新しいパスワード',
|
||||||
|
'Confirm Password': 'パスワードを確認する',
|
||||||
|
'By signing up, you agree to our': 'サインアップすることで、あなたは私たちの ',
|
||||||
|
'Terms of Service': '利用規約',
|
||||||
|
'Privacy Policy': 'プライバシーポリシー',
|
||||||
|
'Already have an account? ': 'すでにアカウントをお持ちですか?',
|
||||||
|
'Reset password': 'パスワードをリセットする',
|
||||||
|
'Enter the code sent to your email': 'メールに送信されたコードを入力してください',
|
||||||
|
'Send code': 'コードを送信',
|
||||||
|
'Resend code': 'コードを再送信',
|
||||||
|
'Back to login': 'ログインに戻る',
|
||||||
|
'Enter your email': 'メールアドレスを入力してください'
|
||||||
|
}
|
||||||
|
}
|
||||||
710
packages/ui/src/amplify/ui.tsx
Normal file
710
packages/ui/src/amplify/ui.tsx
Normal file
@@ -0,0 +1,710 @@
|
|||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Input, InputProps } from '@/components/ui/input'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { FormHTMLAttributes, useEffect, useState } from 'react'
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||||
|
import { AlertCircleIcon, Loader2Icon, LucideEye, LucideEyeClosed, LucideX } from 'lucide-react'
|
||||||
|
import { AuthFormRootContext, t, useAuthFormState } from './core'
|
||||||
|
import * as Auth from 'aws-amplify/auth'
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
function ErrorTip({ error, removeError }: {
|
||||||
|
error: string | { variant?: 'warning' | 'destructive', title?: string, message: string | any },
|
||||||
|
removeError?: () => void
|
||||||
|
}) {
|
||||||
|
if (!error) return null
|
||||||
|
if (typeof error === 'string') {
|
||||||
|
error = { message: error }
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Alert variant={error.variant || 'destructive'} className={'relative'}>
|
||||||
|
<AlertCircleIcon size={18}/>
|
||||||
|
{error.title && <AlertTitle>{error.title}</AlertTitle>}
|
||||||
|
<AlertDescription>
|
||||||
|
<p>
|
||||||
|
{(typeof error.message === 'string' ? error.message : JSON.stringify(error.message))?.split('\n')
|
||||||
|
.map((line: string, idx: number) => {
|
||||||
|
return <span key={idx}>{line}<br/></span>
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</AlertDescription>
|
||||||
|
<a className={'close absolute right-0 top-0 opacity-50 hover:opacity-80 p-2'}
|
||||||
|
onClick={() => removeError?.()}>
|
||||||
|
<LucideX size={16}/>
|
||||||
|
</a>
|
||||||
|
</Alert>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function InputRow(
|
||||||
|
props: InputProps & { label: string | React.ReactNode },
|
||||||
|
) {
|
||||||
|
const { errors, setErrors } = useAuthFormState()
|
||||||
|
const { label, type, ...rest } = props
|
||||||
|
const isPassword = type === 'password'
|
||||||
|
const error = props.name && errors?.[props.name]
|
||||||
|
const [localType, setLocalType] = useState<string>(type || 'text')
|
||||||
|
const [showPassword, setShowPassword] = useState<boolean>(false)
|
||||||
|
const removeError = () => {
|
||||||
|
if (props.name && errors?.[props.name]) {
|
||||||
|
const newErrors = { ...errors }
|
||||||
|
delete newErrors[props.name]
|
||||||
|
setErrors(newErrors)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={'relative w-full flex flex-col gap-3 pb-1'}>
|
||||||
|
<Label htmlFor={props.id}>
|
||||||
|
{label}
|
||||||
|
</Label>
|
||||||
|
<Input type={localType} {...rest as any} />
|
||||||
|
|
||||||
|
{isPassword && (
|
||||||
|
<a className={'absolute px-2 right-1 top-7 py-3 flex items-center opacity-50 hover:opacity-80 select-none'}
|
||||||
|
onClick={() => {
|
||||||
|
setShowPassword(!showPassword)
|
||||||
|
setLocalType(showPassword ? 'password' : 'text')
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{showPassword ? <LucideEye size={14}/> : <LucideEyeClosed size={14}/>}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{error &&
|
||||||
|
<div className={'pt-1'}>
|
||||||
|
<ErrorTip error={error} removeError={removeError}/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function FormGroup(props: FormHTMLAttributes<any>) {
|
||||||
|
const { className, children, ...reset } = props
|
||||||
|
return (
|
||||||
|
<form className={cn('relative flex flex-col justify-center items-center gap-4 w-full', className)}
|
||||||
|
{...reset}>
|
||||||
|
{children}
|
||||||
|
</form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Password must be at least 8 characters
|
||||||
|
// 2. Password must have lowercase characters
|
||||||
|
// 3. Password must have uppercase characters
|
||||||
|
// 4. Password must have symbol characters
|
||||||
|
function validatePasswordPolicy(password: string) {
|
||||||
|
if (!password ||
|
||||||
|
password.length < 8 ||
|
||||||
|
!/[a-z]/.test(password) ||
|
||||||
|
!/[A-Z]/.test(password) ||
|
||||||
|
!/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?~`]/.test(password)
|
||||||
|
) {
|
||||||
|
throw new Error(t('PW_POLICY_TIP'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function useCountDown() {
|
||||||
|
const [countDownNum, setCountDownNum] = useState<number>(0)
|
||||||
|
const startCountDown = () => {
|
||||||
|
setCountDownNum(60)
|
||||||
|
const interval = setInterval(() => {
|
||||||
|
setCountDownNum((num) => {
|
||||||
|
if (num <= 1) {
|
||||||
|
clearInterval(interval)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return num - 1
|
||||||
|
})
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
setCountDownNum(0)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return { countDownNum, startCountDown, setCountDownNum }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LoginForm() {
|
||||||
|
const { setErrors, setCurrentTab, onSessionCallback, userSessionRender } = useAuthFormState()
|
||||||
|
const [loading, setLoading] = useState<boolean>(false)
|
||||||
|
const [sessionUser, setSessionUser] = useState<any>(null)
|
||||||
|
const loadSession = async () => {
|
||||||
|
try {
|
||||||
|
const ret = await Auth.fetchAuthSession()
|
||||||
|
if (!ret?.userSub) throw new Error('no session')
|
||||||
|
const user = await Auth.getCurrentUser()
|
||||||
|
onSessionCallback?.({ ...ret, user })
|
||||||
|
const tokens = ret.tokens
|
||||||
|
setSessionUser({
|
||||||
|
...user, signInUserSession: {
|
||||||
|
idToken: { jwtToken: tokens?.idToken?.toString() },
|
||||||
|
accessToken: { jwtToken: tokens?.accessToken.toString() },
|
||||||
|
refreshToken: null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await (new Promise(resolve => setTimeout(resolve, 100)))
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('no current session:', e)
|
||||||
|
setSessionUser(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// check current auth session
|
||||||
|
loadSession()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (sessionUser === null) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Skeleton className="h-4 w-[250px]"/>
|
||||||
|
<Skeleton className="h-4 w-[200px]"/>
|
||||||
|
</div>)
|
||||||
|
}
|
||||||
|
|
||||||
|
const signOut = async () => {
|
||||||
|
await Auth.signOut()
|
||||||
|
setSessionUser(false)
|
||||||
|
setErrors(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sessionUser?.username) {
|
||||||
|
if (userSessionRender) {
|
||||||
|
if (typeof userSessionRender === 'function') {
|
||||||
|
return userSessionRender({ sessionUser, signOut })
|
||||||
|
}
|
||||||
|
return userSessionRender
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={'w-full text-center'}>
|
||||||
|
<p className={'mb-4'}>{t('You are already logged in as')} <strong>{sessionUser.username}</strong></p>
|
||||||
|
<Button variant={'secondary'} className={'w-full'} onClick={signOut}>
|
||||||
|
{t('Sign out')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormGroup onSubmit={async (e) => {
|
||||||
|
setErrors(null)
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
// get submit form input data
|
||||||
|
const formData = new FormData(e.target as HTMLFormElement)
|
||||||
|
const data = Object.fromEntries(formData.entries())
|
||||||
|
|
||||||
|
// sign in logic here
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
const username = (data.email as string)?.trim()
|
||||||
|
const ret = await Auth.signIn({ username, password: data.password as string })
|
||||||
|
const nextStep = ret?.nextStep?.signInStep
|
||||||
|
if (!nextStep) throw new Error(JSON.stringify(ret))
|
||||||
|
switch (nextStep) {
|
||||||
|
case 'CONFIRM_SIGN_UP':
|
||||||
|
case 'CONFIRM_SIGN_IN_WITH_EMAIL_CODE':
|
||||||
|
case 'CONFIRM_SIGN_IN_WITH_TOTP_CODE':
|
||||||
|
setCurrentTab({ type: 'confirm-code', props: { user: { ...ret, username }, nextStep } })
|
||||||
|
return
|
||||||
|
case 'RESET_PASSWORD':
|
||||||
|
setCurrentTab({ type: 'reset-password', props: { user: { ...ret, username }, nextStep } })
|
||||||
|
return
|
||||||
|
case 'DONE':
|
||||||
|
// signed in
|
||||||
|
await loadSession()
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
throw new Error('Unsupported sign-in step: ' + nextStep)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setErrors({ password: { message: (e as Error).message, title: t('Bad Response.') } })
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<InputRow id="email" type="text" required={true} name="email" autoFocus={true} label={t('Email')}/>
|
||||||
|
<InputRow id="password" type="password" required={true} name="password" label={t('Password')}/>
|
||||||
|
|
||||||
|
<div className={'w-full'}>
|
||||||
|
<Button type="submit" disabled={loading} className={'w-full'}>
|
||||||
|
{loading && <Loader2Icon className="animate-spin mr-1" size={16}/>}
|
||||||
|
{t('Sign in')}
|
||||||
|
</Button>
|
||||||
|
<p className={'pt-4 text-center'}>
|
||||||
|
|
||||||
|
<span className={'text-sm'}>
|
||||||
|
<span className={'opacity-50'}>{t('Don\'t have an account?')} </span>
|
||||||
|
<a
|
||||||
|
onClick={() => setCurrentTab('signup')}
|
||||||
|
className={'underline opacity-60 hover:opacity-80'}
|
||||||
|
>{t('Sign up')}</a>
|
||||||
|
<br/>
|
||||||
|
<span className={'opacity-50'}>{t('or')} </span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<a onClick={() => {
|
||||||
|
setCurrentTab('reset-password')
|
||||||
|
}} className={'text-sm opacity-60 hover:opacity-80 underline'}>
|
||||||
|
{t('Forgot your password?')}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</FormGroup>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SignupForm() {
|
||||||
|
const { setCurrentTab, setErrors } = useAuthFormState()
|
||||||
|
const [loading, setLoading] = useState<boolean>(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormGroup onSubmit={async (e) => {
|
||||||
|
setErrors(null)
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
// get submit form input data
|
||||||
|
const formData = new FormData(e.target as HTMLFormElement)
|
||||||
|
const data = Object.fromEntries(formData.entries()) as any
|
||||||
|
|
||||||
|
try {
|
||||||
|
validatePasswordPolicy(data.password)
|
||||||
|
} catch (e) {
|
||||||
|
setErrors({
|
||||||
|
password: {
|
||||||
|
message: (e as Error).message,
|
||||||
|
title: t('Invalid Password')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.password !== data.confirm_password) {
|
||||||
|
setErrors({
|
||||||
|
confirm_password: {
|
||||||
|
message: t('Passwords do not match.'),
|
||||||
|
title: t('Invalid Password')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
const ret = await Auth.signUp({
|
||||||
|
username: data.username as string,
|
||||||
|
password: data.password as string,
|
||||||
|
options: {
|
||||||
|
userAttributes: {
|
||||||
|
email: data.email as string,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (ret.isSignUpComplete) {
|
||||||
|
// TODO: auto sign in
|
||||||
|
if (ret.nextStep?.signUpStep === 'COMPLETE_AUTO_SIGN_IN') {
|
||||||
|
const { nextStep } = await Auth.autoSignIn()
|
||||||
|
if (nextStep.signInStep === 'DONE') {
|
||||||
|
// signed in
|
||||||
|
setCurrentTab('login')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentTab('login')
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
if (ret.nextStep?.signUpStep === 'CONFIRM_SIGN_UP') {
|
||||||
|
setCurrentTab({
|
||||||
|
type: 'confirm-code',
|
||||||
|
props: {
|
||||||
|
user: { ...ret, username: data.username },
|
||||||
|
nextStep: 'CONFIRM_SIGN_UP'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(e)
|
||||||
|
const error = { title: t('Bad Response.'), message: (e as Error).message }
|
||||||
|
let k = 'confirm_password'
|
||||||
|
if (e.name === 'UsernameExistsException') {
|
||||||
|
k = 'username'
|
||||||
|
}
|
||||||
|
setErrors({ [k]: error })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
<InputRow id="email" type="email" name="email" autoFocus={true} required={true} label={t('Email')}/>
|
||||||
|
<InputRow id="username" type="text" name="username" required={true} label={t('Username')}/>
|
||||||
|
<InputRow id="password" type="password" name="password"
|
||||||
|
required={true}
|
||||||
|
placeholder={t('Password')}
|
||||||
|
label={t('Password')}/>
|
||||||
|
<InputRow id="confirm_password" type="password" name="confirm_password"
|
||||||
|
required={true}
|
||||||
|
placeholder={t('Confirm Password')}
|
||||||
|
label={t('Confirm Password')}/>
|
||||||
|
<div className={'-mt-1'}>
|
||||||
|
<span className={'text-sm opacity-50'}>
|
||||||
|
{t('By signing up, you agree to our')}
|
||||||
|
<a href="https://logseq.com/terms"
|
||||||
|
target={'_blank'}
|
||||||
|
className={'underline hover:opacity-100'}>{t('Terms of Service')}</a>
|
||||||
|
{t(' and ')}
|
||||||
|
<a href="https://logseq.com/privacy-policy"
|
||||||
|
target={'_blank'}
|
||||||
|
className={'underline hover:opacity-100'}>{t('Privacy Policy')}</a>.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={'w-full'}>
|
||||||
|
<Button type="submit" disabled={loading} className={'w-full'}>
|
||||||
|
{loading && <Loader2Icon className="animate-spin mr-1" size={16}/>}
|
||||||
|
{t('Create account')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className={'pt-1 text-center'}>
|
||||||
|
<a onClick={() => setCurrentTab('login')}
|
||||||
|
className={'text-sm opacity-60 hover:opacity-80 underline'}>
|
||||||
|
{t('Back to login')}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</FormGroup>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ResetPasswordForm() {
|
||||||
|
const [isSentCode, setIsSentCode] = useState<boolean>(false)
|
||||||
|
const [sentUsername, setSentUsername] = useState<string>('')
|
||||||
|
const { setCurrentTab, setErrors } = useAuthFormState()
|
||||||
|
const { countDownNum, startCountDown } = useCountDown()
|
||||||
|
const [loading, setLoading] = useState<boolean>(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setErrors({})
|
||||||
|
}, [isSentCode])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormGroup
|
||||||
|
autoComplete={'off'}
|
||||||
|
onSubmit={async (e) => {
|
||||||
|
setErrors(null)
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
// get submit form input data
|
||||||
|
const formData = new FormData(e.target as HTMLFormElement)
|
||||||
|
const data = Object.fromEntries(formData.entries())
|
||||||
|
|
||||||
|
if (!isSentCode) {
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
|
||||||
|
const username = (data.email as string)?.trim()
|
||||||
|
// send reset code
|
||||||
|
const ret = await Auth.resetPassword({ username })
|
||||||
|
console.debug('[Auth] reset pw code sent: ', ret)
|
||||||
|
setSentUsername(username)
|
||||||
|
startCountDown()
|
||||||
|
setIsSentCode(true)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error sending reset code:', error)
|
||||||
|
setErrors({ email: { message: (error as Error).message, title: t('Bad Response.') } })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// confirm reset password
|
||||||
|
if ((data.password as string)?.length < 8) {
|
||||||
|
setErrors({
|
||||||
|
password: {
|
||||||
|
message: t('Password must be at least 8 characters.'),
|
||||||
|
title: t('Invalid Password')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
} else if (data.password !== data.confirm_password) {
|
||||||
|
setErrors({
|
||||||
|
confirm_password: {
|
||||||
|
message: t('Passwords do not match.'),
|
||||||
|
title: t('Invalid Password')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
const ret = await Auth.confirmResetPassword({
|
||||||
|
username: sentUsername,
|
||||||
|
newPassword: data.password as string,
|
||||||
|
confirmationCode: data.code as string
|
||||||
|
})
|
||||||
|
|
||||||
|
console.debug('[Auth] confirm reset pw: ', ret)
|
||||||
|
setCurrentTab('login')
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error confirming reset password:', error)
|
||||||
|
setErrors({ 'confirm_password': { message: (error as Error).message, title: t('Bad Response.') } })
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
{isSentCode ? (
|
||||||
|
<>
|
||||||
|
<div className={'w-full opacity-60 flex justify-end relative h-0 z-[2]'}>
|
||||||
|
{countDownNum > 0 ? (
|
||||||
|
<span className={'text-sm opacity-50 select-none absolute top-3 right-0'}>
|
||||||
|
{countDownNum}s
|
||||||
|
</span>
|
||||||
|
) : (<a onClick={async () => {
|
||||||
|
startCountDown()
|
||||||
|
try {
|
||||||
|
const ret = await Auth.resetPassword({ username: sentUsername })
|
||||||
|
console.debug('[Auth] reset pw code re-sent: ', ret)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error resending reset code:', error)
|
||||||
|
setErrors({ email: { message: (error as Error).message, title: t('Bad Response.') } })
|
||||||
|
} finally {}
|
||||||
|
}} className={'text-sm opacity-70 hover:opacity-90 underline absolute top-3 right-0 select-none'}>
|
||||||
|
{t('Resend code')}
|
||||||
|
</a>)}
|
||||||
|
</div>
|
||||||
|
<InputRow id="code" type="text" name="code" required={true}
|
||||||
|
placeholder={'123456'}
|
||||||
|
autoComplete={'off'}
|
||||||
|
label={t('Enter the code sent to your email')}/>
|
||||||
|
|
||||||
|
<InputRow id="password" type="password" name="password" required={true}
|
||||||
|
placeholder={t('New Password')}
|
||||||
|
label={t('New Password')}/>
|
||||||
|
|
||||||
|
<InputRow label={t('Confirm Password')}
|
||||||
|
id="confirm_password" type="password" name="confirm_password" required={true}
|
||||||
|
placeholder={t('Confirm Password')}/>
|
||||||
|
|
||||||
|
<div className={'w-full'}>
|
||||||
|
<Button type="submit"
|
||||||
|
className={'w-full'}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading && <Loader2Icon className="animate-spin mr-1" size={16}/>}
|
||||||
|
{t('Reset password')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<p className={'pt-4 text-center'}>
|
||||||
|
<a onClick={() => setCurrentTab('login')}
|
||||||
|
className={'text-sm opacity-60 hover:opacity-80 underline'}>
|
||||||
|
{t('Back to login')}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<InputRow id="email" type="email" name="email" required={true}
|
||||||
|
placeholder={'you@xx.com'}
|
||||||
|
autoFocus={true}
|
||||||
|
label={t('Enter your email')}/>
|
||||||
|
<div className={'w-full'}>
|
||||||
|
<Button type="submit"
|
||||||
|
className={'w-full'}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading && <Loader2Icon className="animate-spin mr-1" size={16}/>}
|
||||||
|
{t('Send code')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<p className={'pt-3 text-center'}>
|
||||||
|
<a onClick={() => setCurrentTab('login')}
|
||||||
|
className={'text-sm opacity-60 hover:opacity-80 underline'}>
|
||||||
|
{t('Back to login')}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</FormGroup>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ConfirmWithCodeForm(
|
||||||
|
props: { user: any, nextStep: any }
|
||||||
|
) {
|
||||||
|
const { setCurrentTab, setErrors } = useAuthFormState()
|
||||||
|
const [loading, setLoading] = useState<boolean>(false)
|
||||||
|
const isFromSignIn = props.user?.hasOwnProperty('isSignedIn')
|
||||||
|
const signUpCodeDeliveryDetails = props.user?.nextStep?.codeDeliveryDetails
|
||||||
|
const { countDownNum, startCountDown, setCountDownNum } = useCountDown()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormGroup
|
||||||
|
autoComplete={'off'}
|
||||||
|
onSubmit={async (e) => {
|
||||||
|
setErrors(null)
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
// get submit form input data
|
||||||
|
const formData = new FormData(e.target as HTMLFormElement)
|
||||||
|
const data = Object.fromEntries(formData.entries())
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
if (props.nextStep === 'CONFIRM_SIGN_UP') {
|
||||||
|
const ret = await Auth.confirmSignUp({
|
||||||
|
username: props.user?.username,
|
||||||
|
confirmationCode: data.code as string,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (ret.nextStep?.signUpStep === 'COMPLETE_AUTO_SIGN_IN') {
|
||||||
|
const { nextStep } = await Auth.autoSignIn()
|
||||||
|
if (nextStep.signInStep === 'DONE') {
|
||||||
|
// signed in
|
||||||
|
setCurrentTab('login')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentTab('login')
|
||||||
|
} else {
|
||||||
|
const ret = await Auth.confirmSignIn({
|
||||||
|
challengeResponse: data.code as string,
|
||||||
|
})
|
||||||
|
|
||||||
|
console.debug('confirmSignIn: ', ret)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setErrors({ code: { message: (e as Error).message, title: t('Bad Response.') } })
|
||||||
|
console.error(e)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}}>
|
||||||
|
|
||||||
|
<p className={'pb-2 opacity-60'}>
|
||||||
|
{isFromSignIn ? t('CODE_ON_THE_WAY_TIP') : (
|
||||||
|
signUpCodeDeliveryDetails &&
|
||||||
|
<span>{t('We have sent a numeric verification code to your email address at')} <code>
|
||||||
|
{signUpCodeDeliveryDetails.destination}.
|
||||||
|
</code></span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/*<pre>*/}
|
||||||
|
{/* {JSON.stringify(props.user, null, 2)}*/}
|
||||||
|
{/* {JSON.stringify(props.nextStep, null, 2)}*/}
|
||||||
|
{/*</pre>*/}
|
||||||
|
|
||||||
|
<span className={'w-full flex justify-end relative h-0 z-10'}>
|
||||||
|
{countDownNum > 0 ? (
|
||||||
|
<span className={'text-sm opacity-50 select-none absolute -bottom-8'}>
|
||||||
|
{countDownNum}s
|
||||||
|
</span>
|
||||||
|
) : <a
|
||||||
|
className={'text-sm opacity-50 hover:opacity-80 active:opacity-50 select-none underline absolute -bottom-8'}
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
// resend code
|
||||||
|
try {
|
||||||
|
startCountDown()
|
||||||
|
if (props.nextStep === 'CONFIRM_SIGN_UP') {
|
||||||
|
const ret = await Auth.resendSignUpCode({
|
||||||
|
username: props.user?.username
|
||||||
|
})
|
||||||
|
|
||||||
|
console.debug('resendSignUpCode: ', ret)
|
||||||
|
} else {
|
||||||
|
// await Auth.resendSignInCode(props.user)
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
setErrors({ code: { message: (e as Error).message, title: t('Bad Response.') } })
|
||||||
|
setCountDownNum(0)
|
||||||
|
console.error(e)
|
||||||
|
} finally {}
|
||||||
|
}}>{t('Resend code')}</a>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
<InputRow id="code" type="text" name="code" required={true}
|
||||||
|
placeholder={'123456'}
|
||||||
|
autoComplete={'off'}
|
||||||
|
autoFocus={true}
|
||||||
|
label={t('Enter the code sent to your email')}/>
|
||||||
|
|
||||||
|
<div className={'w-full'}>
|
||||||
|
<Button type="submit"
|
||||||
|
className={'w-full'}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading && <Loader2Icon className="animate-spin mr-1" size={16}/>}
|
||||||
|
{t('Confirm')}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<p className={'pt-4 text-center'}>
|
||||||
|
<a onClick={() => setCurrentTab('login')}
|
||||||
|
className={'text-sm opacity-60 hover:opacity-80 underline'}>
|
||||||
|
{t('Back to login')}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</FormGroup>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LSAuthenticator(props: any) {
|
||||||
|
const [errors, setErrors] = React.useState<string | null>(null)
|
||||||
|
const [currentTab, setCurrentTab] = React.useState<'login' | 'signup' | 'reset-password' | 'confirm-code' | any>('login')
|
||||||
|
const onSessionCallback = React.useCallback((session: any) => {
|
||||||
|
props.onSessionCallback?.(session)
|
||||||
|
}, [props.onSessionCallback])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
setErrors(null)
|
||||||
|
}, [currentTab])
|
||||||
|
|
||||||
|
let content = null
|
||||||
|
// support passing object with type field
|
||||||
|
let _currentTab = currentTab?.type ? currentTab.type : currentTab
|
||||||
|
let _currentTabProps = currentTab?.props || {}
|
||||||
|
|
||||||
|
switch (_currentTab) {
|
||||||
|
case 'login':
|
||||||
|
content = <LoginForm/>
|
||||||
|
break
|
||||||
|
case 'signup':
|
||||||
|
content = <SignupForm/>
|
||||||
|
break
|
||||||
|
case 'reset-password':
|
||||||
|
content = <ResetPasswordForm/>
|
||||||
|
break
|
||||||
|
case 'confirm-code':
|
||||||
|
content = <ConfirmWithCodeForm {..._currentTabProps}/>
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthFormRootContext.Provider value={{
|
||||||
|
errors, setErrors, setCurrentTab,
|
||||||
|
onSessionCallback, userSessionRender: props.children
|
||||||
|
}}>
|
||||||
|
{props.titleRender?.(_currentTab, t(_currentTab))}
|
||||||
|
<div className={'ls-authenticator-content'}>
|
||||||
|
{content}
|
||||||
|
</div>
|
||||||
|
</AuthFormRootContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
40
packages/ui/src/i18n.ts
Normal file
40
packages/ui/src/i18n.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
export type TranslateFn = (
|
||||||
|
locale: string,
|
||||||
|
dicts: Record<string, any>,
|
||||||
|
key: string,
|
||||||
|
...args: any
|
||||||
|
) => string
|
||||||
|
|
||||||
|
let _nsDicts = {}
|
||||||
|
let _locale: string = 'en'
|
||||||
|
let _translate: TranslateFn = (
|
||||||
|
locale: string,
|
||||||
|
dicts: Record<string, any>,
|
||||||
|
key: string,
|
||||||
|
...args: any
|
||||||
|
) => {
|
||||||
|
return dicts[locale]?.[key] || args[0] || key
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setTranslate(t: TranslateFn) {
|
||||||
|
_translate = t
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setLocale(locale: string) {
|
||||||
|
_locale = locale
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setNSDicts(ns: string, dicts: Record<string, string>) {
|
||||||
|
(_nsDicts as any)[ns] = dicts
|
||||||
|
}
|
||||||
|
|
||||||
|
export const translate = (
|
||||||
|
ns: string,
|
||||||
|
key: string,
|
||||||
|
...args: any
|
||||||
|
) => {
|
||||||
|
const dicts = (_nsDicts as any)[ns] || {}
|
||||||
|
return _translate(
|
||||||
|
_nsDicts?.hasOwnProperty(_locale) ? _locale : 'en',
|
||||||
|
dicts, key, ...args)
|
||||||
|
}
|
||||||
@@ -93,10 +93,14 @@ import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
|
|||||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||||
import * as uniqolor from 'uniqolor'
|
import * as uniqolor from 'uniqolor'
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
|
import { setLocale, setTranslate } from './i18n'
|
||||||
|
import * as amplifyAuth from './amplify'
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
var LSUI: any
|
var LSUI: any
|
||||||
var LSUtils: any
|
var LSUtils: any
|
||||||
|
var LSI18N: any
|
||||||
|
var LSAuth: any
|
||||||
}
|
}
|
||||||
|
|
||||||
const shadui = {
|
const shadui = {
|
||||||
@@ -199,6 +203,13 @@ function setupGlobals() {
|
|||||||
isDev: process.env.NODE_ENV === 'development',
|
isDev: process.env.NODE_ENV === 'development',
|
||||||
uniqolor,
|
uniqolor,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.LSI18N = {
|
||||||
|
setTranslate,
|
||||||
|
setLocale,
|
||||||
|
}
|
||||||
|
|
||||||
|
window.LSAuth = amplifyAuth
|
||||||
}
|
}
|
||||||
|
|
||||||
// setup
|
// setup
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ html * {
|
|||||||
html[data-theme=light] {
|
html[data-theme=light] {
|
||||||
--accent: var(--rx-gray-12-hsl);
|
--accent: var(--rx-gray-12-hsl);
|
||||||
--accent-foreground: var(--rx-gray-02-hsl);
|
--accent-foreground: var(--rx-gray-02-hsl);
|
||||||
--input: var(--rx-gray-03-hsl);
|
--input: var(--rx-gray-05-hsl);
|
||||||
--secondary: 240 4.8% 95.9%;
|
--secondary: 240 4.8% 95.9%;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ html[data-theme=dark] {
|
|||||||
--muted: 0 0% 15%;
|
--muted: 0 0% 15%;
|
||||||
--popover: 0 0% 7%;
|
--popover: 0 0% 7%;
|
||||||
--popover-foreground: 0 0 95%;
|
--popover-foreground: 0 0 95%;
|
||||||
--input: 0 0% 25%;
|
--input: 0 0% 16%;
|
||||||
}
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
|
|||||||
@@ -49,7 +49,6 @@
|
|||||||
<script defer src="./js/react-dom.production.min.js"></script>
|
<script defer src="./js/react-dom.production.min.js"></script>
|
||||||
<script defer src="./js/ui.js"></script>
|
<script defer src="./js/ui.js"></script>
|
||||||
<script defer src="./js/main.js"></script>
|
<script defer src="./js/main.js"></script>
|
||||||
<script defer src="./js/amplify.js"></script>
|
|
||||||
<script defer src="./js/prop-types.min.js"></script>
|
<script defer src="./js/prop-types.min.js"></script>
|
||||||
<script defer src="./js/tabler-icons-react.min.js"></script>
|
<script defer src="./js/tabler-icons-react.min.js"></script>
|
||||||
<script defer src="./js/tabler.ext.js"></script>
|
<script defer src="./js/tabler.ext.js"></script>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
<html lang="en" data-color="logseq">
|
<html lang="en" data-color="logseq">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover, user-scalable=no">
|
||||||
<link href="./css/silkhq.css" rel="stylesheet" type="text/css">
|
<link href="./css/silkhq.css" rel="stylesheet" type="text/css">
|
||||||
<link href="./css/style.css" rel="stylesheet" type="text/css">
|
<link href="./css/style.css" rel="stylesheet" type="text/css">
|
||||||
<title>Logseq: A privacy-first platform for knowledge management and collaboration</title>
|
<title>Logseq: A privacy-first platform for knowledge management and collaboration</title>
|
||||||
@@ -23,7 +23,6 @@
|
|||||||
<script defer src="./js/tabler-icons-react.min.js"></script>
|
<script defer src="./js/tabler-icons-react.min.js"></script>
|
||||||
<script defer src="./js/tabler.ext.js"></script>
|
<script defer src="./js/tabler.ext.js"></script>
|
||||||
<script defer src="./js/ui.js"></script>
|
<script defer src="./js/ui.js"></script>
|
||||||
<script defer src="./js/amplify.js"></script>
|
|
||||||
<script defer src="./js/silkhq.js"></script>
|
<script defer src="./js/silkhq.js"></script>
|
||||||
<script defer src="./js/main.js"></script>
|
<script defer src="./js/main.js"></script>
|
||||||
<script defer src="./js/code-editor.js"></script>
|
<script defer src="./js/code-editor.js"></script>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
(ns frontend.components.theme
|
(ns frontend.components.theme
|
||||||
(:require [electron.ipc :as ipc]
|
(:require [clojure.string :as string]
|
||||||
|
[electron.ipc :as ipc]
|
||||||
[frontend.components.settings :as settings]
|
[frontend.components.settings :as settings]
|
||||||
[frontend.config :as config]
|
[frontend.config :as config]
|
||||||
[frontend.context.i18n :refer [t]]
|
[frontend.context.i18n :refer [t]]
|
||||||
@@ -68,7 +69,9 @@
|
|||||||
|
|
||||||
(hooks/use-effect!
|
(hooks/use-effect!
|
||||||
#(let [doc js/document.documentElement]
|
#(let [doc js/document.documentElement]
|
||||||
(.setAttribute doc "lang" preferred-language)))
|
(.setAttribute doc "lang" preferred-language)
|
||||||
|
(some-> preferred-language (string/lower-case) (js/LSI18N.setLocale)))
|
||||||
|
[preferred-language])
|
||||||
|
|
||||||
(hooks/use-effect!
|
(hooks/use-effect!
|
||||||
#(js/setTimeout
|
#(js/setTimeout
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
import {Amplify} from '@aws-amplify/core';
|
|
||||||
|
|
||||||
Amplify.configure({
|
|
||||||
Auth: {
|
|
||||||
// REQUIRED only for Federated Authentication - Amazon Cognito Identity Pool ID
|
|
||||||
// identityPoolId: 'XX-XXXX-X:XXXXXXXX-XXXX-1234-abcd-1234567890ab',
|
|
||||||
|
|
||||||
// REQUIRED - Amazon Cognito Region
|
|
||||||
region: 'us-east-1',
|
|
||||||
|
|
||||||
// OPTIONAL - Amazon Cognito Federated Identity Pool Region
|
|
||||||
// Required only if it's different from Amazon Cognito Region
|
|
||||||
// identityPoolRegion: 'XX-XXXX-X',
|
|
||||||
|
|
||||||
// OPTIONAL - Amazon Cognito User Pool ID
|
|
||||||
userPoolId: 'us-east-1_ldvDmC9Fe',
|
|
||||||
|
|
||||||
// OPTIONAL - Amazon Cognito Web Client ID (26-char alphanumeric string)
|
|
||||||
userPoolWebClientId: '41m82unjghlea984vjpk887qcr',
|
|
||||||
|
|
||||||
// OPTIONAL - Enforce user authentication prior to accessing AWS resources or not
|
|
||||||
// mandatorySignIn: false,
|
|
||||||
|
|
||||||
// OPTIONAL - This is used when autoSignIn is enabled for Auth.signUp
|
|
||||||
// 'code' is used for Auth.confirmSignUp, 'link' is used for email link verification
|
|
||||||
// signUpVerificationMethod: 'code', // 'code' | 'link'
|
|
||||||
|
|
||||||
// OPTIONAL - Configuration for cookie storage
|
|
||||||
// Note: if the secure flag is set to true, then the cookie transmission requires a secure protocol
|
|
||||||
cookieStorage: {
|
|
||||||
domain: "localhost",
|
|
||||||
path: "/",
|
|
||||||
expires: 365,
|
|
||||||
sameSite: "strict",
|
|
||||||
secure: true,
|
|
||||||
},
|
|
||||||
|
|
||||||
// OPTIONAL - customized storage object
|
|
||||||
// storage: MyStorage,
|
|
||||||
|
|
||||||
// OPTIONAL - Manually set the authentication flow type. Default is 'USER_SRP_AUTH'
|
|
||||||
authenticationFlowType: 'USER_SRP_AUTH',
|
|
||||||
|
|
||||||
//
|
|
||||||
// // OPTIONAL - Manually set key value pairs that can be passed to Cognito Lambda Triggers
|
|
||||||
// clientMetadata: {myCustomKey: 'myCustomValue'},
|
|
||||||
//
|
|
||||||
// // OPTIONAL - Hosted UI configuration
|
|
||||||
// oauth: {
|
|
||||||
// domain: 'your_cognito_domain',
|
|
||||||
// scope: ['phone', 'email', 'profile', 'openid', 'aws.cognito.signin.user.admin'],
|
|
||||||
// redirectSignIn: 'http://localhost:3000/',
|
|
||||||
// redirectSignOut: 'http://localhost:3000/',
|
|
||||||
// responseType: 'code' // or 'token', note that REFRESH token will only be generated when the responseType is code
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
(ns frontend.components.user.login
|
(ns frontend.components.user.login
|
||||||
(:require [cljs-bean.core :as bean]
|
(:require [cljs-bean.core :as bean]
|
||||||
[clojure.string :as string]
|
[clojure.string :as string]
|
||||||
[dommy.core :refer-macros [sel]]
|
[dommy.core :refer-macros [sel by-id]]
|
||||||
[frontend.config :as config]
|
[frontend.config :as config]
|
||||||
[frontend.handler.notification :as notification]
|
[frontend.handler.notification :as notification]
|
||||||
[frontend.handler.route :as route-handler]
|
[frontend.handler.route :as route-handler]
|
||||||
@@ -17,24 +17,24 @@
|
|||||||
|
|
||||||
(defn sign-out!
|
(defn sign-out!
|
||||||
[]
|
[]
|
||||||
(try (.signOut js/LSAmplify.Auth)
|
(try (.signOut js/LSAuth.Auth)
|
||||||
(catch :default e (js/console.warn e))))
|
(catch :default e (js/console.warn e))))
|
||||||
|
|
||||||
(defn- setup-configure!
|
(defn setup-configure!
|
||||||
[]
|
[]
|
||||||
#_:clj-kondo/ignore
|
#_:clj-kondo/ignore
|
||||||
(def setupAuthConfigure! (.-setupAuthConfigure js/LSAmplify))
|
(defn setupAuthConfigure! [config]
|
||||||
|
(.init js/LSAuth (bean/->js {:authCognito (merge config {:loginWith {:email true}})})))
|
||||||
#_:clj-kondo/ignore
|
#_:clj-kondo/ignore
|
||||||
(def LSAuthenticator
|
(def LSAuthenticator
|
||||||
(adapt-class (.-LSAuthenticator js/LSAmplify)))
|
(adapt-class (.-LSAuthenticator js/LSAuth)))
|
||||||
|
|
||||||
(.setLanguage js/LSAmplify.I18n (or (:preferred-language @state/state) "en"))
|
|
||||||
(setupAuthConfigure!
|
(setupAuthConfigure!
|
||||||
#js {:region config/REGION,
|
{:region config/REGION,
|
||||||
:userPoolId config/USER-POOL-ID,
|
:userPoolId config/USER-POOL-ID,
|
||||||
:userPoolWebClientId config/COGNITO-CLIENT-ID,
|
:userPoolClientId config/COGNITO-CLIENT-ID,
|
||||||
:identityPoolId config/IDENTITY-POOL-ID,
|
:identityPoolId config/IDENTITY-POOL-ID,
|
||||||
:oauthDomain config/OAUTH-DOMAIN}))
|
:oauthDomain config/OAUTH-DOMAIN}))
|
||||||
|
|
||||||
(rum/defc user-pane
|
(rum/defc user-pane
|
||||||
[_sign-out! user]
|
[_sign-out! user]
|
||||||
@@ -55,45 +55,24 @@
|
|||||||
|
|
||||||
(rum/defc page-impl
|
(rum/defc page-impl
|
||||||
[]
|
[]
|
||||||
(let [[ready?, set-ready?] (rum/use-state false)
|
(let [*ref-el (rum/use-ref nil)
|
||||||
[tab, set-tab!] (rum/use-state :login)
|
[tab set-tab!] (rum/use-state nil)]
|
||||||
*ref-el (rum/use-ref nil)]
|
|
||||||
|
|
||||||
(hooks/use-effect!
|
|
||||||
(fn [] (setup-configure!)
|
|
||||||
(set-ready? true)
|
|
||||||
(js/setTimeout
|
|
||||||
(fn []
|
|
||||||
(when-let [^js el (some-> (rum/deref *ref-el) (.querySelector ".amplify-tabs"))]
|
|
||||||
(let [btn1 (.querySelector el "button")]
|
|
||||||
(.addEventListener el "pointerdown"
|
|
||||||
(fn [^js e]
|
|
||||||
(if (= (.-target e) btn1)
|
|
||||||
(set-tab! :login)
|
|
||||||
(set-tab! :create-account)))))))))
|
|
||||||
[])
|
|
||||||
|
|
||||||
(hooks/use-effect!
|
|
||||||
(fn []
|
|
||||||
(when-let [^js el (rum/deref *ref-el)]
|
|
||||||
(js/setTimeout
|
|
||||||
#(some-> (.querySelector el (str "input[name=" (if (= tab :login) "username" "email") "]"))
|
|
||||||
(.focus)) 100)))
|
|
||||||
[tab])
|
|
||||||
|
|
||||||
[:div.cp__user-login
|
[:div.cp__user-login
|
||||||
{:ref *ref-el}
|
{:ref *ref-el
|
||||||
(when ready?
|
:id (str "user-auth-" tab)}
|
||||||
(LSAuthenticator
|
(LSAuthenticator
|
||||||
{:termsLink "https://blog.logseq.com/terms/"}
|
{:titleRender (fn [key title]
|
||||||
(fn [^js op]
|
(set-tab! key)
|
||||||
(let [sign-out!' (.-signOut op)
|
(shui/card-header
|
||||||
^js user-proxy (.-user op)
|
{:class "px-0"}
|
||||||
^js user (try (js/JSON.parse (js/JSON.stringify user-proxy))
|
(shui/card-title
|
||||||
(catch js/Error e
|
{:class "capitalize"}
|
||||||
(js/console.error "Error: Amplify user payload:" e)))
|
(string/replace title "-" " "))))
|
||||||
user' (bean/->clj user)]
|
:onSessionCallback #()}
|
||||||
(user-pane sign-out!' user')))))]))
|
(fn [^js op]
|
||||||
|
(let [sign-out!' (.-signOut op)
|
||||||
|
user' (bean/->clj (.-sessionUser op))]
|
||||||
|
(user-pane sign-out!' user'))))]))
|
||||||
|
|
||||||
(rum/defcs modal-inner <
|
(rum/defcs modal-inner <
|
||||||
shortcut/disable-all-shortcuts
|
shortcut/disable-all-shortcuts
|
||||||
@@ -109,7 +88,9 @@
|
|||||||
(shui/dialog-open!
|
(shui/dialog-open!
|
||||||
(fn [_close] (modal-inner))
|
(fn [_close] (modal-inner))
|
||||||
{:label "user-login"
|
{:label "user-login"
|
||||||
:content-props {:onPointerDownOutside #(let [inputs (sel "form[data-amplify-form] input:not([type=checkbox])")
|
:content-props {:onPointerDownOutside #(if (by-id "#user-auth-login")
|
||||||
inputs (some->> inputs (map (fn [^js e] (.-value e))) (remove string/blank?))]
|
(let [inputs (sel ".ls-authenticator-content form input:not([type=checkbox])")
|
||||||
(when (seq inputs)
|
inputs (some->> inputs (map (fn [^js e] (.-value e))) (remove string/blank?))]
|
||||||
(.preventDefault %)))}}))
|
(when (seq inputs)
|
||||||
|
(.preventDefault %)))
|
||||||
|
(.preventDefault %))}}))
|
||||||
|
|||||||
@@ -1,126 +1,38 @@
|
|||||||
.cp__user-login {
|
.cp__user-login {
|
||||||
[data-amplify-authenticator] [data-amplify-router] {
|
span.opacity-50, a.opacity-60 {
|
||||||
--amplify-components-authenticator-router-background-color: var(--ls-primary-background-color);
|
@apply opacity-80;
|
||||||
--amplify-components-field-label-color: var(--ls-primary-text-color);
|
|
||||||
--amplify-components-authenticator-router-border-color: var(--ls-border-color);
|
|
||||||
--amplify-components-tabs-item-color: var(--ls-primary-text-color);
|
|
||||||
--amplify-components-tabs-item-active-color: var(--ls-primary-text-color);
|
|
||||||
--amplify-components-tabs-item-hover-color: var(--ls-primary-text-color);
|
|
||||||
--amplify-components-tabs-item-active-border-color: var(--ls-tertiary-background-color);
|
|
||||||
--amplify-components-tabs-border-width: 0;
|
|
||||||
--amplify-components-authenticator-state-inactive-background-color: var(--ls-tertiary-background-color);
|
|
||||||
--amplify-components-tabs-item-active-background-color: var(--ls-primary-background-color);
|
|
||||||
--amplify-components-button-border-color: var(--ls-border-color);
|
|
||||||
--amplify-components-textfield-border-color: var(--ls-border-color);
|
|
||||||
--amplify-components-button-primary-background-color: var(--color-indigo-600);
|
|
||||||
--amplify-components-text-color: var(--ls-primary-text-color);
|
|
||||||
--amplify-components-button-hover-background-color: var(--ls-primary-background-color);
|
|
||||||
--amplify-components-button-border-width: 0;
|
|
||||||
--amplify-internal-button-loading-background-color: var(--ls-header-button-background);
|
|
||||||
--amplify-components-authenticator-router-border-width: 1px;
|
|
||||||
--amplify-components-button-color: var(--ls-primary-text-color);
|
|
||||||
--amplify-components-divider-label-background-color: var(--ls-primary-background-color);
|
|
||||||
--amplify-components-divider-label-color: var(--ls-primary-text-color);
|
|
||||||
--amplify-components-heading-color: var(--ls-primary-text-color);
|
|
||||||
--amplify-components-button-link-hover-background-color: transparent;
|
|
||||||
--amplify-components-button-link-active-background-color: transparent;
|
|
||||||
--amplify-components-textfield-color: var(--ls-primary-text-color);
|
|
||||||
--amplify-components-checkbox-icon-background-color: var(--color-indigo-600);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-amplify-authenticator] [data-amplify-router] {
|
p {
|
||||||
@apply overflow-hidden rounded-[6px] shadow-2xl;
|
@apply text-[inherit];
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-amplify-authenticator] [data-amplify-container] {
|
.ui__alert {
|
||||||
place-self: unset;
|
@apply bg-red-300 dark:border-red-800 dark:bg-red-800/90 dark:text-red-200;
|
||||||
}
|
|
||||||
|
|
||||||
[data-amplify-authenticator] [data-amplify-form] {
|
svg {
|
||||||
@apply px-4 py-2;
|
@apply dark:text-red-200;
|
||||||
|
}
|
||||||
|
|
||||||
@screen sm {
|
&-description {
|
||||||
@apply px-6 py-4;
|
@apply -mb-3;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 30rem) {
|
|
||||||
[data-amplify-authenticator] [data-amplify-container] {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui__dialog-content[label=user-login] {
|
.ui__dialog-content[label=user-login] {
|
||||||
@apply flex items-center top-0 p-0 border-none w-auto;
|
@apply flex items-center top-0 px-6 pt-0 w-auto;
|
||||||
|
|
||||||
|
.ui__card-header {
|
||||||
|
@apply pb-7;
|
||||||
|
}
|
||||||
|
|
||||||
.ui__dialog-main-content {
|
.ui__dialog-main-content {
|
||||||
@apply p-0 min-w-fit relative max-w-[600px] sm:max-w-[90vw] sm:w-[500px];
|
@apply p-0 w-[70vw] relative max-w-[500px] sm:w-[440px];
|
||||||
}
|
}
|
||||||
|
|
||||||
.ui__modal-close-wrap {
|
.ui__modal-close-wrap {
|
||||||
@apply z-10 top-[4px];
|
@apply z-10 top-[4px];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cp__user {
|
|
||||||
&-login {
|
|
||||||
::placeholder {
|
|
||||||
color: var(--ls-primary-text-color);
|
|
||||||
opacity: .3;
|
|
||||||
}
|
|
||||||
|
|
||||||
[data-indicator-position=top] > .amplify-tabs-item {
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.amplify-tabs-item {
|
|
||||||
transition: none;
|
|
||||||
|
|
||||||
&:focus {
|
|
||||||
color: var(--ls-primary-text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: .9;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.amplify-field-group {
|
|
||||||
@apply relative;
|
|
||||||
|
|
||||||
.amplify-button {
|
|
||||||
color: var(--ls-primary-text-color);
|
|
||||||
|
|
||||||
&:active, &:hover, &:focus {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.amplify-field-group__outer-end {
|
|
||||||
@apply absolute right-0 top-0 bottom-0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.amplify-input {
|
|
||||||
border-radius: 4px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.amplify-checkboxfield {
|
|
||||||
@apply text-sm;
|
|
||||||
|
|
||||||
.amplify-field__error-message {
|
|
||||||
color: var(--ls-primary-text-color);
|
|
||||||
opacity: .4;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.amplify-text--error {
|
|
||||||
color: var(--ls-error-text-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.federated-sign-in-container {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
[frontend.components.content :as cp-content]
|
[frontend.components.content :as cp-content]
|
||||||
[frontend.components.editor :as editor]
|
[frontend.components.editor :as editor]
|
||||||
[frontend.components.page :as page]
|
[frontend.components.page :as page]
|
||||||
|
[frontend.components.user.login :as user.login]
|
||||||
[frontend.components.reference :as reference]
|
[frontend.components.reference :as reference]
|
||||||
[frontend.components.whiteboard :as whiteboard]
|
[frontend.components.whiteboard :as whiteboard]
|
||||||
[frontend.config :as config]
|
[frontend.config :as config]
|
||||||
@@ -143,6 +144,7 @@
|
|||||||
|
|
||||||
(register-components-fns!)
|
(register-components-fns!)
|
||||||
(user-handler/restore-tokens-from-localstorage)
|
(user-handler/restore-tokens-from-localstorage)
|
||||||
|
(user.login/setup-configure!)
|
||||||
(state/set-db-restoring! true)
|
(state/set-db-restoring! true)
|
||||||
(when (util/electron?)
|
(when (util/electron?)
|
||||||
(el/listen!))
|
(el/listen!))
|
||||||
|
|||||||
@@ -108,6 +108,20 @@
|
|||||||
(when (string/starts-with? key prefix)
|
(when (string/starts-with? key prefix)
|
||||||
(js/localStorage.removeItem key)))))
|
(js/localStorage.removeItem key)))))
|
||||||
|
|
||||||
|
(defn auto-fill-refresh-token-from-cognito!
|
||||||
|
[]
|
||||||
|
(let [prefix "CognitoIdentityServiceProvider."
|
||||||
|
refresh-token-key (some #(when (string/starts-with? % prefix)
|
||||||
|
(when (string/ends-with? % "refreshToken")
|
||||||
|
%))
|
||||||
|
(js/Object.keys js/localStorage))]
|
||||||
|
(when refresh-token-key
|
||||||
|
(let [refresh-token (js/localStorage.getItem refresh-token-key)]
|
||||||
|
(when (and refresh-token (not= refresh-token "undefined"))
|
||||||
|
(state/set-auth-refresh-token refresh-token)
|
||||||
|
(js/localStorage.setItem "refresh-token" refresh-token)))))
|
||||||
|
)
|
||||||
|
|
||||||
(defn- clear-tokens
|
(defn- clear-tokens
|
||||||
([]
|
([]
|
||||||
(state/set-auth-id-token nil)
|
(state/set-auth-id-token nil)
|
||||||
@@ -206,6 +220,7 @@
|
|||||||
(:jwtToken (:idToken session))
|
(:jwtToken (:idToken session))
|
||||||
(:jwtToken (:accessToken session))
|
(:jwtToken (:accessToken session))
|
||||||
(:token (:refreshToken session)))
|
(:token (:refreshToken session)))
|
||||||
|
(auto-fill-refresh-token-from-cognito!)
|
||||||
(state/pub-event! [:user/fetch-info-and-graphs]))
|
(state/pub-event! [:user/fetch-info-and-graphs]))
|
||||||
|
|
||||||
(defn ^:export login-with-username-password-e2e
|
(defn ^:export login-with-username-password-e2e
|
||||||
|
|||||||
@@ -2,13 +2,10 @@
|
|||||||
@apply fixed bottom-[100px] h-[70px] p-1.5 rounded-md overflow-y-hidden overflow-x-auto
|
@apply fixed bottom-[100px] h-[70px] p-1.5 rounded-md overflow-y-hidden overflow-x-auto
|
||||||
bg-[var(--ls-secondary-background-color)] z-[99999];
|
bg-[var(--ls-secondary-background-color)] z-[99999];
|
||||||
|
|
||||||
box-shadow:
|
box-shadow: /* bottom = shadow-lg */ 0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||||
/* bottom = shadow-lg */
|
0 4px 6px -4px rgba(0, 0, 0, 0.1),
|
||||||
0 10px 15px -3px rgba(0,0,0,0.1),
|
/* top = lighter (closer to shadow-md) */ 0 -6px 10px -4px rgba(0, 0, 0, 0.08),
|
||||||
0 4px 6px -4px rgba(0,0,0,0.1),
|
0 -2px 4px -4px rgba(0, 0, 0, 0.08);
|
||||||
/* top = lighter (closer to shadow-md) */
|
|
||||||
0 -6px 10px -4px rgba(0,0,0,0.08),
|
|
||||||
0 -2px 4px -4px rgba(0,0,0,0.08);
|
|
||||||
|
|
||||||
.action-bar-commands {
|
.action-bar-commands {
|
||||||
@apply relative flex w-full;
|
@apply relative flex w-full;
|
||||||
@@ -144,3 +141,21 @@ html.is-zoomed-native-ios {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html.has-mobile-keyboard {
|
||||||
|
.ui__dialog-overlay {
|
||||||
|
&:has(.app-login-modal) {
|
||||||
|
@apply overflow-y-auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui__dialog-content {
|
||||||
|
&.app-login-modal {
|
||||||
|
margin-bottom: 460px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ui__dialog-content[label=user-login] {
|
||||||
|
@apply rounded-lg pb-3;
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
:class "text-1xl flex flex-1 w-full my-8"
|
:class "text-1xl flex flex-1 w-full my-8"
|
||||||
:on-click #(shui/dialog-open! login/page-impl
|
:on-click #(shui/dialog-open! login/page-impl
|
||||||
{:close-btn? false
|
{:close-btn? false
|
||||||
|
:label "user-login"
|
||||||
:align :top
|
:align :top
|
||||||
:content-props {:class "app-login-modal"}})}
|
:content-props {:class "app-login-modal"}})}
|
||||||
"Login")
|
"Login")
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
@import "inter-ui/inter.css";
|
@import "inter-ui/inter.css";
|
||||||
@import "photoswipe/dist/photoswipe.css";
|
@import "photoswipe/dist/photoswipe.css";
|
||||||
@import "shepherd.js/dist/css/shepherd.css";
|
@import "shepherd.js/dist/css/shepherd.css";
|
||||||
@import "packages/amplify/dist/amplify.css";
|
|
||||||
@import "packages/tldraw/apps/tldraw-logseq/src/styles.css";
|
@import "packages/tldraw/apps/tldraw-logseq/src/styles.css";
|
||||||
@import "katex/dist/katex.min.css";
|
@import "katex/dist/katex.min.css";
|
||||||
@import "codemirror/lib/codemirror.css";
|
@import "codemirror/lib/codemirror.css";
|
||||||
|
|||||||
@@ -129,7 +129,8 @@ module.exports = {
|
|||||||
'./resources/**/*.html',
|
'./resources/**/*.html',
|
||||||
'./deps/shui/src/**/*.cljs',
|
'./deps/shui/src/**/*.cljs',
|
||||||
'./deps/shui/src/**/*.cljc',
|
'./deps/shui/src/**/*.cljc',
|
||||||
'./packages/ui/@/components/**/*.{ts,tsx}'
|
'./packages/ui/@/components/**/*.{ts,tsx}',
|
||||||
|
'./packages/ui/src/amplify/**/*.{ts,tsx}'
|
||||||
],
|
],
|
||||||
safelist: [
|
safelist: [
|
||||||
'bg-black', 'bg-white', 'capitalize-first',
|
'bg-black', 'bg-white', 'capitalize-first',
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
@import "inter-ui/inter.css";
|
@import "inter-ui/inter.css";
|
||||||
|
|
||||||
@import "photoswipe/dist/photoswipe.css";
|
@import "photoswipe/dist/photoswipe.css";
|
||||||
@import "packages/amplify/dist/amplify.css";
|
|
||||||
@import "katex/dist/katex.min.css";
|
@import "katex/dist/katex.min.css";
|
||||||
@import "codemirror/lib/codemirror.css";
|
@import "codemirror/lib/codemirror.css";
|
||||||
@import "codemirror/theme/solarized.css";
|
@import "codemirror/theme/solarized.css";
|
||||||
|
|||||||
Reference in New Issue
Block a user