Access control

This commit is contained in:
DrMint 2023-07-26 01:14:10 +02:00
parent 1cda674782
commit e98fabfd0e
32 changed files with 571 additions and 206 deletions

28
package-lock.json generated
View File

@ -21,6 +21,7 @@
"devDependencies": {
"@types/dotenv": "^8.2.0",
"@types/express": "^4.17.9",
"@types/react-router-dom": "^5.3.3",
"copyfiles": "^2.4.1",
"nodemon": "^2.0.6",
"prettier": "^3.0.0",
@ -2998,6 +2999,12 @@
"@types/node": "*"
}
},
"node_modules/@types/history": {
"version": "4.7.11",
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz",
"integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==",
"dev": true
},
"node_modules/@types/html-minifier-terser": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz",
@ -3098,6 +3105,27 @@
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-router": {
"version": "5.1.20",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz",
"integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==",
"dev": true,
"dependencies": {
"@types/history": "^4.7.11",
"@types/react": "*"
}
},
"node_modules/@types/react-router-dom": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz",
"integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==",
"dev": true,
"dependencies": {
"@types/history": "^4.7.11",
"@types/react": "*",
"@types/react-router": "*"
}
},
"node_modules/@types/react-transition-group": {
"version": "4.4.6",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz",

View File

@ -30,6 +30,7 @@
"devDependencies": {
"@types/dotenv": "^8.2.0",
"@types/express": "^4.17.9",
"@types/react-router-dom": "^5.3.3",
"copyfiles": "^2.4.1",
"nodemon": "^2.0.6",
"prettier": "^3.0.0",

View File

@ -0,0 +1,11 @@
import { Access } from "payload/config";
import { Recorder } from "../../types/collections";
import { RecordersRoles } from "../../constants";
import { isUndefined } from "../../utils/asserts";
export const mustBeAdminOrSelf: Access = ({ req }) => {
const user = req.user as Recorder | undefined;
if (isUndefined(user)) return false;
if (user.role.includes(RecordersRoles.Admin)) return true;
return { id: { equals: user.id } };
};

View File

@ -0,0 +1,8 @@
import { Recorder } from "../../types/collections";
import { isUndefined } from "../../utils/asserts";
export const mustHaveAtLeastOneRole = ({ req }): boolean => {
const user = req.user as Recorder | undefined;
if (isUndefined(user)) return false;
return user.role.length > 0;
};

View File

@ -0,0 +1,9 @@
import { Recorder } from "../types/collections";
import { RecordersRoles } from "../constants";
import { isUndefined } from "../utils/asserts";
export const mustBeAdmin = ({ req }): boolean => {
const user = req.user as Recorder | undefined;
if (isUndefined(user)) return false;
return user.role.includes(RecordersRoles.Admin);
};

View File

@ -22,6 +22,7 @@ export const ContentFolders = buildCollectionConfig(
admin: {
useAsTitle: fields.slug,
defaultColumns: [fields.slug, fields.translations],
disableDuplicate: true,
group: CollectionGroup.Collections,
},
timestamps: false,

View File

@ -16,6 +16,7 @@ export const ContentThumbnails = buildCollectionConfig(
defaultSort: fields.filename,
admin: {
useAsTitle: fields.filename,
disableDuplicate: true,
group: CollectionGroup.Media,
},
upload: {

View File

@ -9,6 +9,9 @@ import { fileField } from "../../fields/fileField/fileField";
import { contentBlocks } from "./Blocks/blocks";
import { ContentThumbnails } from "../ContentThumbnails/ContentThumbnails";
import { buildVersionedCollectionConfig } from "../../utils/versionedCollectionConfig";
import { beforeDuplicatePiping } from "../../hooks/beforeDuplicatePiping";
import { beforeDuplicateUnpublish } from "../../hooks/beforeDuplicateUnpublish";
import { beforeDuplicateAddCopyTo } from "../../hooks/beforeDuplicateAddCopyTo";
const fields = {
slug: "slug",
@ -53,6 +56,12 @@ export const Contents = buildVersionedCollectionConfig(
fields.status,
],
group: CollectionGroup.Collections,
hooks: {
beforeDuplicate: beforeDuplicatePiping([
beforeDuplicateUnpublish,
beforeDuplicateAddCopyTo(fields.slug),
]),
},
preview: (doc) => `https://accords-library.com/contents/${doc.slug}`,
},
fields: [

View File

@ -0,0 +1,39 @@
import { mustBeAdmin } from "../../accesses/mustBeAdmin";
import { CollectionGroup } from "../../constants";
import { buildCollectionConfig } from "../../utils/collectionConfig";
const fields = {
id: "id",
} as const satisfies Record<string, string>;
export const Currencies = buildCollectionConfig(
{
singular: "Currency",
plural: "Currencies",
},
() => ({
defaultSort: fields.id,
admin: {
useAsTitle: fields.id,
defaultColumns: [fields.id],
disableDuplicate: true,
group: CollectionGroup.Meta,
},
access: { create: mustBeAdmin, update: mustBeAdmin },
timestamps: false,
fields: [
{
name: fields.id,
type: "text",
unique: true,
required: true,
validate: (value) => {
if (/^[A-Z]{3}$/g.test(value)) {
return true;
}
return "The code must be a valid ISO 4217 currency code (e.g: EUR, CAD...)";
},
},
],
})
);

View File

@ -15,6 +15,7 @@ export const Files = buildCollectionConfig(
defaultSort: fields.filename,
admin: {
useAsTitle: fields.filename,
disableDuplicate: true,
group: CollectionGroup.Media,
},
fields: [

View File

@ -5,6 +5,9 @@ import { localizedFields } from "../../fields/translatedFields/translatedFields"
import { Key } from "../../types/collections";
import { isDefined } from "../../utils/asserts";
import { buildCollectionConfig } from "../../utils/collectionConfig";
import { mustBeAdmin } from "../../accesses/mustBeAdmin";
import { beforeDuplicateAddCopyTo } from "../../hooks/beforeDuplicateAddCopyTo";
import { QuickFilters } from "../../components/QuickFilters";
const fields = {
slug: "slug",
@ -27,6 +30,26 @@ export const Keys: CollectionConfig = buildCollectionConfig(
useAsTitle: fields.slug,
defaultColumns: [fields.slug, fields.type, fields.translations],
group: CollectionGroup.Meta,
components: {
BeforeListTable: [
() =>
QuickFilters({
route: "/admin/collections/keys",
filters: [
{ label: "Wordings", filter: "where[type][equals]=Wordings" },
{ label: "∅ English", filter: "where[translations.language][not_equals]=en" },
{ label: "∅ French", filter: "where[translations.language][not_equals]=fr" },
],
}),
],
},
hooks: {
beforeDuplicate: beforeDuplicateAddCopyTo(fields.slug),
},
},
access: {
create: mustBeAdmin,
delete: mustBeAdmin,
},
timestamps: false,
versions: false,

View File

@ -1,5 +1,6 @@
import { CollectionGroup } from "../constants";
import { buildCollectionConfig } from "../utils/collectionConfig";
import { mustBeAdmin } from "../../accesses/mustBeAdmin";
import { CollectionGroup } from "../../constants";
import { buildCollectionConfig } from "../../utils/collectionConfig";
const fields = {
id: "id",
@ -16,8 +17,10 @@ export const Languages = buildCollectionConfig(
admin: {
useAsTitle: fields.name,
defaultColumns: [fields.name, fields.id],
disableDuplicate: true,
group: CollectionGroup.Meta,
},
access: { create: mustBeAdmin, update: mustBeAdmin },
timestamps: false,
fields: [
{

View File

@ -16,6 +16,7 @@ export const LibraryItemThumbnails = buildCollectionConfig(
defaultSort: fields.filename,
admin: {
useAsTitle: fields.filename,
disableDuplicate: true,
group: CollectionGroup.Media,
},
upload: {

View File

@ -11,8 +11,13 @@ import { isDefined, isUndefined } from "../../utils/asserts";
import { LibraryItemThumbnails } from "../LibraryItemThumbnails/LibraryItemThumbnails";
import { LibraryItem } from "../../types/collections";
import { Keys } from "../Keys/Keys";
import { Languages } from "../Languages";
import { Languages } from "../Languages/Languages";
import { buildVersionedCollectionConfig } from "../../utils/versionedCollectionConfig";
import { beforeDuplicateAddCopyTo } from "../../hooks/beforeDuplicateAddCopyTo";
import { beforeDuplicateUnpublish } from "../../hooks/beforeDuplicateUnpublish";
import { beforeDuplicatePiping } from "../../hooks/beforeDuplicatePiping";
import { Currencies } from "../Currencies/Currencies";
import { optionalGroupField } from "../../fields/optionalGroupField/optionalGroupField";
const fields = {
status: "status",
@ -29,6 +34,9 @@ const fields = {
width: "width",
height: "height",
thickness: "thickness",
price: "price",
priceAmount: "amount",
priceCurrency: "currency",
releaseDate: "releaseDate",
itemType: "itemType",
textual: "textual",
@ -39,19 +47,24 @@ const fields = {
textualLanguages: "languages",
audio: "audio",
audioSubtype: "audioSubtype",
scans: "scans",
scansCover: "cover",
scansCoverFront: "front",
scansCoverSpine: "spine",
scansCoverBack: "back",
scansDustjacket: "dustjacket",
scansDustjacketFront: "front",
scansDustjacketSpine: "spine",
scansDustjacketBack: "back",
scansObibelt: "obibelt",
scansObibeltFront: "front",
scansObibeltSpine: "spine",
scansObibeltBack: "back",
scansPages: "pages",
scansPagesPage: "page",
scansPagesImage: "image",
} as const satisfies Record<string, string>;
const validateSizeValue = (value?: number) => {
if (isDefined(value) && value <= 0) return "This value must be greater than 0";
return true;
};
const validateRequiredSizeValue = (value?: number) => {
if (isUndefined(value)) return "This field is required.";
if (value <= 0) return "This value must be greater than 0.";
return true;
};
export const LibraryItems = buildVersionedCollectionConfig(
{
singular: "Library Item",
@ -63,9 +76,15 @@ export const LibraryItems = buildVersionedCollectionConfig(
useAsTitle: fields.slug,
description:
"A comprehensive list of all Yokoverses side materials (books, novellas, artbooks, \
stage plays, manga, drama CDs, and comics).",
stage plays, manga, drama CDs, and comics).",
defaultColumns: [fields.slug, fields.thumbnail, fields.status],
group: CollectionGroup.Collections,
hooks: {
beforeDuplicate: beforeDuplicatePiping([
beforeDuplicateUnpublish,
beforeDuplicateAddCopyTo(fields.slug),
]),
},
preview: (doc) => `https://accords-library.com/library/${doc.slug}`,
},
fields: [
@ -135,9 +154,115 @@ stage plays, manga, drama CDs, and comics).",
},
],
},
optionalGroupField({
name: fields.scans,
fields: [
optionalGroupField({
name: fields.scansCover,
fields: [
{
name: "size",
type: "group",
type: "row",
fields: [
imageField({
name: fields.scansCoverFront,
relationTo: LibraryItemThumbnails.slug,
admin: { width: "33%" },
}),
imageField({
name: fields.scansCoverSpine,
relationTo: LibraryItemThumbnails.slug,
admin: { width: "33%" },
}),
imageField({
name: fields.scansCoverBack,
relationTo: LibraryItemThumbnails.slug,
admin: { width: "33%" },
}),
],
},
],
}),
optionalGroupField({
name: fields.scansDustjacket,
label: "Dust Jacket",
labels: { singular: "Dust Jacket", plural: "Dust Jackets" },
fields: [
{
type: "row",
fields: [
imageField({
name: fields.scansDustjacketFront,
relationTo: LibraryItemThumbnails.slug,
admin: { width: "33%" },
}),
imageField({
name: fields.scansDustjacketSpine,
relationTo: LibraryItemThumbnails.slug,
admin: { width: "33%" },
}),
imageField({
name: fields.scansDustjacketBack,
relationTo: LibraryItemThumbnails.slug,
admin: { width: "33%" },
}),
],
},
],
}),
optionalGroupField({
name: fields.scansObibelt,
label: "Obi Belt",
labels: { singular: "Obi Belt", plural: "Obi Belts" },
fields: [
{
type: "row",
fields: [
imageField({
name: fields.scansObibeltFront,
relationTo: LibraryItemThumbnails.slug,
admin: { width: "33%" },
}),
imageField({
name: fields.scansObibeltSpine,
relationTo: LibraryItemThumbnails.slug,
admin: { width: "33%" },
}),
imageField({
name: fields.scansObibeltBack,
relationTo: LibraryItemThumbnails.slug,
admin: { width: "33%" },
}),
],
},
],
}),
{
name: fields.scansPages,
type: "array",
fields: [
{
type: "row",
fields: [
{
name: fields.scansPagesPage,
type: "number",
required: true,
admin: { width: "33%" },
},
imageField({
name: fields.scansPagesImage,
relationTo: LibraryItemThumbnails.slug,
required: true,
admin: { width: "66%" },
}),
],
},
],
},
],
}),
optionalGroupField({
name: fields.size,
admin: { condition: (data) => !data.digital },
fields: [
{
@ -146,25 +271,49 @@ stage plays, manga, drama CDs, and comics).",
{
name: fields.width,
type: "number",
validate: validateRequiredSizeValue,
required: true,
admin: { step: 1, width: "33%", description: "in mm." },
},
{
name: fields.height,
type: "number",
validate: validateRequiredSizeValue,
required: true,
admin: { step: 1, width: "33%", description: "in mm." },
},
{
name: fields.thickness,
type: "number",
validate: validateSizeValue,
admin: { step: 1, width: "33%", description: "in mm." },
},
],
},
],
}),
optionalGroupField({
name: fields.price,
admin: { className: "group-array" },
fields: [
{
type: "row",
fields: [
{
name: fields.priceAmount,
type: "number",
required: true,
min: 0,
admin: { width: "50%" },
},
{
name: fields.priceCurrency,
type: "relationship",
relationTo: Currencies.slug,
required: true,
admin: { allowCreate: false, width: "50%" },
},
],
},
],
}),
{
name: fields.itemType,
type: "radio",

View File

@ -16,6 +16,7 @@ export const PostThumbnails = buildCollectionConfig(
defaultSort: fields.filename,
admin: {
useAsTitle: fields.filename,
disableDuplicate: true,
group: CollectionGroup.Media,
},
upload: {

View File

@ -8,6 +8,9 @@ import { removeTranslatorsForTranscripts } from "./hooks/beforeValidate";
import { Keys } from "../Keys/Keys";
import { PostThumbnails } from "../PostThumbnails/PostThumbnails";
import { buildVersionedCollectionConfig } from "../../utils/versionedCollectionConfig";
import { beforeDuplicatePiping } from "../../hooks/beforeDuplicatePiping";
import { beforeDuplicateUnpublish } from "../../hooks/beforeDuplicateUnpublish";
import { beforeDuplicateAddCopyTo } from "../../hooks/beforeDuplicateAddCopyTo";
const fields = {
slug: "slug",
@ -36,9 +39,15 @@ export const Posts = buildVersionedCollectionConfig(
useAsTitle: fields.slug,
description:
"News articles written by our Recorders! Here you will find announcements about \
new merch/items releases, guides, theories, unboxings, showcases...",
new merch/items releases, guides, theories, unboxings, showcases...",
defaultColumns: [fields.slug, fields.thumbnail, fields.categories],
group: CollectionGroup.Collections,
hooks: {
beforeDuplicate: beforeDuplicatePiping([
beforeDuplicateUnpublish,
beforeDuplicateAddCopyTo(fields.slug),
]),
},
preview: (doc) => `https://accords-library.com/news/${doc.slug}`,
},
hooks: {

View File

@ -16,6 +16,7 @@ export const RecorderThumbnails = buildCollectionConfig(
defaultSort: fields.filename,
admin: {
useAsTitle: fields.filename,
disableDuplicate: true,
group: CollectionGroup.Media,
},
upload: {

View File

@ -1,10 +1,13 @@
import { localizedFields } from "../../fields/translatedFields/translatedFields";
import { Languages } from "../Languages";
import { beforeDuplicate } from "./hooks/beforeDuplicate";
import { CollectionGroup } from "../../constants";
import { Languages } from "../Languages/Languages";
import { CollectionGroup, RecordersRoles } from "../../constants";
import { RecorderThumbnails } from "../RecorderThumbnails/RecorderThumbnails";
import { imageField } from "../../fields/imageField/imageField";
import { buildCollectionConfig } from "../../utils/collectionConfig";
import { mustBeAdmin } from "../../accesses/mustBeAdmin";
import { mustBeAdminOrSelf } from "../../accesses/collections/mustBeAdminOrSelf";
import { beforeLoginMustHaveAtLeastOneRole } from "./hooks/beforeLoginMustHaveAtLeastOneRole";
import { QuickFilters } from "../../components/QuickFilters";
const fields = {
username: "username",
@ -13,6 +16,7 @@ const fields = {
biographies: "biographies",
biography: "biography",
avatar: "avatar",
role: "role",
} as const satisfies Record<string, string>;
export const Recorders = buildCollectionConfig(
@ -24,17 +28,43 @@ export const Recorders = buildCollectionConfig(
defaultSort: fields.username,
admin: {
useAsTitle: fields.username,
hooks: { beforeDuplicate },
description:
"Recorders are contributors of the Accord's Library project. Create a Recorder here to be able to credit them in other collections",
"Recorders are contributors of the Accord's Library project. Ask an admin to create a \
Recorder here to be able to credit them in other collections.",
defaultColumns: [
fields.username,
fields.avatar,
fields.anonymize,
fields.biographies,
fields.languages,
fields.role,
],
disableDuplicate: true,
group: CollectionGroup.Meta,
components: {
BeforeListTable: [
() =>
QuickFilters({
route: "/admin/collections/recorders",
filters: [
{ label: "Admins", filter: "where[role][equals]=Admin" },
{ label: "Recorders", filter: "where[role][equals]=Recorder" },
{ label: "∅ Role", filter: "where[role][not_in]=Admin,Recorder" },
{ label: "Anonymized", filter: "where[anonymize][equals]=true" },
],
}),
],
},
},
auth: true,
access: {
unlock: mustBeAdmin,
update: mustBeAdminOrSelf,
delete: mustBeAdmin,
create: mustBeAdmin,
},
hooks: {
beforeLogin: [beforeLoginMustHaveAtLeastOneRole],
},
timestamps: false,
fields: [
@ -75,6 +105,20 @@ export const Recorders = buildCollectionConfig(
},
fields: [{ name: fields.biography, type: "textarea" }],
}),
{
name: fields.role,
type: "select",
access: {
update: mustBeAdmin,
create: mustBeAdmin,
},
hasMany: true,
options: Object.entries(RecordersRoles).map(([value, label]) => ({
label,
value,
})),
admin: { position: "sidebar" },
},
{
name: fields.anonymize,
type: "checkbox",

View File

@ -1,9 +0,0 @@
import { BeforeDuplicate } from "payload/types";
import { Recorder } from "../../../types/collections";
export const beforeDuplicate: BeforeDuplicate<Recorder> = ({ data }) => {
return {
...data,
id: `${data.id}-copy`,
};
};

View File

@ -0,0 +1,7 @@
import { BeforeLoginHook } from "payload/dist/collections/config/types";
export const beforeLoginMustHaveAtLeastOneRole: BeforeLoginHook = ({ user }) => {
if (user.role.length === 0) {
throw new Error("User is not authorized to log-in.");
}
};

View File

@ -1,59 +0,0 @@
import { CollectionGroup, UserRoles } from "../constants";
import { Recorders } from "./Recorders/Recorders";
import { buildCollectionConfig } from "../utils/collectionConfig";
const fields = {
recorder: "recorder",
name: "name",
email: "email",
role: "role",
} as const satisfies Record<string, string>;
export const Users = buildCollectionConfig(
{
singular: "User",
plural: "Users",
},
() => ({
auth: true,
defaultSort: fields.recorder,
admin: {
useAsTitle: fields.name,
defaultColumns: [fields.recorder, fields.name, fields.email, fields.role],
group: CollectionGroup.Administration,
},
timestamps: false,
fields: [
{
type: "row",
fields: [
{
name: fields.recorder,
type: "relationship",
relationTo: Recorders.slug,
admin: { width: "33%" },
},
{
name: fields.name,
type: "text",
required: true,
unique: true,
admin: { width: "33%" },
},
{
name: fields.role,
required: true,
defaultValue: [UserRoles.Recorder],
type: "select",
hasMany: true,
options: Object.entries(UserRoles).map(([value, label]) => ({
label,
value,
})),
admin: { width: "33%" },
},
],
},
],
})
);

View File

@ -0,0 +1,47 @@
import React from "react";
import { styled } from "styled-components";
import { Link } from "react-router-dom";
type Props = {
route: string;
filters: { label: string; filter: string }[];
};
export const QuickFilters = ({ route, filters }: Props) => {
return (
<Container>
<div>Quick Filters:</div>
<FilterContainer>
<FilterCell label="None" to={route} />
{filters.map(({ label, filter }, index) => (
<FilterCell key={index} label={label} to={`${route}?${filter}`} />
))}
</FilterContainer>
</Container>
);
};
type FilterProps = {
label: string;
to: string;
};
const FilterCell = ({ label, to }: FilterProps) => (
<Link className="pill pill--has-action" to={to}>
{label}
</Link>
);
const Container = styled.div`
display: flex;
place-items: center;
gap: 1rem;
margin-top: -1rem;
margin-bottom: 2rem;
`;
const FilterContainer = styled.div`
display: flex;
place-items: center;
gap: 0.5rem;
`;

View File

@ -2,7 +2,6 @@ export enum CollectionGroup {
Collections = "Collections",
Media = "Media",
Meta = "Meta",
Administration = "Administration",
}
export enum KeysTypes {
@ -43,7 +42,12 @@ export enum LibraryItemsTextualPageOrders {
RightToLeft = "Right to left",
}
export enum UserRoles {
export enum RecordersRoles {
Admin = "Admin",
Recorder = "Recorder",
}
export enum CollectionStatus {
Draft = "draft",
Published = "published",
}

View File

@ -0,0 +1,14 @@
import { ArrayField } from "payload/types";
type Props = Omit<ArrayField, "type" | "maxRows" | "minRows">;
export const optionalGroupField = ({
admin: { className = "", ...otherAdmin } = {},
...otherProps
}: Props): ArrayField => ({
...otherProps,
type: "array",
minRows: 0,
maxRows: 1,
admin: { ...otherAdmin, className: `${className} group-array` },
});

View File

@ -1,7 +1,7 @@
import { ArrayField, Field } from "payload/types";
import { hasDuplicates } from "../../utils/validation";
import { isDefined, isUndefined } from "../../utils/asserts";
import { Languages } from "../../collections/Languages";
import { Languages } from "../../collections/Languages/Languages";
import { RowLabel } from "./RowLabel";
import { Cell } from "./Cell";

View File

@ -0,0 +1,5 @@
import { BeforeDuplicate } from "payload/types";
export const beforeDuplicateAddCopyTo =
(fieldName: string): BeforeDuplicate =>
({ data }) => ({ ...data, [fieldName]: `${data[fieldName]}-copy` });

View File

@ -0,0 +1,5 @@
import { BeforeDuplicate } from "payload/types";
export const beforeDuplicatePiping = (hooks: BeforeDuplicate[]): BeforeDuplicate => {
return ({ data: initialData }) => hooks.reduce((data, hook) => hook({ data }), initialData);
};

View File

@ -0,0 +1,7 @@
import { BeforeDuplicate } from "payload/types";
import { CollectionStatus } from "../constants";
export const beforeDuplicateUnpublish: BeforeDuplicate = ({ data }) => ({
...data,
_status: CollectionStatus.Draft,
});

View File

@ -1,7 +1,6 @@
import { buildConfig } from "payload/config";
import path from "path";
import { Users } from "./collections/Users";
import { Languages } from "./collections/Languages";
import { Languages } from "./collections/Languages/Languages";
import { Recorders } from "./collections/Recorders/Recorders";
import { Posts } from "./collections/Posts/Posts";
import { Keys } from "./collections/Keys/Keys";
@ -15,11 +14,12 @@ import { ContentThumbnails } from "./collections/ContentThumbnails/ContentThumbn
import { ContentFolders } from "./collections/ContentFolders/ContentFolders";
import { Logo } from "./components/Logo";
import { Icon } from "./components/Icon";
import { Currencies } from "./collections/Currencies/Currencies";
export default buildConfig({
serverURL: "https://dashboard.accords-library.com",
admin: {
user: Users.slug,
user: Recorders.slug,
components: { graphics: { Logo, Icon } },
meta: {
favicon: "/public/favicon.ico",
@ -39,9 +39,9 @@ export default buildConfig({
PostThumbnails,
Files,
Languages,
Currencies,
Recorders,
Keys,
Users,
],
globals: [],
telemetry: false,

View File

@ -45,3 +45,18 @@ html[data-theme="light"] {
--color-base-950: #11100b;
--color-base-1000: #000000;
}
.field-type.array-field.group-array {
.array-field__header-actions,
.collapsible__drag,
.collapsible__toggle,
.collapsible__header-wrap,
.collapsible__indicator,
.array-actions__add,
.array-actions__duplicate {
display: none;
}
.collapsible__actions-wrap {
z-index: 1;
}
}

View File

@ -25,19 +25,18 @@ export type ContentFoldersTranslation = {
export interface Config {
collections: {
"library-items": LibraryItem;
'library-items': LibraryItem;
contents: Content;
"content-folders": ContentFolder;
'content-folders': ContentFolder;
posts: Post;
"content-thumbnails": ContentThumbnail;
"library-item-thumbnails": LibraryItemThumbnail;
"recorder-thumbnails": RecorderThumbnail;
"post-thumbnails": PostThumbnail;
'content-thumbnails': ContentThumbnail;
'library-item-thumbnails': LibraryItemThumbnail;
'recorder-thumbnails': RecorderThumbnail;
'post-thumbnails': PostThumbnail;
files: File;
languages: Language;
recorders: Recorder;
keys: Key;
users: User;
};
globals: {};
}
@ -53,50 +52,55 @@ export interface LibraryItem {
digital: boolean;
downloadable: boolean;
size?: {
width?: number;
height?: number;
width: number;
height: number;
thickness?: number;
};
itemType?: "Textual" | "Audio" | "Video" | "Game" | "Other";
id?: string;
}[];
price?: {
priceAmount: number;
id?: string;
}[];
itemType?: 'Textual' | 'Audio' | 'Video' | 'Game' | 'Other';
textual?: {
subtype?:
| {
value: string;
relationTo: "keys";
relationTo: 'keys';
}[]
| {
value: Key;
relationTo: "keys";
relationTo: 'keys';
}[];
languages?:
| {
value: string;
relationTo: "languages";
relationTo: 'languages';
}[]
| {
value: Language;
relationTo: "languages";
relationTo: 'languages';
}[];
pageCount?: number;
bindingType?: "Paperback" | "Hardcover";
pageOrder?: "LeftToRight" | "RightToLeft";
bindingType?: 'Paperback' | 'Hardcover';
pageOrder?: 'LeftToRight' | 'RightToLeft';
};
audio?: {
audioSubtype?:
| {
value: string;
relationTo: "keys";
relationTo: 'keys';
}[]
| {
value: Key;
relationTo: "keys";
relationTo: 'keys';
}[];
};
releaseDate?: string;
lastModifiedBy: string | User;
lastModifiedBy: string | Recorder;
updatedAt: string;
createdAt: string;
_status?: "draft" | "published";
_status?: 'draft' | 'published';
}
export interface LibraryItemThumbnail {
id: string;
@ -139,27 +143,30 @@ export interface Key {
id: string;
slug: string;
type:
| "Contents"
| "LibraryAudio"
| "LibraryVideo"
| "LibraryTextual"
| "LibraryGroup"
| "Library"
| "Weapons"
| "GamePlatforms"
| "Categories"
| "Wordings";
| 'Contents'
| 'LibraryAudio'
| 'LibraryVideo'
| 'LibraryTextual'
| 'LibraryGroup'
| 'Library'
| 'Weapons'
| 'GamePlatforms'
| 'Categories'
| 'Wordings';
translations?: CategoryTranslations;
}
export interface Language {
id: string;
name: string;
}
export interface User {
export interface Recorder {
id: string;
recorder: string | Recorder;
name: string;
role: ("Admin" | "Recorder")[];
username: string;
avatar?: string | RecorderThumbnail;
languages?: string[] | Language[];
biographies?: RecorderBiographies;
role?: ('Admin' | 'Recorder')[];
anonymize: boolean;
email: string;
resetPasswordToken?: string;
resetPasswordExpiration?: string;
@ -169,14 +176,6 @@ export interface User {
lockUntil?: string;
password?: string;
}
export interface Recorder {
id: string;
username: string;
avatar?: string | RecorderThumbnail;
languages?: string[] | Language[];
biographies?: RecorderBiographies;
anonymize: boolean;
}
export interface RecorderThumbnail {
id: string;
updatedAt: string;
@ -213,15 +212,15 @@ export interface Content {
categories?:
| {
value: string;
relationTo: "keys";
relationTo: 'keys';
}[]
| {
value: Key;
relationTo: "keys";
relationTo: 'keys';
}[];
type?: {
value: string | Key;
relationTo: "keys";
relationTo: 'keys';
};
translations: {
language: string | Language;
@ -240,10 +239,10 @@ export interface Content {
audio?: string | File;
id?: string;
}[];
lastModifiedBy: string | User;
lastModifiedBy: string | Recorder;
updatedAt: string;
createdAt: string;
_status?: "draft" | "published";
_status?: 'draft' | 'published';
}
export interface ContentThumbnail {
id: string;
@ -280,25 +279,19 @@ export interface TextBlock {
}[];
id?: string;
blockName?: string;
blockType: "textBlock";
blockType: 'textBlock';
}
export interface Section {
content?: (Section_Section | Section_Tabs | TranscriptBlock | QuoteBlock | TextBlock)[];
id?: string;
blockName?: string;
blockType: "section";
blockType: 'section';
}
export interface Section_Section {
content?: (
| Section_Section_Section
| Section_Section_Tabs
| TranscriptBlock
| QuoteBlock
| TextBlock
)[];
content?: (Section_Section_Section | Section_Section_Tabs | TranscriptBlock | QuoteBlock | TextBlock)[];
id?: string;
blockName?: string;
blockType: "section";
blockType: 'section';
}
export interface Section_Section_Section {
content?: (
@ -310,25 +303,25 @@ export interface Section_Section_Section {
)[];
id?: string;
blockName?: string;
blockType: "section";
blockType: 'section';
}
export interface Section_Section_Section_Section {
content?: (Section_Section_Section_Section_Section | TranscriptBlock | QuoteBlock | TextBlock)[];
id?: string;
blockName?: string;
blockType: "section";
blockType: 'section';
}
export interface Section_Section_Section_Section_Section {
content?: (TranscriptBlock | QuoteBlock | TextBlock)[];
id?: string;
blockName?: string;
blockType: "section";
blockType: 'section';
}
export interface TranscriptBlock {
lines: (LineBlock | CueBlock)[];
id?: string;
blockName?: string;
blockType: "transcriptBlock";
blockType: 'transcriptBlock';
}
export interface LineBlock {
content: {
@ -336,13 +329,13 @@ export interface LineBlock {
}[];
id?: string;
blockName?: string;
blockType: "lineBlock";
blockType: 'lineBlock';
}
export interface CueBlock {
content: string;
id?: string;
blockName?: string;
blockType: "cueBlock";
blockType: 'cueBlock';
}
export interface QuoteBlock {
from: string;
@ -351,120 +344,120 @@ export interface QuoteBlock {
}[];
id?: string;
blockName?: string;
blockType: "quoteBlock";
blockType: 'quoteBlock';
}
export interface Section_Section_Section_Tabs {
tabs?: Section_Section_Section_Tabs_Tab[];
id?: string;
blockName?: string;
blockType: "tabs";
blockType: 'tabs';
}
export interface Section_Section_Section_Tabs_Tab {
content?: (Section_Section_Section_Tabs_Tab_Section | TranscriptBlock | QuoteBlock | TextBlock)[];
id?: string;
blockName?: string;
blockType: "tab";
blockType: 'tab';
}
export interface Section_Section_Section_Tabs_Tab_Section {
content?: (TranscriptBlock | QuoteBlock | TextBlock)[];
id?: string;
blockName?: string;
blockType: "section";
blockType: 'section';
}
export interface Section_Section_Tabs {
tabs?: Section_Section_Tabs_Tab[];
id?: string;
blockName?: string;
blockType: "tabs";
blockType: 'tabs';
}
export interface Section_Section_Tabs_Tab {
content?: (Section_Section_Tabs_Tab_Section | TranscriptBlock | QuoteBlock | TextBlock)[];
id?: string;
blockName?: string;
blockType: "tab";
blockType: 'tab';
}
export interface Section_Section_Tabs_Tab_Section {
content?: (Section_Section_Tabs_Tab_Section_Section | TranscriptBlock | QuoteBlock | TextBlock)[];
id?: string;
blockName?: string;
blockType: "section";
blockType: 'section';
}
export interface Section_Section_Tabs_Tab_Section_Section {
content?: (TranscriptBlock | QuoteBlock | TextBlock)[];
id?: string;
blockName?: string;
blockType: "section";
blockType: 'section';
}
export interface Section_Tabs {
tabs?: Section_Tabs_Tab[];
id?: string;
blockName?: string;
blockType: "tabs";
blockType: 'tabs';
}
export interface Section_Tabs_Tab {
content?: (Section_Tabs_Tab_Section | TranscriptBlock | QuoteBlock | TextBlock)[];
id?: string;
blockName?: string;
blockType: "tab";
blockType: 'tab';
}
export interface Section_Tabs_Tab_Section {
content?: (Section_Tabs_Tab_Section_Section | TranscriptBlock | QuoteBlock | TextBlock)[];
id?: string;
blockName?: string;
blockType: "section";
blockType: 'section';
}
export interface Section_Tabs_Tab_Section_Section {
content?: (Section_Tabs_Tab_Section_Section_Section | TranscriptBlock | QuoteBlock | TextBlock)[];
id?: string;
blockName?: string;
blockType: "section";
blockType: 'section';
}
export interface Section_Tabs_Tab_Section_Section_Section {
content?: (TranscriptBlock | QuoteBlock | TextBlock)[];
id?: string;
blockName?: string;
blockType: "section";
blockType: 'section';
}
export interface Tabs {
tabs?: Tabs_Tab[];
id?: string;
blockName?: string;
blockType: "tabs";
blockType: 'tabs';
}
export interface Tabs_Tab {
content?: (Tabs_Tab_Section | TranscriptBlock | QuoteBlock | TextBlock)[];
id?: string;
blockName?: string;
blockType: "tab";
blockType: 'tab';
}
export interface Tabs_Tab_Section {
content?: (Tabs_Tab_Section_Section | TranscriptBlock | QuoteBlock | TextBlock)[];
id?: string;
blockName?: string;
blockType: "section";
blockType: 'section';
}
export interface Tabs_Tab_Section_Section {
content?: (Tabs_Tab_Section_Section_Section | TranscriptBlock | QuoteBlock | TextBlock)[];
id?: string;
blockName?: string;
blockType: "section";
blockType: 'section';
}
export interface Tabs_Tab_Section_Section_Section {
content?: (Tabs_Tab_Section_Section_Section_Section | TranscriptBlock | QuoteBlock | TextBlock)[];
id?: string;
blockName?: string;
blockType: "section";
blockType: 'section';
}
export interface Tabs_Tab_Section_Section_Section_Section {
content?: (TranscriptBlock | QuoteBlock | TextBlock)[];
id?: string;
blockName?: string;
blockType: "section";
blockType: 'section';
}
export interface File {
id: string;
filename: string;
type: "LibraryScans" | "LibrarySoundtracks" | "ContentVideo" | "ContentAudio";
type: 'LibraryScans' | 'LibrarySoundtracks' | 'ContentVideo' | 'ContentAudio';
updatedAt: string;
createdAt: string;
}
@ -475,20 +468,20 @@ export interface ContentFolder {
subfolders?:
| {
value: string;
relationTo: "content-folders";
relationTo: 'content-folders';
}[]
| {
value: ContentFolder;
relationTo: "content-folders";
relationTo: 'content-folders';
}[];
contents?:
| {
value: string;
relationTo: "contents";
relationTo: 'contents';
}[]
| {
value: Content;
relationTo: "contents";
relationTo: 'contents';
}[];
}
export interface Post {
@ -498,20 +491,20 @@ export interface Post {
authors:
| {
value: string;
relationTo: "recorders";
relationTo: 'recorders';
}[]
| {
value: Recorder;
relationTo: "recorders";
relationTo: 'recorders';
}[];
categories?:
| {
value: string;
relationTo: "keys";
relationTo: 'keys';
}[]
| {
value: Key;
relationTo: "keys";
relationTo: 'keys';
}[];
translations: {
language: string | Language;
@ -527,10 +520,10 @@ export interface Post {
}[];
publishedDate: string;
hidden?: boolean;
lastModifiedBy: string | User;
lastModifiedBy: string | Recorder;
updatedAt: string;
createdAt: string;
_status?: "draft" | "published";
_status?: 'draft' | 'published';
}
export interface PostThumbnail {
id: string;

View File

@ -1,29 +1,26 @@
import { CollectionBeforeChangeHook, CollectionConfig, RelationshipField } from "payload/types";
import { Users } from "../collections/Users";
import {
BuildCollectionConfig,
GenerationFunctionProps,
buildCollectionConfig,
} from "./collectionConfig";
import { Recorders } from "../collections/Recorders/Recorders";
const fields = { lastModifiedBy: "lastModifiedBy" };
const beforeChangeLastModifiedBy: CollectionBeforeChangeHook = async ({
data: { updatedBy, ...data },
req,
}) => {
console.log(data, req.user);
return {
}) => ({
...data,
[fields.lastModifiedBy]: req.user.id,
};
};
});
const lastModifiedByField = (): RelationshipField => ({
name: fields.lastModifiedBy,
type: "relationship",
required: true,
relationTo: Users.slug,
relationTo: Recorders.slug,
admin: { readOnly: true, position: "sidebar" },
});