2023-03-24 16:33:14 +01:00
|
|
|
// Dependencies
|
2023-09-04 12:18:52 +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-08-21 16:14:58 +02:00
|
|
|
import { shortDate, formatNumber } from 'shared/utils.mjs'
|
2023-09-04 08:40:05 +02:00
|
|
|
// Context
|
|
|
|
import { LoadingStatusContext } from 'shared/context/loading-status-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'
|
2023-04-22 15:01:57 +02:00
|
|
|
import { useRouter } from 'next/router'
|
2023-02-19 20:49:15 +01:00
|
|
|
// Components
|
2023-08-24 18:39:12 +02:00
|
|
|
import { BackToAccountButton, DisplayRow, NumberBullet } from './shared.mjs'
|
2023-07-23 18:42:06 +02:00
|
|
|
import { Popout } from 'shared/components/popout/index.mjs'
|
2023-08-21 17:58:08 +02:00
|
|
|
import { LeftIcon, PlusIcon, CopyIcon, RightIcon, TrashIcon } from 'shared/components/icons.mjs'
|
2023-08-24 18:39:12 +02:00
|
|
|
import { PageLink, Link } from 'shared/components/link.mjs'
|
2023-08-23 18:49:21 +02:00
|
|
|
import { StringInput, ListInput, FormControl } from 'shared/components/inputs.mjs'
|
2023-08-24 19:42:32 +02:00
|
|
|
import { DynamicOrgDocs } from 'shared/components/dynamic-docs/org.mjs'
|
2023-02-19 20:49:15 +01:00
|
|
|
|
2023-08-21 16:14:58 +02:00
|
|
|
export const ns = ['account', 'status']
|
2023-02-19 20:49:15 +01:00
|
|
|
|
|
|
|
const ExpiryPicker = ({ t, expires, setExpires }) => {
|
2023-08-21 16:14:58 +02:00
|
|
|
const router = useRouter()
|
|
|
|
const { locale } = router
|
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')}
|
2023-08-21 16:14:58 +02:00
|
|
|
<b> {shortDate(locale, expires)}</b>
|
2023-02-19 20:49:15 +01:00
|
|
|
</Popout>
|
|
|
|
</>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
const CopyInput = ({ text }) => {
|
2023-08-21 16:14:58 +02:00
|
|
|
const { t } = useTranslation(['status'])
|
2023-09-04 08:40:05 +02:00
|
|
|
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
2023-02-19 20:49:15 +01:00
|
|
|
|
|
|
|
const [copied, setCopied] = useState(false)
|
|
|
|
|
|
|
|
const showCopied = () => {
|
|
|
|
setCopied(true)
|
2023-08-21 16:14:58 +02:00
|
|
|
setLoadingStatus([true, t('copiedToClipboard'), true, true])
|
|
|
|
window.setTimeout(() => setCopied(false), 2000)
|
2023-02-19 20:49:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-08-21 16:14:58 +02:00
|
|
|
export const Apikey = ({ apikey }) => {
|
|
|
|
const { t } = useTranslation(ns)
|
|
|
|
const router = useRouter()
|
|
|
|
const { locale } = router
|
|
|
|
|
|
|
|
return apikey ? (
|
|
|
|
<div>
|
2023-08-23 17:47:21 +02:00
|
|
|
<DisplayRow title={t('keyName')}>{apikey.name}</DisplayRow>
|
|
|
|
<DisplayRow title={t('created')}>{shortDate(locale, apikey.createdAt)}</DisplayRow>
|
|
|
|
<DisplayRow title={t('expires')}>{shortDate(locale, apikey.expiresAt)}</DisplayRow>
|
|
|
|
<DisplayRow title="Key ID">{apikey.key}</DisplayRow>
|
2023-08-21 16:14:58 +02:00
|
|
|
<div className="flex flex-row flex-wrap md:gap-2 md:items-center md:justify-between mt-8">
|
|
|
|
<button
|
|
|
|
className="w-full md:w-auto btn btn-secondary pr-6 flex flex-row items-center gap-2"
|
|
|
|
onClick={() => router.push('/account/apikeys')}
|
|
|
|
>
|
|
|
|
<LeftIcon />
|
|
|
|
{t('apikeys')}
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
) : null
|
|
|
|
}
|
|
|
|
|
|
|
|
const ShowKey = ({ apikey, t, clear }) => {
|
2023-04-30 20:31:28 +02:00
|
|
|
const router = useRouter()
|
2023-08-21 16:14:58 +02:00
|
|
|
const { locale } = router
|
2023-04-30 20:31:28 +02:00
|
|
|
|
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
<Popout warning compact>
|
|
|
|
{t('keySecretWarning')}
|
|
|
|
</Popout>
|
2023-08-23 17:47:21 +02:00
|
|
|
<DisplayRow title={t('keyName')}>{apikey.name}</DisplayRow>
|
|
|
|
<DisplayRow title={t('created')}>{shortDate(locale, apikey.createdAt)}</DisplayRow>
|
|
|
|
<DisplayRow title={t('created')}>{shortDate(locale, apikey.expiresAt)}</DisplayRow>
|
|
|
|
<DisplayRow title="Key ID">
|
2023-04-30 20:31:28 +02:00
|
|
|
<CopyInput text={apikey.key} />
|
2023-08-23 17:47:21 +02:00
|
|
|
</DisplayRow>
|
|
|
|
<DisplayRow title="Key Secret">
|
2023-04-30 20:31:28 +02:00
|
|
|
<CopyInput text={apikey.secret} />
|
2023-08-23 17:47:21 +02:00
|
|
|
</DisplayRow>
|
2023-08-21 16:14:58 +02:00
|
|
|
<div className="flex flex-row flex-wrap md:gap-2 md:items-center md:justify-between mt-8">
|
|
|
|
<button
|
|
|
|
className="w-full md:w-auto btn btn-secondary pr-6 flex flex-row items-center gap-2"
|
|
|
|
onClick={() => router.push('/account/apikeys')}
|
|
|
|
>
|
|
|
|
<LeftIcon />
|
|
|
|
{t('apikeys')}
|
|
|
|
</button>
|
|
|
|
<button className="btn btn-primary w-full mt-2 md:w-auto md:mt-0" onClick={clear}>
|
2023-08-21 17:58:08 +02:00
|
|
|
<PlusIcon />
|
2023-08-21 16:14:58 +02:00
|
|
|
{t('newApikey')}
|
|
|
|
</button>
|
|
|
|
</div>
|
2023-04-30 20:31:28 +02:00
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|
2023-02-19 20:49:15 +01:00
|
|
|
|
2023-08-24 19:01:48 +02:00
|
|
|
const NewKey = ({ account, setGenerate, backend }) => {
|
2023-02-19 20:49:15 +01:00
|
|
|
const [name, setName] = useState('')
|
|
|
|
const [level, setLevel] = useState(1)
|
2023-08-21 16:14:58 +02:00
|
|
|
const [expires, setExpires] = useState(Date.now())
|
2023-02-19 20:49:15 +01:00
|
|
|
const [apikey, setApikey] = useState(false)
|
2023-09-04 08:40:05 +02:00
|
|
|
const { setLoadingStatus } = useContext(LoadingStatusContext)
|
2023-08-23 18:49:21 +02:00
|
|
|
const { t, i18n } = useTranslation(ns)
|
2023-08-24 19:42:32 +02:00
|
|
|
// FIXME: implement a solution for loading docs dynamically the is simple and work as expected
|
|
|
|
const docs = {}
|
|
|
|
for (const option of ['name', 'expiry', 'level']) {
|
|
|
|
docs[option] = <DynamicOrgDocs language={i18n.language} path={`site/apikeys/${option}`} />
|
|
|
|
}
|
2023-02-19 20:49:15 +01:00
|
|
|
|
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-08-21 16:14:58 +02:00
|
|
|
setLoadingStatus([true, 'processingUpdate'])
|
2023-02-19 20:49:15 +01:00
|
|
|
const result = await backend.createApikey({
|
|
|
|
name,
|
|
|
|
level,
|
2023-08-21 16:14:58 +02:00
|
|
|
expiresIn: Math.floor((expires.valueOf() - Date.now().valueOf()) / 1000),
|
2023-02-19 20:49:15 +01:00
|
|
|
})
|
2023-04-22 15:01:57 +02:00
|
|
|
if (result.success) {
|
2023-08-21 16:14:58 +02:00
|
|
|
setLoadingStatus([true, 'nailedIt', true, true])
|
2023-04-22 15:01:57 +02:00
|
|
|
setApikey(result.data.apikey)
|
2023-08-21 16:14:58 +02:00
|
|
|
} else setLoadingStatus([true, 'backendError', true, false])
|
2023-02-19 20:49:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const clear = () => {
|
|
|
|
setApikey(false)
|
|
|
|
setGenerate(false)
|
2023-08-21 16:14:58 +02:00
|
|
|
setName('')
|
|
|
|
setLevel(1)
|
2023-02-19 20:49:15 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div>
|
|
|
|
{apikey ? (
|
2023-08-21 16:14:58 +02:00
|
|
|
<ShowKey {...{ apikey, t, clear }} />
|
2023-02-19 20:49:15 +01:00
|
|
|
) : (
|
|
|
|
<>
|
2023-08-23 18:49:21 +02:00
|
|
|
<StringInput
|
2023-08-26 09:27:27 +02:00
|
|
|
id="apikey-name"
|
2023-08-23 18:49:21 +02:00
|
|
|
label={t('keyName')}
|
|
|
|
docs={docs.name}
|
|
|
|
current={name}
|
|
|
|
update={setName}
|
|
|
|
valid={(val) => val.length > 0}
|
|
|
|
placeholder={'Alicia Key'}
|
|
|
|
/>
|
|
|
|
<FormControl label={t('keyExpires')} docs={docs.expiry}>
|
|
|
|
<ExpiryPicker {...{ t, expires, setExpires }} />
|
|
|
|
</FormControl>
|
|
|
|
<ListInput
|
2023-08-26 09:27:27 +02:00
|
|
|
id="apikey-level"
|
2023-08-23 18:49:21 +02:00
|
|
|
label={t('keyLevel')}
|
|
|
|
docs={docs.level}
|
|
|
|
list={levels.map((l) => ({
|
|
|
|
val: l,
|
|
|
|
label: (
|
|
|
|
<div className="flex flex-row items-center w-full justify-between">
|
|
|
|
<span>{t(`keyLevel${l}`)}</span>
|
|
|
|
<NumberBullet nr={l} color="secondary" />
|
|
|
|
</div>
|
|
|
|
),
|
|
|
|
}))}
|
|
|
|
current={level}
|
|
|
|
update={setLevel}
|
2023-02-19 20:49:15 +01:00
|
|
|
/>
|
|
|
|
<div className="flex flex-row gap-2 items-center w-full my-8">
|
|
|
|
<button
|
2023-08-23 18:49:21 +02:00
|
|
|
className="btn btn-primary capitalize w-full md:w-auto"
|
2023-02-19 20:49:15 +01:00
|
|
|
disabled={name.length < 1}
|
|
|
|
onClick={createKey}
|
|
|
|
>
|
|
|
|
{t('newApikey')}
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
</>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-04-22 15:01:57 +02:00
|
|
|
// Component for the 'new/apikey' page
|
2023-08-23 12:18:20 +02:00
|
|
|
export const NewApikey = () => {
|
2023-04-28 21:23:06 +02:00
|
|
|
// Hooks
|
2023-08-20 18:48:40 +02:00
|
|
|
const { account } = useAccount()
|
|
|
|
const backend = useBackend()
|
2023-04-22 15:01:57 +02:00
|
|
|
|
2023-04-28 21:23:06 +02:00
|
|
|
// State
|
2023-04-22 15:01:57 +02:00
|
|
|
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 (
|
2023-08-21 16:14:58 +02:00
|
|
|
<div className="max-w-2xl xl:pl-4">
|
2023-04-28 21:23:06 +02:00
|
|
|
<NewKey
|
|
|
|
{...{
|
|
|
|
account,
|
2023-08-20 18:53:35 +02:00
|
|
|
generate,
|
2023-04-28 21:23:06 +02:00
|
|
|
setGenerate,
|
|
|
|
backend,
|
|
|
|
keyAdded,
|
|
|
|
}}
|
|
|
|
/>
|
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 = () => {
|
2023-08-21 16:14:58 +02:00
|
|
|
const router = useRouter()
|
|
|
|
const { locale } = router
|
2023-04-28 21:23:06 +02:00
|
|
|
|
|
|
|
// Hooks
|
2023-08-20 18:48:40 +02:00
|
|
|
const { account } = useAccount()
|
|
|
|
const backend = useBackend()
|
2023-02-19 20:49:15 +01:00
|
|
|
const { t } = useTranslation(ns)
|
2023-09-04 08:40:05 +02:00
|
|
|
const { setLoadingStatus, LoadingProgress } = useContext(LoadingStatusContext)
|
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-08-21 16:14:58 +02:00
|
|
|
const [selected, setSelected] = useState({})
|
|
|
|
const [refresh, setRefresh] = useState(0)
|
|
|
|
|
|
|
|
// Helper var to see how many are selected
|
|
|
|
const selCount = Object.keys(selected).length
|
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()
|
2023-08-21 16:14:58 +02:00
|
|
|
}, [refresh])
|
|
|
|
|
|
|
|
// Helper method to toggle single selection
|
|
|
|
const toggleSelect = (id) => {
|
|
|
|
const newSelected = { ...selected }
|
|
|
|
if (newSelected[id]) delete newSelected[id]
|
|
|
|
else newSelected[id] = 1
|
|
|
|
setSelected(newSelected)
|
|
|
|
}
|
2023-02-25 18:19:11 +01:00
|
|
|
|
2023-08-21 16:14:58 +02:00
|
|
|
// Helper method to toggle select all
|
|
|
|
const toggleSelectAll = () => {
|
2023-08-21 17:58:08 +02:00
|
|
|
if (selCount === keys.length) setSelected({})
|
2023-08-21 16:14:58 +02:00
|
|
|
else {
|
|
|
|
const newSelected = {}
|
|
|
|
for (const key of keys) newSelected[key.id] = 1
|
|
|
|
setSelected(newSelected)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Helper to delete one or more apikeys
|
|
|
|
const removeSelectedApikeys = async () => {
|
|
|
|
let i = 0
|
|
|
|
for (const key in selected) {
|
|
|
|
i++
|
|
|
|
await backend.removeApikey(key)
|
|
|
|
setLoadingStatus([
|
|
|
|
true,
|
2023-08-24 19:01:48 +02:00
|
|
|
<LoadingProgress val={i} max={selCount} msg={t('removingApikeys')} key="linter" />,
|
2023-08-21 16:14:58 +02:00
|
|
|
])
|
|
|
|
}
|
|
|
|
setSelected({})
|
|
|
|
setRefresh(refresh + 1)
|
|
|
|
setLoadingStatus([true, 'nailedIt', true, true])
|
|
|
|
}
|
2023-02-19 20:49:15 +01:00
|
|
|
|
|
|
|
return (
|
2023-08-21 16:14:58 +02:00
|
|
|
<div className="max-w-4xl xl:pl-4">
|
2023-08-23 18:49:21 +02:00
|
|
|
<p className="text-center md:text-right">
|
|
|
|
<Link
|
|
|
|
className="btn btn-primary capitalize w-full md:w-auto"
|
|
|
|
bottom
|
|
|
|
primary
|
|
|
|
href="/new/apikey"
|
|
|
|
>
|
|
|
|
<PlusIcon />
|
2023-08-21 16:14:58 +02:00
|
|
|
{t('newApikey')}
|
|
|
|
</Link>
|
|
|
|
</p>
|
|
|
|
{selCount ? (
|
|
|
|
<button className="btn btn-error" onClick={removeSelectedApikeys}>
|
|
|
|
<TrashIcon /> {selCount} {t('apikeys')}
|
|
|
|
</button>
|
|
|
|
) : null}
|
|
|
|
<table className="table table-auto">
|
|
|
|
<thead className="border border-base-300 border-b-2 border-t-0 border-x-0">
|
|
|
|
<tr className="b">
|
|
|
|
<th className="text-base-300 text-base">
|
|
|
|
<input
|
|
|
|
type="checkbox"
|
|
|
|
className="checkbox checkbox-secondary"
|
|
|
|
onClick={toggleSelectAll}
|
|
|
|
checked={keys.length === selCount}
|
|
|
|
/>
|
|
|
|
</th>
|
|
|
|
<th className="text-base-300 text-base">{t('keyName')}</th>
|
|
|
|
<th className="text-base-300 text-base">
|
|
|
|
<span className="hidden md:inline">{t('keyLevel')}</span>
|
|
|
|
<span role="img" className="inline md:hidden">
|
|
|
|
🔐
|
|
|
|
</span>
|
|
|
|
</th>
|
|
|
|
<th className="text-base-300 text-base">{t('keyExpires')}</th>
|
|
|
|
<th className="text-base-300 text-base hidden md:block">{t('apiCalls')}</th>
|
|
|
|
</tr>
|
|
|
|
</thead>
|
|
|
|
<tbody>
|
|
|
|
{keys.map((apikey, i) => (
|
|
|
|
<tr key={i}>
|
|
|
|
<td className="text-base font-medium">
|
|
|
|
<input
|
|
|
|
type="checkbox"
|
|
|
|
checked={selected[apikey.id] ? true : false}
|
|
|
|
className="checkbox checkbox-secondary"
|
|
|
|
onClick={() => toggleSelect(apikey.id)}
|
|
|
|
/>
|
|
|
|
</td>
|
|
|
|
<td className="text-base font-medium">
|
|
|
|
<PageLink href={`/account/apikeys/${apikey.id}`} txt={apikey.name} />
|
|
|
|
</td>
|
|
|
|
<td className="text-base font-medium">
|
|
|
|
{apikey.level}
|
|
|
|
<small className="hidden md:inline pl-2 text-base-300 italic">
|
|
|
|
({t(`keyLevel${apikey.level}`)})
|
|
|
|
</small>
|
|
|
|
</td>
|
|
|
|
<td className="text-base font-medium">
|
|
|
|
{shortDate(locale, apikey.expiresAt, false)}
|
|
|
|
</td>
|
|
|
|
<td className="text-base font-medium hidden md:block">
|
|
|
|
{formatNumber(apikey.calls)}
|
|
|
|
</td>
|
|
|
|
</tr>
|
2023-02-25 18:19:11 +01:00
|
|
|
))}
|
2023-08-21 16:14:58 +02:00
|
|
|
</tbody>
|
|
|
|
</table>
|
|
|
|
<BackToAccountButton />
|
|
|
|
{account.control < 5 ? (
|
|
|
|
<Popout link>
|
|
|
|
<h5>{t('keyDocsTitle')}</h5>
|
|
|
|
<p>{t('keyDocsMsg')}</p>
|
|
|
|
<p className="text-right">
|
|
|
|
<a
|
|
|
|
className="btn btn-secondary mt-2"
|
|
|
|
href="https://freesewing.dev/reference/backend/apikeys"
|
|
|
|
>
|
|
|
|
FreeSewing.dev
|
|
|
|
<RightIcon />
|
|
|
|
</a>
|
|
|
|
</p>
|
|
|
|
</Popout>
|
|
|
|
) : null}
|
2023-02-25 18:19:11 +01:00
|
|
|
</div>
|
2023-02-19 20:49:15 +01:00
|
|
|
)
|
|
|
|
}
|