Merge pull request #681 from SkynetLabs/unpin-from-dashboard

unpin skylink from dashboard
This commit is contained in:
Ivaylo Novakov 2021-09-29 14:52:13 +02:00 committed by GitHub
commit f42a3a9c20
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 129 additions and 31 deletions

View File

@ -0,0 +1 @@
- added unpinning skylinks from account dashboard

View File

@ -97,7 +97,7 @@
preserve_host: true preserve_host: true
url: "http://accounts:3000" url: "http://accounts:3000"
match: match:
url: "http://oathkeeper<{,:4455}>/<{login,logout,user,user/uploads,user/downloads,user/stats}>" url: "http://oathkeeper<{,:4455}>/<{login,logout,user,user/uploads,user/uploads/*,user/downloads,user/stats}>"
methods: methods:
- GET - GET
- POST - POST

View File

@ -10,6 +10,6 @@ RUN yarn --frozen-lockfile
COPY public ./public COPY public ./public
COPY src ./src COPY src ./src
COPY styles ./styles COPY styles ./styles
COPY next.config.js postcss.config.js tailwind.config.js ./ COPY postcss.config.js tailwind.config.js ./
CMD ["sh", "-c", "env | grep -E 'NEXT_PUBLIC|KRATOS|STRIPE' > .env.local && yarn build && yarn start"] CMD ["sh", "-c", "env | grep -E 'NEXT_PUBLIC|KRATOS|STRIPE' > .env.local && yarn build && yarn start"]

View File

@ -1,3 +0,0 @@
module.exports = {
webpack5: true,
};

View File

@ -23,11 +23,13 @@
"http-status-codes": "2.1.4", "http-status-codes": "2.1.4",
"ky": "0.25.1", "ky": "0.25.1",
"next": "11.1.2", "next": "11.1.2",
"normalize.css": "8.0.1",
"postcss": "8.3.8", "postcss": "8.3.8",
"prettier": "2.4.1", "prettier": "2.4.1",
"pretty-bytes": "5.6.0", "pretty-bytes": "5.6.0",
"react": "17.0.2", "react": "17.0.2",
"react-dom": "17.0.2", "react-dom": "17.0.2",
"react-toastify": "8.0.2",
"skynet-js": "3.0.2", "skynet-js": "3.0.2",
"stripe": "8.176.0", "stripe": "8.176.0",
"superagent": "6.1.0", "superagent": "6.1.0",

View File

@ -1,7 +1,7 @@
import { useEffect } from "react"; import { useEffect } from "react";
import classnames from "classnames"; import classnames from "classnames";
function Button({ children, disabled, className, ...props }) { function Button({ children, disabled, ...props }) {
return ( return (
<button <button
type="button" type="button"
@ -10,8 +10,7 @@ function Button({ children, disabled, className, ...props }) {
{ {
"hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500": !disabled, "hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500": !disabled,
"cursor-auto opacity-50": disabled, "cursor-auto opacity-50": disabled,
}, }
className
)} )}
disabled={disabled} disabled={disabled}
{...props} {...props}
@ -21,7 +20,26 @@ function Button({ children, disabled, className, ...props }) {
); );
} }
export default function Table({ items, count, headers, actions, offset, setOffset, pageSize = 10 }) { function ButtonAction({ children, disabled, ...props }) {
return (
<button
type="button"
className={classnames(
"inline-flex items-center px-2.5 py-1.5 border border-gray-300 shadow-sm text-xs font-medium rounded text-gray-700 bg-white",
{
"hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500": !disabled,
"cursor-auto opacity-50": disabled,
}
)}
disabled={disabled}
{...props}
>
{children}
</button>
);
}
export default function Table({ items, count, headers, mutate, actions, offset, setOffset, pageSize = 10 }) {
useEffect(() => { useEffect(() => {
if (offset < 0) setOffset(0); if (offset < 0) setOffset(0);
else if (offset >= count && count > 0) setOffset(Math.floor(count / pageSize - 1) * pageSize); else if (offset >= count && count > 0) setOffset(Math.floor(count / pageSize - 1) * pageSize);
@ -45,8 +63,8 @@ export default function Table({ items, count, headers, actions, offset, setOffse
{name} {name}
</th> </th>
))} ))}
{actions.map(({ key, name }) => ( {actions.map(({ name }, index) => (
<th key={key} scope="col" className="relative px-6 py-3"> <th key={index} scope="col" className="relative px-6 py-3">
<span className="sr-only">{name}</span> <span className="sr-only">{name}</span>
</th> </th>
))} ))}
@ -56,7 +74,7 @@ export default function Table({ items, count, headers, actions, offset, setOffse
{items && items.length ? ( {items && items.length ? (
items.map((row, index) => ( items.map((row, index) => (
<tr className={index % 2 ? "bg-gray-100" : "bg-white"} key={index}> <tr className={index % 2 ? "bg-gray-100" : "bg-white"} key={index}>
{headers.map(({ key, formatter, href, nowrap = true }) => ( {headers.map(({ key, formatter, href, nowrap }) => (
<td <td
key={key} key={key}
className={`${nowrap ? "whitespace-nowrap" : ""} px-6 py-4 text-sm font-medium text-gray-900`} className={`${nowrap ? "whitespace-nowrap" : ""} px-6 py-4 text-sm font-medium text-gray-900`}
@ -77,11 +95,9 @@ export default function Table({ items, count, headers, actions, offset, setOffse
)) || <>&mdash;</>} )) || <>&mdash;</>}
</td> </td>
))} ))}
{actions.map(({ key, name, action }) => ( {actions.map(({ name, action }, index) => (
<td key={key} className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium"> <td key={index} className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<a href="#" className="text-green-600 hover:text-green-900" onClick={action}> <ButtonAction onClick={() => action(row, mutate)}>{name}</ButtonAction>
{name}
</a>
</td> </td>
))} ))}
</tr> </tr>
@ -107,15 +123,11 @@ export default function Table({ items, count, headers, actions, offset, setOffse
<span className="font-medium">{count}</span> results <span className="font-medium">{count}</span> results
</p> </p>
</div> </div>
<div className="flex-1 flex justify-between sm:justify-end"> <div className="flex-1 flex justify-between sm:justify-end space-x-3">
<Button disabled={offset - pageSize < 0} onClick={() => setOffset(offset - pageSize)}> <Button disabled={offset - pageSize < 0} onClick={() => setOffset(offset - pageSize)}>
Previous Previous
</Button> </Button>
<Button <Button disabled={offset + pageSize >= count} onClick={() => setOffset(offset + pageSize)}>
className="ml-3"
disabled={offset + pageSize >= count}
onClick={() => setOffset(offset + pageSize)}
>
Next Next
</Button> </Button>
</div> </div>

View File

@ -1,6 +1,9 @@
import { Elements } from "@stripe/react-stripe-js"; import { Elements } from "@stripe/react-stripe-js";
import { loadStripe } from "@stripe/stripe-js"; import { loadStripe } from "@stripe/stripe-js";
import { ToastContainer } from "react-toastify";
import Head from "next/head"; import Head from "next/head";
import "normalize.css";
import "react-toastify/dist/ReactToastify.css";
import "tailwindcss/tailwind.css"; import "tailwindcss/tailwind.css";
import "@fontsource/metropolis/all.css"; import "@fontsource/metropolis/all.css";
@ -14,6 +17,7 @@ function MyApp({ Component, pageProps }) {
<title key="title">Skynet</title> <title key="title">Skynet</title>
</Head> </Head>
<Component {...pageProps} /> <Component {...pageProps} />
<ToastContainer bodyClassName={() => "Toastify__toast-body text-sm font-medium text-palette-500"} />
</Elements> </Elements>
); );
} }

View File

@ -10,6 +10,8 @@ export default async (req, res) => {
res.setHeader("Set-Cookie", header["set-cookie"]); res.setHeader("Set-Cookie", header["set-cookie"]);
res.redirect(req.query.return_to ?? "/"); res.redirect(req.query.return_to ?? "/");
} catch (error) { } catch (error) {
console.log(`Cookie is present but authentication failed: ${error.message}`);
// credentials were correct but accounts service failed // credentials were correct but accounts service failed
res.redirect("/.ory/kratos/public/self-service/browser/flows/logout"); res.redirect("/.ory/kratos/public/self-service/browser/flows/logout");
} }

View File

@ -32,6 +32,8 @@ export async function getServerSideProps(context) {
throw new Error(`Failed to retrieve flow ${flow} with code ${status}`); throw new Error(`Failed to retrieve flow ${flow} with code ${status}`);
} catch (error) { } catch (error) {
console.log(`Unexpected error retrieving login flow: ${error.message}`);
return { return {
redirect: { redirect: {
permanent: false, permanent: false,

View File

@ -35,6 +35,8 @@ export async function getServerSideProps(context) {
throw new Error(`Failed to retrieve flow ${flow} with code ${status}`); throw new Error(`Failed to retrieve flow ${flow} with code ${status}`);
} catch (error) { } catch (error) {
console.log(`Unexpected error retrieving registration flow: ${error.message}`);
return { return {
redirect: { redirect: {
permanent: false, permanent: false,

View File

@ -12,8 +12,25 @@ const apiPrefix = process.env.NODE_ENV === "development" ? "/api/stubs" : "";
const getSkylinkLink = ({ skylink }) => skynetClient.getSkylinkUrl(skylink); const getSkylinkLink = ({ skylink }) => skynetClient.getSkylinkUrl(skylink);
const getRelativeDate = ({ downloadedOn }) => dayjs(downloadedOn).format("YYYY-MM-DD HH:mm:ss"); const getRelativeDate = ({ downloadedOn }) => dayjs(downloadedOn).format("YYYY-MM-DD HH:mm:ss");
const headers = [ const headers = [
{ key: "name", name: "Name", nowrap: false, href: getSkylinkLink }, {
{ key: "skylink", name: "Skylink" }, key: "name",
name: "File",
formatter: ({ name, skylink }) => (
<>
<p>
<a
href={getSkylinkLink({ skylink })}
className="text-green-600 hover:text-green-900 break-all"
target="_blank"
rel="noopener noreferrer"
>
{name}
</a>
</p>
<p className="text-gray-500 text-xs">{skylink}</p>
</>
),
},
{ key: "size", name: "Size", formatter: ({ size }) => prettyBytes(size) }, { key: "size", name: "Size", formatter: ({ size }) => prettyBytes(size) },
{ key: "downloadedOn", name: "Accessed on", formatter: getRelativeDate }, { key: "downloadedOn", name: "Accessed on", formatter: getRelativeDate },
]; ];

View File

@ -1,6 +1,8 @@
import dayjs from "dayjs"; import dayjs from "dayjs";
import prettyBytes from "pretty-bytes"; import prettyBytes from "pretty-bytes";
import { useState } from "react"; import { useState } from "react";
import ky from "ky/umd";
import { toast } from "react-toastify";
import Layout from "../components/Layout"; import Layout from "../components/Layout";
import Table from "../components/Table"; import Table from "../components/Table";
import authServerSideProps from "../services/authServerSideProps"; import authServerSideProps from "../services/authServerSideProps";
@ -12,12 +14,42 @@ const apiPrefix = process.env.NODE_ENV === "development" ? "/api/stubs" : "";
const getSkylinkLink = ({ skylink }) => skynetClient.getSkylinkUrl(skylink); const getSkylinkLink = ({ skylink }) => skynetClient.getSkylinkUrl(skylink);
const getRelativeDate = ({ uploadedOn }) => dayjs(uploadedOn).format("YYYY-MM-DD HH:mm:ss"); const getRelativeDate = ({ uploadedOn }) => dayjs(uploadedOn).format("YYYY-MM-DD HH:mm:ss");
const headers = [ const headers = [
{ key: "name", name: "Name", nowrap: false, href: getSkylinkLink }, {
{ key: "skylink", name: "Skylink" }, key: "name",
name: "File",
formatter: ({ name, skylink }) => (
<>
<p>
<a
href={getSkylinkLink({ skylink })}
className="text-green-600 hover:text-green-900 break-all"
target="_blank"
rel="noopener noreferrer"
>
{name}
</a>
</p>
<p className="text-gray-500 text-xs">{skylink}</p>
</>
),
},
{ key: "size", name: "Size", formatter: ({ size }) => prettyBytes(size) }, { key: "size", name: "Size", formatter: ({ size }) => prettyBytes(size) },
{ key: "uploadedOn", name: "Uploaded on", formatter: getRelativeDate }, { key: "uploadedOn", name: "Uploaded on", formatter: getRelativeDate },
]; ];
const actions = []; const actions = [
{
name: "Unpin Skylink",
action: async ({ skylink }, mutate) => {
await toast.promise(ky.delete(`/user/uploads/${skylink}`), {
pending: "Unpinning Skylink",
success: "Skylink unpinned",
error: (error) => error.message,
});
mutate();
},
},
];
export const getServerSideProps = authServerSideProps(async (context, api) => { export const getServerSideProps = authServerSideProps(async (context, api) => {
const initialData = await api.get("user/uploads?pageSize=10&offset=0").json(); const initialData = await api.get("user/uploads?pageSize=10&offset=0").json();
@ -27,7 +59,7 @@ export const getServerSideProps = authServerSideProps(async (context, api) => {
export default function Uploads({ initialData }) { export default function Uploads({ initialData }) {
const [offset, setOffset] = useState(0); const [offset, setOffset] = useState(0);
const { data } = useAccountsApi(`${apiPrefix}/user/uploads?pageSize=10&offset=${offset}`, { const { data, mutate } = useAccountsApi(`${apiPrefix}/user/uploads?pageSize=10&offset=${offset}`, {
initialData: offset === 0 ? initialData : undefined, initialData: offset === 0 ? initialData : undefined,
revalidateOnMount: true, revalidateOnMount: true,
}); });
@ -38,7 +70,7 @@ export default function Uploads({ initialData }) {
return ( return (
<Layout title="Your uploads"> <Layout title="Your uploads">
<Table {...data} headers={headers} actions={actions} setOffset={setOffset} /> <Table {...data} mutate={mutate} headers={headers} actions={actions} setOffset={setOffset} />
</Layout> </Layout>
); );
} }

View File

@ -4,7 +4,17 @@ const isProduction = process.env.NODE_ENV === "production";
export default function authServerSideProps(getServerSideProps) { export default function authServerSideProps(getServerSideProps) {
return function authenticate(context) { return function authenticate(context) {
if (isProduction && (!("ory_kratos_session" in context.req.cookies) || !("skynet-jwt" in context.req.cookies))) { const authCookies = ["ory_kratos_session", "skynet-jwt"];
if (isProduction && !authCookies.every((cookie) => context.req.cookies[cookie])) {
// it is higly unusual that some of the cookies would be set but other would not
if (authCookies.some((cookie) => context.req.cookies[cookie])) {
console.log(
"Unexpected auth cookies state!",
authCookies.map((cookie) => `[${cookie}] is ${context.req.cookies[cookie] ? "set" : "not set"}`)
);
}
return { return {
redirect: { redirect: {
permanent: false, permanent: false,

View File

@ -640,6 +640,11 @@ clipboardy@2.3.0:
execa "^1.0.0" execa "^1.0.0"
is-wsl "^2.1.1" is-wsl "^2.1.1"
clsx@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==
color-convert@^1.9.0: color-convert@^1.9.0:
version "1.9.3" version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@ -2090,6 +2095,11 @@ normalize-range@^0.1.2:
resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942"
integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI= integrity sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=
normalize.css@8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/normalize.css/-/normalize.css-8.0.1.tgz#9b98a208738b9cc2634caacbc42d131c97487bf3"
integrity sha512-qizSNPO93t1YUuUhP22btGOo3chcvDFqFaj2TRybP0DMxkHOCTYwp3n34fel4a31ORXy4m1Xq0Gyqpb5m33qIg==
npm-run-path@^2.0.0: npm-run-path@^2.0.0:
version "2.0.2" version "2.0.2"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f"
@ -2523,6 +2533,13 @@ react-refresh@0.8.3:
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
integrity sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg== integrity sha512-X8jZHc7nCMjaCqoU+V2I0cOhNW+QMBwSUkeXnTi8IPe6zaRWfn60ZzvFDZqWPfmSJfjub7dDW1SP0jaHWLu/hg==
react-toastify@8.0.2:
version "8.0.2"
resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-8.0.2.tgz#11f73b3a847fcffeb47b1823e8974e9895f98fae"
integrity sha512-0Nud2d0VD4LIevgkB4L8NYoQ5plTpfqgj2CRVxs58SGA/TTO+2Ojz4C1bLUdGUWsw0zuWqd4GJqxNuMIv0cXMw==
dependencies:
clsx "^1.1.1"
react@17.0.2: react@17.0.2:
version "17.0.2" version "17.0.2"
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"