2023-03-24 16:33:14 +01:00
|
|
|
// Dependencies
|
2023-04-28 21:23:06 +02:00
|
|
|
import { useState, useEffect, useContext } from 'react'
|
2023-02-19 20:49:15 +01:00
|
|
|
import { useTranslation } from 'next-i18next'
|
|
|
|
import { DateTime } from 'luxon'
|
|
|
|
import { CopyToClipboard } from 'react-copy-to-clipboard'
|
2023-04-28 21:23:06 +02:00
|
|
|
// Context
|
|
|
|
import { LoadingContext } from 'shared/context/loading-context.mjs'
|
|
|
|
import { ModalContext } from 'shared/context/modal-context.mjs'
|
2023-03-24 16:33:14 +01:00
|
|
|
// Hooks
|
|
|
|
import { useAccount } from 'shared/hooks/use-account.mjs'
|
|
|
|
import { useBackend } from 'shared/hooks/use-backend.mjs'
|
|
|
|
import { useToast } from 'shared/hooks/use-toast.mjs'
|
2023-04-22 15:01:57 +02:00
|
|
|
import { useRouter } from 'next/router'
|
2023-02-19 20:49:15 +01:00
|
|
|
// Components
|
|
|
|
import { BackToAccountButton, Choice } from './shared.mjs'
|
|
|
|
import { Popout } from 'shared/components/popout.mjs'
|
|
|
|
import { WebLink } from 'shared/components/web-link.mjs'
|
|
|
|
import { CopyIcon } from 'shared/components/icons.mjs'
|
2023-04-30 17:50:15 +02:00
|
|
|
import { Collapse, useCollapseButton } from 'shared/components/collapse.mjs'
|
2023-02-25 18:19:11 +01:00
|
|
|
import { TrashIcon } from 'shared/components/icons.mjs'
|
2023-02-25 18:53:20 +01:00
|
|
|
import { LeftIcon } from 'shared/components/icons.mjs'
|
2023-04-16 10:45:36 +02:00
|
|
|
import { ModalWrapper } from 'shared/components/wrappers/modal.mjs'
|
2023-02-19 20:49:15 +01:00
|
|
|
|
|
|
|
export const ns = ['account', 'toast']
|
|
|
|
|
|
|
|
const ExpiryPicker = ({ t, expires, setExpires }) => {
|
2023-02-25 18:19:11 +01:00
|
|
|
const [months, setMonths] = useState(1)
|
2023-02-19 20:49:15 +01:00
|
|
|
|
|
|
|
// Run update when component mounts
|
2023-02-25 18:19:11 +01:00
|
|
|
useEffect(() => update(months), [])
|
2023-02-19 20:49:15 +01:00
|
|
|
|
|
|
|
const update = (evt) => {
|
|
|
|
const value = typeof evt === 'number' ? evt : evt.target.value
|
2023-02-25 18:19:11 +01:00
|
|
|
setExpires(DateTime.now().plus({ months: value }))
|
|
|
|
setMonths(value)
|
2023-02-19 20:49:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<div className="flex flex-row gap-2 items-center">
|
|
|
|
<input
|
|
|
|
type="range"
|
|
|
|
min="0"
|
2023-02-25 18:19:11 +01:00
|
|
|
max={24}
|
|
|
|
value={months}
|
2023-02-19 20:49:15 +01:00
|
|
|
className="range range-secondary"
|
|
|
|
onChange={update}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
<Popout note compact>
|
|
|
|
{t('keyExpiresDesc')}
|
|
|
|
<b> {expires.toHTTP()}</b>
|
|
|
|
</Popout>
|
|
|
|
</>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
const CopyInput = ({ text }) => {
|
|
|
|
const { t } = useTranslation(['toast'])
|
|
|
|
const toast = useToast()
|
|
|
|
|
|
|
|
const [copied, setCopied] = useState(false)
|
|
|
|
|
|
|
|
const showCopied = () => {
|
|
|
|
setCopied(true)
|
|
|
|
toast.success(<span>{t('copiedToClipboard')}</span>)
|
|
|
|
window.setTimeout(() => setCopied(false), 3000)
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="flex flez-row gap-2 items-center w-full">
|
2023-02-25 18:19:11 +01:00
|
|
|
<input
|
|
|
|
readOnly
|
|
|
|
value={text}
|
|
|
|
className="input w-full input-bordered flex flex-row"
|
|
|
|
type="text"
|
|
|
|
/>
|
2023-02-19 20:49:15 +01:00
|
|
|
<CopyToClipboard text={text} onCopy={showCopied}>
|
|
|
|
<button className={`btn ${copied ? 'btn-success' : 'btn-secondary'}`}>
|
|
|
|
<CopyIcon />
|
|
|
|
</button>
|
|
|
|
</CopyToClipboard>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
const Row = ({ title, children }) => (
|
|
|
|
<div className="flex flex-row flex-wrap items-center lg:gap-4 my-2">
|
2023-02-25 18:53:20 +01:00
|
|
|
<div className="w-24 text-left md:text-right block md:inline font-bold pr-4">{title}</div>
|
2023-02-19 20:49:15 +01:00
|
|
|
<div className="grow">{children}</div>
|
|
|
|
</div>
|
|
|
|
)
|
2023-04-30 20:31:28 +02:00
|
|
|
const ShowKey = ({ apikey, t, clear, standalone }) => {
|
|
|
|
const router = useRouter()
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
<Popout warning compact>
|
|
|
|
{t('keySecretWarning')}
|
|
|
|
</Popout>
|
|
|
|
<Row title={t('keyName')}>{apikey.name}</Row>
|
|
|
|
<Row title={t('created')}>{DateTime.fromISO(apikey.createdAt).toHTTP()}</Row>
|
|
|
|
<Row title={t('expires')}>{DateTime.fromISO(apikey.expiresAt).toHTTP()}</Row>
|
|
|
|
<Row title="Key ID">
|
|
|
|
<CopyInput text={apikey.key} />
|
|
|
|
</Row>
|
|
|
|
<Row title="Key Secret">
|
|
|
|
<CopyInput text={apikey.secret} />
|
|
|
|
</Row>
|
|
|
|
<button
|
|
|
|
className="btn btn-secondary mt-8 pr-6 flex flex-row items-center gap-2"
|
|
|
|
onClick={standalone ? () => router.push('/account/apikeys') : clear}
|
|
|
|
>
|
|
|
|
<LeftIcon />
|
|
|
|
{t('apikeys')}
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|
2023-02-19 20:49:15 +01:00
|
|
|
|
2023-04-28 21:23:06 +02:00
|
|
|
const NewKey = ({
|
|
|
|
t,
|
|
|
|
account,
|
|
|
|
setGenerate,
|
|
|
|
keyAdded,
|
|
|
|
backend,
|
|
|
|
toast,
|
|
|
|
startLoading,
|
|
|
|
stopLoading,
|
2023-04-30 17:50:15 +02:00
|
|
|
closeCollapseButton,
|
2023-04-30 20:31:28 +02:00
|
|
|
standalone = false,
|
2023-04-30 17:50:15 +02:00
|
|
|
title = true,
|
2023-04-28 21:23:06 +02:00
|
|
|
}) => {
|
2023-02-19 20:49:15 +01:00
|
|
|
const [name, setName] = useState('')
|
|
|
|
const [level, setLevel] = useState(1)
|
|
|
|
const [expires, setExpires] = useState(DateTime.now())
|
|
|
|
const [apikey, setApikey] = useState(false)
|
|
|
|
|
2023-03-24 16:33:14 +01:00
|
|
|
const levels = account.role === 'admin' ? [0, 1, 2, 3, 4, 5, 6, 7, 8] : [0, 1, 2, 3, 4]
|
2023-02-19 20:49:15 +01:00
|
|
|
|
|
|
|
const createKey = async () => {
|
2023-04-28 21:23:06 +02:00
|
|
|
startLoading()
|
2023-02-19 20:49:15 +01:00
|
|
|
const result = await backend.createApikey({
|
|
|
|
name,
|
|
|
|
level,
|
|
|
|
expiresIn: Math.floor((expires.valueOf() - DateTime.now().valueOf()) / 1000),
|
|
|
|
})
|
2023-04-22 15:01:57 +02:00
|
|
|
if (result.success) {
|
2023-02-19 20:49:15 +01:00
|
|
|
toast.success(<span>{t('nailedIt')}</span>)
|
2023-04-22 15:01:57 +02:00
|
|
|
setApikey(result.data.apikey)
|
2023-02-25 18:19:11 +01:00
|
|
|
keyAdded()
|
2023-02-19 20:49:15 +01:00
|
|
|
} else toast.for.backendError()
|
2023-04-28 21:23:06 +02:00
|
|
|
stopLoading()
|
2023-04-30 20:31:28 +02:00
|
|
|
if (closeCollapseButton) closeCollapseButton()
|
2023-02-19 20:49:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const clear = () => {
|
|
|
|
setApikey(false)
|
|
|
|
setGenerate(false)
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div>
|
2023-04-30 17:50:15 +02:00
|
|
|
{title ? <h2>{t('newApikey')}</h2> : null}
|
2023-02-19 20:49:15 +01:00
|
|
|
{apikey ? (
|
|
|
|
<>
|
2023-04-30 20:31:28 +02:00
|
|
|
<ShowKey {...{ apikey, t, clear, standalone }} />
|
2023-02-19 20:49:15 +01:00
|
|
|
</>
|
|
|
|
) : (
|
|
|
|
<>
|
2023-04-30 17:50:15 +02:00
|
|
|
<h5>{t('keyName')}</h5>
|
2023-02-19 20:49:15 +01:00
|
|
|
<p>{t('keyNameDesc')}</p>
|
|
|
|
<input
|
|
|
|
value={name}
|
|
|
|
onChange={(evt) => setName(evt.target.value)}
|
|
|
|
className="input w-full input-bordered flex flex-row"
|
|
|
|
type="text"
|
|
|
|
placeholder={'Alicia key'}
|
|
|
|
/>
|
2023-04-30 17:50:15 +02:00
|
|
|
<h5 className="mt-4">{t('keyExpires')}</h5>
|
2023-02-19 20:49:15 +01:00
|
|
|
<ExpiryPicker {...{ t, expires, setExpires }} />
|
2023-04-30 17:50:15 +02:00
|
|
|
<h5 className="mt-4">{t('keyLevel')}</h5>
|
2023-02-19 20:49:15 +01:00
|
|
|
{levels.map((l) => (
|
|
|
|
<Choice val={l} t={t} update={setLevel} current={level} key={l}>
|
|
|
|
<span className="block text-lg leading-5">{t(`keyLevel${l}`)}</span>
|
|
|
|
</Choice>
|
|
|
|
))}
|
|
|
|
<div className="flex flex-row gap-2 items-center w-full my-8">
|
|
|
|
<button
|
|
|
|
className="btn btn-primary grow capitalize"
|
|
|
|
disabled={name.length < 1}
|
|
|
|
onClick={createKey}
|
|
|
|
>
|
|
|
|
{t('newApikey')}
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-04-30 17:50:15 +02:00
|
|
|
const Apikey = ({ apikey, t, account, backend, keyAdded, startLoading, stopLoading }) => {
|
2023-04-28 21:23:06 +02:00
|
|
|
const { setModal } = useContext(ModalContext)
|
2023-02-25 18:19:11 +01:00
|
|
|
const toast = useToast()
|
|
|
|
|
|
|
|
const fields = {
|
|
|
|
id: 'ID',
|
|
|
|
name: t('keyName'),
|
|
|
|
level: t('keyLevel'),
|
|
|
|
expiresAt: t('expires'),
|
|
|
|
createdAt: t('created'),
|
|
|
|
}
|
|
|
|
|
|
|
|
const expired = DateTime.fromISO(apikey.expiresAt).valueOf() < DateTime.now().valueOf()
|
|
|
|
|
|
|
|
const remove = async () => {
|
2023-04-28 21:23:06 +02:00
|
|
|
startLoading()
|
2023-02-25 18:19:11 +01:00
|
|
|
const result = await backend.removeApikey(apikey.id)
|
|
|
|
if (result) toast.success(t('gone'))
|
|
|
|
else toast.for.backendError()
|
|
|
|
// This just forces a refresh of the list from the server
|
|
|
|
// We obviously did not add a key here, but rather removed one
|
|
|
|
keyAdded()
|
2023-04-28 21:23:06 +02:00
|
|
|
stopLoading()
|
2023-02-25 18:19:11 +01:00
|
|
|
}
|
|
|
|
|
2023-02-25 18:53:20 +01:00
|
|
|
const removeModal = () => {
|
2023-04-28 21:23:06 +02:00
|
|
|
setModal(
|
|
|
|
<ModalWrapper slideFrom="top">
|
2023-02-25 18:53:20 +01:00
|
|
|
<h2>{t('areYouCertain')}</h2>
|
|
|
|
<p>{t('deleteKeyWarning')}</p>
|
|
|
|
<p className="flex flex-row gap-4 items-center justify-center">
|
|
|
|
<button className="btn btn-neutral btn-outline px-8">{t('cancel')}</button>
|
|
|
|
<button className="btn btn-error px-8" onClick={remove}>
|
|
|
|
{t('delete')}
|
|
|
|
</button>
|
|
|
|
</p>
|
2023-04-16 10:45:36 +02:00
|
|
|
</ModalWrapper>
|
2023-02-25 18:53:20 +01:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-02-25 18:19:11 +01:00
|
|
|
const title = (
|
|
|
|
<div className="flex flex-row gap-2 items-center inline-block justify-around w-full">
|
|
|
|
<span>{apikey.name}</span>
|
|
|
|
<span className="font-normal">
|
|
|
|
{t('expires')}: <b>{DateTime.fromISO(apikey.expiresAt).toLocaleString()}</b>
|
|
|
|
</span>
|
|
|
|
<span className="opacity-50">|</span>
|
|
|
|
<span className="font-normal">
|
|
|
|
{t('keyLevel')}: <b>{apikey.level}</b>
|
|
|
|
</span>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Collapse
|
2023-04-30 17:50:15 +02:00
|
|
|
title={title}
|
|
|
|
openTitle={apikey.name}
|
|
|
|
primary
|
2023-02-25 18:19:11 +01:00
|
|
|
valid={!expired}
|
|
|
|
buttons={[
|
|
|
|
<button
|
2023-04-30 17:50:15 +02:00
|
|
|
key="rm"
|
|
|
|
className="btn btn-error hover:text-error-content border-0"
|
2023-03-24 16:33:14 +01:00
|
|
|
onClick={account.control > 4 ? remove : removeModal}
|
2023-02-25 18:19:11 +01:00
|
|
|
>
|
2023-02-25 20:06:00 +01:00
|
|
|
<TrashIcon key="button2" />
|
2023-02-25 18:19:11 +01:00
|
|
|
</button>,
|
|
|
|
]}
|
|
|
|
>
|
|
|
|
{expired ? (
|
|
|
|
<Popout warning compact>
|
|
|
|
<b>{t('keyExpired')}</b>
|
|
|
|
</Popout>
|
|
|
|
) : null}
|
2023-02-25 18:53:20 +01:00
|
|
|
{Object.entries(fields).map(([key, title]) => (
|
2023-02-25 20:06:00 +01:00
|
|
|
<Row title={title} key={key}>
|
|
|
|
{apikey[key]}
|
|
|
|
</Row>
|
2023-02-25 18:53:20 +01:00
|
|
|
))}
|
2023-02-25 18:19:11 +01:00
|
|
|
</Collapse>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-04-22 15:01:57 +02:00
|
|
|
// Component for the 'new/apikey' page
|
2023-04-30 20:31:28 +02:00
|
|
|
export const NewApikey = ({ standalone = false }) => {
|
2023-04-28 21:23:06 +02:00
|
|
|
// Context
|
|
|
|
const { startLoading, stopLoading } = useContext(LoadingContext)
|
|
|
|
|
|
|
|
// Hooks
|
2023-04-22 15:01:57 +02:00
|
|
|
const { account, token } = useAccount()
|
|
|
|
const backend = useBackend(token)
|
|
|
|
const { t } = useTranslation(ns)
|
|
|
|
const toast = useToast()
|
|
|
|
|
2023-04-28 21:23:06 +02:00
|
|
|
// State
|
2023-04-22 15:01:57 +02:00
|
|
|
const [keys, setKeys] = useState([])
|
|
|
|
const [generate, setGenerate] = useState(false)
|
|
|
|
const [added, setAdded] = useState(0)
|
|
|
|
|
2023-04-28 21:23:06 +02:00
|
|
|
// Helper method to force refresh
|
2023-04-22 15:01:57 +02:00
|
|
|
const keyAdded = () => setAdded(added + 1)
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className="max-w-xl xl:pl-4">
|
2023-04-28 21:23:06 +02:00
|
|
|
<NewKey
|
|
|
|
{...{
|
|
|
|
t,
|
|
|
|
account,
|
|
|
|
setGenerate,
|
|
|
|
backend,
|
|
|
|
toast,
|
|
|
|
keyAdded,
|
2023-04-30 20:31:28 +02:00
|
|
|
standalone,
|
2023-04-28 21:23:06 +02:00
|
|
|
startLoading,
|
|
|
|
stopLoading,
|
|
|
|
}}
|
|
|
|
/>
|
2023-04-22 15:01:57 +02:00
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Component for the account/apikeys page
|
2023-04-28 21:23:06 +02:00
|
|
|
export const Apikeys = () => {
|
|
|
|
// Context
|
|
|
|
const { startLoading, stopLoading, loading } = useContext(LoadingContext)
|
|
|
|
|
|
|
|
// Hooks
|
2023-04-16 17:13:18 +02:00
|
|
|
const { account, token } = useAccount()
|
2023-03-24 16:33:14 +01:00
|
|
|
const backend = useBackend(token)
|
2023-02-19 20:49:15 +01:00
|
|
|
const { t } = useTranslation(ns)
|
|
|
|
const toast = useToast()
|
2023-04-30 17:50:15 +02:00
|
|
|
const { CollapseButton, closeCollapseButton } = useCollapseButton()
|
2023-02-19 20:49:15 +01:00
|
|
|
|
2023-04-28 21:23:06 +02:00
|
|
|
// State
|
2023-02-25 18:19:11 +01:00
|
|
|
const [keys, setKeys] = useState([])
|
2023-02-19 20:49:15 +01:00
|
|
|
const [generate, setGenerate] = useState(false)
|
2023-02-25 18:19:11 +01:00
|
|
|
const [added, setAdded] = useState(0)
|
2023-02-19 20:49:15 +01:00
|
|
|
|
2023-04-28 21:23:06 +02:00
|
|
|
// Effects
|
2023-02-25 18:19:11 +01:00
|
|
|
useEffect(() => {
|
|
|
|
const getApikeys = async () => {
|
2023-03-27 18:49:09 +02:00
|
|
|
const result = await backend.getApikeys()
|
|
|
|
if (result.success) setKeys(result.data.apikeys)
|
2023-02-25 18:19:11 +01:00
|
|
|
}
|
|
|
|
getApikeys()
|
|
|
|
}, [added])
|
|
|
|
|
2023-04-28 21:23:06 +02:00
|
|
|
// Helper method to force refresh
|
2023-02-25 18:19:11 +01:00
|
|
|
const keyAdded = () => setAdded(added + 1)
|
2023-02-19 20:49:15 +01:00
|
|
|
|
|
|
|
return (
|
2023-03-27 19:07:48 +02:00
|
|
|
<div className="max-w-xl xl:pl-4">
|
2023-02-25 18:19:11 +01:00
|
|
|
{generate ? (
|
2023-04-28 21:23:06 +02:00
|
|
|
<NewKey
|
|
|
|
{...{
|
|
|
|
t,
|
|
|
|
account,
|
|
|
|
setGenerate,
|
|
|
|
backend,
|
|
|
|
toast,
|
|
|
|
keyAdded,
|
|
|
|
startLoading,
|
|
|
|
stopLoading,
|
|
|
|
}}
|
|
|
|
/>
|
2023-02-25 18:19:11 +01:00
|
|
|
) : (
|
|
|
|
<>
|
|
|
|
<h2>{t('apikeys')}</h2>
|
|
|
|
{keys.map((apikey) => (
|
2023-04-30 17:50:15 +02:00
|
|
|
<Apikey
|
|
|
|
{...{ account, apikey, t, backend, keyAdded, startLoading, stopLoading }}
|
|
|
|
key={apikey.id}
|
|
|
|
/>
|
2023-02-25 18:19:11 +01:00
|
|
|
))}
|
2023-04-30 17:50:15 +02:00
|
|
|
<CollapseButton
|
|
|
|
title={t('newApikey')}
|
2023-03-27 19:07:48 +02:00
|
|
|
className="btn btn-primary w-full capitalize mt-4"
|
2023-04-30 17:50:15 +02:00
|
|
|
bottom
|
|
|
|
primary
|
2023-03-27 19:07:48 +02:00
|
|
|
>
|
2023-04-30 17:50:15 +02:00
|
|
|
<NewKey
|
|
|
|
title={false}
|
|
|
|
{...{
|
|
|
|
t,
|
|
|
|
account,
|
|
|
|
setGenerate,
|
|
|
|
backend,
|
|
|
|
toast,
|
|
|
|
keyAdded,
|
|
|
|
startLoading,
|
|
|
|
stopLoading,
|
|
|
|
closeCollapseButton,
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</CollapseButton>
|
2023-04-28 21:23:06 +02:00
|
|
|
<BackToAccountButton loading={loading} />
|
2023-03-24 16:33:14 +01:00
|
|
|
{account.control < 5 ? (
|
2023-02-25 20:06:00 +01:00
|
|
|
<Popout tip>
|
|
|
|
<h5>Refer to FreeSewing.dev for details (English only)</h5>
|
|
|
|
<p>
|
|
|
|
This is an advanced feature aimed at developers or anyone who wants to interact with
|
|
|
|
our backend directly. For details, please refer to{' '}
|
|
|
|
<WebLink
|
|
|
|
href="https://freesewing.dev/reference/backend/api/apikeys"
|
|
|
|
txt="the API keys reference documentation"
|
|
|
|
/>{' '}
|
2023-02-26 14:19:43 +01:00
|
|
|
on <WebLink href="https://freesewing.dev/" txt="FreeSewing.dev" />, our site for
|
2023-02-25 20:06:00 +01:00
|
|
|
developers and contributors.
|
|
|
|
</p>
|
|
|
|
</Popout>
|
|
|
|
) : null}
|
2023-02-25 18:19:11 +01:00
|
|
|
</>
|
|
|
|
)}
|
|
|
|
</div>
|
2023-02-19 20:49:15 +01:00
|
|
|
)
|
|
|
|
}
|