├── .gitignore
├── CNAME
├── LICENSE
├── README.md
├── _config.yml
├── _data
└── navigation.yml
├── _includes
└── head
│ └── custom.html
├── admin
├── .eslintrc.cjs
├── .gitignore
├── .prettierrc.json
├── .vscode
│ └── extensions.json
├── dist
│ ├── assets
│ │ ├── Autocomplete-P8uNMzjf.js
│ │ ├── Checkbox-xBLiHrYK.js
│ │ ├── Color-BI1EptyT.js
│ │ ├── Combobox-BzsnNK90.js
│ │ ├── Date-2WkyCTxd.js
│ │ ├── FilesView-DBubzPcf.css
│ │ ├── FilesView-DpxfpJ-I.js
│ │ ├── Image-KH4HD0zm.js
│ │ ├── Image-hEc3DOqi.css
│ │ ├── Images-BYrGx0Vh.css
│ │ ├── Images-DNp4xmDu.js
│ │ ├── LoginView-CqvYop6P.css
│ │ ├── LoginView-DoTNu-IV.js
│ │ ├── Markdown-BbjKacP0.js
│ │ ├── Markdown-DjYTd6Zz.css
│ │ ├── Number-C9GxbshK.js
│ │ ├── PageDetails-BczciiEp.js
│ │ ├── PageDetails-D0v-PlsS.css
│ │ ├── PagesView-B4WiDGRv.js
│ │ ├── PagesView-C-94Jo-m.css
│ │ ├── Plaintext-yL1SvkOj.js
│ │ ├── Radio-DPUiKW3l.js
│ │ ├── Range-rc27Zhut.js
│ │ ├── Select-arf17DoR.js
│ │ ├── Slider-GEMU6uF-.js
│ │ ├── String-2Ct3yEMZ.js
│ │ ├── Switch-CtwBo_h3.js
│ │ ├── Table-Drh1Nup_.js
│ │ ├── Text-TjwpRchm.js
│ │ ├── bg-full-DfnNwNLA.jpg
│ │ ├── ckeditor5-BlBZuSiD.css
│ │ ├── ckeditor5-DOqJdLtc.js
│ │ ├── index-CzuiuYVM.js
│ │ ├── materialdesignicons-webfont-B7mPwVP_.ttf
│ │ ├── materialdesignicons-webfont-CSr8KVlo.eot
│ │ ├── materialdesignicons-webfont-Dp5v-WZN.woff2
│ │ └── materialdesignicons-webfont-PXm3-2wK.woff
│ ├── favicon.ico
│ ├── index.css
│ └── index.js
├── index.html
├── package-lock.json
├── package.json
├── src
│ ├── App.vue
│ ├── assets
│ │ └── base.css
│ ├── components
│ │ ├── AsideCount.vue
│ │ ├── AsideList.vue
│ │ ├── AsideMeta.vue
│ │ ├── ElementDetails.vue
│ │ ├── ElementDetailsElement.vue
│ │ ├── ElementDetailsRefs.vue
│ │ ├── ElementList.vue
│ │ ├── ElementListItems.vue
│ │ ├── Fields.vue
│ │ ├── FileDetails.vue
│ │ ├── FileDetailsFile.vue
│ │ ├── FileDetailsRefs.vue
│ │ ├── FileList.vue
│ │ ├── FileListItems.vue
│ │ ├── History.vue
│ │ ├── Navigation.vue
│ │ ├── PageDetails.vue
│ │ ├── PageDetailsContent.vue
│ │ ├── PageDetailsContentList.vue
│ │ ├── PageDetailsPage.vue
│ │ ├── PageDetailsPageConfig.vue
│ │ ├── PageDetailsPageMeta.vue
│ │ ├── PageDetailsPageProps.vue
│ │ ├── PageDetailsPreview.vue
│ │ ├── PageList.vue
│ │ ├── PageListItems.vue
│ │ ├── Schema.vue
│ │ ├── SchemaItems.vue
│ │ ├── User.vue
│ │ └── __tests__
│ │ │ └── HelloWorld.cy.js
│ ├── fields
│ │ ├── Audio.vue
│ │ ├── Autocomplete.vue
│ │ ├── Checkbox.vue
│ │ ├── Color.vue
│ │ ├── Combobox.vue
│ │ ├── Date.vue
│ │ ├── File.vue
│ │ ├── Html.vue
│ │ ├── Image.vue
│ │ ├── Images.vue
│ │ ├── Items.vue
│ │ ├── Markdown.vue
│ │ ├── Number.vue
│ │ ├── Plaintext.vue
│ │ ├── Radio.vue
│ │ ├── Range.vue
│ │ ├── Select.vue
│ │ ├── Slider.vue
│ │ ├── String.vue
│ │ ├── Switch.vue
│ │ ├── Table.vue
│ │ ├── Text.vue
│ │ ├── Url.vue
│ │ └── Video.vue
│ ├── graphql.js
│ ├── log.js
│ ├── main.js
│ ├── routes.js
│ ├── stores.js
│ ├── utils.js
│ └── views
│ │ └── LoginView.vue
└── vite.config.js
├── assets
├── android-chrome-192x192.png
├── android-chrome-512x512.png
├── apple-touch-icon.png
├── favicon-16x16.png
├── favicon-32x32.png
└── laravel-cms.svg
├── composer.json
├── config
└── cms.php
├── database
├── migrations
│ ├── 2023_02_18_202800_create_pages_table.php
│ ├── 2023_02_18_202810_create_elements_table.php
│ ├── 2023_02_18_202820_create_files_table.php
│ ├── 2023_02_18_202830_create_versions_table.php
│ ├── 2023_02_18_202840_create_page_element_table.php
│ ├── 2023_02_18_202840_create_version_element_table.php
│ ├── 2023_02_18_202850_create_element_file_table.php
│ ├── 2023_02_18_202850_create_page_file_table.php
│ ├── 2023_02_18_202850_create_version_file_table.php
│ └── 2023_02_18_202920_add_users_cmseditor.php
└── seeders
│ └── CmsSeeder.php
├── docs
├── graphql-authentication.md
├── graphql-datatypes.md
├── graphql-introduction.md
├── graphql-pages.md
├── jsonapi-content-only.md
├── jsonapi-introduction.md
├── jsonapi-root-megamenu.md
├── jsonapi-root-navigation.md
├── jsonapi-subpages-breadcrumb.md
└── laravel-cms-erm.svg
├── favicon.ico
├── graphql
└── cms.graphql
├── package-lock.json
├── phpunit.xml
├── public
├── article.css
├── blog.css
├── cms.css
├── prism.css
└── prism.js
├── site.webmanifest
├── src
├── CmsServiceProvider.php
├── Commands
│ ├── Install.php
│ ├── Publish.php
│ ├── Serve.php
│ └── User.php
├── Concerns
│ └── Tenancy.php
├── GraphQL
│ ├── Exception.php
│ ├── Mutations
│ │ ├── AddElement.php
│ │ ├── AddFile.php
│ │ ├── AddPage.php
│ │ ├── CmsLogin.php
│ │ ├── CmsLogout.php
│ │ ├── DropElement.php
│ │ ├── DropFile.php
│ │ ├── DropPage.php
│ │ ├── KeepElement.php
│ │ ├── KeepFile.php
│ │ ├── KeepPage.php
│ │ ├── MovePage.php
│ │ ├── PubElement.php
│ │ ├── PubFile.php
│ │ ├── PubPage.php
│ │ ├── PurgeElement.php
│ │ ├── PurgeFile.php
│ │ ├── PurgePage.php
│ │ ├── SaveElement.php
│ │ ├── SaveFile.php
│ │ └── SavePage.php
│ └── Query.php
├── Http
│ └── Controllers
│ │ └── PageController.php
├── JsonApi
│ └── V1
│ │ ├── Controllers
│ │ └── JsonapiController.php
│ │ ├── Elements
│ │ └── ElementSchema.php
│ │ ├── Pages
│ │ ├── PageCollectionQuery.php
│ │ ├── PageQuery.php
│ │ └── PageSchema.php
│ │ └── Server.php
├── Models
│ ├── Element.php
│ ├── File.php
│ ├── Page.php
│ └── Version.php
├── Permission.php
├── Policies
│ ├── ElementPolicy.php
│ ├── FilePolicy.php
│ └── PagePolicy.php
├── Scopes
│ ├── Status.php
│ ├── Tenancy.php
│ └── Timeframe.php
├── Tenancy.php
├── helpers.php
└── server.php
├── tests
├── CommandTest.php
├── GraphqlAuthTest.php
├── GraphqlElementTest.php
├── GraphqlFileTest.php
├── GraphqlPageTest.php
├── HelpersTest.php
├── JsonapiTest.php
├── TestAbstract.php
├── User.php
└── default-schema.graphql
└── views
├── admin.blade.php
├── article.blade.php
├── cards.blade.php
├── code.blade.php
├── heading.blade.php
├── html.blade.php
├── image-text.blade.php
├── image.blade.php
├── invalid.blade.php
├── page.blade.php
├── paragraph.blade.php
├── table.blade.php
└── video.blade.php
/.gitignore:
--------------------------------------------------------------------------------
1 | .phpunit.result.cache
2 | composer.lock
3 | vendor/
4 |
--------------------------------------------------------------------------------
/CNAME:
--------------------------------------------------------------------------------
1 | laravel-cms.org
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Aimeos
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/_config.yml:
--------------------------------------------------------------------------------
1 | remote_theme: "mmistakes/minimal-mistakes@4.24.0"
2 | plugins:
3 | - jekyll-include-cache
4 | include: ["docs"]
5 | name: "Aimeos"
6 | description: "Simple like Wordpress, powerful as Contentful! The easy, flexible and scalable API-first Laravel CMS package - Open Source"
7 | title: "Laravel CMS"
8 | title_separator: "|"
9 | og_image: /assets/laravel-cms.svg
10 | comments:
11 | provider: "disqus"
12 | disqus:
13 | shortname: "laravel-cms-org"
14 | lunr:
15 | search_within_pages: true
16 | defaults:
17 | - scope:
18 | path: ""
19 | type: pages
20 | values:
21 | classes: wide
22 | layout: single
23 | read_time: false
24 | author_profile: false
25 | share: false
26 | comments: true
27 | sidebar:
28 | nav: "docs"
29 |
--------------------------------------------------------------------------------
/_data/navigation.yml:
--------------------------------------------------------------------------------
1 | main:
2 | - title: "Frontend API"
3 | url: /jsonapi/introduction/
4 | - title: "Backend API"
5 | url: /graphql/introduction/
6 | - title: "Github"
7 | url: https://github.com/aimeos/laravel-cms
8 |
9 | docs:
10 | - title: "Frontend JSON:API"
11 | children:
12 | - title: "Introduction"
13 | url: /jsonapi/introduction/
14 | - title: "Root page with navigation"
15 | url: /jsonapi/root-navigation/
16 | - title: "Root page with mega-menu"
17 | url: /jsonapi/root-megamenu/
18 | - title: "Sub-pages with breadcrumb"
19 | url: /jsonapi/subpages-breadcrumb/
20 |
21 | - title: "Backend GraphQL API"
22 | children:
23 | - title: "Introduction"
24 | url: /graphql/introduction/
25 | - title: "Data types"
26 | url: /graphql/data-types/
27 | - title: "Authentication"
28 | url: /graphql/authentication/
29 | - title: "Managing pages"
30 | url: /graphql/pages/
31 |
--------------------------------------------------------------------------------
/_includes/head/custom.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/admin/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | /* eslint-env node */
2 | require('@rushstack/eslint-patch/modern-module-resolution')
3 |
4 | module.exports = {
5 | root: true,
6 | 'extends': [
7 | 'plugin:vue/vue3-essential',
8 | 'eslint:recommended',
9 | '@vue/eslint-config-prettier/skip-formatting'
10 | ],
11 | overrides: [
12 | {
13 | files: [
14 | '**/__tests__/*.{cy,spec}.{js,ts,jsx,tsx}',
15 | 'cypress/e2e/**/*.{cy,spec}.{js,ts,jsx,tsx}'
16 | ],
17 | 'extends': [
18 | 'plugin:cypress/recommended'
19 | ]
20 | }
21 | ],
22 | parserOptions: {
23 | ecmaVersion: 'latest'
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/admin/.gitignore:
--------------------------------------------------------------------------------
1 | dist/index.html
2 |
3 | # Logs
4 | logs
5 | *.log
6 | npm-debug.log*
7 | yarn-debug.log*
8 | yarn-error.log*
9 | pnpm-debug.log*
10 | lerna-debug.log*
11 |
12 | node_modules
13 | .DS_Store
14 | dist-ssr
15 | coverage
16 | *.local
17 |
18 | /cypress/videos/
19 | /cypress/screenshots/
20 |
21 | # Editor directories and files
22 | .vscode/*
23 | !.vscode/extensions.json
24 | .idea
25 | *.suo
26 | *.ntvs*
27 | *.njsproj
28 | *.sln
29 | *.sw?
30 |
--------------------------------------------------------------------------------
/admin/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/prettierrc",
3 | "semi": false,
4 | "tabWidth": 2,
5 | "singleQuote": true,
6 | "printWidth": 100,
7 | "trailingComma": "none"
8 | }
--------------------------------------------------------------------------------
/admin/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
3 | }
4 |
--------------------------------------------------------------------------------
/admin/dist/assets/Autocomplete-P8uNMzjf.js:
--------------------------------------------------------------------------------
1 | import{_ as a,c as n,V as d,o as m}from"../index.js";const u={props:["modelValue","config"],emits:["update:modelValue"]};function s(l,o,e,i,c,r){return m(),n(d,{items:e.config.options||[],label:e.config.label||"",modelValue:e.modelValue,"onUpdate:modelValue":o[0]||(o[0]=t=>l.$emit("update:modelValue",t)),density:"comfortable",variant:"underlined"},null,8,["items","label","modelValue"])}const p=a(u,[["render",s]]);export{p as default};
2 |
--------------------------------------------------------------------------------
/admin/dist/assets/Checkbox-xBLiHrYK.js:
--------------------------------------------------------------------------------
1 | import{_ as t,c as d,a as n,o as u}from"../index.js";const s={props:["modelValue","config"],emits:["update:modelValue"]};function c(o,e,l,m,r,p){return u(),d(n,{label:l.config.label||"",modelValue:l.modelValue,"onUpdate:modelValue":e[0]||(e[0]=a=>o.$emit("update:modelValue",a))},null,8,["label","modelValue"])}const i=t(s,[["render",c]]);export{i as default};
2 |
--------------------------------------------------------------------------------
/admin/dist/assets/Color-BI1EptyT.js:
--------------------------------------------------------------------------------
1 | import{_ as t,c as r,b as d,o as s}from"../index.js";const u={props:["modelValue","config"],emits:["update:modelValue"]};function n(o,e,l,m,p,c){return s(),r(d,{modelValue:l.modelValue,"onUpdate:modelValue":e[0]||(e[0]=a=>o.$emit("update:modelValue",a))},null,8,["modelValue"])}const V=t(u,[["render",n]]);export{V as default};
2 |
--------------------------------------------------------------------------------
/admin/dist/assets/Combobox-BzsnNK90.js:
--------------------------------------------------------------------------------
1 | import{_ as t,c as i,d as n,o as m}from"../index.js";const u={props:["modelValue","config"],emits:["update:modelValue"]};function d(o,l,e,c,s,f){return m(),i(n,{items:e.config.options||[],label:e.config.label||"",multiple:e.config.multiple,chips:e.config.multiple,modelValue:e.modelValue,"onUpdate:modelValue":l[0]||(l[0]=a=>o.$emit("update:modelValue",a)),density:"comfortable",variant:"underlined",clearable:""},null,8,["items","label","multiple","chips","modelValue"])}const p=t(u,[["render",d]]);export{p as default};
2 |
--------------------------------------------------------------------------------
/admin/dist/assets/Date-2WkyCTxd.js:
--------------------------------------------------------------------------------
1 | import{_ as l,c as s,e as d,o as n}from"../index.js";const u={props:["modelValue","config"],emits:["update:modelValue"]};function m(o,e,a,r,c,p){return n(),s(d,{modelValue:a.modelValue,"onUpdate:modelValue":e[0]||(e[0]=t=>o.$emit("update:modelValue",t)),"show-adjacent-months":""},null,8,["modelValue"])}const V=l(u,[["render",m]]);export{V as default};
2 |
--------------------------------------------------------------------------------
/admin/dist/assets/FilesView-DBubzPcf.css:
--------------------------------------------------------------------------------
1 | nav.v-navigation-drawer{background-color:#ebf4ff}.v-sheet{padding:1rem 1%}
2 |
--------------------------------------------------------------------------------
/admin/dist/assets/FilesView-DpxfpJ-I.js:
--------------------------------------------------------------------------------
1 | import{b as m,P as u,N as f}from"./PageDetails-BczciiEp.js";import{_ as v,c as g,B as o,am as V,C as B,o as N,h as n,aa as $,ac as C,a1 as t,a6 as l,U as i,j as r,a2 as d}from"../index.js";import"./index-CzuiuYVM.js";const U={components:{Navigation:f,PageDetails:u},data:()=>({nav:null,aside:null,bgUrl:m})};function b(a,e,k,w,y,c){const p=B("Navigation");return N(),g(V,null,{default:o(()=>[n($,{elevation:2,density:"compact",image:a.bgUrl},{prepend:o(()=>[n(l,{onClick:e[0]||(e[0]=i(s=>a.nav=!a.nav,["stop"]))},{default:o(()=>[n(r,{size:"x-large"},{default:o(()=>[t(d(a.nav?"mdi-close":"mdi-menu"),1)]),_:1})]),_:1})]),default:o(()=>[n(C,null,{default:o(()=>e[3]||(e[3]=[t("Files")])),_:1}),n(l,{onClick:e[1]||(e[1]=i(s=>a.aside=!a.aside,["stop"]))},{default:o(()=>[n(r,{size:"x-large"},{default:o(()=>[t(d(a.aside?"mdi-chevron-right":"mdi-chevron-left"),1)]),_:1})]),_:1})]),_:1},8,["image"]),n(p,{modelValue:a.nav,"onUpdate:modelValue":e[2]||(e[2]=s=>a.nav=s)},null,8,["modelValue"])]),_:1})}const F=v(U,[["render",b]]);export{F as default};
2 |
--------------------------------------------------------------------------------
/admin/dist/assets/Image-KH4HD0zm.js:
--------------------------------------------------------------------------------
1 | import{g as n}from"./index-CzuiuYVM.js";import{_ as f,f as s,c as h,g as u,h as m,i as p,j as g,k as c,u as V,o,l as _}from"../index.js";const v={props:["modelValue","config"],emits:["update:modelValue"],setup(){return{app:V()}},data(){return{index:Math.floor(Math.random()*1e5)}},methods:{add(t){const e=t.target.files||t.dataTransfer.files||[];e.length&&(this.$emit("update:modelValue",{path:URL.createObjectURL(e[0]),uploading:!0}),this.$apollo.mutate({mutation:n`mutation($file: Upload!) {
2 | addFile(file: $file) {
3 | id
4 | mime
5 | name
6 | path
7 | previews
8 | }
9 | }`,variables:{file:e[0]},context:{hasUpload:!0}}).then(a=>{var i;if(a.errors)throw a.errors;const r=((i=a.data)==null?void 0:i.addFile)||{};r.previews=JSON.parse(r.previews)||{},URL.revokeObjectURL(this.modelValue.path),this.$emit("update:modelValue",r)}).catch(a=>{console.error("addFile()",a)}))},remove(){var t;(t=this.modelValue)!=null&&t.id&&this.$apollo.mutate({mutation:n`mutation($id: ID!) {
10 | dropFile(id: $id) {
11 | id
12 | }
13 | }`,variables:{id:this.modelValue.id}}).then(e=>{if(e.errors)throw e.errors;this.$emit("update:modelValue",null)}).catch(e=>{console.error(`dropFile(${code})`,e)})},srcset(t){let e=[];for(const a in t)e.push(`${this.url(t[a])} ${a}w`);return e.join(", ")},url(t){return t.startsWith("http")||t.startsWith("blob:")?t:this.app.urlfile.replace(/\/+$/g,"")+"/"+t}}},k={key:0,class:"image"},b={key:1,class:"image file-input"},y=["id"],w=["for"];function x(t,e,a,r,i,l){return a.modelValue?(o(),s("div",k,[a.modelValue.uploading?(o(),h(_,{key:0,color:"primary",height:"5",indeterminate:"",rounded:""})):u("",!0),m(p,{draggable:!1,src:l.url(a.modelValue.path),srcset:l.srcset(a.modelValue.previews)},null,8,["src","srcset"]),a.modelValue.id?(o(),s("button",{key:1,onClick:e[0]||(e[0]=d=>l.remove()),title:"Remove image",type:"button"},[m(g,{icon:"mdi-trash-can",role:"img"})])):u("",!0)])):(o(),s("div",b,[c("input",{type:"file",onInput:e[1]||(e[1]=d=>l.add(d)),id:"image-"+i.index,value:null,accept:"image/*",hidden:""},null,40,y),c("label",{for:"image-"+i.index},"Add file",8,w)]))}const U=f(v,[["render",x],["__scopeId","data-v-f7a48c39"]]);export{U as default};
14 |
--------------------------------------------------------------------------------
/admin/dist/assets/Image-hEc3DOqi.css:
--------------------------------------------------------------------------------
1 | .image[data-v-f7a48c39],.file-input[data-v-f7a48c39]{display:inline-flex;border:1px solid #808080;border-radius:.5rem;position:relative;height:178px;width:178px;margin:1px}.file-input label[data-v-f7a48c39]{display:flex;flex-wrap:wrap;align-content:center;justify-content:center;height:176px;width:176px}.image button[data-v-f7a48c39]{position:absolute;background-color:rgba(var(--v-theme-primary),.75);border-radius:.5rem;padding:.75rem;color:#fff;right:0;top:0}.v-progress-linear[data-v-f7a48c39]{position:absolute;z-index:1}
2 |
--------------------------------------------------------------------------------
/admin/dist/assets/Images-BYrGx0Vh.css:
--------------------------------------------------------------------------------
1 | .images[data-v-ebd9caae],.images .sortable[data-v-ebd9caae]{display:flex;flex-wrap:wrap}.image[data-v-ebd9caae],.file-input[data-v-ebd9caae]{display:inline-flex;border:1px solid #808080;border-radius:.5rem;position:relative;height:178px;width:178px;margin:1px}.file-input label[data-v-ebd9caae]{display:flex;flex-wrap:wrap;align-content:center;justify-content:center;height:176px;width:176px}.image button[data-v-ebd9caae]{position:absolute;background-color:rgba(var(--v-theme-primary),.75);border-radius:.5rem;padding:.75rem;color:#fff;right:0;top:0}.v-progress-linear[data-v-ebd9caae]{position:absolute;z-index:1}
2 |
--------------------------------------------------------------------------------
/admin/dist/assets/LoginView-CqvYop6P.css:
--------------------------------------------------------------------------------
1 | body{background-image:url(./bg-full-DfnNwNLA.jpg);background-size:cover}#app{display:flex;align-items:center;justify-content:center;height:100vh}.login .v-card{width:20rem;padding:1rem;background-color:#10446b;color:#fff;opacity:0}.login.show .v-card{opacity:1;transition:opacity .5s}.login .v-card-actions{justify-content:center}.login .v-theme--light,.login .v-field--error,.login .v-field--error:not(.v-field--disabled) .v-field__clearable>.v-icon{--v-theme-error: 255,167,38}.login .error{animation:shake .82s cubic-bezier(.36,.07,.19,.97) both;transform:translateZ(0)}@keyframes shake{10%,90%{transform:translate3d(-1px,0,0)}20%,80%{transform:translate3d(2px,0,0)}30%,50%,70%{transform:translate3d(-4px,0,0)}40%,60%{transform:translate3d(4px,0,0)}}
2 |
--------------------------------------------------------------------------------
/admin/dist/assets/LoginView-DoTNu-IV.js:
--------------------------------------------------------------------------------
1 | import{g as i}from"./index-CzuiuYVM.js";import{_ as m,c as u,B as r,U as p,W as t,X as f,Y as g,u as w,o as h,h as o,Z as V,$ as v,a0 as $,M as d,j as y,a1 as l,a2 as b,a3 as C,a4 as S,a5 as L,a6 as c}from"../index.js";const q={setup(){return{app:w()}},apollo:{me:{query:i`query{
2 | me {
3 | cmseditor
4 | name
5 | }
6 | }`,error(e){console.dir(e),this.message=e.message,this.failure=!0},update(e){this.handle(e.me)}}},data:()=>({creds:{email:"",password:""},form:null,error:null,loading:!1,login:!1,show:!1}),methods:{cmslogin(){if(!this.creds.email||!this.creds.password)return!1;this.error=null,this.loading=!0,this.$apollo.mutate({mutation:i`mutation ($email: String!, $password: String!) {
7 | cmsLogin(email: $email, password: $password) {
8 | cmseditor
9 | name
10 | }
11 | }`,variables:{email:this.creds.email,password:this.creds.password}}).then(e=>{this.handle(e.data.cmsLogin||null)}).catch(e=>{console.error(`cmsLogin(email: ${this.creds.email})`,e),this.error=e.message}).finally(()=>{this.loading=!1})},handle(e){e?e.name?e.cmseditor?(this.app.me=e.name,this.error=null,g.push("/pages")):this.error="Not a CMS editor":this.error=error.message:this.login=!0}}};function k(e,s,B,F,M,n){return h(),u(f,{class:t(["login",{show:e.login}]),modelValue:e.form,"onUpdate:modelValue":s[4]||(s[4]=a=>e.form=a),onSubmit:s[5]||(s[5]=p(a=>n.cmslogin(),["prevent"]))},{default:r(()=>[o(V,{loading:e.loading,class:t(["elevation-2",{error:e.error}])},{title:r(()=>s[6]||(s[6]=[l(" Login ")])),default:r(()=>[o(v,null,{default:r(()=>[o(d,{modelValue:e.creds.email,"onUpdate:modelValue":s[0]||(s[0]=a=>e.creds.email=a),label:"E-Mail",variant:"underlined","validate-on":"blur",rules:[a=>!!a||"Field is required",a=>!!a.match(/.+@.+/)||"Invalid e-mail address"],autocomplete:"username",autofocus:""},null,8,["modelValue","rules"]),o(d,{modelValue:e.creds.password,"onUpdate:modelValue":s[3]||(s[3]=a=>e.creds.password=a),type:e.show?"text":"password",label:"Password",variant:"underlined",rules:[a=>!!a||"Field is required"],autocomplete:"current-password"},{"append-inner":r(()=>[o(y,{onClick:s[1]||(s[1]=a=>e.show=!e.show),onKeydown:s[2]||(s[2]=a=>[13,32].includes(a.keyCode)?e.show=!e.show:!1)},{default:r(()=>[l(b(e.show?"mdi-eye-off":"mdi-eye"),1)]),_:1})]),_:1},8,["modelValue","type","rules"]),$(o(C,{color:"error",icon:"mdi-alert-octagon",text:"Error: "+e.error},null,8,["text"]),[[S,e.error]])]),_:1}),o(L,null,{default:r(()=>[o(c,{type:"submit",variant:"outlined",disabled:e.form!=!0},{default:r(()=>s[7]||(s[7]=[l("Login")])),_:1},8,["disabled"])]),_:1})]),_:1},8,["loading","class"])]),_:1},8,["class","modelValue"])}const N=m(q,[["render",k]]);export{N as default};
12 |
--------------------------------------------------------------------------------
/admin/dist/assets/Markdown-DjYTd6Zz.css:
--------------------------------------------------------------------------------
1 | .ck-editor__editable_inline{min-height:200px}
2 |
--------------------------------------------------------------------------------
/admin/dist/assets/Number-C9GxbshK.js:
--------------------------------------------------------------------------------
1 | import{_ as t,c as n,E as u,o as d}from"../index.js";const m={props:["modelValue","config"],emits:["update:modelValue"]};function r(o,e,l,s,c,i){return d(),n(u,{label:l.config.label||"",modelValue:l.modelValue,"onUpdate:modelValue":e[0]||(e[0]=a=>o.$emit("update:modelValue",a)),density:"comfortable",variant:"outlined"},null,8,["label","modelValue"])}const f=t(m,[["render",r]]);export{f as default};
2 |
--------------------------------------------------------------------------------
/admin/dist/assets/PageDetails-D0v-PlsS.css:
--------------------------------------------------------------------------------
1 | .v-navigation-drawer[data-v-ab757f2b]{z-index:0!important}.v-navigation-drawer--right .v-expansion-panel--active>.v-expansion-panel-title{min-height:unset}.v-navigation-drawer--right .v-expansion-panel{background-color:inherit}.v-navigation-drawer--right .v-list-item__content{color:#d0d0d0}.v-navigation-drawer--right .v-list-item--active .v-list-item__content{color:#fff}.v-navigation-drawer--right .v-list-item__overlay{opacity:0}.v-expansion-panel:nth-of-type(odd) .v-expansion-panel-title[data-v-9a99315b]{background-color:#ebf4ff}.icon[data-v-64544cac]{width:3rem}.v-timeline--vertical.v-timeline.v-timeline--side-end .v-timeline-item .v-timeline-item__body[data-v-a249e51e]{padding-inline-start:2.5%}.v-dialog .v-overlay__content>.v-card>.v-card-item+.v-card-text[data-v-a249e51e]{padding:1rem 2.5%}.v-timeline--vertical.v-timeline.v-timeline--side-end .v-timeline-item .v-timeline-item__opposite[data-v-a249e51e]{display:none}.v-timeline--vertical.v-timeline--justify-auto[data-v-a249e51e]{grid-template-columns:0 min-content auto}.v-card-text[data-v-a249e51e]{white-space:pre}.added[data-v-a249e51e]{background-color:#00ff0030}.removed[data-v-a249e51e]{background-color:#ff000030}.v-expansion-panel:nth-of-type(odd) .v-expansion-panel-title[data-v-8a7dcee5]{background-color:#ebf4ff}.btn-group[data-v-8a7dcee5]{padding:1rem 0}.header[data-v-8a7dcee5]{display:flex;justify-content:end}.subtabs[data-v-85522f67]{margin-bottom:2rem}.btn-group[data-v-85522f67]{padding:1rem 0}div.v-expansion-panel:nth-of-type(odd) .v-expansion-panel-title[data-v-361f44ef]{background-color:#40749b10}.v-expansion-panel.v-expansion-panel--active .v-expansion-panel-title[data-v-361f44ef]{background-color:#40749b!important;color:#fff}.v-expansion-panel-title[data-v-361f44ef]{padding:8px 16px}.v-expansion-panel-title .v-selection-control[data-v-361f44ef]{flex:none}.v-expansion-panel-title .content-type[data-v-361f44ef]{min-width:10rem}.v-expansion-panel-title .content-title[data-v-361f44ef]{font-weight:700}.v-expansion-panel:nth-of-type(odd) .v-expansion-panel-title[data-v-42ea131b]{background-color:#fafafa!important}.header[data-v-42ea131b]{display:flex;align-items:center;justify-content:space-between;padding:0 1rem;margin:0 .5rem 1rem}.bulk[data-v-42ea131b]{display:flex;align-items:center}.v-expansion-panel[data-v-42ea131b]{border-inline-start:3px solid inherit!important;border-radius:0}.v-expansion-panel-title__overlay[data-v-42ea131b]{background-color:transparent!important}.v-expansion-panel--active[data-v-42ea131b]{border-color:navy!important}.panel-heading[data-v-42ea131b]{overflow:hidden}.subtext[data-v-42ea131b]{display:block;overflow:hidden;max-width:25rem;max-height:1.75rem;padding-top:.25rem;text-overflow:ellipsis;white-space:nowrap;color:gray;font-size:80%}.btn-group[data-v-42ea131b]{padding:1rem 0}.v-input.search[data-v-42ea131b]{max-width:30rem}iframe{width:100%;padding:0;margin:0;border:none;min-height:calc(100vh - 6.5rem)}.v-app-bar-title[data-v-cc5223bf]{cursor:pointer}.v-toolbar__content>.v-toolbar-title[data-v-cc5223bf]{margin-inline-start:0}.v-toolbar-title__placeholder[data-v-cc5223bf]{text-align:center}.v-badge--dot .v-badge__badge[data-v-cc5223bf]{margin-inline-start:.5rem}
2 |
--------------------------------------------------------------------------------
/admin/dist/assets/PagesView-C-94Jo-m.css:
--------------------------------------------------------------------------------
1 | .header{display:flex;flex-wrap:wrap;align-items:flex-end;justify-content:space-between;margin-bottom:1rem}.v-input.search{width:100%;order:3}.draft{background-color:#ffe0c0;border-radius:50%}.drag-placeholder{height:48px}.drag-placeholder-wrapper .tree-node-inner{background-color:#fafafa}.tree-node-inner{display:flex;align-items:center;border-bottom:1px solid #103050;padding:.5rem 0}.node-content,.node-url{cursor:pointer}.node-content{justify-content:space-between;margin:0 1%;width:100%}.node-text,.node-url{text-overflow:ellipsis;overflow:hidden}.node-url,.page-title{font-size:80%;color:gray}.node-url{text-align:end;align-self:end}.page-time{vertical-align:text-top}.page-domain{color:initial}.page-name,.page-title,.page-domain{display:block}.page-title{height:2.5rem}.page-domain,.page-to{white-space:nowrap;text-overflow:ellipsis;overflow:hidden}.status-disabled .page-name{text-decoration:line-through}.status-hidden .node-text{color:gray}.trashed{text-decoration:line-through}.loading{animation:blink 1s;animation-direction:alternate;animation-iteration-count:infinite}@keyframes blink{0%{opacity:0}to{opacity:1}}.load{animation:rotate 2s;animation-iteration-count:infinite}@keyframes rotate{to{transform:rotate(360deg)}}@media (min-width: 500px){.v-input.search{max-width:30rem;margin:0 1rem;width:unset;order:unset}.tree-node-inner{padding:.25rem 0}.node-content{display:flex}.node-text,.node-url{max-width:50%}}@media (min-width: 960px){.page-title{height:1.25rem}}.page-tree,.page-details{position:absolute;min-height:100vh;right:0;left:0;top:0}
2 |
--------------------------------------------------------------------------------
/admin/dist/assets/Plaintext-yL1SvkOj.js:
--------------------------------------------------------------------------------
1 | import{_ as t,c as n,G as d,o as r}from"../index.js";const u={props:["modelValue","config"],emits:["update:modelValue"]};function s(l,e,a,m,c,i){return r(),n(d,{label:a.config.label||"",modelValue:a.modelValue,"onUpdate:modelValue":e[0]||(e[0]=o=>l.$emit("update:modelValue",o)),density:"comfortable",variant:"underlined",clearable:""},null,8,["label","modelValue"])}const p=t(u,[["render",s]]);export{p as default};
2 |
--------------------------------------------------------------------------------
/admin/dist/assets/Radio-DPUiKW3l.js:
--------------------------------------------------------------------------------
1 | import{_ as s,c as t,H as d,o as a,B as r,f as i,F as m,D as c,I as p}from"../index.js";const f={props:["modelValue","config"],emits:["update:modelValue"]};function V(u,l,o,_,B,g){return a(),t(d,{modelValue:o.modelValue,"onUpdate:modelValue":l[0]||(l[0]=e=>u.$emit("update:modelValue",e))},{default:r(()=>[(a(!0),i(m,null,c(o.config.options||[],(e,n)=>(a(),t(p,{label:e,value:n},null,8,["label","value"]))),256))]),_:1},8,["modelValue"])}const x=s(f,[["render",V]]);export{x as default};
2 |
--------------------------------------------------------------------------------
/admin/dist/assets/Range-rc27Zhut.js:
--------------------------------------------------------------------------------
1 | import{_ as t,c as d,J as n,o as s}from"../index.js";const u={props:["modelValue","config"],emits:["update:modelValue"]};function r(o,e,a,m,p,c){return s(),d(n,{modelValue:a.modelValue,"onUpdate:modelValue":e[0]||(e[0]=l=>o.$emit("update:modelValue",l))},null,8,["modelValue"])}const V=t(u,[["render",r]]);export{V as default};
2 |
--------------------------------------------------------------------------------
/admin/dist/assets/Select-arf17DoR.js:
--------------------------------------------------------------------------------
1 | import{_ as a,c as i,K as n,o as m}from"../index.js";const u={props:["modelValue","config"],emits:["update:modelValue"]};function c(t,l,e,d,s,f){return m(),i(n,{items:e.config.options||[],label:e.config.label||"",multiple:e.config.multiple,chips:e.config.multiple,modelValue:e.modelValue,"onUpdate:modelValue":l[0]||(l[0]=o=>t.$emit("update:modelValue",o)),density:"comfortable",variant:"underlined","item-title":"label"},null,8,["items","label","multiple","chips","modelValue"])}const p=a(u,[["render",c]]);export{p as default};
2 |
--------------------------------------------------------------------------------
/admin/dist/assets/Slider-GEMU6uF-.js:
--------------------------------------------------------------------------------
1 | import{_ as t,c as d,L as r,o as s}from"../index.js";const u={props:["modelValue","config"],emits:["update:modelValue"]};function n(o,e,l,m,p,c){return s(),d(r,{modelValue:l.modelValue,"onUpdate:modelValue":e[0]||(e[0]=a=>o.$emit("update:modelValue",a))},null,8,["modelValue"])}const V=t(u,[["render",n]]);export{V as default};
2 |
--------------------------------------------------------------------------------
/admin/dist/assets/String-2Ct3yEMZ.js:
--------------------------------------------------------------------------------
1 | import{_ as t,c as n,M as d,o as r}from"../index.js";const u={props:["modelValue","config"],emits:["update:modelValue"]};function s(a,e,l,i,m,c){return r(),n(d,{label:l.config.label||"",modelValue:l.modelValue,"onUpdate:modelValue":e[0]||(e[0]=o=>a.$emit("update:modelValue",o)),density:"comfortable",variant:"underlined",clearable:""},null,8,["label","modelValue"])}const p=t(u,[["render",s]]);export{p as default};
2 |
--------------------------------------------------------------------------------
/admin/dist/assets/Switch-CtwBo_h3.js:
--------------------------------------------------------------------------------
1 | import{_ as t,c as n,N as d,o as s}from"../index.js";const u={props:["modelValue","config"],emits:["update:modelValue"]};function c(o,e,l,m,i,r){return s(),n(d,{label:l.config.label||"",modelValue:l.modelValue,"onUpdate:modelValue":e[0]||(e[0]=a=>o.$emit("update:modelValue",a)),inset:""},null,8,["label","modelValue"])}const f=t(u,[["render",c]]);export{f as default};
2 |
--------------------------------------------------------------------------------
/admin/dist/assets/Table-Drh1Nup_.js:
--------------------------------------------------------------------------------
1 | import{_ as t,c as r,g as d,G as n,o as u}from"../index.js";const s={props:["modelValue","config"],emits:["update:modelValue"]};function m(e,a,l,p,c,i){return e.field.type==="table"?(u(),r(n,{key:0,placeholder:`val;val;val
2 | val;val;val`,"auto-grow":!0,modelValue:l.modelValue,"onUpdate:modelValue":a[0]||(a[0]=o=>e.$emit("update:modelValue",o)),variant:"underlined",density:"comfortable",clearable:""},null,8,["modelValue"])):d("",!0)}const V=t(s,[["render",m]]);export{V as default};
3 |
--------------------------------------------------------------------------------
/admin/dist/assets/Text-TjwpRchm.js:
--------------------------------------------------------------------------------
1 | import{_ as l,m as i,n as d,o as c,F as m,r as u,s as p,R as f,l as k,t as _,I as g,u as V,v as C,A as F,L as h,x as v}from"./ckeditor5-DOqJdLtc.js";import{_ as x,c as L,C as b,o as A}from"../index.js";const B={components:{Ckeditor:l},props:["modelValue","config"],emits:["update:modelValue"],data(){return{editor:v}},computed:{ckconfig(){return{licenseKey:"GPL",plugins:[i,d,c,m,u,p,f,k,_,g,V,C,F,h],toolbar:["undo","redo","removeFormat","|","bold","italic","strikethrough","code","link","|","fullscreen"]}}}};function P(o,e,a,R,t,s){const r=b("ckeditor");return A(),L(r,{config:s.ckconfig,editor:t.editor,modelValue:a.modelValue,"onUpdate:modelValue":e[0]||(e[0]=n=>o.$emit("update:modelValue",n))},null,8,["config","editor","modelValue"])}const I=x(B,[["render",P]]);export{I as default};
2 |
--------------------------------------------------------------------------------
/admin/dist/assets/bg-full-DfnNwNLA.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aimeos/laravel-cms/b449f93e5b9e370dc0e521e6d2cd5edfa5bfbcf7/admin/dist/assets/bg-full-DfnNwNLA.jpg
--------------------------------------------------------------------------------
/admin/dist/assets/materialdesignicons-webfont-B7mPwVP_.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aimeos/laravel-cms/b449f93e5b9e370dc0e521e6d2cd5edfa5bfbcf7/admin/dist/assets/materialdesignicons-webfont-B7mPwVP_.ttf
--------------------------------------------------------------------------------
/admin/dist/assets/materialdesignicons-webfont-CSr8KVlo.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aimeos/laravel-cms/b449f93e5b9e370dc0e521e6d2cd5edfa5bfbcf7/admin/dist/assets/materialdesignicons-webfont-CSr8KVlo.eot
--------------------------------------------------------------------------------
/admin/dist/assets/materialdesignicons-webfont-Dp5v-WZN.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aimeos/laravel-cms/b449f93e5b9e370dc0e521e6d2cd5edfa5bfbcf7/admin/dist/assets/materialdesignicons-webfont-Dp5v-WZN.woff2
--------------------------------------------------------------------------------
/admin/dist/assets/materialdesignicons-webfont-PXm3-2wK.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aimeos/laravel-cms/b449f93e5b9e370dc0e521e6d2cd5edfa5bfbcf7/admin/dist/assets/materialdesignicons-webfont-PXm3-2wK.woff
--------------------------------------------------------------------------------
/admin/dist/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aimeos/laravel-cms/b449f93e5b9e370dc0e521e6d2cd5edfa5bfbcf7/admin/dist/favicon.ico
--------------------------------------------------------------------------------
/admin/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Laravel CMS test environment
8 |
9 |
10 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/admin/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "admin",
3 | "version": "0.0.0",
4 | "private": true,
5 | "type": "module",
6 | "description": "Laravel CMS admin",
7 | "keywords": [
8 | "laravel",
9 | "cms",
10 | "vue"
11 | ],
12 | "scripts": {
13 | "dev": "vite",
14 | "build": "vite build",
15 | "preview": "vite preview",
16 | "test:e2e": "start-server-and-test preview http://localhost:4173 'cypress run --e2e'",
17 | "test:e2e:dev": "start-server-and-test 'vite dev --port 4173' http://localhost:4173 'cypress open --e2e'",
18 | "test:unit": "cypress run --component",
19 | "test:unit:dev": "cypress open --component",
20 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore",
21 | "format": "prettier --write src/"
22 | },
23 | "dependencies": {
24 | "@ckeditor/ckeditor5-vue": "^7.3.0",
25 | "@he-tree/vue": "^2.9",
26 | "@vue/apollo-option": "^4.2",
27 | "apollo-link-batch-http": "^1.2",
28 | "apollo-upload-client": "^18.0.1",
29 | "ckeditor5": "^45.0.0",
30 | "diff": "^7.0",
31 | "pinia": "^3.0",
32 | "sass": "^1.86",
33 | "vite-plugin-vuetify": "^2.1",
34 | "vue": "^3.5",
35 | "vue-draggable-plus": "^0.6.0",
36 | "vue-router": "^4.5",
37 | "vue3-observe-visibility": "^1.0.3",
38 | "vuedraggable": "^4.1.0",
39 | "vuetify": "^3.8"
40 | },
41 | "devDependencies": {
42 | "@mdi/font": "^7.4",
43 | "@rushstack/eslint-patch": "^1.11",
44 | "@vitejs/plugin-vue": "^5.2",
45 | "@vue/eslint-config-prettier": "^10.2.0",
46 | "cypress": "^14.2",
47 | "eslint": "^9.0",
48 | "eslint-plugin-cypress": "^4.2",
49 | "eslint-plugin-vue": "^10.0",
50 | "prettier": "^3.5",
51 | "start-server-and-test": "^2.0",
52 | "vite": "^6.3"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/admin/src/components/AsideCount.vue:
--------------------------------------------------------------------------------
1 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | {{ key }}
71 |
72 |
73 |
74 |
75 | {{ code }}
76 | {{ value }}
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
108 |
--------------------------------------------------------------------------------
/admin/src/components/AsideList.vue:
--------------------------------------------------------------------------------
1 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | {{ item.title }}
25 |
26 |
27 |
28 |
29 |
30 |
31 |
42 |
--------------------------------------------------------------------------------
/admin/src/components/AsideMeta.vue:
--------------------------------------------------------------------------------
1 |
38 |
39 |
40 |
41 |
42 | Meta data
43 |
44 |
45 |
46 | {{ key }}
47 | {{ value }}
48 |
49 |
50 |
51 |
52 |
53 |
54 |
73 |
--------------------------------------------------------------------------------
/admin/src/components/ElementDetailsElement.vue:
--------------------------------------------------------------------------------
1 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
81 |
82 |
83 |
93 |
94 |
95 |
96 |
97 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
--------------------------------------------------------------------------------
/admin/src/components/ElementDetailsRefs.vue:
--------------------------------------------------------------------------------
1 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 | Pages
84 |
85 |
86 |
87 |
88 | ID |
89 | URL |
90 | Name |
91 |
92 |
93 |
94 |
95 | {{ v.id }} |
96 | {{ v.slug }} |
97 | {{ v.name }} |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 | Versions
106 |
107 |
108 |
109 |
110 | ID |
111 | Type |
112 | Published |
113 |
114 |
115 |
116 |
117 | {{ v.id }} |
118 | {{ v.type }} |
119 | {{ v.published }} |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
--------------------------------------------------------------------------------
/admin/src/components/ElementList.vue:
--------------------------------------------------------------------------------
1 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | {{ drawer.nav ? 'mdi-close' : 'mdi-menu' }}
37 |
38 |
39 |
40 |
41 | Shared elements
42 |
43 |
44 |
45 |
46 |
47 |
48 | {{ drawer.aside ? 'mdi-chevron-right' : 'mdi-chevron-left' }}
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
74 |
75 |
76 |
78 |
--------------------------------------------------------------------------------
/admin/src/components/Fields.vue:
--------------------------------------------------------------------------------
1 |
71 |
72 |
73 |
74 | {{ field.label || code }}
75 |
86 |
87 |
88 |
89 |
--------------------------------------------------------------------------------
/admin/src/components/FileList.vue:
--------------------------------------------------------------------------------
1 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | {{ drawer.nav ? 'mdi-close' : 'mdi-menu' }}
36 |
37 |
38 |
39 |
40 | Files
41 |
42 |
43 |
44 |
45 |
46 |
47 | {{ drawer.aside ? 'mdi-chevron-right' : 'mdi-chevron-left' }}
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
73 |
74 |
75 |
77 |
--------------------------------------------------------------------------------
/admin/src/components/Navigation.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
18 | Pages
19 |
20 |
21 | Shared elements
22 |
23 |
24 | Files
25 |
26 |
27 |
28 |
29 |
30 |
37 |
--------------------------------------------------------------------------------
/admin/src/components/PageDetailsContent.vue:
--------------------------------------------------------------------------------
1 |
102 |
103 |
104 |
105 |
106 |
107 |
108 | {{ section }}
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
132 |
133 |
134 |
135 |
136 |
137 |
146 |
147 |
148 |
158 |
--------------------------------------------------------------------------------
/admin/src/components/PageDetailsPage.vue:
--------------------------------------------------------------------------------
1 |
59 |
60 |
61 |
62 |
63 |
64 | Details
68 | Meta
72 | Config
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
89 |
90 |
91 |
92 |
98 |
99 |
100 |
101 |
107 |
108 |
109 |
110 |
111 |
112 |
118 |
--------------------------------------------------------------------------------
/admin/src/components/PageDetailsPreview.vue:
--------------------------------------------------------------------------------
1 |
29 |
30 |
31 |
32 |
33 |
34 |
43 |
--------------------------------------------------------------------------------
/admin/src/components/PageList.vue:
--------------------------------------------------------------------------------
1 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | {{ drawer.nav ? 'mdi-close' : 'mdi-menu' }}
36 |
37 |
38 |
39 |
40 | Pages
41 |
42 |
43 |
44 |
45 |
46 |
47 | {{ drawer.aside ? 'mdi-chevron-right' : 'mdi-chevron-left' }}
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
73 |
74 |
75 |
77 |
--------------------------------------------------------------------------------
/admin/src/components/Schema.vue:
--------------------------------------------------------------------------------
1 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | Shared elements
21 |
22 |
23 |
24 |
25 |
26 |
27 |
38 |
--------------------------------------------------------------------------------
/admin/src/components/SchemaItems.vue:
--------------------------------------------------------------------------------
1 |
41 |
42 |
43 |
44 | {{ name }}
45 |
46 |
47 |
48 |
49 |
50 |
51 |
53 |
54 |
55 |
56 | {{ item.type }}
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
88 |
--------------------------------------------------------------------------------
/admin/src/components/User.vue:
--------------------------------------------------------------------------------
1 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | {{ user.name }}
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
56 |
--------------------------------------------------------------------------------
/admin/src/components/__tests__/HelloWorld.cy.js:
--------------------------------------------------------------------------------
1 | import HelloWorld from '../HelloWorld.vue'
2 |
3 | describe('HelloWorld', () => {
4 | it('playground', () => {
5 | cy.mount(HelloWorld, { props: { msg: 'Hello Cypress' } })
6 | })
7 |
8 | it('renders properly', () => {
9 | cy.mount(HelloWorld, { props: { msg: 'Hello Cypress' } })
10 | cy.get('h1').should('contain', 'Hello Cypress')
11 | })
12 | })
13 |
--------------------------------------------------------------------------------
/admin/src/fields/Audio.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
25 |
29 |
34 |
35 |
54 |
55 |
56 |
57 | Name: {{ file.name }}
58 | Mime: {{ file.mime }}
59 | Editor: {{ file.editor }}
60 | Updated: {{ (new Date(file.updated_at)).toLocaleString() }}
61 |
62 |
63 |
64 |
65 |
66 |
69 |
70 |
71 |
72 |
73 |
75 |
--------------------------------------------------------------------------------
/admin/src/fields/Checkbox.vue:
--------------------------------------------------------------------------------
1 |
24 |
25 |
26 |
34 |
35 |
--------------------------------------------------------------------------------
/admin/src/fields/Color.vue:
--------------------------------------------------------------------------------
1 |
30 |
31 |
32 |
42 |
43 |
--------------------------------------------------------------------------------
/admin/src/fields/Combobox.vue:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
30 |
31 |
--------------------------------------------------------------------------------
/admin/src/fields/Date.vue:
--------------------------------------------------------------------------------
1 |
44 |
45 |
46 |
64 |
65 |
--------------------------------------------------------------------------------
/admin/src/fields/Html.vue:
--------------------------------------------------------------------------------
1 |
38 |
39 |
40 |
53 |
54 |
--------------------------------------------------------------------------------
/admin/src/fields/Image.vue:
--------------------------------------------------------------------------------
1 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
50 |
55 |
60 |
61 |
80 |
81 |
82 |
83 | Name: {{ file.name }}
84 | Mime: {{ file.mime }}
85 | Editor: {{ file.editor }}
86 | Updated: {{ (new Date(file.updated_at)).toLocaleString() }}
87 |
88 |
89 |
90 |
91 |
92 |
95 |
96 |
97 |
98 |
99 |
106 |
--------------------------------------------------------------------------------
/admin/src/fields/Markdown.vue:
--------------------------------------------------------------------------------
1 |
50 |
51 |
52 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/admin/src/fields/Number.vue:
--------------------------------------------------------------------------------
1 |
46 |
47 |
48 |
64 |
65 |
--------------------------------------------------------------------------------
/admin/src/fields/Plaintext.vue:
--------------------------------------------------------------------------------
1 |
44 |
45 |
46 |
61 |
62 |
--------------------------------------------------------------------------------
/admin/src/fields/Radio.vue:
--------------------------------------------------------------------------------
1 |
24 |
25 |
26 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/admin/src/fields/Range.vue:
--------------------------------------------------------------------------------
1 |
24 |
25 |
26 |
36 |
37 | {{ modelValue[0] }}
38 |
39 |
40 | {{ modelValue[1] }}
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/admin/src/fields/Select.vue:
--------------------------------------------------------------------------------
1 |
38 |
39 |
40 |
56 |
57 |
--------------------------------------------------------------------------------
/admin/src/fields/Slider.vue:
--------------------------------------------------------------------------------
1 |
24 |
25 |
26 |
36 |
37 |
38 |
39 |
40 | {{ modelValue }}
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/admin/src/fields/String.vue:
--------------------------------------------------------------------------------
1 |
44 |
45 |
46 |
61 |
62 |
--------------------------------------------------------------------------------
/admin/src/fields/Switch.vue:
--------------------------------------------------------------------------------
1 |
24 |
25 |
26 |
36 |
37 |
--------------------------------------------------------------------------------
/admin/src/fields/Table.vue:
--------------------------------------------------------------------------------
1 |
50 |
51 |
52 |
68 |
69 |
--------------------------------------------------------------------------------
/admin/src/fields/Text.vue:
--------------------------------------------------------------------------------
1 |
49 |
50 |
51 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/admin/src/fields/Url.vue:
--------------------------------------------------------------------------------
1 |
56 |
57 |
58 |
72 |
73 |
--------------------------------------------------------------------------------
/admin/src/fields/Video.vue:
--------------------------------------------------------------------------------
1 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
25 |
31 |
36 |
37 |
56 |
57 |
58 |
59 | Name: {{ file.name }}
60 | Mime: {{ file.mime }}
61 | Editor: {{ file.editor }}
62 | Updated: {{ (new Date(file.updated_at)).toLocaleString() }}
63 |
64 |
65 |
66 |
67 |
68 |
71 |
72 |
73 |
74 |
75 |
81 |
--------------------------------------------------------------------------------
/admin/src/graphql.js:
--------------------------------------------------------------------------------
1 | import { BatchHttpLink } from "apollo-link-batch-http"
2 | import { createApolloProvider } from '@vue/apollo-option'
3 | import { ApolloClient, ApolloLink, InMemoryCache } from '@apollo/client/core'
4 | import createUploadLink from 'apollo-upload-client/createUploadLink.mjs'
5 |
6 |
7 | const node = document.querySelector('#app')
8 | const httpLink = ApolloLink.split(
9 | operation => operation.getContext().hasUpload,
10 | createUploadLink({
11 | uri: node?.dataset?.urlgraphql || '/graphql',
12 | credentials: 'include'
13 | }),
14 | new BatchHttpLink({
15 | uri: node?.dataset?.urlgraphql || '/graphql',
16 | batchMax: 50,
17 | batchInterval: 20,
18 | credentials: 'include'
19 | })
20 | )
21 |
22 | const apolloClient = new ApolloClient({cache: new InMemoryCache(), link: httpLink})
23 | const apolloProvider = createApolloProvider({defaultClient: apolloClient})
24 |
25 | export default apolloProvider
26 | export { apolloClient }
--------------------------------------------------------------------------------
/admin/src/log.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Logging plugin for the application
3 | */
4 | export default {
5 | install(app) {
6 | app.config.globalProperties.$log = (...args) => {
7 | console.error(...args)
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/admin/src/main.js:
--------------------------------------------------------------------------------
1 | import { createPinia } from 'pinia'
2 | import { createApp, defineAsyncComponent } from 'vue'
3 | import VueObserveVisibility from 'vue3-observe-visibility'
4 | import apolloProvider from './graphql'
5 |
6 | import '@mdi/font/css/materialdesignicons.css'
7 | import 'vuetify/styles'
8 | import './assets/base.css'
9 |
10 | import { createVuetify } from 'vuetify'
11 | import * as components from 'vuetify/components'
12 | import * as directives from 'vuetify/directives'
13 |
14 | import logger from './log'
15 | import router from './routes'
16 | import App from './App.vue'
17 |
18 |
19 | const app = createApp(App)
20 | const pinia = createPinia()
21 | const vuetify = createVuetify({
22 | components,
23 | directives,
24 | icons: {
25 | defaultSet: 'mdi',
26 | },
27 | theme: {
28 | themes: {
29 | light: {
30 | dark: false,
31 | colors: {
32 | primary: '#40749b',
33 | background: '#F8FAFC'
34 | }
35 | },
36 | },
37 | },
38 | })
39 |
40 |
41 | const fields = import.meta.glob("@/fields/*.vue");
42 |
43 | for(const path in fields) {
44 | const name = path.split("/").at(-1).split(".")[0]
45 | app.component(name, defineAsyncComponent(fields[path]));
46 | }
47 |
48 |
49 | app.use(logger)
50 | .use(pinia)
51 | .use(router)
52 | .use(vuetify)
53 | .use(apolloProvider)
54 | .use(VueObserveVisibility)
55 | .mount('#app')
56 |
--------------------------------------------------------------------------------
/admin/src/routes.js:
--------------------------------------------------------------------------------
1 | import { createRouter, createWebHistory } from 'vue-router'
2 | import { useAuthStore, useMessageStore } from './stores'
3 |
4 | const router = createRouter({
5 | history: createWebHistory(document.querySelector('#app')?.dataset?.urlbase || ''),
6 | routes: [
7 | {
8 | path: '/',
9 | name: 'login',
10 | component: () => import('./views/LoginView.vue')
11 | },
12 | {
13 | path: '/pages',
14 | name: 'page:view',
15 | component: () => import('./components/PageList.vue'),
16 | meta: {
17 | auth: true
18 | }
19 | },
20 | {
21 | path: '/elements',
22 | name: 'element:view',
23 | component: () => import('./components/ElementList.vue'),
24 | meta: {
25 | auth: true
26 | }
27 | },
28 | {
29 | path: '/files',
30 | name: 'file:view',
31 | component: () => import('./components/FileList.vue'),
32 | meta: {
33 | auth: true
34 | }
35 | }
36 | ]
37 | })
38 |
39 | router.beforeEach(async (to, from, next) => {
40 | const auth = useAuthStore()
41 | const message = useMessageStore()
42 | const authenticated = await auth.isAuthenticated()
43 |
44 | if(to.matched.some(record => record.meta.auth) && !authenticated) {
45 | auth.intended(to.fullPath)
46 | next({name: 'login'})
47 | } else if(to.name !== 'login' && !auth.can(to.name)) {
48 | message.add('You do not have permission to access ' + to.fullPath, 'error')
49 | return next(false)
50 | } else {
51 | next()
52 | }
53 | })
54 |
55 | export default router
56 |
--------------------------------------------------------------------------------
/admin/src/utils.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Generates a unique content ID based on the current date and time.
3 | *
4 | * @returns String Unique content ID
5 | */
6 | const contentid = (function () {
7 | const BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'
8 | const EPOCH = new Date('2025-01-01T00:00:00Z').getTime();
9 |
10 | let counter = 0
11 |
12 | return function() {
13 | counter = (counter + 1) % 16
14 |
15 | // IDs will repeat after 45.3 years
16 | const time = Math.floor(Date.now() - EPOCH) / 333
17 | const value = (time << 4) | counter;
18 |
19 | return Array.from({ length: 6 }, (_, i) =>
20 | BASE64[(value >> (6 * (5 - i))) & 63]
21 | ).join('');
22 | }
23 | })()
24 |
25 | export { contentid }
26 |
--------------------------------------------------------------------------------
/admin/vite.config.js:
--------------------------------------------------------------------------------
1 | import { fileURLToPath, URL } from 'node:url'
2 |
3 | import { defineConfig } from 'vite'
4 | import vue from '@vitejs/plugin-vue'
5 | import vuetify from 'vite-plugin-vuetify'
6 |
7 | // https://vitejs.dev/config/
8 | export default defineConfig({
9 | plugins: [
10 | vue(),
11 | vuetify()
12 | ],
13 | resolve: {
14 | alias: {
15 | '@': fileURLToPath(new URL('./src', import.meta.url))
16 | }
17 | },
18 | build: {
19 | rollupOptions: {
20 | output: {
21 | entryFileNames: '[name].js',
22 | assetFileNames: (asset) => {
23 | return asset.names.includes('index.css') ? 'index.css' : 'assets/[name]-[hash][extname]'
24 | }
25 | }
26 | }
27 | },
28 | experimental: {
29 | renderBuiltUrl: (filename, { type, hostType }) => {
30 | if(type === 'asset' && ['css', 'html'].includes(hostType) === false) {
31 | return { runtime: `new URL(${JSON.stringify(filename)}, import.meta.url).href` }
32 | } else {
33 | return { relative: true }
34 | }
35 | },
36 | },
37 | })
38 |
--------------------------------------------------------------------------------
/assets/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aimeos/laravel-cms/b449f93e5b9e370dc0e521e6d2cd5edfa5bfbcf7/assets/android-chrome-192x192.png
--------------------------------------------------------------------------------
/assets/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aimeos/laravel-cms/b449f93e5b9e370dc0e521e6d2cd5edfa5bfbcf7/assets/android-chrome-512x512.png
--------------------------------------------------------------------------------
/assets/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aimeos/laravel-cms/b449f93e5b9e370dc0e521e6d2cd5edfa5bfbcf7/assets/apple-touch-icon.png
--------------------------------------------------------------------------------
/assets/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aimeos/laravel-cms/b449f93e5b9e370dc0e521e6d2cd5edfa5bfbcf7/assets/favicon-16x16.png
--------------------------------------------------------------------------------
/assets/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aimeos/laravel-cms/b449f93e5b9e370dc0e521e6d2cd5edfa5bfbcf7/assets/favicon-32x32.png
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "aimeos/laravel-cms",
3 | "description": "Laravel CMS",
4 | "keywords": ["laravel", "cms", "package", "api", "graphql", "jsonapi", "multi-tenant", "multi-language"],
5 | "homepage": "https://laravel-cms.org",
6 | "type": "library",
7 | "license": "MIT",
8 | "prefer-stable": true,
9 | "minimum-stability": "dev",
10 | "require": {
11 | "laravel/framework": "^11.0||^12.0",
12 | "laravel-json-api/laravel": "^5.1",
13 | "mll-lab/graphql-php-scalars": "^6.4",
14 | "nuwave/lighthouse": "^6.54",
15 | "ezyang/htmlpurifier": "^4.18",
16 | "league/commonmark": "^2.6",
17 | "kalnoy/nestedset": "^6.0",
18 | "symfony/uid": "^7.0",
19 | "intervention/image": "^3.11"
20 | },
21 | "require-dev": {
22 | "orchestra/testbench": "^9.0||^10.0",
23 | "laravel-json-api/testing": "^3.1"
24 | },
25 | "autoload": {
26 | "psr-4": {
27 | "Aimeos\\Cms\\": "src",
28 | "Database\\Seeders\\": "database/seeders/"
29 | },
30 | "files": [
31 | "src/helpers.php"
32 | ],
33 | "classmap": [
34 | "src"
35 | ]
36 | },
37 | "autoload-dev": {
38 | "psr-4": {
39 | "Aimeos\\Cms\\": "tests"
40 | },
41 | "classmap": [
42 | "tests"
43 | ]
44 | },
45 | "extra": {
46 | "laravel": {
47 | "providers": [
48 | "Aimeos\\Cms\\CmsServiceProvider"
49 | ]
50 | }
51 | },
52 | "scripts": {
53 | "post-autoload-dump": [
54 | "@php vendor/bin/testbench package:discover --ansi"
55 | ]
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/config/cms.php:
--------------------------------------------------------------------------------
1 | env( 'DB_CONNECTION', 'mysql' ),
15 |
16 | /*
17 | |--------------------------------------------------------------------------
18 | | Cache store
19 | |--------------------------------------------------------------------------
20 | |
21 | | Use the cache store defined in ./config/cache.php to store rendered pages
22 | | for fast response times.
23 | |
24 | */
25 | 'cache' => env( 'APP_DEBUG' ) ? 'array' : 'file',
26 |
27 | /*
28 | |--------------------------------------------------------------------------
29 | | Filesystem disk
30 | |--------------------------------------------------------------------------
31 | |
32 | | Use the filesystem disk defined in ./config/filesystems.php to store the
33 | | uploaded files. By default, they are stored in the ./public/storage/cms/
34 | | folder but this can be any supported cloud storage too.
35 | |
36 | */
37 | 'disk' => env( 'CMS_DISK', 'public' ),
38 |
39 | /*
40 | |--------------------------------------------------------------------------
41 | | Prune deleted records
42 | |--------------------------------------------------------------------------
43 | |
44 | | Number of days after deleted pages, elements and files will be finally
45 | | removed. Disable pruning with FALSE as value.
46 | |
47 | */
48 | 'prune' => 30,
49 |
50 | /*
51 | |--------------------------------------------------------------------------
52 | | Page template
53 | |--------------------------------------------------------------------------
54 | |
55 | | The configured Blade template will be used for rendering all CMS pages.
56 | |
57 | */
58 | 'view' => 'cms::page',
59 | ];
--------------------------------------------------------------------------------
/database/migrations/2023_02_18_202800_create_pages_table.php:
--------------------------------------------------------------------------------
1 | create('cms_pages', function (Blueprint $table) {
17 | $table->id();
18 | $table->string('tenant_id', 250);
19 | $table->string('name');
20 | $table->string('slug');
21 | $table->string('to');
22 | $table->string('title');
23 | $table->string('domain');
24 | $table->string('lang', 5)->nullable();
25 | $table->string('tag', 30);
26 | $table->string('type', 30);
27 | $table->string('theme', 30);
28 | $table->smallInteger('cache');
29 | $table->smallInteger('status');
30 | $table->json('meta');
31 | $table->json('config');
32 | $table->json('contents');
33 | $table->string('editor');
34 | $table->softDeletes();
35 | $table->timestamps();
36 | $table->nestedSet();
37 |
38 | $table->unique(['slug', 'domain', 'lang', 'tenant_id']);
39 | $table->index(['_lft', '_rgt', 'tenant_id', 'status']);
40 | $table->index(['tag', 'lang', 'tenant_id', 'status']);
41 | $table->index(['lang', 'tenant_id', 'status']);
42 | $table->index(['parent_id', 'tenant_id']);
43 | $table->index(['domain', 'tenant_id']);
44 | $table->index(['to', 'tenant_id']);
45 | $table->index(['name', 'tenant_id']);
46 | $table->index(['title', 'tenant_id']);
47 | $table->index(['type', 'tenant_id']);
48 | $table->index(['theme', 'tenant_id']);
49 | $table->index(['cache', 'tenant_id']);
50 | $table->index(['editor', 'tenant_id']);
51 | $table->index(['deleted_at']);
52 | });
53 | }
54 |
55 | /**
56 | * Reverse the migrations.
57 | *
58 | * @return void
59 | */
60 | public function down()
61 | {
62 | Schema::connection(config('cms.db', 'sqlite'))->dropIfExists('cms_pages');
63 | }
64 | };
65 |
--------------------------------------------------------------------------------
/database/migrations/2023_02_18_202810_create_elements_table.php:
--------------------------------------------------------------------------------
1 | create('cms_elements', function (Blueprint $table) {
17 | $table->uuid('id');
18 | $table->string('tenant_id');
19 | $table->string('type', 50);
20 | $table->string('lang', 5)->nullable();
21 | $table->string('name');
22 | $table->json('data');
23 | $table->string('editor');
24 | $table->timestamps();
25 | $table->softDeletes();
26 |
27 | $table->primary('id');
28 | $table->index(['type', 'tenant_id']);
29 | $table->index(['lang', 'tenant_id']);
30 | $table->index(['name', 'tenant_id']);
31 | $table->index(['editor', 'tenant_id']);
32 | $table->index('deleted_at');
33 | });
34 | }
35 |
36 | /**
37 | * Reverse the migrations.
38 | *
39 | * @return void
40 | */
41 | public function down()
42 | {
43 | Schema::connection(config('cms.db', 'sqlite'))->dropIfExists('cms_elements');
44 | }
45 | };
46 |
--------------------------------------------------------------------------------
/database/migrations/2023_02_18_202820_create_files_table.php:
--------------------------------------------------------------------------------
1 | create('cms_files', function (Blueprint $table) {
17 | $table->uuid('id');
18 | $table->string('tenant_id');
19 | $table->string('mime', 100);
20 | $table->string('tag', 30);
21 | $table->string('name');
22 | $table->string('path');
23 | $table->json('previews');
24 | $table->json('description');
25 | $table->string('editor');
26 | $table->softDeletes();
27 | $table->timestamps();
28 |
29 | $table->primary('id');
30 | $table->index(['mime', 'tenant_id']);
31 | $table->index(['tag', 'tenant_id']);
32 | $table->index(['name', 'tenant_id']);
33 | $table->index(['editor', 'tenant_id']);
34 | $table->index('deleted_at');
35 | });
36 | }
37 |
38 | /**
39 | * Reverse the migrations.
40 | *
41 | * @return void
42 | */
43 | public function down()
44 | {
45 | Schema::connection(config('cms.db', 'sqlite'))->dropIfExists('cms_files');
46 | }
47 | };
48 |
--------------------------------------------------------------------------------
/database/migrations/2023_02_18_202830_create_versions_table.php:
--------------------------------------------------------------------------------
1 | create('cms_versions', function (Blueprint $table) {
17 | $table->uuid('id');
18 | $table->string('tenant_id');
19 | $table->string('versionable_id', 36);
20 | $table->string('versionable_type', 50);
21 | $table->boolean('published');
22 | $table->datetime('publish_at')->nullable();
23 | $table->string('lang', 5)->nullable();
24 | $table->json('contents')->nullable();
25 | $table->json('data');
26 | $table->string('editor');
27 | $table->timestamp('created_at');
28 |
29 | $table->primary('id');
30 | $table->index(['versionable_id', 'versionable_type', 'created_at', 'tenant_id'], 'idx_versions_id_type_created_tenantid');
31 | $table->index(['publish_at', 'published']);
32 | });
33 | }
34 |
35 |
36 | /**
37 | * Reverse the migrations.
38 | *
39 | * @return void
40 | */
41 | public function down()
42 | {
43 | Schema::connection(config('cms.db', 'sqlite'))->dropIfExists('cms_versions');
44 | }
45 | };
46 |
--------------------------------------------------------------------------------
/database/migrations/2023_02_18_202840_create_page_element_table.php:
--------------------------------------------------------------------------------
1 | create('cms_page_element', function (Blueprint $table) {
17 | $table->foreignId('page_id')->constrained('cms_pages')->cascadeOnUpdate()->cascadeOnDelete();
18 | $table->foreignUuid('element_id')->constrained('cms_elements')->cascadeOnUpdate()->cascadeOnDelete();
19 |
20 | $table->unique(['page_id', 'element_id']);
21 | });
22 | }
23 |
24 | /**
25 | * Reverse the migrations.
26 | *
27 | * @return void
28 | */
29 | public function down()
30 | {
31 | Schema::connection(config('cms.db', 'sqlite'))->dropIfExists('cms_page_element');
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/database/migrations/2023_02_18_202840_create_version_element_table.php:
--------------------------------------------------------------------------------
1 | create('cms_version_element', function (Blueprint $table) {
17 | $table->foreignUuid('version_id')->constrained('cms_versions')->cascadeOnUpdate()->cascadeOnDelete();
18 | $table->foreignUuid('element_id')->constrained('cms_elements')->cascadeOnUpdate()->cascadeOnDelete();
19 |
20 | $table->unique(['version_id', 'element_id']);
21 | });
22 | }
23 |
24 | /**
25 | * Reverse the migrations.
26 | *
27 | * @return void
28 | */
29 | public function down()
30 | {
31 | Schema::connection(config('cms.db', 'sqlite'))->dropIfExists('cms_version_element');
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/database/migrations/2023_02_18_202850_create_element_file_table.php:
--------------------------------------------------------------------------------
1 | create('cms_element_file', function (Blueprint $table) {
17 | $table->foreignUuid('element_id')->constrained('cms_elements')->cascadeOnUpdate()->cascadeOnDelete();
18 | $table->foreignUuid('file_id')->constrained('cms_files')->cascadeOnUpdate()->cascadeOnDelete();
19 |
20 | $table->unique(['element_id', 'file_id']);
21 | });
22 | }
23 |
24 | /**
25 | * Reverse the migrations.
26 | *
27 | * @return void
28 | */
29 | public function down()
30 | {
31 | Schema::connection(config('cms.db', 'sqlite'))->dropIfExists('cms_element_file');
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/database/migrations/2023_02_18_202850_create_page_file_table.php:
--------------------------------------------------------------------------------
1 | create('cms_page_file', function (Blueprint $table) {
17 | $table->foreignId('page_id')->constrained('cms_pages')->cascadeOnUpdate()->cascadeOnDelete();
18 | $table->foreignUuid('file_id')->constrained('cms_files')->cascadeOnUpdate()->cascadeOnDelete();
19 |
20 | $table->unique(['page_id', 'file_id']);
21 | });
22 | }
23 |
24 | /**
25 | * Reverse the migrations.
26 | *
27 | * @return void
28 | */
29 | public function down()
30 | {
31 | Schema::connection(config('cms.db', 'sqlite'))->dropIfExists('cms_page_file');
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/database/migrations/2023_02_18_202850_create_version_file_table.php:
--------------------------------------------------------------------------------
1 | create('cms_version_file', function (Blueprint $table) {
17 | $table->foreignUuid('version_id')->constrained('cms_versions')->cascadeOnUpdate()->cascadeOnDelete();
18 | $table->foreignUuid('file_id')->constrained('cms_files')->cascadeOnUpdate()->cascadeOnDelete();
19 |
20 | $table->unique(['version_id', 'file_id']);
21 | });
22 | }
23 |
24 | /**
25 | * Reverse the migrations.
26 | *
27 | * @return void
28 | */
29 | public function down()
30 | {
31 | Schema::connection(config('cms.db', 'sqlite'))->dropIfExists('cms_version_file');
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/database/migrations/2023_02_18_202920_add_users_cmseditor.php:
--------------------------------------------------------------------------------
1 | integer('cmseditor')->default(0);
18 | });
19 | }
20 |
21 |
22 | /**
23 | * Reverse the migrations.
24 | *
25 | * @return void
26 | */
27 | public function down()
28 | {
29 | Schema::table('users', function (Blueprint $table) {
30 | $table->dropColumn('cmseditor');
31 | });
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/docs/graphql-authentication.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Authentication"
3 | permalink: /graphql/authentication/
4 | excerpt: "How to authenticate at Laravel CMS using the GraphQL API"
5 | ---
6 |
7 | * [Login](#login)
8 | * [Retrieve user](#retrieve-user)
9 | * [Logout](#logout)
10 |
11 | Laravel CMS tries to authenticate against entries of the Laravel `users` table. To be able to use the GraphQL API, they need to be editors (use the `artisan` command to set the editor role):
12 |
13 | ```bash
14 | php artisan cms:editor editor@example.com
15 | ```
16 |
17 | ## Login
18 |
19 | To authenticate for editing content:
20 |
21 | ```graphql
22 | mutation {
23 | cmsLogin(email: "editor@example.com", password: "secret") {
24 | name
25 | email
26 | }
27 | }
28 | ```
29 |
30 | ```json
31 | {
32 | "data": {
33 | "cmsLogin": {
34 | "name": "A CMS editor",
35 | "email": "editor@example.com"
36 | }
37 | }
38 | }
39 | ```
40 |
41 | ## Retrieve user
42 |
43 | Retrieve information about the authenticated user:
44 |
45 | ```graphql
46 | query {
47 | me {
48 | name
49 | email
50 | }
51 | }
52 | ```
53 |
54 | ```json
55 | {
56 | "data": {
57 | "me": {
58 | "name": "A CMS editor",
59 | "email": "editor@example.com"
60 | }
61 | }
62 | }
63 | ```
64 |
65 | ## Logout
66 |
67 | To log the current user out of the application:
68 |
69 | ```graphql
70 | mutation {
71 | cmsLogout {
72 | name
73 | email
74 | }
75 | }
76 | ```
77 |
78 | ```json
79 | {
80 | "data": {
81 | "cmsLogout": {
82 | "name": "A CMS editor",
83 | "email": "editor@example.com"
84 | }
85 | }
86 | }
87 | ```
88 |
--------------------------------------------------------------------------------
/docs/jsonapi-subpages-breadcrumb.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: "Sub-pages with breadcrumb"
3 | permalink: /jsonapi/subpages-breadcrumb/
4 | excerpt: "How to retrieve the sub-pages with content and ancestors from Laravel CMS using the JSON:API to build page with breadcrumb"
5 | ---
6 |
7 | To retrieve a specific page whose URL you've gotten from one of the previous responses (in the `links/self` attribute of the item) with its ancestor pages for the breadcrumb and its shared content elements (add `?include=ancestors,elements` to the page URL):
8 |
9 | ```
10 | http://mydomain.tld/api/cms/pages/2?include=ancestors,elements
11 | ```
12 |
13 | The `included` section contains the list of shared element elements that should be displayed at the page if `include=elements` is added as parameter to the JSON:API URL the anchestor pages if the `include` parameter also contains `ancestors`, e.g.:
14 |
15 | ```json
16 | {
17 | "meta": {
18 | "baseurl": "http:\/\/mydomain.tld\/storage\/"
19 | },
20 | "jsonapi": {
21 | "version": "1.0"
22 | },
23 | "links": {
24 | "self": "http:\/\/mydomain.tld\/cms\/pages\/2"
25 | },
26 | "data": {
27 | "type": "pages",
28 | "id": "2",
29 | "attributes": {
30 | "parent_id": 1,
31 | "lang": "",
32 | "slug": "blog",
33 | "name": "Blog",
34 | "title": "Blog | Laravel CMS",
35 | "tag": "blog",
36 | "to": "",
37 | "domain": "",
38 | "has": true,
39 | "cache": 5,
40 | "data": [
41 | {
42 | "text": "Blog example",
43 | "type": "cms::heading"
44 | },
45 | {
46 | "type": "cms::blog"
47 | }
48 | ],
49 | "meta": null,
50 | "config": null,
51 | "createdAt": "2023-05-01T09:36:30.000000Z",
52 | "updatedAt": "2023-05-01T09:36:30.000000Z"
53 | },
54 | "relationships": {
55 | "elements": {
56 | "data": []
57 | },
58 | "ancestors": {
59 | "data": [
60 | {
61 | "type": "pages",
62 | "id": "1"
63 | }
64 | ]
65 | }
66 | },
67 | "links": {
68 | "self": "http:\/\/mydomain.tld\/cms\/pages\/2"
69 | }
70 | },
71 | "included": [
72 | {
73 | "type": "pages",
74 | "id": "1",
75 | "attributes": {
76 | "parent_id": null,
77 | "lang": "",
78 | "slug": "",
79 | "name": "Home",
80 | "title": "Home | Laravel CMS",
81 | "tag": "root",
82 | "to": "",
83 | "domain": "mydomain.tld",
84 | "has": true,
85 | "cache": 5,
86 | "data": null,
87 | "meta": null,
88 | "config": null,
89 | "createdAt": "2023-05-01T09:36:30.000000Z",
90 | "updatedAt": "2023-05-01T09:36:30.000000Z"
91 | },
92 | "links": {
93 | "self": "http:\/\/mydomain.tld\/cms\/pages\/1"
94 | }
95 | }
96 | ]
97 | }
98 | ```
99 |
--------------------------------------------------------------------------------
/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aimeos/laravel-cms/b449f93e5b9e370dc0e521e6d2cd5edfa5bfbcf7/favicon.ico
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "laravel-cms",
3 | "lockfileVersion": 3,
4 | "requires": true,
5 | "packages": {}
6 | }
7 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | ./src/
15 |
16 |
17 |
18 |
19 | ./tests/
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/public/article.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aimeos/laravel-cms/b449f93e5b9e370dc0e521e6d2cd5edfa5bfbcf7/public/article.css
--------------------------------------------------------------------------------
/public/blog.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aimeos/laravel-cms/b449f93e5b9e370dc0e521e6d2cd5edfa5bfbcf7/public/blog.css
--------------------------------------------------------------------------------
/public/cms.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/aimeos/laravel-cms/b449f93e5b9e370dc0e521e6d2cd5edfa5bfbcf7/public/cms.css
--------------------------------------------------------------------------------
/public/prism.css:
--------------------------------------------------------------------------------
1 | /* PrismJS 1.29.0
2 | https://prismjs.com/download.html#themes=prism-okaidia&languages=markup+css+clike+javascript+aspnet+bash+c+csharp+cpp+csp+css-extras+diff+docker+gettext+git+go+graphql+http+ini+java+json+kotlin+lua+makefile+markdown+markup-templating+nginx+objectivec+perl+php+php-extras+powershell+python+jsx+tsx+regex+ruby+rust+scss+scala+smalltalk+sql+toml+twig+typescript+typoscript+uri+visual-basic+wasm+xml-doc+yaml */
3 | code[class*=language-],pre[class*=language-]{color:#f8f8f2;background:0 0;text-shadow:0 1px rgba(0,0,0,.3);font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto;border-radius:.3em}:not(pre)>code[class*=language-],pre[class*=language-]{background:#272822}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#8292a2}.token.punctuation{color:#f8f8f2}.token.namespace{opacity:.7}.token.constant,.token.deleted,.token.property,.token.symbol,.token.tag{color:#f92672}.token.boolean,.token.number{color:#ae81ff}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#a6e22e}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url,.token.variable{color:#f8f8f2}.token.atrule,.token.attr-value,.token.class-name,.token.function{color:#e6db74}.token.keyword{color:#66d9ef}.token.important,.token.regex{color:#fd971f}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}
4 |
--------------------------------------------------------------------------------
/site.webmanifest:
--------------------------------------------------------------------------------
1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
--------------------------------------------------------------------------------
/src/CmsServiceProvider.php:
--------------------------------------------------------------------------------
1 | loadViewsFrom( $basedir . '/views', 'cms' );
31 | $this->loadMigrationsFrom( $basedir . '/database/migrations' );
32 |
33 | $this->publishes( [$basedir . '/public' => public_path( 'vendor/cms' )], 'public' );
34 | $this->publishes( [$basedir . '/config/cms.php' => config_path( 'cms.php' )], 'config' );
35 | $this->publishes( [$basedir . '/admin/dist' => public_path( 'vendor/cms/admin' )], 'admin' );
36 | $this->publishes( [$basedir . '/graphql' => base_path( 'graphql' )], 'admin' );
37 |
38 |
39 | if( $this->app->runningInConsole() )
40 | {
41 | $this->commands( [
42 | \Aimeos\Cms\Commands\Install::class,
43 | \Aimeos\Cms\Commands\Publish::class,
44 | \Aimeos\Cms\Commands\Serve::class,
45 | \Aimeos\Cms\Commands\User::class,
46 | ] );
47 | }
48 |
49 | \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::skipWhen( function( $request ) {
50 | return $request->is( trim( config( 'lighthouse.route.uri' ), '/' ) );
51 | } );
52 | }
53 |
54 |
55 | /**
56 | * Register the service provider.
57 | */
58 | public function register()
59 | {
60 | }
61 | }
--------------------------------------------------------------------------------
/src/Commands/Publish.php:
--------------------------------------------------------------------------------
1 | where( 'published', false )
34 | ->chunk( 100, function( $versions ) {
35 |
36 | foreach( $versions as $version )
37 | {
38 | try
39 | {
40 | $id = $version->versionable_id;
41 | $type = $version->versionable_type;
42 |
43 | app( $type )::findOrFail( $id )->publish( $version );
44 | }
45 | catch( \Exception $e )
46 | {
47 | $this->error( "Failed to publish ID {$id} of {$type}: " . $e->getMessage() );
48 | }
49 | }
50 |
51 | } );
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Commands/Serve.php:
--------------------------------------------------------------------------------
1 | host().':'.$this->port(),
42 | '-d',
43 | 'upload_max_filesize=100M',
44 | '-d',
45 | 'post_max_size=100M',
46 | __DIR__.'/../server.php',
47 | ];
48 | }
49 |
50 |
51 | /**
52 | * Get the console command options.
53 | *
54 | * @return array
55 | */
56 | protected function getOptions()
57 | {
58 | return [
59 | ['host', null, InputOption::VALUE_OPTIONAL, 'The host address to serve the application on', Env::get('SERVER_HOST', 'localhost')],
60 | ['port', null, InputOption::VALUE_OPTIONAL, 'The port to serve the application on', Env::get('SERVER_PORT')],
61 | ['tries', null, InputOption::VALUE_OPTIONAL, 'The max number of ports to attempt to serve from', 10],
62 | ['no-reload', null, InputOption::VALUE_NONE, 'Do not reload the development server on .env file changes'],
63 | ];
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/src/Commands/User.php:
--------------------------------------------------------------------------------
1 | argument( 'email' );
36 | $value = $this->option( 'disable' ) ? 0 : 0x7fffffff;
37 |
38 | if( ( $user = \Illuminate\Foundation\Auth\User::where( 'email', $email )->first() ) === null )
39 | {
40 | $user = (new \Illuminate\Foundation\Auth\User())->forceFill( [
41 | 'password' => Hash::make( $this->option( 'password' ) ?: $this->secret( 'Password' ) ),
42 | 'email' => $email,
43 | 'name' => $email,
44 | ] );
45 | }
46 |
47 | $user->forceFill( ['cmseditor' => $value] )->save();
48 |
49 | if( $value ) {
50 | $this->info( sprintf( ' Enabled [%1$s] as CMS user', $email ) );
51 | } else {
52 | $this->info( sprintf( ' Disabled [%1$s] as CMS user', $email ) );
53 | }
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/src/Concerns/Tenancy.php:
--------------------------------------------------------------------------------
1 | setAttribute( $model->getTenantColumn(), \Aimeos\Cms\Tenancy::value() );
38 | } );
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/GraphQL/Exception.php:
--------------------------------------------------------------------------------
1 | reason = $reason;
17 | }
18 |
19 |
20 | /**
21 | * Returns true when exception message is safe to be displayed to a client.
22 | */
23 | public function isClientSafe(): bool
24 | {
25 | return true;
26 | }
27 |
28 |
29 | /**
30 | * Data to include within the "extensions" key of the formatted error.
31 | *
32 | * @return array
33 | */
34 | public function getExtensions(): array
35 | {
36 | return ['reason' => $this->reason];
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/GraphQL/Mutations/AddElement.php:
--------------------------------------------------------------------------------
1 | transaction( function() use ( $element, $args ) {
21 |
22 | $editor = Auth::user()?->name ?? request()->ip();
23 |
24 | $element->fill( $args['input'] ?? [] );
25 | $element->tenant_id = \Aimeos\Cms\Tenancy::value();
26 | $element->data = $args['input']['data'] ?? [];
27 | $element->editor = $editor;
28 | $element->save();
29 |
30 | $element->files()->attach( $args['files'] ?? [] );
31 |
32 | }, 3 );
33 |
34 | return $element;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/GraphQL/Mutations/AddFile.php:
--------------------------------------------------------------------------------
1 | isValid() ) {
20 | throw new Exception( 'Invalid file upload' );
21 | }
22 |
23 | $file = new File();
24 | $file->fill( $args['input'] ?? [] );
25 | $file->editor = Auth::user()?->name ?? request()->ip();
26 | $file->name = $file->name ?: pathinfo( $upload->getClientOriginalName(), PATHINFO_BASENAME );
27 | $file->mime = $upload->getClientMimeType();
28 |
29 | try
30 | {
31 | $file->addFile( $upload )->addPreviews( $args['preview'] ?? $upload );
32 | $file->save();
33 | }
34 | catch( \Throwable $t )
35 | {
36 | $file->removePreviews()->removeFile();
37 | throw $t;
38 | }
39 |
40 | return $file;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/GraphQL/Mutations/AddPage.php:
--------------------------------------------------------------------------------
1 | transaction( function() use ( $page, $args ) {
21 |
22 | $editor = Auth::user()?->name ?? request()->ip();
23 |
24 | $page->fill( $args['input'] ?? [] );
25 | $page->tenant_id = \Aimeos\Cms\Tenancy::value();
26 | $page->editor = $editor;
27 |
28 | if( isset( $args['ref'] ) ) {
29 | $page->beforeNode( Page::withTrashed()->findOrFail( $args['ref'] ) );
30 | }
31 | elseif( isset( $args['parent'] ) ) {
32 | $page->appendToNode( Page::withTrashed()->findOrFail( $args['parent'] ) );
33 | }
34 |
35 | $page->save();
36 | $page->files()->attach( $args['files'] ?? [] );
37 | $page->elements()->attach( $args['elements'] ?? [] );
38 |
39 | }, 3 );
40 |
41 | return $page;
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/GraphQL/Mutations/CmsLogin.php:
--------------------------------------------------------------------------------
1 | $args
15 | */
16 | public function __invoke( $rootValue, array $args ): User
17 | {
18 | $guard = Auth::guard();
19 |
20 | if( !$guard->attempt( $args ) ) {
21 | throw new Exception( 'Invalid credentials' );
22 | }
23 |
24 | return $guard->user();
25 | }
26 | }
--------------------------------------------------------------------------------
/src/GraphQL/Mutations/CmsLogout.php:
--------------------------------------------------------------------------------
1 | $args
14 | */
15 | public function __invoke( $rootValue, array $args ): ?User
16 | {
17 | $guard = Auth::guard();
18 | $user = $guard->user();
19 |
20 | $guard->logout();
21 | return $user;
22 | }
23 | }
--------------------------------------------------------------------------------
/src/GraphQL/Mutations/DropElement.php:
--------------------------------------------------------------------------------
1 | whereIn( 'id', $args['id'] )->get();
18 | $editor = Auth::user()?->name ?? request()->ip();
19 |
20 | foreach( $items as $item )
21 | {
22 | $item->editor = $editor;
23 | $item->delete();
24 | }
25 |
26 | return $items->all();
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/GraphQL/Mutations/DropFile.php:
--------------------------------------------------------------------------------
1 | whereIn( 'id', $args['id'] )->get();
18 | $editor = Auth::user()?->name ?? request()->ip();
19 |
20 | foreach( $items as $item )
21 | {
22 | $item->editor = $editor;
23 | $item->delete();
24 | }
25 |
26 | return $items->all();
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/GraphQL/Mutations/DropPage.php:
--------------------------------------------------------------------------------
1 | whereIn( 'id', $args['id'] )->get();
19 | $editor = Auth::user()?->name ?? request()->ip();
20 |
21 | foreach( $items as $item )
22 | {
23 | $item->editor = $editor;
24 | $item->delete();
25 |
26 | Cache::forget( Page::key( $item ) );
27 | }
28 |
29 | return $items->all();
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/GraphQL/Mutations/KeepElement.php:
--------------------------------------------------------------------------------
1 | whereIn( 'id', $args['id'] )->get();
18 | $editor = Auth::user()?->name ?? request()->ip();
19 |
20 | foreach( $items as $item )
21 | {
22 | $item->editor = $editor;
23 | $item->restore();
24 | }
25 |
26 | return $items->all();
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/GraphQL/Mutations/KeepFile.php:
--------------------------------------------------------------------------------
1 | whereIn( 'id', $args['id'] )->get();
18 | $editor = Auth::user()?->name ?? request()->ip();
19 |
20 | foreach( $items as $item )
21 | {
22 | $item->editor = $editor;
23 | $item->restore();
24 | }
25 |
26 | return $items->all();
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/GraphQL/Mutations/KeepPage.php:
--------------------------------------------------------------------------------
1 | whereIn( 'id', $args['id'] )->get();
18 | $editor = Auth::user()?->name ?? request()->ip();
19 |
20 | foreach( $items as $item )
21 | {
22 | $item->editor = $editor;
23 | $item->restore();
24 | }
25 |
26 | return $items->all();
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/GraphQL/Mutations/MovePage.php:
--------------------------------------------------------------------------------
1 | findOrFail( $args['id'] );
19 | $page->editor = Auth::user()?->name ?? request()->ip();
20 |
21 | if( isset( $args['ref'] ) ) {
22 | $page->beforeNode( Page::withTrashed()->findOrFail( $args['ref'] ) );
23 | }
24 | elseif( isset( $args['parent'] ) ) {
25 | $page->appendToNode( Page::withTrashed()->findOrFail( $args['parent'] ) );
26 | }
27 | else {
28 | DB::connection( config( 'cms.db', 'sqlite' ) )->transaction( fn() => $page->saveAsRoot(), 3 );
29 | return $page;
30 | }
31 |
32 | DB::connection( config( 'cms.db', 'sqlite' ) )->transaction( fn() => $page->save(), 3 );
33 |
34 | return $page;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/GraphQL/Mutations/PubElement.php:
--------------------------------------------------------------------------------
1 | whereIn( 'id', $args['id'] )->get();
18 | $editor = Auth::user()?->name ?? request()->ip();
19 |
20 | foreach( $items as $item )
21 | {
22 | if( $latest = $item->latest )
23 | {
24 | if( $args['at'] ?? null )
25 | {
26 | $latest->publish_at = $args['at'];
27 | $latest->editor = $editor;
28 | $latest->save();
29 | continue;
30 | }
31 |
32 | $item->publish( $latest );
33 | }
34 | }
35 |
36 | return $items->all();
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/GraphQL/Mutations/PubFile.php:
--------------------------------------------------------------------------------
1 | whereIn( 'id', $args['id'] )->get();
18 | $editor = Auth::user()?->name ?? request()->ip();
19 |
20 | foreach( $items as $item )
21 | {
22 | if( $latest = $item->latest )
23 | {
24 | if( $args['at'] ?? null )
25 | {
26 | $latest->publish_at = $args['at'];
27 | $latest->editor = $editor;
28 | $latest->save();
29 | continue;
30 | }
31 |
32 | $item->publish( $latest );
33 | }
34 | }
35 |
36 | return $items->all();
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/GraphQL/Mutations/PubPage.php:
--------------------------------------------------------------------------------
1 | whereIn( 'id', $args['id'] )->get();
19 | $editor = Auth::user()?->name ?? request()->ip();
20 |
21 | foreach( $items as $item )
22 | {
23 | if( $latest = $item->latest )
24 | {
25 | if( $args['at'] ?? null )
26 | {
27 | $latest->publish_at = $args['at'];
28 | $latest->editor = $editor;
29 | $latest->save();
30 | continue;
31 | }
32 |
33 | $item->publish( $latest );
34 | Cache::forget( Page::key( $item ) );
35 | }
36 | }
37 |
38 | return $items->all();
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/GraphQL/Mutations/PurgeElement.php:
--------------------------------------------------------------------------------
1 | whereIn( 'id', $args['id'] )->get();
17 | Element::whereIn( 'id', $items->pluck( 'id' ) )->forceDelete();
18 |
19 | return $items->all();
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/src/GraphQL/Mutations/PurgeFile.php:
--------------------------------------------------------------------------------
1 | whereIn( 'id', $args['id'] )->get();
19 |
20 | foreach( $items as $item ) {
21 | $item->purge();
22 | }
23 |
24 | return $items->all();
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/GraphQL/Mutations/PurgePage.php:
--------------------------------------------------------------------------------
1 | whereIn( 'id', $args['id'] )->get();
19 |
20 | foreach( $items as $item )
21 | {
22 | DB::connection( config( 'cms.db', 'sqlite' ) )->transaction( fn() => $item->forceDelete(), 3 );
23 | Cache::forget( Page::key( $item ) );
24 | }
25 |
26 | return $items->all();
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/GraphQL/Mutations/SaveElement.php:
--------------------------------------------------------------------------------
1 | findOrFail( $args['id'] );
20 |
21 | DB::connection( config( 'cms.db', 'sqlite' ) )->transaction( function() use ( $element, $args ) {
22 |
23 | $version = $element->versions()->create( [
24 | 'editor' => Auth::user()?->name ?? request()->ip(),
25 | 'lang' => $args['input']['lang'] ?? '',
26 | 'data' => $args['input'] ?? [],
27 | ] );
28 |
29 | $version->files()->sync( $args['files'] ?? [] );
30 |
31 | // MySQL doesn't support offsets for DELETE
32 | $ids = Version::where( 'versionable_id', $element->id )
33 | ->where( 'versionable_type', Element::class )
34 | ->orderBy( 'id', 'desc' )
35 | ->skip( 10 )
36 | ->take( 10 )
37 | ->pluck( 'id' );
38 |
39 | if( !$ids->isEmpty() ) {
40 | Version::whereIn( 'id', $ids )->forceDelete();
41 | }
42 |
43 | }, 3 );
44 |
45 | return $element;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/GraphQL/Mutations/SaveFile.php:
--------------------------------------------------------------------------------
1 | findOrFail( $args['id'] );
19 | $file = clone $orig;
20 | $file->fill( $args['input'] ?? [] );
21 |
22 | $upload = $args['file'] ?? null;
23 |
24 | if( $upload instanceof UploadedFile ) {
25 | $file->addFile( $upload );
26 | }
27 |
28 | try
29 | {
30 | $preview = $args['preview'] ?? null;
31 |
32 | if( $preview instanceof UploadedFile ) {
33 | $file->addPreviews( $preview );
34 | } elseif( $upload instanceof UploadedFile ) {
35 | $file->addPreviews( $upload );
36 | } elseif( $preview === false ) {
37 | $file->previews = [];
38 | }
39 | }
40 | catch( \Throwable $t )
41 | {
42 | $file->removePreviews()->removeFile();
43 | throw $t;
44 | }
45 |
46 | $editor = Auth::user()?->name ?? request()->ip();
47 | $file->versions()->create( [
48 | 'editor' => $editor,
49 | 'data' => [
50 | 'tag' => $file->tag,
51 | 'name' => $file->name,
52 | 'mime' => $file->mime,
53 | 'path' => $file->path,
54 | 'previews' => $file->previews,
55 | 'description' => $file->description,
56 | ],
57 | ] );
58 |
59 | $file->removeVersions();
60 |
61 | return $orig;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/GraphQL/Mutations/SavePage.php:
--------------------------------------------------------------------------------
1 | findOrFail( $args['id'] );
21 |
22 | DB::connection( config( 'cms.db', 'sqlite' ) )->transaction( function() use ( $page, $args ) {
23 |
24 | $data = $args['input'] ?? [];
25 | unset( $data['contents'] );
26 |
27 | $version = $page->versions()->create([
28 | 'editor' => Auth::user()?->name ?? request()->ip(),
29 | 'contents' => $args['input']['contents'] ?? null,
30 | 'lang' => $args['input']['lang'] ?? null,
31 | 'data' => $data,
32 | ]);
33 |
34 | $version->elements()->sync( $args['elements'] ?? [] );
35 | $version->files()->sync( $args['files'] ?? [] );
36 |
37 | // MySQL doesn't support offsets for DELETE
38 | $ids = Version::where( 'versionable_id', $page->id )
39 | ->where( 'versionable_type', Page::class )
40 | ->orderBy( 'id', 'desc' )
41 | ->skip( 10 )
42 | ->take( 10 )
43 | ->pluck( 'id' );
44 |
45 | if( !$ids->isEmpty() ) {
46 | Version::whereIn( 'id', $ids )->forceDelete();
47 | }
48 |
49 | }, 3 );
50 |
51 | return $page;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/Http/Controllers/PageController.php:
--------------------------------------------------------------------------------
1 | input( 'preview' ) && Gate::allowIf( fn( $user ) => $user->cmseditor > 0 ) )
42 | {
43 | $page = Page::where( 'slug', $slug )
44 | ->where( 'domain', $domain )
45 | ->where( 'lang', $lang )
46 | ->firstOrFail();
47 |
48 | $page->fill( $page->latest()->data );
49 | $page->cache = 0; // don't cache sub-parts in preview requests
50 |
51 | if( $page->to ) {
52 | return !str_starts_with( $page->to, 'http' ) ? redirect( $page->to ) : redirect()->away( $page->to );
53 | }
54 |
55 | return view( config( 'cms.view', 'cms::page' ), ['page' => $page] )->render();
56 | }
57 |
58 | $cache = Cache::store( config( 'cms.cache', 'file' ) );
59 | $key = Page::key( $slug, $lang, $domain );
60 |
61 | if( $html = $cache->get( $key ) ) {
62 | return $html;
63 | }
64 |
65 | $page = Page::where( 'slug', $slug )
66 | ->where( 'domain', $domain )
67 | ->where( 'lang', $lang )
68 | ->where( 'status', '>', 0 )
69 | ->firstOrFail();
70 |
71 | if( $page->to ) {
72 | return !str_starts_with( $page->to, 'http' ) ? redirect( $page->to ) : redirect()->away( $page->to );
73 | }
74 |
75 | $html = view( config( 'cms.view', 'cms::page' ), ['page' => $page] )->render();
76 |
77 | if( $page->cache ) {
78 | $cache->put( $key, $html, now()->addMinutes( (int) $page->cache ) );
79 | }
80 |
81 | return $html;
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/JsonApi/V1/Controllers/JsonapiController.php:
--------------------------------------------------------------------------------
1 | withMeta( ['baseurl' => Storage::disk( config( 'cms.disk', 'public' ) )->url( '' )] )
32 | ->withQueryParameters( $query );
33 | }
34 |
35 |
36 | /**
37 | * Adds global meta data to related resource response.
38 | *
39 | * @param Page|null $page Page model
40 | * @param mixed $data Fetched data
41 | * @param ResourceQuery $request Query object
42 | * @param Relation Relation type object
43 | * @return RelatedResponse
44 | */
45 | public function readRelatedElements( ?Page $page, $data, ResourceQuery $request ) : RelatedResponse
46 | {
47 | return RelatedResponse::make( $page, 'elements', $data )
48 | ->withMeta( ['baseurl' => Storage::disk( config( 'cms.disk', 'public' ) )->url( '' )] )
49 | ->withQueryParameters( $request );
50 | }
51 |
52 |
53 | /**
54 | * Adds global meta data to collection resource response.
55 | *
56 | * @param mixed $data Fetched data
57 | * @param PageCollectionQuery $query Page collection query
58 | * @return DataResponse
59 | */
60 | public function searched( $data, PageCollectionQuery $query ) : DataResponse
61 | {
62 | return DataResponse::make( $data )
63 | ->withMeta( ['baseurl' => Storage::disk( config( 'cms.disk', 'public' ) )->url( '' )] )
64 | ->withQueryParameters( $query );
65 | }
66 | }
--------------------------------------------------------------------------------
/src/JsonApi/V1/Elements/ElementSchema.php:
--------------------------------------------------------------------------------
1 | 1];
23 |
24 | /**
25 | * Disables "self" links for element items.
26 | */
27 | protected bool $selfLink = false;
28 |
29 | /**
30 | * The model the schema corresponds to.
31 | *
32 | * @var string
33 | */
34 | public static string $model = Element::class;
35 |
36 |
37 | /**
38 | * Determine if the resource is authorizable.
39 | *
40 | * @return bool
41 | */
42 | public function authorizable(): bool
43 | {
44 | return false;
45 | }
46 |
47 |
48 | /**
49 | * Get the resource fields.
50 | *
51 | * @return array
52 | */
53 | public function fields(): array
54 | {
55 | return [
56 | ID::make(),
57 | Str::make( 'type' )->readOnly(),
58 | Str::make( 'lang' )->readOnly(),
59 | ArrayHash::make( 'data' )->readOnly(),
60 | DateTime::make( 'createdAt' )->readOnly(),
61 | ];
62 | }
63 |
64 |
65 | /**
66 | * Get the resource filters.
67 | *
68 | * @return array
69 | */
70 | public function filters(): array
71 | {
72 | return [];
73 | }
74 |
75 |
76 | /**
77 | * Get the resource paginator.
78 | *
79 | * @return Paginator|null
80 | */
81 | public function pagination(): ?Paginator
82 | {
83 | return PagePagination::make()->withDefaultPerPage( 50 );
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/JsonApi/V1/Pages/PageCollectionQuery.php:
--------------------------------------------------------------------------------
1 | [
20 | 'nullable',
21 | 'array',
22 | JsonApiRule::fieldSets(),
23 | ],
24 | 'filter' => [
25 | 'nullable',
26 | 'array',
27 | JsonApiRule::filter(),
28 | ],
29 | 'include' => [
30 | 'nullable',
31 | 'string',
32 | JsonApiRule::includePaths()->forget(
33 | 'children.children',
34 | ),
35 | ],
36 | 'page' => [
37 | 'nullable',
38 | 'array',
39 | JsonApiRule::page(),
40 | ],
41 | 'page.number' => [
42 | 'integer',
43 | 'between:1,100',
44 | ],
45 | 'page.size' => [
46 | 'integer',
47 | 'between:1,100',
48 | ],
49 | 'sort' => JsonApiRule::notSupported(),
50 | ];
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/JsonApi/V1/Pages/PageQuery.php:
--------------------------------------------------------------------------------
1 | [
20 | 'nullable',
21 | 'array',
22 | JsonApiRule::fieldSets(),
23 | ],
24 | 'filter' => [
25 | 'nullable',
26 | 'array',
27 | JsonApiRule::filter(),
28 | ],
29 | 'include' => [
30 | 'nullable',
31 | 'string',
32 | JsonApiRule::includePaths()->forget(
33 | 'children.children',
34 | ),
35 | ],
36 | 'page' => JsonApiRule::notSupported(),
37 | 'sort' => JsonApiRule::notSupported(),
38 | ];
39 | }
40 | }
--------------------------------------------------------------------------------
/src/JsonApi/V1/Server.php:
--------------------------------------------------------------------------------
1 | baseUri ) ) {
58 | $this->baseUri = Route::has( 'cms.pages' ) ? str_replace( '/pages', '', Url::route( 'cms.pages' ) ) : '/cms';
59 | }
60 |
61 | return $this->baseUri;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/Models/Version.php:
--------------------------------------------------------------------------------
1 | '',
34 | 'lang' => '',
35 | 'data' => '{}',
36 | 'contents' => null,
37 | 'publish_at' => null,
38 | 'published' => false,
39 | 'editor' => '',
40 | ];
41 |
42 | /**
43 | * The automatic casts for the attributes.
44 | *
45 | * @var array
46 | */
47 | protected $casts = [
48 | 'lang' => 'string',
49 | 'data' => 'object',
50 | 'contents' => 'array',
51 | ];
52 |
53 | /**
54 | * The attributes that are mass assignable.
55 | *
56 | * @var array
57 | */
58 | protected $fillable = [
59 | 'publish_at',
60 | 'contents',
61 | 'editor',
62 | 'data',
63 | 'lang',
64 | ];
65 |
66 | /**
67 | * The table associated with the model.
68 | *
69 | * @var string
70 | */
71 | protected $table = 'cms_versions';
72 |
73 |
74 | /**
75 | * Get the shared element attached to the version.
76 | */
77 | public function elements() : BelongsToMany
78 | {
79 | return $this->belongsToMany( Element::class, 'cms_version_element' );
80 | }
81 |
82 |
83 | /**
84 | * Get all files referenced by the versioned data.
85 | */
86 | public function files() : BelongsToMany
87 | {
88 | return $this->belongsToMany( File::class, 'cms_version_file' );
89 | }
90 |
91 |
92 | /**
93 | * Get the connection name for the model.
94 | */
95 | public function getConnectionName()
96 | {
97 | return config( 'cms.db', 'sqlite' );
98 | }
99 |
100 |
101 | /**
102 | * Disables using the updated_at column.
103 | * Versions are never updated, each one is created as a new entry.
104 | */
105 | public function getUpdatedAtColumn()
106 | {
107 | return null;
108 | }
109 |
110 |
111 | /**
112 | * Get the parent versionable model (page, file or element).
113 | */
114 | public function versionable() : MorphTo
115 | {
116 | return $this->morphTo();
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/Permission.php:
--------------------------------------------------------------------------------
1 | 0b00000000000000000000000000000001,
23 | 'page:save' => 0b00000000000000000000000000000010,
24 | 'page:add' => 0b00000000000000000000000000000100,
25 | 'page:drop' => 0b00000000000000000000000000001000,
26 | 'page:keep' => 0b00000000000000000000000000010000,
27 | 'page:purge' => 0b00000000000000000000000000100000,
28 | 'page:publish' => 0b00000000000000000000000001000000,
29 | 'page:move' => 0b00000000000000000000000010000000,
30 |
31 | 'element:view' => 0b00000000000000000000000100000000,
32 | 'element:save' => 0b00000000000000000000001000000000,
33 | 'element:add' => 0b00000000000000000000010000000000,
34 | 'element:drop' => 0b00000000000000000000100000000000,
35 | 'element:keep' => 0b00000000000000000001000000000000,
36 | 'element:purge' => 0b00000000000000000010000000000000,
37 | 'element:publish' => 0b00000000000000000100000000000000,
38 |
39 | 'file:view' => 0b00000000000000010000000000000000,
40 | 'file:save' => 0b00000000000000100000000000000000,
41 | 'file:add' => 0b00000000000001000000000000000000,
42 | 'file:drop' => 0b00000000000010000000000000000000,
43 | 'file:keep' => 0b00000000000100000000000000000000,
44 | 'file:purge' => 0b00000000001000000000000000000000,
45 | 'file:publish' => 0b00000000010000000000000000000000,
46 | ];
47 |
48 | /**
49 | * Anonymous callback which allows or denies actions.
50 | */
51 | public static ?\Closure $callback = null;
52 |
53 |
54 | /**
55 | * Checks if the user has the permission for the requested action.
56 | *
57 | * @param string action Name of the requested action, e.g. "page:view"
58 | * @param \App\Models\User|null $user Laravel user object
59 | * @return bool TRUE of the user is allowed to perform the action, FALSE if not
60 | */
61 | public static function can( string $action, ?User $user ) : bool
62 | {
63 | if( $closure = self::$callback ) {
64 | return $closure( $action, $user );
65 | }
66 |
67 | if( $action === '*' ) {
68 | return $user && $user->cmseditor > 0;
69 | }
70 |
71 | return $user && isset( self::$can[$action] ) && self::$can[$action] & $user->cmseditor;
72 | }
73 |
74 |
75 | /**
76 | * Returns the available actions and their permissions.
77 | *
78 | * @param \App\Models\User|null $user Laravel user object
79 | * @return array List of actions as keys and booleans as values indicating if the user has permission for the action
80 | */
81 | public static function get( ?User $user ) : array
82 | {
83 | $map = [];
84 |
85 | foreach( self::$can as $action => $bit ) {
86 | $map[$action] = self::can( $action, $user );
87 | }
88 |
89 | return $map;
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/Policies/ElementPolicy.php:
--------------------------------------------------------------------------------
1 | cmseditor ) ) {
30 | $builder->where( $model->qualifyColumn( 'status' ), '>', 0 );
31 | }
32 | }
33 |
34 |
35 | /**
36 | * Adds additional macros to the query builder.
37 | *
38 | * @param \Illuminate\Database\Eloquent\Builder $builder Query builder
39 | */
40 | public function extend( Builder $builder )
41 | {
42 | $builder->macro( 'withoutStatus', function( Builder $builder ) {
43 | return $builder->withoutGlobalScope( $this );
44 | });
45 | }
46 | }
--------------------------------------------------------------------------------
/src/Scopes/Tenancy.php:
--------------------------------------------------------------------------------
1 | where( $model->qualifyColumn( $model->getTenantColumn() ), \Aimeos\Cms\Tenancy::value() );
29 | }
30 |
31 |
32 | /**
33 | * Adds additional macros to the query builder.
34 | *
35 | * @param \Illuminate\Database\Eloquent\Builder $builder Query builder
36 | */
37 | public function extend( Builder $builder )
38 | {
39 | $builder->macro( 'withoutTenancy', function( Builder $builder ) {
40 | return $builder->withoutGlobalScope( $this );
41 | });
42 | }
43 | }
--------------------------------------------------------------------------------
/src/Scopes/Timeframe.php:
--------------------------------------------------------------------------------
1 | where( function( Builder $query ) use ( $model ) {
33 | return $query->where( $model->qualifyColumn( 'start' ), null )
34 | ->orWhere( $model->qualifyColumn( 'start' ), '>=', date( 'Y-m-d H:i:00' ) );
35 | } )
36 | ->where( function( Builder $query ) use ( $model ) {
37 | return $query->where( $model->qualifyColumn( 'end' ), null )
38 | ->orWhere( $model->qualifyColumn( 'end' ), '<=', date( 'Y-m-d H:i:00' ) );
39 | } );
40 | }
41 | }
42 |
43 |
44 | /**
45 | * Adds additional macros to the query builder.
46 | *
47 | * @param \Illuminate\Database\Eloquent\Builder $builder Query builder
48 | */
49 | public function extend( Builder $builder )
50 | {
51 | $builder->macro( 'withoutTimeframe', function( Builder $builder ) {
52 | return $builder->withoutGlobalScope( $this );
53 | });
54 | }
55 | }
--------------------------------------------------------------------------------
/src/Tenancy.php:
--------------------------------------------------------------------------------
1 | $path ) {
15 | $list[] = cmsurl( $path ) . ' ' . $width . 'w';
16 | }
17 |
18 | return implode( ',', $list );
19 | }
20 | }
21 |
22 |
23 | if( !function_exists( 'cmsurl' ) )
24 | {
25 | function cmsurl( string $path ): string
26 | {
27 | if( \Illuminate\Support\Str::startsWith( $path, ['data:', 'http:', 'https:'] ) ) {
28 | return $path;
29 | }
30 |
31 | return \Illuminate\Support\Facades\Storage::disk( config( 'cms.disk', 'public' ) )->url( $path );
32 | }
33 | }
34 |
35 |
36 | if( !function_exists( 'cmsasset' ) )
37 | {
38 | function cmsasset( string $path ): string
39 | {
40 | return asset( $path ) . '?v=' . filemtime( public_path( $path ) );
41 | }
42 | }
--------------------------------------------------------------------------------
/src/server.php:
--------------------------------------------------------------------------------
1 | seed( CmsSeeder::class );
16 |
17 | $this->artisan('cms:publish')->assertExitCode( 0 );
18 |
19 | $this->assertEquals( 1, Page::where( 'slug', 'hidden' )->firstOrFail()?->status );
20 | $this->assertEquals( 'Powered by Laravel CMS!', Element::where( 'name', 'Shared footer' )->firstOrFail()?->data->text );
21 | $this->assertEquals( (object) [
22 | 'en' => 'Test file description',
23 | 'de' => 'Beschreibung der Testdatei',
24 | ], File::where( 'tag', 'test' )->firstOrFail()?->description );
25 | }
26 | }
--------------------------------------------------------------------------------
/tests/GraphqlAuthTest.php:
--------------------------------------------------------------------------------
1 | set( 'lighthouse.schema_path', __DIR__ . '/default-schema.graphql' );
20 | $app['config']->set( 'lighthouse.namespaces.models', ['App\Models', 'Aimeos\\Cms\\Models'] );
21 | $app['config']->set( 'lighthouse.namespaces.mutations', ['Aimeos\\Cms\\GraphQL\\Mutations'] );
22 | }
23 |
24 |
25 | protected function getPackageProviders( $app )
26 | {
27 | return array_merge( parent::getPackageProviders( $app ), [
28 | 'Nuwave\Lighthouse\LighthouseServiceProvider'
29 | ] );
30 | }
31 |
32 |
33 | protected function setUp(): void
34 | {
35 | parent::setUp();
36 | $this->bootRefreshesSchemaCache();
37 |
38 | $this->user = \App\Models\User::create([
39 | 'name' => 'Test',
40 | 'email' => 'editor@testbench',
41 | 'password' => \Illuminate\Support\Facades\Hash::make('secret'),
42 | 'cmseditor' => 1
43 | ]);
44 | }
45 |
46 |
47 | public function testLogin()
48 | {
49 | $user = \App\Models\User::where('email', 'editor@testbench')->firstOrFail();
50 |
51 | $attr = collect($user->getAttributes())->except(['cmseditor', 'password', 'secret', 'remember_token'])->all();
52 | $expected = ['id' => (string) $user->id] + $attr;
53 |
54 | $this->expectsDatabaseQueryCount( 1 );
55 |
56 | $response = $this->graphQL( "
57 | mutation {
58 | cmsLogin(email: \"editor@testbench\", password: \"secret\") {
59 | id
60 | email
61 | name
62 | email_verified_at
63 | created_at
64 | updated_at
65 | }
66 | }
67 | " )->assertJson( [
68 | 'data' => [
69 | 'cmsLogin' => $expected,
70 | ]
71 | ] );
72 | }
73 |
74 |
75 | public function testLogout()
76 | {
77 | $user = \App\Models\User::where('email', 'editor@testbench')->firstOrFail();
78 |
79 | $attr = collect($user->getAttributes())->except(['cmseditor', 'password', 'secret', 'remember_token'])->all();
80 | $expected = ['id' => (string) $user->id] + $attr;
81 |
82 | $this->expectsDatabaseQueryCount( 0 );
83 |
84 | $response = $this->actingAs( $this->user )->graphQL( "
85 | mutation {
86 | cmsLogout {
87 | id
88 | email
89 | name
90 | email_verified_at
91 | created_at
92 | updated_at
93 | }
94 | }
95 | " )->assertJson( [
96 | 'data' => [
97 | 'cmsLogout' => $expected,
98 | ]
99 | ] );
100 | }
101 |
102 |
103 | public function testMe()
104 | {
105 | $user = \App\Models\User::where('email', 'editor@testbench')->firstOrFail();
106 |
107 | $attr = collect($user->getAttributes())->except(['cmseditor', 'password', 'secret', 'remember_token'])->all();
108 | $expected = ['id' => (string) $user->id] + $attr;
109 |
110 | $this->expectsDatabaseQueryCount( 0 );
111 |
112 | $response = $this->actingAs( $this->user )->graphQL( "{
113 | me {
114 | id
115 | email
116 | name
117 | email_verified_at
118 | created_at
119 | updated_at
120 | }
121 | }" )->assertJson( [
122 | 'data' => [
123 | 'me' => $expected,
124 | ]
125 | ] );
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/tests/HelpersTest.php:
--------------------------------------------------------------------------------
1 | assertEquals( '/storage/not/exists.jpg 1w', cmsimage( [1 => 'not/exists.jpg'] ) );
11 | }
12 |
13 |
14 | public function testCmsUrl()
15 | {
16 | $this->assertEquals( 'data:ABCD', cmsurl( 'data:ABCD' ) );
17 | $this->assertEquals( '/storage/not/exists.jpg', cmsurl( 'not/exists.jpg' ) );
18 | $this->assertEquals( 'http://example.com/not/exists.jpg', cmsurl( 'http://example.com/not/exists.jpg' ) );
19 | $this->assertEquals( 'https://example.com/not/exists.jpg', cmsurl( 'https://example.com/not/exists.jpg' ) );
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/tests/TestAbstract.php:
--------------------------------------------------------------------------------
1 | loadLaravelMigrations(['--database' => 'testing']);
24 | }
25 |
26 |
27 | protected function defineEnvironment( $app )
28 | {
29 | $app['config']->set('database.connections.testing', [
30 | 'driver' => 'sqlite',
31 | 'database' => ':memory:',
32 | 'prefix' => 'test_',
33 | ]);
34 |
35 | $app['config']->set( 'cms.db', 'testing' );
36 |
37 | \Aimeos\Cms\Tenancy::$callback = function() {
38 | return 'test';
39 | };
40 | }
41 |
42 |
43 | protected function getPackageProviders( $app )
44 | {
45 | return [
46 | 'Aimeos\Cms\CmsServiceProvider',
47 | 'Kalnoy\Nestedset\NestedSetServiceProvider',
48 | ];
49 | }
50 | }
--------------------------------------------------------------------------------
/tests/User.php:
--------------------------------------------------------------------------------
1 | '',
10 | 'email' => '',
11 | 'password' => '',
12 | 'cmseditor' => 0,
13 | ];
14 |
15 | protected $fillable = [
16 | 'name',
17 | 'email',
18 | 'password',
19 | 'cmseditor',
20 | ];
21 | }
22 |
--------------------------------------------------------------------------------
/tests/default-schema.graphql:
--------------------------------------------------------------------------------
1 | "A datetime string with format `Y-m-d H:i:s`, e.g. `2018-05-23 13:43:32`."
2 | scalar DateTime @scalar(class: "Nuwave\\Lighthouse\\Schema\\Types\\Scalars\\DateTime")
3 |
4 | "Indicates what fields are available at the top level of a query operation."
5 | type Query {
6 | "Find a single user by an identifying attribute."
7 | user(
8 | "Search by primary key."
9 | id: ID @eq @rules(apply: ["prohibits:email", "required_without:email"])
10 |
11 | "Search by email address."
12 | email: String @eq @rules(apply: ["prohibits:id", "required_without:id", "email"])
13 | ): User @find
14 |
15 | "List multiple users."
16 | users(
17 | "Filters by name. Accepts SQL LIKE wildcards `%` and `_`."
18 | name: String @where(operator: "like")
19 | ): [User!]! @paginate(defaultCount: 10)
20 | }
21 |
22 | "Account of a person who utilizes this application."
23 | type User {
24 | "Unique primary key."
25 | id: ID!
26 |
27 | "Non-unique name."
28 | name: String!
29 |
30 | "Unique email address."
31 | email: String!
32 |
33 | "When the email was verified."
34 | email_verified_at: DateTime
35 |
36 | "When the account was created."
37 | created_at: DateTime!
38 |
39 | "When the account was last updated."
40 | updated_at: DateTime!
41 | }
42 |
43 | #import ../graphql/cms.graphql
--------------------------------------------------------------------------------
/views/admin.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | @if( !config('app.debug') )
9 |
10 | @endif
11 |
12 | Laravel CMS Admin
13 |
14 |
15 |
16 |
17 |
18 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/views/article.blade.php:
--------------------------------------------------------------------------------
1 | @pushOnce('css')
2 |
3 | @endPushOnce
4 |
5 |
6 |
{{ $title }}
7 |
8 | @includeIf('cms::image', ['main' => true] + ($intro ?? []) ))
9 |
10 |
11 | = (new \League\CommonMark\GithubFlavoredMarkdownConverter([
12 | 'html_input' => 'escape',
13 | 'allow_unsafe_links' => false,
14 | 'max_nesting_level' => 25
15 | ]))->convert($text ?? '')
16 | ?>
17 |
18 |
--------------------------------------------------------------------------------
/views/cards.blade.php:
--------------------------------------------------------------------------------
1 |
2 | @foreach($cards ?? [] as $card)
3 |
4 |
7 |
8 |
9 | @foreach( $card['image']['previews'] ?? [] as $width => $path )
10 |
13 | @endforeach
14 |
18 |
19 |
{{ $card['name'] ?? '' }}
20 |
21 | = (new \League\CommonMark\GithubFlavoredMarkdownConverter([
22 | 'html_input' => 'escape',
23 | 'allow_unsafe_links' => false,
24 | 'max_nesting_level' => 25
25 | ]))->convert($card['text'] ?? '')
26 | ?>
27 |
28 |
29 |
30 | @endforeach
31 |
--------------------------------------------------------------------------------
/views/code.blade.php:
--------------------------------------------------------------------------------
1 | @pushOnce('css')
2 |
3 | @endPushOnce
4 |
5 | @pushOnce('js')
6 |
7 | @endPushOnce
8 |
9 |
10 | {{ $text ?? '' }}
11 |
--------------------------------------------------------------------------------
/views/heading.blade.php:
--------------------------------------------------------------------------------
1 |
2 | {{ $text ?? '' }}
3 |
4 |
--------------------------------------------------------------------------------
/views/html.blade.php:
--------------------------------------------------------------------------------
1 |
2 | {!! $text ?? '' !!}
3 |
--------------------------------------------------------------------------------
/views/image-text.blade.php:
--------------------------------------------------------------------------------
1 |
2 | @includeIf('cms::image', $image ?? [] )
3 |
4 | = (new \League\CommonMark\GithubFlavoredMarkdownConverter([
5 | 'html_input' => 'escape',
6 | 'allow_unsafe_links' => false,
7 | 'max_nesting_level' => 25
8 | ]))->convert($text ?? '')
9 | ?>
10 |
11 |
--------------------------------------------------------------------------------
/views/image.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 | @foreach( $image['previews'] ?? [] as $width => $path )
4 |
7 | @endforeach
8 |
12 |
13 |
--------------------------------------------------------------------------------
/views/invalid.blade.php:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/views/paragraph.blade.php:
--------------------------------------------------------------------------------
1 |
2 | = (new \League\CommonMark\GithubFlavoredMarkdownConverter([
3 | 'html_input' => 'escape',
4 | 'allow_unsafe_links' => false,
5 | 'max_nesting_level' => 25
6 | ]))->convert($text ?? '')
7 | ?>
8 |
--------------------------------------------------------------------------------
/views/table.blade.php:
--------------------------------------------------------------------------------
1 |
2 | @if($title ?? null)
3 |
4 | {{ $title ?? '' }}
5 |
6 | @endif
7 |
8 | @foreach((array) str_getcsv($content ?? '') as $rowidx => $row)
9 | @foreach((array) $row as $colidx => $col)
10 | @if($rowidx === 0 && ($header ?? null) === 'row'
11 | || $colidx === 0 && ($header ?? null) === 'col'
12 | || ($rowidx === 0 || $colidx === 0) && ($header ?? null) === 'row+col')
13 |
14 | @else
15 | |
16 | @endif
17 | = (new \League\CommonMark\GithubFlavoredMarkdownConverter([
18 | 'html_input' => 'escape',
19 | 'allow_unsafe_links' => false,
20 | 'max_nesting_level' => 25
21 | ]))->convert((string) $col)
22 | ?>
23 | @if($rowidx === 0 && ($header ?? null) === 'row'
24 | || $colidx === 0 && ($header ?? null) === 'col'
25 | || ($rowidx === 0 || $colidx === 0) && ($header ?? null) === 'row+col')
26 |
27 | @else
28 | |
29 | @endif
30 | @endforeach
31 | @endforeach
32 | |
33 |
2 | {{ __('Download') }}:
{{ cmsurl( $source ?? '' ) }}
3 |
--------------------------------------------------------------------------------