diff --git a/changelog/items/key-updates/unpin-skylink.md b/changelog/items/key-updates/unpin-skylink.md new file mode 100644 index 00000000..ab403ff9 --- /dev/null +++ b/changelog/items/key-updates/unpin-skylink.md @@ -0,0 +1 @@ +- added unpinning skylinks from account dashboard diff --git a/docker/kratos/oathkeeper/access-rules.yml b/docker/kratos/oathkeeper/access-rules.yml index a45f75b2..75bdee65 100644 --- a/docker/kratos/oathkeeper/access-rules.yml +++ b/docker/kratos/oathkeeper/access-rules.yml @@ -97,7 +97,7 @@ preserve_host: true url: "http://accounts:3000" 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: - GET - POST diff --git a/packages/dashboard/Dockerfile b/packages/dashboard/Dockerfile index 888d7c51..8d93a036 100644 --- a/packages/dashboard/Dockerfile +++ b/packages/dashboard/Dockerfile @@ -10,6 +10,6 @@ RUN yarn --frozen-lockfile COPY public ./public COPY src ./src 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"] diff --git a/packages/dashboard/next.config.js b/packages/dashboard/next.config.js deleted file mode 100644 index d62def0f..00000000 --- a/packages/dashboard/next.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - webpack5: true, -}; diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json index 44ac4351..ab74f41f 100644 --- a/packages/dashboard/package.json +++ b/packages/dashboard/package.json @@ -23,11 +23,13 @@ "http-status-codes": "2.1.4", "ky": "0.25.1", "next": "11.1.2", + "normalize.css": "8.0.1", "postcss": "8.3.8", "prettier": "2.4.1", "pretty-bytes": "5.6.0", "react": "17.0.2", "react-dom": "17.0.2", + "react-toastify": "8.0.2", "skynet-js": "3.0.2", "stripe": "8.176.0", "superagent": "6.1.0", diff --git a/packages/dashboard/src/components/Table.js b/packages/dashboard/src/components/Table.js index e3ae6d5e..1c01b1fa 100644 --- a/packages/dashboard/src/components/Table.js +++ b/packages/dashboard/src/components/Table.js @@ -1,7 +1,7 @@ import { useEffect } from "react"; import classnames from "classnames"; -function Button({ children, disabled, className, ...props }) { +function Button({ children, disabled, ...props }) { return ( + ); +} + +export default function Table({ items, count, headers, mutate, actions, offset, setOffset, pageSize = 10 }) { useEffect(() => { if (offset < 0) setOffset(0); 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} ))} - {actions.map(({ key, name }) => ( - + {actions.map(({ name }, index) => ( + {name} ))} @@ -56,7 +74,7 @@ export default function Table({ items, count, headers, actions, offset, setOffse {items && items.length ? ( items.map((row, index) => ( - {headers.map(({ key, formatter, href, nowrap = true }) => ( + {headers.map(({ key, formatter, href, nowrap }) => ( —} ))} - {actions.map(({ key, name, action }) => ( - - - {name} - + {actions.map(({ name, action }, index) => ( + + action(row, mutate)}>{name} ))} @@ -107,15 +123,11 @@ export default function Table({ items, count, headers, actions, offset, setOffse {count} results

-
+
-
diff --git a/packages/dashboard/src/pages/_app.js b/packages/dashboard/src/pages/_app.js index 5be206d2..811d3a0d 100644 --- a/packages/dashboard/src/pages/_app.js +++ b/packages/dashboard/src/pages/_app.js @@ -1,6 +1,9 @@ import { Elements } from "@stripe/react-stripe-js"; import { loadStripe } from "@stripe/stripe-js"; +import { ToastContainer } from "react-toastify"; import Head from "next/head"; +import "normalize.css"; +import "react-toastify/dist/ReactToastify.css"; import "tailwindcss/tailwind.css"; import "@fontsource/metropolis/all.css"; @@ -14,6 +17,7 @@ function MyApp({ Component, pageProps }) { Skynet + "Toastify__toast-body text-sm font-medium text-palette-500"} /> ); } diff --git a/packages/dashboard/src/pages/api/accounts/login.js b/packages/dashboard/src/pages/api/accounts/login.js index 4890d52b..89bad4dc 100644 --- a/packages/dashboard/src/pages/api/accounts/login.js +++ b/packages/dashboard/src/pages/api/accounts/login.js @@ -10,6 +10,8 @@ export default async (req, res) => { res.setHeader("Set-Cookie", header["set-cookie"]); res.redirect(req.query.return_to ?? "/"); } catch (error) { + console.log(`Cookie is present but authentication failed: ${error.message}`); + // credentials were correct but accounts service failed res.redirect("/.ory/kratos/public/self-service/browser/flows/logout"); } diff --git a/packages/dashboard/src/pages/auth/login.js b/packages/dashboard/src/pages/auth/login.js index 481f8d44..f9e8ab45 100644 --- a/packages/dashboard/src/pages/auth/login.js +++ b/packages/dashboard/src/pages/auth/login.js @@ -32,6 +32,8 @@ export async function getServerSideProps(context) { throw new Error(`Failed to retrieve flow ${flow} with code ${status}`); } catch (error) { + console.log(`Unexpected error retrieving login flow: ${error.message}`); + return { redirect: { permanent: false, diff --git a/packages/dashboard/src/pages/auth/registration.js b/packages/dashboard/src/pages/auth/registration.js index 33230c54..23414f72 100644 --- a/packages/dashboard/src/pages/auth/registration.js +++ b/packages/dashboard/src/pages/auth/registration.js @@ -35,6 +35,8 @@ export async function getServerSideProps(context) { throw new Error(`Failed to retrieve flow ${flow} with code ${status}`); } catch (error) { + console.log(`Unexpected error retrieving registration flow: ${error.message}`); + return { redirect: { permanent: false, diff --git a/packages/dashboard/src/pages/downloads.js b/packages/dashboard/src/pages/downloads.js index 942efd11..d18257f7 100644 --- a/packages/dashboard/src/pages/downloads.js +++ b/packages/dashboard/src/pages/downloads.js @@ -12,8 +12,25 @@ const apiPrefix = process.env.NODE_ENV === "development" ? "/api/stubs" : ""; const getSkylinkLink = ({ skylink }) => skynetClient.getSkylinkUrl(skylink); const getRelativeDate = ({ downloadedOn }) => dayjs(downloadedOn).format("YYYY-MM-DD HH:mm:ss"); const headers = [ - { key: "name", name: "Name", nowrap: false, href: getSkylinkLink }, - { key: "skylink", name: "Skylink" }, + { + key: "name", + name: "File", + formatter: ({ name, skylink }) => ( + <> +

+ + {name} + +

+

{skylink}

+ + ), + }, { key: "size", name: "Size", formatter: ({ size }) => prettyBytes(size) }, { key: "downloadedOn", name: "Accessed on", formatter: getRelativeDate }, ]; diff --git a/packages/dashboard/src/pages/uploads.js b/packages/dashboard/src/pages/uploads.js index d78288f7..ff452f49 100644 --- a/packages/dashboard/src/pages/uploads.js +++ b/packages/dashboard/src/pages/uploads.js @@ -1,6 +1,8 @@ import dayjs from "dayjs"; import prettyBytes from "pretty-bytes"; import { useState } from "react"; +import ky from "ky/umd"; +import { toast } from "react-toastify"; import Layout from "../components/Layout"; import Table from "../components/Table"; 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 getRelativeDate = ({ uploadedOn }) => dayjs(uploadedOn).format("YYYY-MM-DD HH:mm:ss"); const headers = [ - { key: "name", name: "Name", nowrap: false, href: getSkylinkLink }, - { key: "skylink", name: "Skylink" }, + { + key: "name", + name: "File", + formatter: ({ name, skylink }) => ( + <> +

+ + {name} + +

+

{skylink}

+ + ), + }, { key: "size", name: "Size", formatter: ({ size }) => prettyBytes(size) }, { 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) => { 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 }) { 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, revalidateOnMount: true, }); @@ -38,7 +70,7 @@ export default function Uploads({ initialData }) { return ( - +
); } diff --git a/packages/dashboard/src/services/authServerSideProps.js b/packages/dashboard/src/services/authServerSideProps.js index bc7bf43c..83c848d3 100644 --- a/packages/dashboard/src/services/authServerSideProps.js +++ b/packages/dashboard/src/services/authServerSideProps.js @@ -4,7 +4,17 @@ const isProduction = process.env.NODE_ENV === "production"; export default function authServerSideProps(getServerSideProps) { 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 { redirect: { permanent: false, diff --git a/packages/dashboard/yarn.lock b/packages/dashboard/yarn.lock index d22c46cc..bbf6091b 100644 --- a/packages/dashboard/yarn.lock +++ b/packages/dashboard/yarn.lock @@ -640,6 +640,11 @@ clipboardy@2.3.0: execa "^1.0.0" 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: version "1.9.3" 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" 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: version "2.0.2" 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" 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: version "17.0.2" resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"