Compare commits

...

343 commits

Author SHA1 Message Date
ro
37c5b6b28f
quick layout fix for restore backup ui
restore from backup screen was a bit wonky because the forms where
running together, so threw in some breaks so it lays properly.
2024-11-08 11:37:31 -06:00
ro
eadbbc59ae Merge pull request '[draft] Make init fresh page more responsive' (#118) from xarvh/fipamo:responsive-init-fresh into develop
Reviewed-on: https://koodu.h-i.works///projects/fipamo/pulls/118
Reviewed-by: ro
2024-11-08 18:28:57 +01:00
f88b088b50 separate and clean up atomic css 2024-11-06 18:57:27 +01:00
ro
84c6fa7a1a
theme color correction, editor responsive tweaks
the theme color for the dashboard was incorrect, so that has been
updated and the responsive sticky for the text formatter in the page
editor went funky with posts that had a lot of text so that's been
adjusted so it behaves as it should when it has to resize and stick
2024-11-02 14:07:53 -06:00
ro
f53bb588c5
patch for page preview
page preview from editor was not working because it was not passing the
page uuid correctly so it can be identified and rendered unto the theme
viewer.
2024-11-01 15:23:22 -06:00
ro
9c432db209
fix for link encoding issue
small bug where links in the archive and tag templates where being
rendered as url encoded strings, so that needed to be decoded so it
displays as normal text
2024-10-29 12:05:50 -06:00
aa449b12b2 Make init fresh page more responsive 2024-10-29 17:31:23 +01:00
ro
a7354da38f
remove uploaded files page fixed
there was a minor bug that was preventing uploaded files to be removed
from the editor page even thought the remove button was pressed.

found and fixed
2024-09-04 14:52:52 -06:00
ro
c85e145774
added getbyIndex for pages, urlencode page titles
the theme controller was grabbing the index by its page name, which was
bad because that might change. that was replaced with a getById function
since the index id will always be 0 since it's the first page. this is a
seperate function from getByUuid which is a unique indentifier for each
page which was being used interchangably before the fix. all of those
references have been cleaned up to reference which type of id is needed

there was also a bug that happened on rendering when there were special
characters in the title. this was solved by saving the title as a
urlencoded string and then just decodded when it was needed for display
on the front end
2024-09-04 14:32:36 -06:00
ro
a0f7bff67c
updated sanitizer, fixed render on save
the module used to sanitize html is no longer maintained, so updated
that methodology to use symfony's very sexy sanitizer

checked to make sure render on save was working as well.
2024-09-04 13:29:06 -06:00
ro
0e6d4bd2b4
Case patch directory pathing
app was erroring out because classes in the upkeep directory could not
be found due to a case mix up where 'Upkeep' and 'UpKeep' were both
being used causing a path error because the actual directory is 'Upkeep.'
2024-08-30 16:14:15 -06:00
ro
2c8bd10391
added link to contributiong guide to readme
added a link straight to the contribution guide in the repo for easy
access
2024-08-05 15:56:45 -06:00
ro
5f40e7b98d
Contribution Guide
Made a md file for the contribution guide to give guidance to people who
want to help with the project.

It's a living document, so changes will be made as needed.
2024-08-05 15:53:34 -06:00
ro
38c907a08d
Updated project description
updated the README to detail what the project is and its goals. it's the
first draft so it will change over time, but it provides a bit more
insight into what Fipamo is
2024-08-04 14:37:41 -06:00
ro
ea9dbf025e
Settings API, Part 1
the first part of the setting API has been restored, with the methods
sync, publish and nav-sync restored to working order under the new API
pattern. Documentation for the API will be completed after the upgrade
is complete.

There was also a script error when trying to edit a page from the link
menu on the dashboard, so that has been fixed as well
2024-07-26 15:10:37 -06:00
ro
1d206b927d
navigation sync bug fix, api & settings cleanup
there was an error when reordering menu items due to a missing variable
to a service class, so that was patched

also removed unecessary code from api url definitions and settings
service class
2024-07-26 12:43:45 -06:00
ro
d8ed8b62c0
Page Admin API, part 2
page tasks have been changed to accept JSON data for the sake of
consistency across the API. The only API method that will accept form
data is file uploads.

Also restored the post, put and delete pattern for better organization
and clarity describing what each page method is for
2024-07-24 14:57:04 -06:00
ro
321feb9b92
Page Admin API, part 1
API accessible tasks (create, update, delete) have been updated to the
new format.

The controller needs to be cleaned up because it's a bit heavy and the
new API flow still needs to be properly documented, but it's a good
start
2024-07-23 17:46:45 -06:00
ro
4337a20fb8
API Improvements #116
API Security has been reworked to check if request is secure, verifies
the API token created on site setup given to every member, and then
confirms the system is accepting API requests by way of the API enabled
toggle in settings

API usage is now only meant for backend use, so this needs to be noted
in the docs
2024-07-17 16:41:11 -06:00
ro
c5afbb9131
API Decouplng Part 2
removed all remaining API requests from the front end and removed the
FipamoAdminAPI js file, changing it to ContentRequest as it now handles
posting data to the system directly, authenticating it self by checking
the embedded CSRF token that regulary rotates

also renamed MaintenanceManager to Maintenance request and moved it to
the same dir as ContentRequest as they are both libraries that connect
to the backend
2024-07-17 15:08:10 -06:00
ro
3d17771f76
API Decoupling, Part 1
The first part of improving the API is removing all admin functions from
the front end so those no admin methods will be available client side.

The urls in the FipamoAdmin js file have been changed to post directly to the
system and they are handled from there. To account for this change
controller routes for every standard method have been created for better
organization and readability.

The FipamoAdmin js file will be integrated with the rest of the front
end code and will not be seperate library
2024-07-13 14:23:01 -06:00
ro
458b076f73
FEATURE: full asset restore from backup archive
the current restore process only restored images and not the additional
file types that are allowed, so that has been added

also tweaked the reset request to include the correct token so the
request does not fail API authorization
2024-07-09 16:26:01 -06:00
ro
d51071b2d3
added additional asset folders to back up script
the current file backup included images, but the additional asset types
such as sound files, video and docs were not being added if they
existed, so those have been added.

also tweaked the front end script to include the correct token for the
API so the backup request doesn't fail the auth check
2024-07-09 14:27:03 -06:00
ro
4520c9b608
FIX: misnamed sorting attribute for uploaded files
when creating new ui elements for uploaded files, the 'data-source'
attribute was being created to store the upload path for the file which
is used to save that info in the page doc.

that attribute should be 'data-id' as that is what sortablejs looks for
when sorting element data into arrays
2024-07-09 13:38:02 -06:00
ro
9f6d1970ce
updated wiki link on README
Wiki link in README was still pointing to the old repo, so that needed
to be corrected
2024-07-06 17:45:28 -06:00
ro
4113418c83
reorganized api, added token validation
regrouped api calls for better organization and to add a bit more
security. it now checks to make sure the incoming token matches the
current session to authorize requests
2024-07-06 17:41:32 -06:00
ro
ac543f3856
UPDATE: page deletion
page editor was missing soft delete of page as it had not been wired up
to the new api yet. oops
2024-07-06 16:23:23 -06:00
ro
d24df50d96
FEATURE: Full site restore
content restore was already enabled, so the last step was completing
file restorartion if there was a file archive uploaded

now that this has been added, full site restore from backup archives now
works.
2024-07-05 13:17:53 -06:00
ro
4e880092c1
FEATURE: File Backup
Turned on file backups that grabs uploaded images in the public
directory and saves them in a zip so they can be downloaded and
archived.
2024-07-03 14:30:32 -06:00
ro
bc7b1fe7ec
FEATURE: Restore to default
plugged in a new feature that will allow the site to be reset to its
default state, clearing out all content and configurations to start
fresh
2024-07-02 17:09:27 -06:00
ro
4723db98d5
ignore scripts/theme folder
removed files from the script/theme folder under assets because this
content will be changed dynamically when a site is published
2024-07-02 15:07:54 -06:00
ro
68469d958b
Fix for #115, theme css subdir deleted on publish
Patch for a bug that was deleteting the subdirectories of the theme css
folder holding additional theme assets  when being published and tested in the theme kit.

also removed an unneccessary helper script and organized directory
actions in their own helper file to keep it all tidy
2024-06-30 19:50:27 -06:00
ro
fb3aff6c85
automate environment file creation
added a post install script to the composer.json file to automate the
creation of the .env file which hold app specific configs

also added a helper function to create new keys in the future
2024-06-30 18:45:41 -06:00
ro
928d5468db
removed hard-coded page id in theme controller
theme controller class was using hard coded uuids to find and display
page data, so that's been tweaked so that is no longer needed and finds
what it needs based on what time of view is being requested

also connected it the the preview 👁️ button in the page edit screen so
page layouts based on the current theme can be seen before they are
published
2024-06-26 17:10:24 -06:00
ro
ed6f028598
added layout varible to sorting, updated ignore
data sorting needed a new variable to differentiate between index page
rendering and standard page rendering, so that was added to the return
as well as updating the render script to include the variable that
wasn't being added for tags

also updated the git ignore file to ignore global images that are not
used by the base system. the script theme folder needs to be ignore as
well as that is going to updated by the theme itself for front end
coding and not used by the system
2024-06-26 14:08:53 -06:00
ro
7c027beb92
menu update fix, asset path tweak
when pages where being unpinned from the main menu, there was an error
resulting from a missing method reference in the class

also rearranged some path references in the asset service class for the
sake of better organization
2024-06-25 17:01:10 -06:00
ro
37120efe18
asset moving for themes fixed, settings save patch
When testing themes, the script wasn't moving all assets that were in
subdirectories of the theme css folder, so that's been fixed so it moves
everything when testing a theme and rendering the site

there was also an issue with saving settings options because the script
was referencing email data that was no longer being provided from the
front end, so it was erroring out trying to save it. those references
have been removed so it's smooth sailing
2024-06-25 15:33:42 -06:00
ro
7d78476870
restore site error path
there was minor error with pathing to remove the back up zip once the
restore process finised

fixed
2024-06-12 15:09:58 -06:00
ro
e02f99da94
restore asset url check
in the restore init process, there is a space that restores assets from
a previous install but grabbing them directly from the site and saving
them to the install.

a check was added so this script only runs if that url has been set in
the init restore form
2024-06-12 14:59:07 -06:00
ro
59b9f481af
removed hard-coded environment paths; updated .env
there were places in the codebase where path to fipamo specific paths
and file were hard-coded into the system, reducing flexibility and
customozation for people that want to define their own path.

those hard-coded elements have been removed and replaced with references
to the env file so those paths can be changed to whatever is needed

the .env.example file has also been updated to contain default paths for
easier installation.
2024-06-12 14:53:10 -06:00
ro
e0994b32ab
mail error capture (#113)
tweaked error catching to return a message when the mail action fails
2024-06-05 13:50:45 -06:00
ro
b37e64d062
Updated Mailer (#113)
Removed email settings from settings.json and moved them to the .env
file to use Laravel's mailer functionality.

references to the old mailer has been removed from settings.json,
data sorting class and the settings template file

the front end script has also been updated to accomodate different
message types, starting with a test message so members can make sure the
settings defined in the .env are working
2024-06-05 13:33:11 -06:00
ro
9923f087a6
fix for tag sorting and html page render
when sorting tags on a fresh install, a fatal error happens when the tag
array is empty because it looks for a var that doesn't exist yet. that's
been patched

the settings json file was still referencing the old version of the base
theme, so it was crashing because it doesn't exist anymore. updating it
to the new theme name clears this error
2024-06-04 15:19:46 -06:00
ro
8704fdcfe1
fix for status check
legacy reference to depracated auth class was removed
2024-06-04 14:58:55 -06:00
ro
c7aef33845
fix for #114 - incorrect variable reference
the settings page was failing to load because it was referencing a
parameter in the mail config data array that did not exist.

that's been fixed so the page loads properly
2024-06-04 14:23:41 -06:00
ro
728a2e5a69
incorrect error capture for creating member secret
the init process was erroring out because instead of re-running the
script to make a valid token, it was not capturing the correct error, so
it would just fail

also the folder for saving config files needed to be created to save
them, so that addition was made as well
2024-06-02 21:33:20 -06:00
ro
2f405ffb6d
incorrect path to template files
init process was looking for template settings file in incorrect
location. fixed
2024-06-02 21:04:42 -06:00
ro
787c00a75b
improper reference in init class
init class needed to be udpated to correct the reference to the doc
services class.
2024-06-02 21:00:52 -06:00
ro
35c22913f8
incorrect path to init service class
there was a fatal error in the init process because it was looking for a
class that had been moved but the reference had not been updated, which
is now corrected
2024-06-02 20:52:01 -06:00
ro
b268bfb49e
enviroment config template updated
the example file containing environment variables was updated to include fipamo
specific settings
2024-05-31 13:04:26 -06:00
ro
91aa2703b6
avatar and background image upload fixed
uploading new member avatar and background images weren't uploading to
the correct location and the approprate files weren't being updated, so
that was been fixed.

the folks template in the init directory also needed be updated because
the system was looking for 'avatar' instead of 'avi' which was resulting
in some mismatch calls that were resulting in not found errors. the
variable is now 'avatar' everywhere for the sake of consistency.
2024-05-31 12:56:38 -06:00
ro
cd056336cd
file upload fix
found a minor bug that was preventing file uploads, so that was fixed.

also commented out some buttons that are for features that have not been
added just yet
2024-05-28 13:16:50 -06:00
ro
db5ee3f87f
settings responsive pt. 1
ripped out all of the old responsive styles from the setting layou and
plugged in the new format.

still needs some polish but the new structure is in place, so progress
will be easier
2024-05-28 13:00:25 -06:00
ro
bdca7f4ace
fixed mobile menu
some script got removed during the restruction that included the js
handler for the mobile menu, so that was replaced with a css toggle to
keep it simple
2024-05-28 11:38:32 -06:00
ro
a5cf8816f0
themes setting tabbed styled
the themes tab has been brought into alignment with the new design, so
the only thing that remains is redoing responsive styles for the
settings page

whew
2024-05-26 14:38:46 -06:00
ro
f53716dba9
fixed form field styles
edits to the settings page threw of some form styles throughout the the
dashboard, so some quick css edits were needed to polish them up
2024-05-26 13:16:04 -06:00
ro
9bc0ae8bed
site features tab restyled
the section housing site features has been restyled and brough into
alignment with the new design

still needs responsive work
2024-05-24 16:22:11 -06:00
ro
428e74ae60
member profile tab styled
redid the css for the member profile tab under settings.

responsive style still need to be addressed, but that will happen after
all tabs have been converted to the new layout
2024-05-22 15:15:21 -06:00
ro
ab158c99b0
settings page tab conversion part 1
the settings page needed to be updated to make space for incoming
features and settins, so a tab system has been plugged in to reorganize
the options availble

of course, everything needs to be restyled to match the new comps
https://www.figma.com/design/JQSy6XtVtSkiIxzXEM46IU/Fipamo-Dash?node-id=1204%3A285&t=kDgzU2ONXr3k5yUe-1
but the base structure is in, so now it's just about getting the css
right
2024-05-21 19:15:47 -06:00
ro
8c375e6ba1
update member info from settings
setting sync is working but member data was not being updated in the
folks file or in the current active session, so that's been addressed

still need to turn on avatar updating as well, but that is tied to
updating the settings page, so that will be handled when image uploads
for that area are reactivated
2024-05-13 13:36:06 -06:00
ro
064407aa88
implemented member repo class
a class for members was needed for long term handling of member
functions like login, update, status checking, etc so that class was
created and the AuthService class was removed as it was redundant and
it's functionaity moved to the member class
2024-05-13 12:06:05 -06:00
ro
36d04c8f68
reorganized services
service classes are beginning to swell as there functionality is being
fleshed out, so a new organizational structure was needed to make sure
class sizes don't become too large and to increase site managability and
legibilty as more features get added and the code base grows.

data is for retrieving, managing site information, assets interact with external files
and upkeep is for maintenance.

some additional tweaks were also made to the options menu template to
prep it for it's transition to a toolbar component
2024-05-12 22:14:53 -06:00
ro
a2053df931
edits for issues #104 and #110
logo images did not have an alt tag, so they've been added

buttons on the options menu were generic, so an interactive description
was added through the use of aria labels
2024-05-09 13:28:15 -06:00
ro
7f1654d13b
controller reorganization
cleaned up the controller directory and renamed files to more approriate
names.

made the appropriate changes to RouteController as well as tweaking the
routing so it's simpler and easier to follow
2024-05-09 11:24:12 -06:00
ro
d8ee4926b1
keep RouteController
had some comments referencng a the idea for a refactor of routing, but
decided to stick with RouteController for now becuase of the
flexibility.

if improvements are to be made, they will be made in that class.
2024-05-08 21:42:08 -06:00
ro
e09d85292a
added labels to forms (#102), init templates added
added some labels to login and set up forms for better accesibility and
clarity about what info is being requested.

found a bug in setting where the site would error out if it can't find
the correct config files instead of going to set up, so when config
files aren't present, it loads template files to continue to process.
still thinking about a better fix for this

also discovered that the init files needed for a fresh install had been
removed, so those are back in place
2024-05-08 15:55:27 -06:00
ro
770959137a
removed nested buttons from href tags #105
main navigation had buttons nested inside of href tags to house previous
font icons that were removed, so the nesting is no longer needed and it
could introduce accessibility propblems, so it needed to be cleande up

now they are hrefs styled as the previous buttons so there is no visual
change in the UI
2024-05-08 13:23:42 -06:00
ro
bd25c0b104
route controller auth checking
there was no auth checking for dashboard and theme routing, so that's
been added
2024-05-08 12:27:56 -06:00
ro
b6425e2c07
quick clean up
changes to front end scripting has rendered some files obsolete so they
have been removed.

removed some todo notes that have already been implemented
2024-05-07 14:17:18 -06:00
ro
e224410cfa
navigation route fix
the route to show menu order was erroring because it was calling a
misspelled method, so that was fixed

also added some notes to improve the StartController
2024-05-06 15:18:05 -06:00
ro
cb99c44a33
implemented dynamic page rendering
html page rendering works, so the next step was to get dynamic page
rendering up and running, meaning instead of pages being exported to
html, they are instead rendered on the fly when the corresponding URL is
hit.

the StartController that handles page routing needs to be organized a
bit better as there is some confusion between index and regular page
files so it's clear where requests are going and what is the expected
response
2024-05-06 15:09:24 -06:00
ro
ceeb4a2573
page routing rework
routing needed more nuance than what was possible in the web routing
controller, so a new RouteContoller was created to identify requests and
then sending them to the correct controller to get the appropriatie page

this was necessary because routing the previous was erroring out because
when the system was looking for pages to display with dynamic page
creation it would get confused with prexisting routes and choose to
display whatever the Start Controller was capturing, ignoring routes
defined in the web controller.

but that's been cleaned up without having to re-write everything and
continues to use existing controllers
2024-05-06 13:37:26 -06:00
ro
6cb9631a46
intergrated the fipamo theme kit
a basic preview engine has been added to ease the process of editing
pages. currently it previews all basic templates and custom created
pages

this is will replace the external fipamo theme kit tool, which will be
archived
2024-04-25 15:03:06 -06:00
ro
8bf640ee18
edited asset moving class
conversion of markdown files to html works fine, but the coresponding
css, js and image assets were not being moved, so the class responsible
for moving them was edited so theme assets are moved to their
    appropriate directories when the site is published.

also made some css and image edits, and removed a legacy css files that
were no longer in use
2024-04-25 13:17:24 -06:00
ro
a92e124957
default theme clean up
the default theme included with fipamo was a bit janky, so that's been
cleaned to bring it more inline with current accessiblility standards,
an updated mobile friendly menu, updated the logo and plugged in the
fresh colors.

the layout is still a bit boring but the main purpose of the default is
to show how templating works, so it needs to be kept as simple as
possible

but it still has some room for making it pop a bit more.
2024-04-23 13:41:41 -06:00
ro
69eb9905a2
update tags on page save
the list of tags was not being updated when an edited page was saved,
which is needed for the front end tags page.

all cleaned up and active
2024-04-22 15:19:06 -06:00
ro
234453b900
update site back up and restore process
the back up process did not include saving document and video assets so
that needed to be updated to save all of those references along with
images

once that was done, the restore process had to be updated to look for
docs and videos and create the corresponding directories to move them
too

the script responsible for moving those assets was also streamlined so a
seperate process was not needed for each asset type.

the last update date in the settings template was displaying
incorrectly, so that was fixed as well.
2024-04-22 13:11:51 -06:00
ro
b2493820e9
activated image transfer for restore process
rather than make a massive downloadable archive file for ever image on
the site (which still may happen), a method has been added to make
copies of imags from an external site and store them on the fresh
install based on the image list saved in the created back up file

it's clean but some additional error checking is needed so the process
does not crash out when a file can't be located and upon completion the
user can be notified of what images did not make it over in the process
2024-04-18 14:36:05 -06:00
ro
2f0e1fdc62
implemented install from scratch
activated the fresh install process for setting up a brand new fipamo
install
2024-04-16 15:36:57 -06:00
ro
08b79cecdf
upgrade to laravel 11
laraval had a milestone update to version 11, so the codebase needed an
updgrade.

there was one package that wasn't needed that was blocking the upgrade
because it was attached to a hard coded dependency in an older version
of laravel so it was removed.

this changed required an edit of some template files, so those changes
were made as well
2024-04-15 15:54:13 -06:00
ro
70e984dc96
added license
GNU GP3 V3 open source license has been added. finally.
2024-04-15 14:35:12 -06:00
ro
8e9752d619
color contrast tweak
secondary highlight color contrast was a bit weak against the primary
color, so that was cleaned up so it pops a bit more
2024-04-05 12:42:26 -06:00
ro
f8005aa60d
turned on backups
ported over the backup functionality from the old build to the new while
making  few tweaks

instead of packaging up all files in the site to create massive zip
files, now a list of files is created for the user and blog directories
that the system will use to download and put them in the appropriate
directories, resulting in a such slimmer backup file.

archiving images my be added at a later date, but will be kept seperate
from the current back up process
2024-04-05 12:29:38 -06:00
ro
1f62e6f816
reworked sorting service class
decided to keep the sorting service class as is and remix how it works to
return data objects rather than just settings information to render
pages.

the overall size of the class is still large but now there are
some opportunites to move around some methodolgy to reduce it.

also made the necessary changes in the render service class and the
theme controller to use the new methodology
2024-03-28 16:42:37 -06:00
ro
9a716acb29
removed string service
moved methods of string service to helper function and got rid of the
string service class

still messing around with the sorter helper to see how dependecies work.
sorting as a helper may be a bit too complex for what help functions are
supposed to be
2024-03-28 15:13:13 -06:00
ro
65220fe06a
added helper function scaffolding
some serivce classes are getting to heavy, so some functionality will be
offloaded to custom helper function for better manageability and
readability.

a couple of place holders have been added to demonstrate how helpers
will be organized
2024-03-27 14:04:19 -06:00
ro
fe36bae01e
removed role from frame and settings script
there was an unused layer that had an impropper role attribute and
improper usage in the settings script, so both have been removed
2024-03-25 13:46:43 -06:00
ro
f2a97af3f5
role removed from navigation edit ui
improperly used role attributes have been removed from navigation edit
template

there was also a duplicate class attribute error in notifications that
has been fixed
2024-03-25 13:36:58 -06:00
ro
f3b565bb1e
removed improper role attribute from book template
incorrect usage of the role attribute has been removed from the book
template and corresponding styles
2024-03-25 13:20:14 -06:00
ro
a10dbcdeff
change role for icons to class
role was being used incorrectly for icon styling, so that has been
corrected to use the class attribute instead
2024-03-25 12:59:24 -06:00
ro
a53723f7c8
changed role attritubet to class in page editor ui
the role attribute is being used incorrectly through out the site, which
is bad for screen reader, so they need to be removed and replaced with
the class attribute

the page editor was the biggest culprit, so that one has been converted
first
2024-03-25 12:53:31 -06:00
ro
3b5c753c9a
cleaned up logos, sprite sheet tweaks
there were some random logos in the global images folder, so that was
cleaned up and replaced with primary and secondary options according to
the current color palette for the sake of flexibility

found a nice repo of svg icons, so experimented with adding one to the
sprite sheet to see if it works, which it does
2024-03-22 20:15:18 -06:00
ro
3c6322ec12
actived menu editing
the last ui page that needed to be added was managing the main
navigation menu for rendered pages, so that's been turned on. menu
items can be added by pinning pages to the menu when editing them and
can be removed by unpinning them or deleting them from the navigation
edit ui

it touched quite a few systems so all of those classes needed to be
edited as well tweaking the front end script to work with the new
modular script format
2024-03-22 14:35:44 -06:00
ro
989a4c7b69
removed all font icons
the notifications and page editor ui elements where the only remaining
space that still contained font icons, so they've been removed and
replaced with svg

some styling tweaks are still needed so that will handlded as the css
edits continue
2024-03-21 20:10:09 -06:00
ro
33d6688af3
sprite test
a quick test to see if more svg icons can be added to the base sprite
file to keep them all in the same place

it worked, so additional icons will be added to sprite file
2024-03-21 16:41:29 -06:00
ro
4d20f62915
replaced icons for pages page
all font icons have been removed for the page listing page
2024-03-21 15:29:32 -06:00
ro
56b0b984d4
icons replaced for settings page
all font icons on the setting page have been removed and replaced with
svg images
2024-03-21 15:09:37 -06:00
ro
8794ffc715
Color Adjustments
The colors just needed to be tweaked a bit for the sake on better
contrast and readability.

Still some trouble spots but will adjust as needed
2024-03-21 14:51:24 -06:00
ro
82984635b5
removed stylelint
stylelint was becoming cumbersome, so I just removed it for now...
2024-03-21 14:04:15 -06:00
ro
8c727d4414
accessibility audit, icons part 1
using a font for icons isn't great for site accessibility, so all font
icons will be removed and replaced with svgs.

nav and sub nav, page options and recent meta have been replaced
2024-03-20 15:13:50 -06:00
ro
252059df19
settings syncing active
turned on the abiity to save settings to config file via the settings
page

the current member session needs to updated by the data coming in but
that will be handled by a specific member service that hasn't been built
yet, so just commenting it out for now

also fixed a minor bug that was stopping the save on render toggle from
working correctly, so now it's saving and updating the status properly
now
2024-03-19 16:35:02 -06:00
ro
eda377aba3
publish site from settings active
with the setting page set up, now the the settings api can be added,
beginning with the ability to render all files from settings.

the base settings api class is set up, so now the rest of the methods
can be added
2024-03-19 15:34:01 -06:00
ro
0951005341
added settings page
it's time to get the setting api running so site options can be editied
so the first step is to get the settings page up and running.

the sorting class is getting a bit heavy, but it will hold the method
for gathering settings page info for now.
2024-03-19 13:19:27 -06:00
ro
2420ea193c
added front controller
added a controller class to handle what is being served when the browser
hits the root directory.

very basic to start as it is just a placeholder until it's filled out
2024-03-18 19:48:59 -06:00
ro
a6b63ca551
implemented render engine
page rendering for tags, archives and markdown pages is now up and
running and being exported to html when requested.

currently it only works when saving a page, so now the settings page
needs to be plugged in so those features can be activated and toggled
through that UI

error checking will probably be added in the near future just in case
something unforeseen happens in the render process, resulting in the
site erroring out.
2024-03-18 16:14:17 -06:00
ro
efb2e4f0bd
created asset service class to handle file moving
theme controller was getting top heavy, so an asset service class was
plugged in to handle moving assets around for theme testing and
eventually to production when HTML rendering is set up
2024-03-16 12:36:51 -06:00
ro
4eed4489f4
page options fixes
with the archive template up and running a bug with page creation and
editing was revealed which was certain options were not being set
correctly due to property not being set correctly when an markdown page
was being edited.

also added a null state check for page submissions that do not have a
layout set, so it defaults to 'page'

also patched theme service class to look for blade templates instead of
twig, which it was still doing
2024-03-15 15:18:10 -06:00
ro
2f2955e865
converted page, archive and tags files
the remaining base template pages have beeen converted to blade as well
as filling out the data they need to render being added to the sorting
service class

theming controller and and sorting service still need to be optimized
but they work so now they can be refined

once they have been cleaned up, the render service is class is ready to
be finished
2024-03-15 14:28:26 -06:00
ro
ba2e75b186
converted index template to blade
started the front end template conversion with the index page
2024-03-14 20:03:46 -06:00
ro
a5583debbd
page rendering, part 1
to complete page rendering, the default theme needed to be converted to
use blade templating. rather than update the theme kit as a seperate
progress, it will be integrated into this codebase so themes can be
developed and tested in app.

the basics for the theme kit are in place, so now conversion of the
defualt theme can be completed.

once the that is done, it can then be used to complete the rendering
engine to export HTML files
2024-03-14 16:58:11 -06:00
ro
950ca6f7ea
added sorting and render service classes
plugged in sorting class to gather the info necessary for the render
class to convert markown files to html and move them to the correct
location in the public diretory
2024-03-14 12:39:43 -06:00
ro
4f7bbcdf86
cleaned up new page creation
editing page works but making new pages was still wonky, so that was
fixed and now page creation works fine

made some minor tweaks to prettier config for css formatting
2024-03-12 16:16:36 -06:00
ro
6a0c583a4f
file upload extensions hot fix
upload script for file types was still looking at mime types instead of
extensions so made that flip so files are sorted properly
2024-03-11 16:42:46 -06:00
ro
6ce5e91624
file upload service added
ported over the new file uploader from the old build and made it a
service make for from some additonal file processing
i.e. image optimization or video converstion before it is saved to the system
2024-03-11 16:32:38 -06:00
ro
bd5f57b9ac
fixed page asset reference paths for saving
switched to using sortjs method for collecting urls for assets
associated with page
2024-03-10 18:47:40 -06:00
ro
7024285b70
page update api up and running
the first part of the page editing API is working again after porting it over
form the old fipamo build. a few changes where made to make the code a
big more managable, but the end to end process works for updating pages.

the remaining page editing methods will be activated after the rendering
engine is in place because that's going to be a pretty siginficant
effort.

but this is a big step.
2024-03-08 14:09:43 -06:00
ro
b9b04f1ab2
page api rewire, pt 1
start building out the new version of the page editing api while making
some changes to the original scripts for added flexibility and using the
full range of HTTP methods that weren't being used before.

currenlty, it's just an end to end test to make sure the plumbing works
as it should data is being passed and can be processed.

now that it all works, the guts of page editing can be completed

update sortablejs to the latest since it's been awhile and got rid of
the old version
2024-03-07 15:42:11 -06:00
ro
65854e3dbd
Fix for api not being to see session data
so there was an error when trying to use services for API controllers
that was happening because the api middleware didn't pass the same info
as web middleware, resulting in session info not being passed to
controllers used in the api

after a bit of reading discovered necessary middleware could be added to
api routing so session data is available in in api routing

whew
2024-03-07 13:45:45 -06:00
ro
166e19a656
AuthAPI, favicon tweak
Got the first part of the API working, which checks to see if there is a
valid session active to set up requests

also some small changes to get the favicon working, yeah, yeah, but it's
cool looking...
2024-03-07 12:04:52 -06:00
ro
f7c9558da2
reorganized controller dir, added check for dash
seperated dash controllers for api controllers in the controller
directory to make them easier to manage

also added middleware to check authorization when accessing dash pages
2024-03-07 11:36:31 -06:00
ro
1e37580869
added front end scripting to recreate API
dropped in js from the old site to begin the process of wiring up the
API, but this time around, scripts will be served directly in browswer
rather than being transpiled through NPM/Babel, eliminating the need for
NPM.

also scripting will new modularized and served specifically for the
requirements of the page loading it. no more front loading everything.
only script that is needed for that page will be retrieved. if no
scripting is needed, none will be loaded.

The only casualty so far has been syntax highlighting due to prismjs
still being a common js module, but either this will be replaced with
another library or a custom syntax enginge will be created at a later
date
2024-03-06 11:53:40 -06:00
ro
b527884c51
added theme service class
page editing was missing the selector to choose what template the page
was using, so a theme class was created to handle retrieving and sorting
what classes where avaiable in the themes directory

still looking for twig files because themes haven't been converted over
yet, but one step at a time
2024-03-06 10:27:41 -06:00
ro
ad57c29e8d
filled out auth service class
expanded the auth service class to store member info in the current
session so validation is easier

also added a token to session data that expires every hour so people
won't be logged in forever and take breaks

hey, you matter too
2024-03-06 09:50:09 -06:00
ro
47c8f7b008
spacing fix, removed scratch file
spacing is weird in blade, so some tweaks were made to account for it

took out a file that was being used for referecne
2024-03-06 09:13:31 -06:00
ro
8a81ec25fc
fix for empty files array in pages
page was erroring out because no files array was being set there are not
files, so put in a quick patch for that
2024-03-05 17:46:15 -06:00
ro
d1c3b4a8f6
page editor template rebuilt
There are some spacing issues that need to be addressed but the page
editor template has been added and the CSS all lines up

scripting is still an issue as the backend that handles content still
isn't in place, but the front facing piece is in place so now those
components can be built

scripting is going to get an overhaul anyway, so this is a good place to
start that process
2024-03-05 15:49:30 -06:00
ro
177f29802b
added page repository and interface
plugged in classes for a page repository to handle editing and
retrieving page content and an interface class for the controller to talk to to
keep the methodoloy seperate from the controller to keep it all clean

now whatever changes that need to be made won't bother the controller
because it will always be looking for the same functions. super sweet
2024-03-05 13:27:27 -06:00
ro
2b437c0173
added page listing template, fixed sub menu items
moved the page listing template over and made all of the apropriate
changes so the CSS lines up as it should

there was also a minor issue that was keeping the sub menu for the start
pages from displaying so that was fixed
2024-03-04 20:06:36 -06:00
ro
57d47909b9
dash controller clean up
there was some stuff in there that wasn't being used anymore, so it's
been removed
2024-03-03 19:50:14 -06:00
ro
eda7fd4d3e
dash index quick clean up
just a quick tweak to make the sorting a bit cleaner
2024-03-03 18:07:23 -06:00
ro
2395278893
added content collector and pagination
plugged in the dash index template which required grabbing markdown
pages and converting them to data the system can use and then pagination
that is used to sort content into pages

start page now switches from login screen to index based on session, but
that might be changes so it's a bit more clean to work with middleware
2024-03-03 17:49:05 -06:00
ro
c77eeafb2c
Login Auth Flow
Added controller to handle the login process and session management
that stores information about the person that has logged in so that info
is available through out the app when logged in
2024-03-03 13:48:22 -06:00
ro
367f20d8fa
Started template conversion
Brought over a template from the old build to see how the conversion
process would go (it's not bad) and see if the CSS would still stick (it
did)

The wiring will be finished when the auth class is completed
2024-03-01 18:16:24 -06:00
ro
a748b2c098
Added Auth and Settings service class
Plugged in a basic auth classs for verifying members and a setting class
to retrieve site settings and return necessary info.

Very bare bones to start just to get it working and prep for the
additional features
2024-03-01 13:34:36 -06:00
ro
4abf8bc911
Added Meta to composer config
Updated the composer file with relavent project information
2024-02-29 14:48:49 -06:00
ro
8d3af5b6e1
Added fipamo content, set up basic routes for test
Added the folder containg Fipamo markdown files to set up a basic route
for the start of the dashboard and quick test to make sure the file
    paths can be read

also added CSS files that will style the new template system, which is
currenlty in twig but will be convereted to blade
2024-02-29 13:00:59 -06:00
ro
ed60baff53
removed cache folder
legacy cache folder for parcel stuff removed
2024-02-29 11:36:24 -06:00
ro
6219d68b01
Upated git ignore file
removes parcel cache from repo
2024-02-29 11:34:31 -06:00
ro
45f857e9b8
The Big Flip
Removed the Slim Framework from the codebase and installed the latest
Laravel version to be the new foundation for the project moving forward.

Code from the old version will now be ported to the new version.
2024-02-29 11:09:17 -06:00
ro
c29513945b
Formating tweaks, minor controller edits
There were some outstanding changes made a few months ago that haven't
been checked in, so getting those as the final commit before the
transition to Laravel
2024-02-29 10:37:40 -06:00
Ro
49c1f2d3f2
Taiga Intergration Test 2
First one used the wrong hook type.

TG-3 #testing
2023-08-05 11:16:46 -07:00
Ro
2ce0a4eb82
Taiga Integration Test
Just seeing if this thing actually work

TG-3 #testing
2023-08-05 10:15:58 -07:00
Ro
599fca18f1
Removed invalid role attributes from Settings page
Cleaned up the settings page by removing the improper usage of the role
tag and replaced it with class to maintain current design integrity.
Also updated the script files that was referencing the old role tags
2023-08-01 15:51:33 -07:00
Ro
bea40049fc
Removed invalid role attributes from dash index
Converted role tags to class to remove the invalid role attributes
used.
2023-07-31 14:21:54 -07:00
Ro
df8e301a0b
Confirming Verify Commit Error
I tried submitting an updated through and IDE and it does not properly
verify signed commits.

Using git from the command line works fine.
2023-07-28 16:03:40 -07:00
Ro
16de29c399
testing verified commits 2023-07-28 16:01:00 -07:00
Ro
ac08c77813
Removed role from header, notification and login
Began the process of removing all invalid role attributes starting with the header, login, and notifications components. Updated the corresponding scripts as well. 

Made a few structural changes to the header to simply it and then tested it to ensure notification still worked.
2023-07-28 15:55:16 -07:00
Ro
2f0b83b492
Added License
Decided to go with GNU General Public License v3.0 because all
improvements to the codebase should be shared with the public
2023-05-16 15:08:48 -07:00
Ro
f824b53f2a
Update versioning to 2.6.1
All the issues in milestone 2.6.1 have been completed, so now it's time for an update
2023-05-14 18:00:53 -07:00
Ro
8461d88dd6
Settings CSS Cleanup
The Settings UI needed some responsive polish, so that's been given a
bit more love.
2023-05-14 17:37:39 -07:00
Ro
a9c88f1430
Edits for #86 and #92
Fixed the issue where the text edit controller would scroll right off
the screen. Now it stick when it's the bottom of the header.

Also changed the background color of page links on the Start and Book
pages to indicate there is no image. It's just cleaner
2023-04-28 16:17:15 -07:00
Ro
fa4b252d9c
Fix for #94
Header images were missing from the archive and tags pages, so this is a
patch to make sure those are working again.
2023-04-28 14:08:49 -07:00
Ro
181225329a
CSS and icon fixes
Fixes for issues #93, #88, #87
2023-04-13 14:31:43 -07:00
Ro
4876c1336e
Updated Composer packages; empty field fix
Composer dependencies were pretty old, so they needed to be upgraded to
the latest.

There was also a minor bug that was triggered when a new page was saved
with empty tags and no images or documents, so that's been patched as
well.
2023-04-09 17:16:24 -07:00
Ro
f1850ce7f7
Site Creation Hot Fix
There was an error in the request to set up a fresh site on the front
end and handling the respective object on the backend that was causing
the process to error out.

Also added the source map for the dash script because it's eventually
going to be fully transparent anyway.
2023-04-06 17:59:31 -07:00
Ro
e7cd52bd12
Fixed script after removing dependency
Removed 'carot-ps' from dependencies so it had to be removed from
scripts calling that package.

Also renamed dash script to 'dash.js' just so it's clear it is for the
dashboard
2023-04-05 17:14:21 -07:00
Ro
f9190c2a41
removed carot-pos from dependecies 2023-04-05 17:05:38 -07:00
Ro
8885ae4c63
updated version nunmber 2023-04-05 15:48:36 -07:00
Ro
405be1a6ed
Emplty Layout Hotfix
When creating new pages, there is no layout, so the system was pushing a
null error when trying to use string_contains and a null string, so
cleaned that up so it defaults to 'page' when that string is empty
2023-04-05 15:16:30 -07:00
Ro
3f9506ac6b
Responsive P 4 - Dash, Index, Editor, Nav, Menu
Cleaned up resonsive for the rest of the remaining pages: the dash
index, page index, page editor, navigation editor and plugged in a
mobile nav that activates when the viewport gets skinny.

Whew.
2023-04-05 14:40:14 -07:00
Ro
e7fd91c152
Responsive Pt 3 - Pass Reset, Site Create/Restore
Rebuilt forms for resetting the password, creating a fresh site and
restoring a site from a backup, as well as adjusting the accompanying
scripts that handle those processes.
2023-03-31 16:20:11 -07:00
Ro
5adf196783
Responsive Part 2 - Login
Updated Login CSS and cleaned up login notifications to alert when
something is amiss.
2023-03-30 14:27:56 -07:00
Ro
2ce86fad2e
Settings CSS Remix
Wasn't feeling the previous CSS responsive structure, so edited it to be
a bit more streamline.

Also fixed small issue with the backup API request.

Made a small change to notifications so the alert stays live while the
system is processing a request rather than going back to an unalert
state.
2023-03-30 13:40:59 -07:00
Ro
bfb0873f5f
Reponsive: Part 1 - Settings
Started cleaning up responsive styles for the site starting with the
Settings section. Still needs some tweaking but the structure for that
section is in so it's just a matter of police.

Some changes need to be made to the main nav as reduced viewport throws
off the alignment.
2023-03-28 18:19:40 -07:00
Ro
97278e3a90
Notifications Rework #81
Integrated the Notifications UI component into the header to streamline
user alerts into the overall experience.

Also added titles to use the space created by moving the notifications
compoenent to it's own space.
2023-03-26 19:47:42 -07:00
Ro
78bfe4596b
Styled Nav Editor
Created new CSS styles for dash nav editor and updated the appropriate
controller scripts.

Also updated the icons for the main nav.
2023-03-25 20:28:38 -07:00
Ro
1b89d1d072
Restyled Settings UI
Rebuilt the css for the Settings UI, which also led to some changed in
the FilesAPI so it knows what to do with specific targets. There's still
some additional styling needed to polish it, but the core structure is
in place so now it can just be tweaked. The controller for this page was
adjusted as needed.

Also moved the settings sub nav convtrols to the header menu since it's
sticky now.
2023-03-25 16:44:35 -07:00
Ro
fcca7357bc
Fixes for removing media items and page deletion
The upload process changed, so some tweaks needed to me made to the page
deletion process, which just marks the page as deleted but keeps the
file. Also updated the file manager to properly delete items from the
display list.

The css for page listings also had to updated [forgot to put that on the
list] so the styles for that were updated and the template pages
adjusted accordingnly.

Also forgot to mention changes to the notification display in the last
commit. It's basic as of right now but it will be enhanced as needed.
2023-03-24 14:57:01 -07:00
Ro
07b422a9c3
CSS Overhaul Part 1
This one is a doozy, so let's breakt it down into what areas where
touched.

-   updated package json to remove unneeded dependencies.
-   rebuilt file uploading to simply a very convuluted process
-   began proces to replace icons with https://tabler-icons.io
-   began process of removing the need for css preprocessor and using
    pure css
        - login completed
        - dashboard index completed
        - page edit ui completed
- page edit ui text editor tweaked so syntax highlighting is cleaner and
  more accurate

The settings and navigation UIs still remain and then polishing the
responsive for the new css structure
2023-03-23 13:55:34 -07:00
Ro
ec1dc49ba1
Login Hotfix
The script that handles logggin in and the form for getting that
information were both posting the info which would result in an
intemittent uncaught error.

An attribute was added to the form so it does not submit at the same
time the JS sends a request.

A minor bug but it was annoying.
2022-11-04 12:52:19 -07:00
Ro
61ae73a9e5
Issue #83 Round 1
First pass for CSS refactor for the dashboard, including the login and
index templates. Still rough but the basic structure is in place for
both as well as the re-worked css that will be added to the repo later
once all the pages have been updated.

Lots to do still but a good start.
2022-11-03 13:46:36 -07:00
Ro
859b75e9f3
Removed links to old repo from ReadMe
I'll probably miss some as the migration completes but let's start with
scrubbing the old repo from the new one.
2022-10-10 13:07:12 -07:00
Ro
a14d4a0a08
Move Repo test
Just a quick update to see if I got all the setting right as I'm
migrating the show to a fresh repo
2022-10-10 12:54:35 -07:00
Ro
7890715ea6
PHP Linting Tweaks
PHP syntax checking was being weird so I spent some time to make some
corrections. The problem was I was just using the wrong protocal, PEAR
when I've been coding to the PSR12 standard. Easy fix.
2022-10-10 11:49:04 -07:00
Ro
77eb8dd1a8
HTTP Method notes for RouteControl
Currently only two http methods are being utilized for route traffic so
classes are getting jumped trying to stuff every action in on or the
other. More methods need to be implemented to better organize route
pathing and subsequent requests
2022-09-19 21:43:57 -07:00
Ro
e431f1afa4
Sign commit test
Testing out commit signing for a bit of extra security.
2022-09-19 21:12:16 -07:00
Ro
254a7f1c38 Scrubbed Moment from codebase
Moment was still being used in some classes so found and replaced all
those instances with Carbon and uninstalled the packaged from composer.
2022-09-19 20:39:15 -07:00
Ro
b092645733 Removed Fipamo classes from composer autoloading
I didn't like the extra step that had to be taken to register new
classes from the command line using composer's auto dump, so a quick
script was implemented to handle Fipamo loading classes seperately so
composer can manage itself, removing the need for updating it whenever I
add a new classs to the codebase
2022-09-19 16:52:29 -07:00
Ro
73e4243231 Removed TODO note for Carbon
Left a stray TODO in there that needed to be removed since Moment has
been replaced with Carbon.
2022-09-18 13:22:02 -07:00
Ro
3260e3b76b Updated dependencies, replaced Moment
Composer package dependencies hadn't been updated in awhile, so a part
of the clean up for the php 8.1 install, that has been handled

Moment was being used to handle date formatting but it hasn't been
updated in awhile either, so I switched to Carbon which is still in
active development.
2022-09-18 13:07:52 -07:00
Are0h
a1c0d86580 update versioning, clean up npm
I updated the versioning to match in both composer.json and pacakge.json
config files. I need to start cleaning up the dependencies in the front
end scripting as well, so I started my updating them to see much work
needs to get done. Not too bad, but it's going to be a pretty
significant effort.
2022-06-16 13:43:52 -07:00
Are0h
197fb005de Added Markdown table conversion and version update
Added markdown table conversion to the html process so we get sexy
tables. Also updated the version number to prep for the latest beta
realease. Ha, took a minute but we're back on track
2022-06-14 17:08:31 -07:00
Are0h
00d41a3664 Update composer packages
Needed to update some packages rely on because it's been a minute since
they've been touched. I need to get better with doing that on a regular
basis.
2022-05-19 14:07:09 -07:00
Are0h
3fa3a9e0e6 removed stray console command for #78 updated
just had a stray trace in there that was removed and correct the issue
referene from the last update. oops.
2022-05-18 16:49:16 -07:00
Are0h
8734baf85e Fix for #76, editing upload que
Whew it took a bit but now the upload que on the page edit page is
editable. You can add and remove files as needed without having to add
all your files at once, which us how it previously worked. Still needs
to be tested a bit but the plumbing is up and running.

Also removed a stray php format config as it is no longer needed.
2022-05-18 16:41:25 -07:00
Are0h
a31dff94cb Updated PHP lint to @PSR12 b/c @PSR2 is deprecated
So, @PSR12 is the recommended coding standard, so I updated the config
and reformatted the appropriate files.

Again. Whew.
2022-05-16 19:14:38 -07:00
Are0h
63eaba08e2 Added config for PHP formatting (PSR2)
I needed some consistent php formatting, so I plugged in a php fixer
config and then reformatted all PHP files so it's all consistent.

Fixed an ID issue with the page-edit template that was causing page
editing to fail.
2022-05-16 17:41:15 -07:00
Are0h
d9c9f7744e Fixed white space issue, page edit style tweaks
There was a white space issue that made text displayed title and tags
text appear missaligned. It turned out to be a small layout problem that
was cleaned up by getting rid of white space in the textarea element
itself.

Also got in there and fixed some lingering css issues that was causing
the svg icon colors to be off in some of the butttons.
2022-05-11 17:41:56 -07:00
Are0h
b2c7dae322 Page Editor style fixes
The page editor UI was looking very shaky, so I went in there and
cleaned it up. The nesting is a bit intense, so I'm gonna think about
better ways of laying that out that aren't so convoluted.
2022-05-10 18:48:34 -07:00
Are0h
2501a19685 UI design tweaks
There were some inconsistencies in the UI due to all the changes that
were made to the styleshets, so I fixed the obvious ones that were show
stoppers.

There's more in there to be smoothed out, so this is just the start.
2022-05-10 18:05:41 -07:00
Are0h
b8b763637f finished scss stylint refactor for remaining pages
finished up refacting all of the style sheets to be brought in line with
the stylint scss standard. the standard will probably change as i tweak
the rules but the foundation is solid and it's rendering so it's a
great place to start

i'll go through the ui to make sure everything is in tact and i'll make
the necessary adjustments

i should probably refactor some structures as well as they nesting is a
bit overly complicated
2022-05-10 17:19:46 -07:00
Are0h
61b9acb280 I swapped out sass for scss file for styling because scss is a bit more
accessible because of its similarity with base css.

I also plugged in stylint to normalize a css standard throughout the
project to work with prettier, which handles formatting

The structure scss file has been brought in line with the new standard
but the remaining need to be adjusted as well. Those will be added with
an additional commit.
2022-05-10 14:13:31 -07:00
Are0h
aa3301fb66 hotfix for images in editor uploads 2022-04-23 11:28:20 -07:00
Are0h
9baaed6d50 formatting changes 2022-04-16 13:21:57 -07:00
Are0h
2e30d6eb26 fix for open graph image render, minor tweaks to class importing 2022-04-14 14:46:17 -07:00
Are0h
7393e4572c readme tweak 2022-03-25 15:01:07 -07:00
are0h
a841063ddb template update and clean up 2022-03-20 15:14:23 -07:00
are0h
692926c816 script check 2022-03-19 20:05:15 -07:00
are0h
b230f3f15d fixes for nav editing 2022-03-19 19:36:15 -07:00
are0h
d7c5fb7a70 dev env tweaks 2022-03-19 18:56:58 -07:00
are0h
d98bccdd1f missed a class reference. oops 2022-03-19 16:24:19 -07:00
are0h
382c314af0 fixes for restore from backup file 2022-03-19 16:15:56 -07:00
Ro
6279ad4730 fixes for site init 2022-03-19 14:55:56 -07:00
Ro
2b7db3cc88 removed unecessary mixins 2022-03-19 14:17:24 -07:00
Ro
a70c98afa0 fixes for site back up and mailer, turned on md list rendering 2022-03-18 16:58:11 -07:00
Ro
8ad3fa38c5 respeced class imports to psr-4 standard, fixed asset moving, upgraded theme rendering 2022-03-18 16:00:51 -07:00
Ro
1351b98ee4 cleaned up create page process, update page edit template to handle empty files 2022-03-15 16:56:37 -07:00
Ro
2210e39aee added remaining upload types, updated templates for new file types 2022-03-13 17:46:42 -07:00
Ro
523b611ac5 fixed weird form post bug, updated page listing display for new assets 2022-03-13 14:22:22 -07:00
Ro
fe74fd6e07 fixed css issue with text edit area 2022-02-23 16:42:25 -08:00
Ro
2ed2cd3803 fix for page-edit template, oops 2022-02-23 16:16:12 -08:00
Ro
f24a6b5099 implemented video uploading, updated styles and templates 2022-02-23 15:48:11 -08:00
Ro
3c52bca8ba started implementation of multiple file uploads and sorting 2022-02-12 18:35:09 +00:00
Ro
8f9021bb7d added progress bar to entry image uploads 2022-01-26 16:25:05 -08:00
Ro
53864becc1 implemented progress upload indicator in notifications 2022-01-26 15:57:27 -08:00
Ro
601fd6b1ab fix for weird css file deletion thing 2022-01-26 13:26:41 -08:00
Ro
b69559541a edit format bar always stays on screen 2022-01-25 15:17:41 -08:00
Ro
8684c8b1ac removed stray var dump... oops 2022-01-17 16:31:05 -08:00
Ro
c15b6cdc5b updated production assets 2022-01-16 13:48:36 -08:00
Ro
7cabb1d1f0 more text editor touch ups, fix for page delete option 2022-01-16 13:33:04 -08:00
Ro
f1a8ef67bc another patch for textarea misalignment bug 2022-01-13 15:43:38 -08:00
Ro
10081c4323 fix for text editor alignment issue. finally 2022-01-12 14:56:24 -08:00
Ro
b70308d990 fixed src removal from image tags with relative urls, add iframe tag to allowed list 2022-01-07 15:45:35 -08:00
Ro
fdf2319783 re-inserted feature img into feature and recent lists. oops 2022-01-04 17:05:57 -08:00
Ro
71dc41e950 fix for page rendering always defaulting to page 2022-01-04 16:56:58 -08:00
Ro
db385d938c not fix for template and empty layout for new page creation 2022-01-04 14:19:30 -08:00
Ro
d2f02eea50 edited ignore file to include new script. duh. 2022-01-04 13:39:28 -08:00
Ro
dad43f4a19 pages marked as menu items render as non-blog pages 2022-01-03 16:52:55 -08:00
Ro
7775c1d409 expanded and streamlined markdown rendering 2021-12-29 17:10:01 -08:00
Ro
0742e06c45 cleand up text editor layout, tweaked editor colors, re-activated editor options 2021-12-28 14:48:54 -08:00
Ro
682406515d added assets compiling scripts, added syntax highlighting for text editor, fix for #73 2021-12-27 16:42:10 -08:00
Ro
c0c3b60fd5 added feature image to tags to display page 2021-12-19 13:53:47 -08:00
Ro
4f4fee807c fix for #69, updated settings ui 2021-12-18 16:09:26 -08:00
Ro
1c2ba579df fix for #68, updated page edit ui 2021-12-18 15:42:16 -08:00
Ro
03c629462b fix for #75, fix for adding image to entry text 2021-12-18 12:05:13 -08:00
Ro
f119bdc773 fix for #72, index page not using page themes 2021-12-18 11:30:41 -08:00
Ro
39775e624d hot fix for adding images two recent and featured links 2021-12-17 14:40:09 -08:00
Ro
7e38b4edb8 quick patch for #66 2021-11-22 16:52:26 -08:00
Ro
3994e97829 moved layout selector in page edit ui 2021-11-22 14:29:50 -08:00
Ro
c867b6c508 adde page preview feature, updated page edit ui with preview option button 2021-11-22 14:24:26 -08:00
Ro
49e53a9638 added styled page layout selector, page renders selected layout 2021-11-18 14:59:59 -08:00
Ro
a8355b2da4 created new Theme data class for theme stuff, added custom page view, added view select for page edit screen 2021-11-17 16:59:53 -08:00
Ro
bbfe37597a fixes for #63, #64, #65, updated composer dependencies 2021-11-14 13:54:36 -08:00
Ro
e6cda301cf #59 - added toggle to render pages when page is edited 2021-11-08 13:52:33 -08:00
Ro
4796431076 completes #58 default theme links adapt to dynamic rendering, no page rendering if dynamic active 2021-11-03 15:19:52 -07:00
Ro
35c780bba6 added dynamic page rendering for index, archives, tages and blog pages 2021-11-02 16:19:03 -07:00
Ro
e5cffd39d7 fix for #60 - render flag false fix 2021-10-27 14:37:42 -07:00
Ro
d6f6f771ac hot fix for page creation (token not being shared on create mode) 2021-10-21 13:27:48 -07:00
Ro
9fe8ce1e4c #56 moves global assets from theme if not present in public dir 2021-10-06 13:17:42 -07:00
Ro
1bb13ce771 #57 notifications ui position tweak 2021-09-23 13:15:03 -07:00
Ro
934d29f4cf added field check for page edits to make sure unnecessary fields are not being added 2021-09-14 12:47:57 -07:00
Ro
ccbf55bb54 added form token auth to page editing, updated API 2021-09-13 15:43:54 -07:00
Ro
fdc6cb2cf2 added form token to session to validate form submission from frontend 2021-09-12 14:40:27 -07:00
Ro
f2450b2be5 fixed incorrect input type (#52), cleaned up site restore process (#53) 2021-08-25 16:12:35 -07:00
Ro
d7c2827115 updated composer dependencies 2021-08-10 11:47:23 -07:00
Ro
8df5e68024 re-activated site back up... for now... 2021-08-09 14:04:49 -07:00
Ro
734c6d36f1 Admin API tweaks, added check for theme render flag for one pagers 2021-08-08 15:09:13 -07:00
Ro
183a620b1e changed auth process to just ask server if session is active 2021-08-03 14:31:13 -07:00
Ro
b3c47d6721 overdid it with the image upload auth, making it a regular upload 2021-08-02 15:34:21 -07:00
Ro
cad32ace74 fixed image upload request that required token authorization 2021-08-02 15:13:46 -07:00
Ro
7d3f2d13a4 adds externalAPI key in settings if not present in original settings file when restoring 2021-08-02 13:52:12 -07:00
Ro
a1932e1a24 added missing eternal API toggle to settings template 2021-08-02 13:06:59 -07:00
Ro
b24f0a6adb quick patch for CORS check while in site init state 2021-08-02 12:52:19 -07:00
Ro
0ec49de8ab admin api commenting tweak 2021-07-22 13:06:52 -07:00
Ro
83b4aff716 added admin API method for getting site and member info 2021-07-21 12:46:14 -07:00
Ro
13c9d05019 tweaks for API classes, filled out commenting for docs, added jsdoc config 2021-07-02 14:45:35 -07:00
Ro
1f1c611afe fixed token check, fixed empty image page post 2021-06-30 14:06:13 -07:00
Ro
ab40219d9b added key check to use AdminAPI, updated front end classes 2021-06-28 13:47:06 -07:00
Ro
2785ef6982 updated AdminAPI for remote use, removed UtilityAPI from public library (methods should only be local) 2021-06-27 13:48:17 -07:00
Ro
b9056b64df more commenting for API classess, edited constructor for Admin and Util, updated backend API, added jsdoc 2021-06-25 14:53:37 -07:00
Ro
ebdda2c089 updated front end controller scripts 2021-06-24 11:58:55 -07:00
Ro
e40e566302 edited and commented Admin API, removed DataEvents class 2021-06-22 13:18:16 -07:00
Ro
f7e3b3b55e edited and commented methods for Utility API, removed DataEvents class 2021-06-22 12:13:11 -07:00
Ro
8101b16cf9 edited method names for Content and Utility API, updated base class 2021-06-21 14:36:06 -07:00
Ro
45fd8be25f reorganized API methods into new files 2021-06-17 13:26:04 -07:00
Ro
2d84ec9e33 added featured and menu options for page API, cleaned up API json return 2021-06-15 12:37:27 -07:00
Ro
24242d4bf0 updated API request URLs to use keys 2021-06-14 13:16:23 -07:00
Ro
8417e692d7 removed pagination from Pages API request, added API request for tags 2021-06-14 12:22:35 -07:00
Ro
65def75433 added api key to settings UI 2021-06-13 14:24:20 -07:00
Ro
3df2720009 added CORS handling, added external API access toggle to settings UI and updated front end script 2021-06-09 13:36:08 -07:00
Ro
2f1f6678b7 changed fipamo logo name to avoid confusion with imported theme logo 2021-06-03 12:26:11 -07:00
Ro
a542b2d884 removed data events from fipamo api, added content api methods, cleaned up front end js 2021-06-02 10:04:04 -07:00
Ro
6cd28c06c0 version update 2021-05-26 19:22:10 +00:00
Ro
b7aafed112 quick fix for composer settings file 2021-05-26 19:21:11 +00:00
Ro
20fc9c64a1 ui overall part 4: responsive touch up 2021-05-25 11:04:30 -07:00
Ro
0765ea4555 ui overall part 3: settings, site menu editor 2021-05-24 14:42:46 -07:00
Ro
033fe5e48b ui overall part 2: dash index, pages index 2021-05-23 19:27:33 -07:00
Ro
8e3d359486 ui overall part 1: login, init, pass reset forms 2021-05-21 14:07:25 -07:00
Ro
b51d63633f expanded composer json 2021-05-10 15:41:35 -07:00
Ro
8f1e3a55e7 #44 password recovery plugged in 2021-05-06 12:45:39 -07:00
Ro
9462b91246 added phpmailer to dependencies for mail services(smtp, mailgun), update js Mailer class 2021-05-03 15:48:20 -07:00
Ro
bedb6fdfe5 added email template, mail api and mail utility class, tweak to Mailer.js 2021-04-30 21:55:23 +00:00
Ro
89f84499b2 make page feature image meta image 2021-04-27 20:09:18 +00:00
Ro
1f46eeffa0 full image url for meta image, fixed minor page template error 2021-04-27 19:47:23 +00:00
Ro
7002ba8014 ignore button clicks when processing requests 2021-04-27 19:38:36 +00:00
Ro
08d034a8d1 fixed meta images in header for default theme 2021-04-27 02:16:39 +00:00
Ro
dd0e5ce1ee updated base theme script 2021-04-27 01:37:23 +00:00
Ro
c89fe5be30 removed redundant restore button, WIP’d mail button, move theme assets on render 2021-04-27 01:28:11 +00:00
Ro
4d44ea2a06 added base style and script assets for dash 2021-04-27 00:31:49 +00:00
Ro
95aecde73b quick patch to render class for dynamic theme rendering 2021-04-26 22:44:55 +00:00
Ro
f3dfd3411a added meta data to page template 2021-04-26 13:19:47 -07:00
Ro
06787aac30 moved site restore to utility class, updated set up script, fixed minor error in front end api restore request 2021-04-25 21:18:01 -07:00
Ro
b8cf4e403a fixed a typo in the README 2021-04-25 19:49:32 -07:00
Ro
53fd1a47c5 fixed minor error when updating avi in settings 2021-04-25 16:00:50 -07:00
Ro
c6f7c56f4a Activated site back up via zip and added download link in settings 2021-04-25 15:48:46 -07:00
Ro
9afec7554c activated site restore from zip, fixed imgs to rendering src 2021-04-24 16:37:25 -07:00
Ro
b98707bb0d install site activated, fixed large image upload issue(php setting), fixed page indexing 2021-04-23 20:54:56 -07:00
Ro
ba38e32dd5 fix for code tag being sanitized on html render 2021-04-23 13:27:21 -07:00
Ro
b09e1c7164 pages rendering to html 2021-04-22 16:04:43 -07:00
Ro
444189e527 created new App class to start up app and move off of index 2021-04-21 22:46:56 -07:00
Ro
9283a7f5b3 tag html rendering implemented 2021-04-21 18:41:32 -07:00
Ro
c78772e2c0 added Sorting class to handle organizing tags and archives organizing for page rendering. added empty Render class for rendering 2021-04-18 17:50:30 -07:00
Ro
39809a60ec added navigation editor template and methods, moved markdown converstion to DocTools class 2021-04-17 17:32:45 -07:00
Ro
594df048cf updating menu items in settings when pages is saved 2021-04-16 22:14:14 -07:00
Ro
20cc4abad9 moved member auth to API class 2021-04-16 20:50:03 -07:00
Ro
179f007fab setting sync and settings image uploading turned back on 2021-04-16 16:15:47 -07:00
Ro
b68d4c190e settings paged plugged in, scoping out page publishing api methodology 2021-04-15 22:19:59 -07:00
Ro
a699fd1926 removed unnecessary api requests 2021-04-14 18:50:19 -07:00
Ro
2b79a52275 minor fix to pagination 2021-04-14 15:08:09 -07:00
Ro
043bd25cf7 current index updates when new page added 2021-04-14 14:51:06 -07:00
Ro
bc6fc33592 display per session status clean up 2021-04-14 12:48:57 -07:00
Ro
9cf0cbfbe4 moved page editing api methodology to its own api class, session check for pages section 2021-04-14 12:01:49 -07:00
Ro
ed2105edcc cleaned up page creation 2021-04-13 20:49:12 -07:00
Ro
1b9252fef0 page editing cleaned up, fixed date formatting, fixed page filtering, add Image API path 2021-04-13 15:20:57 -07:00
Ro
395850adb8 cleaned up date formatting for display 2021-04-10 13:44:43 -07:00
Ro
72c9b3f9e6 rebuilt create/edit page template, added navigation, turned on logout mehthod 2021-04-10 13:19:05 -07:00
Ro
b75d045eec fixed pagination indexing 2021-04-08 15:38:31 -07:00
Ro
8e9b6ac422 plugged in filtered pagination for entry pages 2021-04-08 15:27:05 -07:00
Ro
6c3653277d plugged in page list display 2021-04-07 14:09:40 -07:00
Ro
c4232a0a01 dash index displaying recents, css tweaks, added bulma 2021-04-01 17:37:12 -07:00
Ro
5ad1118244 clean up API, re-connected API to front end, base template tweak 2021-04-01 12:54:03 -07:00
Ro
083faa7c1d removed config files 2021-03-29 13:37:34 -07:00
Ro
1288bbc32b fixed checking session active state bug 2021-03-28 18:37:37 -07:00
Ro
f3aa86d472 git ignore tweak to save index. oops 2021-03-28 15:23:34 -07:00
Ro
0ea15ae4b2 added custom session manager, moved index to safe directory 2021-03-28 15:22:00 -07:00
Ro
b1cc12673c plugged in APIController, turned on login, added lodash! 2021-03-27 13:59:44 -07:00
Ro
f3f2a6502b normalized url routing and cleaned up templating structure 2021-03-26 20:24:16 -07:00
Ro
cf752fd8c0 set up views structure, started Auth class for session management 2021-03-25 14:05:32 -07:00
Ro
31d5e1192c markdown file retrival working, got rid of unnecessary configs 2021-03-24 16:59:28 -07:00
Ro
e459366c46 working out page routing 2021-02-28 17:04:54 -08:00
Ro
3617565210 new scaffolding set up 2020-11-17 14:27:25 -08:00
Ro
67a47355d2 added basic routing and page rendering 2020-11-12 14:01:29 -08:00
Ro
05ff48a51c begin transition to php environment for rebuild 2020-11-11 15:22:58 -08:00
289 changed files with 29152 additions and 19647 deletions

View file

@ -1 +1,13 @@
{ "presets": ["env"] } {
"presets": [],
"plugins": [
[
"prismjs",
{
"languages": ["html", "markdown", "markup"],
"theme": "okaidia",
"css": false
}
]
]
}

18
.editorconfig Normal file
View file

@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[docker-compose.yml]
indent_size = 4

74
.env.example Normal file
View file

@ -0,0 +1,74 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=fipamo
DB_USERNAME=root
DB_PASSWORD=
BROADCAST_DRIVER=log
CACHE_DRIVER=file
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
MEMCACHED_HOST=127.0.0.1
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_HOST=
PUSHER_PORT=443
PUSHER_SCHEME=https
PUSHER_APP_CLUSTER=mt1
VITE_APP_NAME="${APP_NAME}"
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_HOST="${PUSHER_HOST}"
VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
#------------------
# FIPAMO SETTINGS
#------------------
FIPAMO_DIR="../content"
PAGES_PATH="${FIPAMO_DIR}/pages"
THEMES_PATH="${FIPAMO_DIR}/themes"
FIPAMO_CONFIG="${FIPAMO_DIR}/config"
SETTINGS_PATH="${FIPAMO_CONFIG}/settings.json"
FOLKS_PATH="${FIPAMO_CONFIG}/folks.json"
TAGS_PATH="${FIPAMO_CONFIG}/tags.json"
FIPAMO_INIT="${FIPAMO_DIR}/init"
FIPAMO_BACKUPS="${FIPAMO_DIR}/backups"
# MAIL
ADMIN_EMAIL = "you@admin.email"

138
.eslintrc
View file

@ -1,70 +1,70 @@
{ {
"parserOptions": { "parserOptions": {
"ecmaVersion": 7, "ecmaVersion": 7,
"sourceType": "module", "sourceType": "module",
"ecmaFeatures": {} "ecmaFeatures": {}
}, },
"rules": { "rules": {
"constructor-super": 2, "constructor-super": 2,
"for-direction": 2, "for-direction": 2,
"getter-return": 2, "getter-return": 2,
"no-case-declarations": 2, "no-case-declarations": 2,
"no-class-assign": 2, "no-class-assign": 2,
"no-compare-neg-zero": 2, "no-compare-neg-zero": 2,
"no-cond-assign": 2, "no-cond-assign": 2,
"no-console": 1, "no-console": 1,
"no-const-assign": 2, "no-const-assign": 2,
"no-constant-condition": 2, "no-constant-condition": 2,
"no-control-regex": 1, "no-control-regex": 1,
"no-debugger": 2, "no-debugger": 2,
"no-delete-var": 2, "no-delete-var": 2,
"no-dupe-args": 2, "no-dupe-args": 2,
"no-dupe-class-members": 2, "no-dupe-class-members": 2,
"no-dupe-keys": 2, "no-dupe-keys": 2,
"no-duplicate-case": 2, "no-duplicate-case": 2,
"no-empty": 2, "no-empty": 2,
"no-empty-character-class": 2, "no-empty-character-class": 2,
"no-empty-pattern": 2, "no-empty-pattern": 2,
"no-ex-assign": 2, "no-ex-assign": 2,
"no-extra-boolean-cast": 2, "no-extra-boolean-cast": 2,
"no-extra-semi": 2, "no-extra-semi": 2,
"no-fallthrough": 2, "no-fallthrough": 2,
"no-func-assign": 2, "no-func-assign": 2,
"no-global-assign": 2, "no-global-assign": 2,
"no-inner-declarations": 2, "no-inner-declarations": 2,
"no-invalid-regexp": 2, "no-invalid-regexp": 2,
"no-irregular-whitespace": 2, "no-irregular-whitespace": 2,
"no-mixed-spaces-and-tabs": 2, "no-mixed-spaces-and-tabs": 2,
"no-new-symbol": 2, "no-new-symbol": 2,
"no-obj-calls": 2, "no-obj-calls": 2,
"no-octal": 2, "no-octal": 2,
"no-redeclare": 2, "no-redeclare": 2,
"no-regex-spaces": 2, "no-regex-spaces": 2,
"no-self-assign": 2, "no-self-assign": 2,
"no-sparse-arrays": 2, "no-sparse-arrays": 2,
"no-this-before-super": 2, "no-this-before-super": 2,
"no-undef": 2, "no-undef": 2,
"no-unexpected-multiline": 2, "no-unexpected-multiline": 2,
"no-unreachable": 2, "no-unreachable": 2,
"no-unsafe-finally": 2, "no-unsafe-finally": 2,
"no-unsafe-negation": 2, "no-unsafe-negation": 2,
"no-unused-labels": 2, "no-unused-labels": 2,
"no-unused-vars": 2, "no-unused-vars": 1,
"no-useless-escape": 1, "no-useless-escape": 1,
"require-yield": 2, "require-yield": 2,
"use-isnan": 2, "use-isnan": 2,
"valid-typeof": 2, "valid-typeof": 2,
"no-duplicate-imports": 2 "no-duplicate-imports": 2
}, },
"env": { "env": {
"node": true, "node": true,
"browser": true, "browser": true,
"es6": true "es6": true
}, },
"globals": { "globals": {
"_": false, "_": false,
"hljs": false, "hljs": false,
"Sortable": false, "Sortable": false,
"Prism": false "Prism": false
} }
} }

11
.gitattributes vendored Normal file
View file

@ -0,0 +1,11 @@
* text=auto eol=lf
*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
/.github export-ignore
CHANGELOG.md export-ignore
.styleci.yml export-ignore

82
.gitignore vendored
View file

@ -1,24 +1,74 @@
node_modules/ /.phpunit.cache
.sass-cache/ /node_modules
.cache/ /public/build
.nova/ /public/hot
/public/storage
/public/reference
/storage/*.key
/vendor
.env
.env.backup
.env.production
.phpunit.result.cache
Homestead.json
Homestead.yaml
auth.json
npm-debug.log
yarn-error.log
/.fleet
/.idea
/.vscode
/.parcel-cache
.env.local
/.env.local.php
/.env.*.local
/.nova
/public/bundles/
/public/assets/images/examples/
/var/
!public/ !public/
public/* public/*
!public/favicon.ico !public/favicon.ico
!public/assets !public/index.php
!public/assets/
public/assets/* public/assets/*
!public/assets/images !public/assets/css/
public/assets/css/*
!public/assets/css/dash
# ignore scripts/theme because contents will
# change based on theme
!public/assets/scripts/
public/assets/scripts/theme/*
# images
!public/assets/images/
public/assets/images/* public/assets/images/*
!public/assets/images/global/ !public/assets/images/global/
!public/assets/images/global/* public/assets/images/global/*
# base assets used by the app
!public/assets/images/global/close.svg
!public/assets/images/global/default-avi.svg
!public/assets/images/global/default-avi-2.svg
!public/assets/images/global/default-bg.jpg
!public/assets/images/global/fipamo-logo-primary.svg
!public/assets/images/global/fipamo-logo-secondary.svg
!public/assets/images/global/menu.svg
!public/assets/images/global/sprite.svg
!public/assets/images/global/upload-audio.png
!public/assets/images/global/upload-doc.png
!public/assets/images/global/upload-video.png
!content/
content/*
!content/init
!content/themes
content/themes/*
!content/themes/fipamo-default-v2
!content/themes/fipamo-default-v2/*
content/
.ftpconfig
.vscode/
*.swp
site/settings.json
site/folks.json
site/pages.json
site/tags.json
*.DS_Store *.DS_Store
config.codekit3 *.codekit3

74
.php-cs-fixer.php Normal file
View file

@ -0,0 +1,74 @@
<?php
$config = new PhpCsFixer\Config();
return $config
->setRiskyAllowed(true)
->setRules([
'@PSR12' => true,
'array_indentation' => true,
'array_syntax' => [
'syntax' => 'short',
],
'combine_consecutive_unsets' => true,
'method_chaining_indentation' => true,
'class_attributes_separation' => [
'elements' => [
'const' => 'none',
'method' => 'one',
'property' => 'none',
'trait_import' => 'none',
],
],
'multiline_whitespace_before_semicolons' => [
'strategy' => 'no_multi_line',
],
'single_quote' => false,
'binary_operator_spaces' => [
'default' => 'single_space',
'operators' => [
'=' => 'align_single_space_minimal',
'=>' => 'align_single_space_minimal',
],
],
'braces' => [
'allow_single_line_closure' => true,
],
'concat_space' => [
'spacing' => 'one',
],
'declare_equal_normalize' => true,
'function_typehint_space' => true,
'single_line_comment_style' => [
'comment_types' => [
'hash',
],
],
'include' => true,
'lowercase_cast' => true,
'no_extra_blank_lines' => [
'tokens' => [
'curly_brace_block',
'extra',
'parenthesis_brace_block',
'throw',
]
],
'no_multiline_whitespace_around_double_arrow' => true,
'no_spaces_around_offset' => true,
'no_unused_imports' => true,
'no_whitespace_before_comma_in_array' => true,
'no_whitespace_in_blank_line' => true,
'object_operator_without_whitespace' => true,
'single_blank_line_before_namespace' => true,
'ternary_operator_spaces' => true,
'trim_array_spaces' => true,
'unary_operator_spaces' => true,
'whitespace_after_comma_in_array' => true,
'single_line_after_imports' => true,
'ordered_imports' => [
'sort_algorithm' => 'none',
],
//Other rules here...
])
->setLineEnding("\n");

View file

@ -1,6 +1,7 @@
.babelrc .babelrc
README.md README.md
*.pug *.twig
*.sass *.sass
*.json *.json
*.php
*.md

View file

@ -1,17 +1,36 @@
{ {
"arrowParens": "avoid", "overrides": [
"bracketSpacing": true, {
"htmlWhitespaceSensitivity": "css", "files": ".prettierrc",
"insertPragma": false, "options": { "parser": "json" }
"jsxBracketSameLine": false, },
"jsxSingleQuote": true, {
"parser": "babel", "files": "*.css",
"proseWrap": "preserve", "options": {
"requirePragma": false, "tabWidth": 2,
"semi": true, "semi": false,
"singleQuote": true, "singleQuote": false,
"trailingComma": "none", "printWidth": 90
"useTabs": true, }
"tabWidth": 4, },
"printWidth": 100 {
} "files": "*.js",
"options": {
"arrowParens": "avoid",
"bracketSpacing": true,
"htmlWhitespaceSensitivity": "css",
"insertPragma": false,
"bracketSameLine": false,
"jsxSingleQuote": true,
"proseWrap": "preserve",
"requirePragma": false,
"semi": true,
"singleQuote": true,
"trailingComma": "none",
"useTabs": true,
"tabWidth": 4,
"printWidth": 90
}
}
]
}

69
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,69 @@
# Contribute to Fipamo
## Overview
The Fipamo project is a project of h.i. which has the stated [intention](https://h-i.works/about.html) of creating human-centric, consent-based tools for dynamic communities. While Fipamo is a tech project, the goals it adheres to are not and serve to create a healthy environment that will guide development. Those who want to contribute to the project must adhere to the ideal of fostering an inclusive and safe environment.
## Code of Conduct
The culture of h.i. will guide all development and decision-making for the project. The goal of this idea is to create a dynamic environment where contributors can give their opinions and thoughts will be considered and discussed healthily and productively and features that are added to the project will follow this ethos. The following details will be points of priority that will help to maintain this culture.
- **Hate-free environment**. Sexism, racism, queerphobia, Islamaphobia, antisemitism, etc will not be tolerated. There will be no warning for contributors who exhibit this behavior and they will be permanently banned from the project.
- **All project features will be consent-based**, meaning opt-in by default. A feature that is opt-out by default will not be approved.
- **No external tracking**. Fipamo is for managing one's personal information, so tracking information for their content is fine, but this will not include tracking other people's information outside of the project.
- **Constructive criticism**. Contributors will not always agree on how to move forward, so disagreements are expected. These conflicts will be resolved with respectful discussion and engagement transparently so the community will be a part of the process.
## Who can contribute.
As Fipamo is a tech project with the stated goal of creating an independent self-hosted content management platform, people with a background in programming are appreciated. However, as the goal is also to create easy-to-use software that is humanistic in its approach, more than technical people will be needed to achieve this goal. Contributors can include:
- Community Organizers
- Researchers
- Differently abled users
- Graphic Designers
- Testers with little to no technical expertise
- Copywriters from multiple languages
- Digital Influencers
Fipamo is meant to be useful to people from a wide range of backgrounds and experiences, so it will require that range of perspectives. If you have an idea, don't be shy. Insight can come from anywhere.
## How to contribute
The first step in being able to contribute to the project is having an account with the h.i. community that houses all project-related discussions. To request an account, sign up at [awosanma.h-i.works](https://awosanma.h-i.works/) and then send a note to either [Are0h](https://h-i.social/@are0h) or the [Fipamo](https://h-i.social/@Fipamo) project account with a brief intro about you and what you'd like to contribute. Once you are approved, you will be added to the appropriate room.
### Non-technical contributions
Contributors who do not want to contribute code to the project can share their insights and questions in the Fipamo community room made for all contributors to the project. If an idea requires a specific focus, additional rooms can be created to accommodate these needs.
Feature requests, comments on existing features, bugs, etc, will originate in this room. Bugs and new features will be added to the Issues list located at the Fipamo code repository. Non-technical members are welcome to request an account at the repo to file Issues directly themselves.
### Technical contributions
Technical contributors are required to have an account at the Fipamo [repository](https://koodu.h-i.works/projects/fipamo). The repo is invite-only, so to request an account, speak to any moderator in a Fipamo community room.
#### Dependencies
Fipamo is a project based on [Laravel](https://laravel.com), so the only dependencies needed for the environment are PHP and Composer to install the necessary libraries.
[Herd](https://herd.laravel.com) is recommended to make this setup easy, but as long as these two requirements are available on your system, you should be good to go.
For details on how to set up a Fipamo instance, check the wiki.
For details on how to make code contributions to the project, read on.
#### Forking a project
The first step to contributing to an h.i. project is making a copy of it by forking it, which will create a new working instance of the current code without affecting the main codebase. This is accomplished by going to the project's main page and hitting the 'fork' button in the upper right corner of the page. Name your fork whatever you like and now you have a copy of the project that you can tinker with. Good job!
#### Cloning your fork and making a branch for edits
Now that your fork is ready, let's grab a copy of it so you can start making some cool changes. Open up your terminal and clone a copy of your fork to your machine. The URL of your fork can be found on your fork page.
`git clone https://koodu.h-i.works/YOURNAME/YOURFORKNAME.git`.
Assuming everything went well, you should now have a copy of your fork on your machine. Now you're ready to make changes.
To avoid conflicts with other people who will be contributing to your project as well, create a specific branch for the changes you want to make. The name of this branch should relate to the nature of the change you are making, for example, if you are updating the README, a good name for that branch could be `tweaking-the-readme`. It doesn't have to be fancy but what is being changed should be apparent at a glance. Once you've decided on the name create the branch and then check it out to work on it.
```
git branch YOUR-BRANCH-NAME
git checkout YOUR-BRANCH-NAME
```
#### Making changes and adding them to the main branch
Now that you are in your branch for the changes you want to make, pop open your favorite text editor and hack away. As your changes are in your fork with its branch, you don't have to worry about interfering with another contributor's work.
Once you've made the changes you want to see, the next step is to have your changes evaluated to see if it is ready to add to the main project. To do this, create a Pull Request to signal you are ready for your changes to be reviewed. This can be accomplished by going to your fork repository page and hitting the 'Pull Request' tab. The main codebase is the `projects: develop` option and it will be pulling from the branch you created for the changes you made.
If your changes need some work, they will be listed in the pull request and you can make those edits at your convenience. Once the pull request is ready and validated, it will then be merged into the main codebase.

674
LICENSE Normal file
View file

@ -0,0 +1,674 @@
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.

View file

@ -1,11 +1,22 @@
![This is Fipamo](https://playvicio.us/base-assets/images/fipamo-brand.png) # Fipamo
# Fipamo means to save The Fipamo Project was originally a side project of [Ro](https://roiskinda.cool) to help make his development work easier by having a stable base to build from instead of having to build from scratch each time.
The Fipamo project was born from a need for a simple, easy to use no data blog platform that doesn't require much effort to set up and maintain. Fipamo uses Markdown to handle posts and renders them to flat html so you can serve them from anywhere. No complicated set ups. No long list of dependecies. Just write and publish. With the rise of independent social media becoming a permanent topic in mainstream tech discourse, the need for easy-to-use tools to create and maintain a personal online experience that the individual can control is a priority.
Because nobody has time for all that. To address this need, Fipamo has evolved into a self-hosted no-database content management platform that is accessible, easy to understand, and extendable.
# Fipamo Goals
Fipamo will be a full-featured content management platform that makes it easy for people to create and manage their content and retain full control of their creations, with the ability to share their content with other platforms at their discretion.
## Fipamo Planned Features
- Blog-style posting with a variety of media types, i.e. text, images, video, sound, etc
- Micro-blog capabilities through Activity Pub integration
- Sharing content with any platform with any platform with an available API (Instagram, TikTok, WordPress, etc)
- Modern accessibility to serve the needs of differently-abled people.
- Plug-in system to extend functionality
- Easy data portability
# Contribute to Fipamo
Fipamo is a project that wants to foster a healthy community through the values of safety, ease of use, and transparency. The creation of a tool that promotes these attributes cannot be done by one person, so contributors from a variety of backgrounds are welcome to lend their expertise to accomplish this goal.
For details on how to lend a hand, check the [guide](https://koodu.h-i.works/projects/fipamo/src/branch/develop/CONTRIBUTING.md).
## Check the (WIP) Docs to get you started. <br>
[Getting Started](https://code.playvicio.us/Are0h/Fipamo/wiki/00---Start) <br>
[Install](https://code.playvicio.us/Are0h/Fipamo/wiki/01---Install) <br>
[Using Fipamo](https://code.playvicio.us/Are0h/Fipamo/wiki/02-Usage) <br>

27
app/Console/Kernel.php Normal file
View file

@ -0,0 +1,27 @@
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
/**
* Define the application's command schedule.
*/
protected function schedule(Schedule $schedule): void
{
// $schedule->command('inspire')->hourly();
}
/**
* Register the commands for the application.
*/
protected function commands(): void
{
$this->load(__DIR__.'/Commands');
require base_path('routes/console.php');
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
class Handler extends ExceptionHandler
{
/**
* The list of the inputs that are never flashed to the session on validation exceptions.
*
* @var array<int, string>
*/
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];
/**
* Register the exception handling callbacks for the application.
*/
public function register(): void
{
$this->reportable(function (Throwable $e) {
//
});
}
}

View file

@ -0,0 +1,42 @@
<?php
function delete_directory($dirPath, $catch = true)
{
if (is_dir($dirPath)) {
$objects = new DirectoryIterator($dirPath);
foreach ($objects as $object) {
if (!$object->isDot()) {
if ($object->isDir()) {
delete_directory($object->getPathname());
} else {
unlink($object->getPathname());
}
}
}
rmdir($dirPath);
} else {
if ($catch) {
throw new Exception(__FUNCTION__ . '(dirPath): dirPath is not a directory!');
} else {
//just keep going
}
}
}
function copy_directory($src, $dst)
{
if (file_exists($dst)) {
rrmdir($dst);
}
if (is_dir($src)) {
mkdir($dst);
$files = scandir($src);
foreach ($files as $file) {
if ($file != "." && $file != "..") {
copy_directory("$src/$file", "$dst/$file");
}
}
} elseif (file_exists($src)) {
copy($src, $dst);
}
}

View file

@ -0,0 +1,80 @@
<?php
use Illuminate\Encryption\Encrypter;
function createUUID()
{
if (function_exists('com_create_guid') === true) {
return trim(com_create_guid(), '{}');
}
return sprintf(
'%04X%04X-%04X-%04X-%04X-%04X%04X%04X',
mt_rand(0, 65535),
mt_rand(0, 65535),
mt_rand(0, 65535),
mt_rand(16384, 20479),
mt_rand(32768, 49151),
mt_rand(0, 65535),
mt_rand(0, 65535),
mt_rand(0, 65535)
);
}
function safeString($string)
{
return strtolower(
trim(
preg_replace(
'~[^0-9a-z]+~i',
'_',
html_entity_decode(
preg_replace(
'~&([a-z]{1,2})(?:acute|cedil|circ|grave|lig|orn|ring|slash|th|tilde|uml);~i',
'$1',
htmlentities($string, ENT_QUOTES, 'UTF-8')
),
ENT_QUOTES,
'UTF-8'
)
),
'-'
)
);
}
function randomString(int $length)
{
$alphanum = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
$special = '*&!@%^#$';
$alphabet = $alphanum . $special;
$random = openssl_random_pseudo_bytes($length);
$alphabet_length = strlen($alphabet);
$string = '';
for ($i = 0; $i < $length; ++$i) {
$string .= $alphabet[ord($random[$i]) % $alphabet_length];
}
return $string;
}
function createAppKey()
{
return 'base64:' . base64_encode(Encrypter::generateKey(config('app.cipher')));
}
function isHttps()
{
if (
(isset($_SERVER['HTTPS']) && ($_SERVER['HTTPS'] === 'on' || $_SERVER['HTTPS'] == 1)) ||
(isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) === 'https') ||
(isset($_SERVER['HTTP_X_FORWARDED_SCHEME']) && strtolower($_SERVER['HTTP_X_FORWARDED_SCHEME']) === 'https') ||
(isset($_SERVER['HTTP_X_FORWARDED_SSL']) && ($_SERVER['HTTP_X_FORWARDED_SSL'] === 'on' || $_SERVER['HTTP_X_FORWARDED_SSL'] == 1)) ||
(isset($_SERVER['REQUEST_SCHEME']) && strtolower($_SERVER['REQUEST_SCHEME']) === 'https') ||
(isset($_SERVER['SERVER_PORT']) && $_SERVER['SERVER_PORT'] == 443)
) {
return true;
}
return false;
}

View file

@ -0,0 +1,38 @@
<?php
namespace App\Http\Controllers\API;
use App\Http\Controllers\Controller;
use App\Interfaces\MemberRepositoryInterface;
use Illuminate\Http\Request;
class AuthAPIController extends Controller
{
protected $member;
public function __construct(
MemberRepositoryInterface $memberRepo
) {
$this->member = $memberRepo;
}
public function status(Request $request)
{
$result = [];
$data = json_decode($request->getContent());
if ($this->member::status()) {
$result = [
'message' => 'Authorized',
'type' => 'apiUseAuthorized',
'token' => session('token'),
];
} else {
$result = [
'message' => 'Not Authorized',
'type' => 'apiUseNotAuthorized',
];
}
return response()->json($result);
}
}

View file

@ -0,0 +1,45 @@
<?php
namespace App\Http\Controllers\API;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Services\Assets\FileUploadService;
use App\Interfaces\MemberRepositoryInterface;
use App\Services\Data\SettingsService;
class FileUploadAPIController extends Controller
{
protected $upload;
protected $member;
protected $settings;
public function __construct(
FileUploadService $fileUploadService,
MemberRepositoryInterface $memberRepo,
SettingsService $settingsService
) {
$this->upload = $fileUploadService;
$this->member = $memberRepo;
$this->settings = $settingsService;
}
public function upload(Request $request)
{
$result = $this->upload->handleFile($request);
//update configs for specfic uploads
switch ($request['source']) {
case 'avatar-upload':
$member = [];
$member = session('member');
$member['avatar'] = $result['filePath'];
$member = (object) $member;
$this->member->update($member);
break;
case 'background-upload':
$this->settings->updateGlobalData('background', $result['filePath']);
break;
}
return $result;
}
}

View file

@ -0,0 +1,39 @@
<?php
namespace App\Http\Controllers\API;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Services\Upkeep\InitService;
use App\Services\Upkeep\ResetService;
class InitAPIController extends Controller
{
protected $init;
protected $reset;
public function __construct(InitService $initService, ResetService $resetService)
{
$this->init = $initService;
$this->reset = $resetService;
}
//init stuff
public function setupFresh(Request $request)
{
$result = $this->init->fresh(json_decode($request->getContent()));
return response()->json($result)->header('Content-Type', 'application/json');
}
public function setupRestore(Request $request)
{
$result = $this->init->restore($request);
return response()->json($result)->header('Content-Type', 'application/json');
}
public function setupReset(Request $request)
{
$result = $this->reset->site($request);
return response()->json($result)->header('Content-Type', 'application/json');
}
}

View file

@ -0,0 +1,36 @@
<?php
namespace App\Http\Controllers\API;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use App\Mail\SystemEmail;
use Symfony\Component\Mailer\Exception\TransportException;
class MailAPIController extends Controller
{
public function __construct()
{
}
//init stuff
public function sendNotify(Request $request)
{
$result = [];
try {
Mail::to(env('ADMIN_EMAIL'))->send(new SystemEmail($request->content));
$result = [
'type' => 'mail_good',
'message' => 'Mail Sent',
];
} catch (TransportException $e) {
$result = [
'type' => 'mail_not_good',
'message' => 'Mail Not Sent. It\'s cool. Just check mail settings in the .env',
];
}
return response()->json($result)->header('Content-Type', 'application/json');
}
}

View file

@ -0,0 +1,108 @@
<?php
namespace App\Http\Controllers\API;
use App\Interfaces\PageRepositoryInterface;
use App\Interfaces\MemberRepositoryInterface;
use App\Http\Controllers\Controller;
use App\Services\Assets\FileUploadService;
use Illuminate\Http\Request;
use function _\find;
class PageAPIController extends Controller
{
protected $pages;
protected $member;
protected $upload;
public function __construct(
PageRepositoryInterface $pageRepository,
MemberRepositoryInterface $memberRepo,
FileUploadService $fileUploadService,
) {
$this->pages = $pageRepository;
$this->upload = $fileUploadService;
$this->member = $memberRepo;
}
public function update(Request $request)
{
$body = json_decode($request->getContent());
//check to see if required fields are present
if ($body->uuid == '' || $body->uuid == null) {
$error = [
'message' => "[uuidx ] is a required field",
'type' => 'postError',
];
return response()->json($error)->header('Content-Type', 'application/json');
}
$apiKey = $request->header('fipamo-api-key');
$body->slug = strtolower(str_replace(' ', '-', $body->title));
if (!isset($body->imageList)) {
$body->imageList = '';
}
if (!isset($body->fileList)) {
$body->fileList = '';
}
//find member based on key and create a session
$folks = $this->member->getAll();
$member = find($folks, ['key' => $apiKey]);
$request->session()->put('member', $member);
$result = $this->pages->update($body);
//clear session once page is created
session()->flush();
return response()->json($result)->header('Content-Type', 'application/json');
}
public function create(Request $request)
{
$body = json_decode($request->getContent());
//check to see if required fields are present
if ($body->title == '' || $body->title == null) {
$error = [
'message' => "[title] is a required field",
'type' => 'postError',
];
return response()->json($error)->header('Content-Type', 'application/json');
}
$apiKey = $request->header('fipamo-api-key');
$folks = $this->member->getAll();
$body->slug = strtolower(str_replace(' ', '-', $body->title));
if (!isset($body->imageList)) {
$body->imageList = '';
}
if (!isset($body->fileList)) {
$body->fileList = '';
}
//find member based on key and create a session
$member = find($folks, ['key' => $apiKey]);
$request->session()->put('member', $member);
$result = $this->pages->create($body);
//clear session once page is created
session()->flush();
return response()->json($result)->header('Content-Type', 'application/json');
}
public function delete(Request $request)
{
$body = json_decode($request->getContent());
//check to see if required fields are present
if ($body->uuid == '' || $body->uuid == null) {
$error = [
'message' => "[uuidx ] is a required field",
'type' => 'postError',
];
return response()->json($error)->header('Content-Type', 'application/json');
}
$apiKey = $request->header('fipamo-api-key');
$folks = $this->member->getAll();
$member = find($folks, ['key' => $apiKey]);
$request->session()->put('member', $member);
$result = $this->pages->delete($body);
session()->flush();
return response()->json($result)->header('Content-Type', 'application/json');
}
}

View file

@ -0,0 +1,93 @@
<?php
namespace App\Http\Controllers\API;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Services\Data\SettingsService;
use App\Services\Assets\RenderService;
use App\Services\Upkeep\MaintenanceService;
use App\Interfaces\MemberRepositoryInterface;
use App\Services\Assets\AssetService;
class SettingsAPIController extends Controller
{
protected $settings;
protected $render;
protected $mainteance;
protected $member;
protected $assets;
public function __construct(
SettingsService $settingsService,
RenderService $renderService,
MaintenanceService $maintenanceService,
MemberRepositoryInterface $memberRepo,
AssetService $assetService
) {
$this->settings = $settingsService;
$this->render = $renderService;
$this->maintenance = $maintenanceService;
$this->member = $memberRepo;
$this->assets = $assetService;
}
public function publish(Request $request)
{
$this->assets->moveToTheme(true);
$result = $this->render->publishAll();
return response()->json($result)->header('Content-Type', 'application/json');
}
public function sync(Request $request)
{
$body = json_decode($request->getContent());
//update member if needed
$this->member->update($body->member);
//sync settings
$result = $this->settings->sync($body);
return response()->json($result)->header('Content-Type', 'application/json');
}
public function navSync(Request $request)
{
$body = json_decode($request->getContent());
$result = $this->settings->navSync($body);
return response()->json($result)->header('Content-Type', 'application/json');
}
public function createBackup(Request $request)
{
$body = json_decode($request->getContent());
if ($body->task == 'content_backup') {
return response()->json(
$this->maintenance->createContentBackUp()
)->header('Content-Type', 'application/json');
} else {
return response()->json(
$this->maintenance->createFileBackUp()
)->header('Content-Type', 'application/json');
}
}
public function downloadBackup(Request $request)
{
if ($this->member::status()) {
$latest = '';
$file = '';
if (explode('/', $request->getRequestUri())[4] == 'content-download') {
$latest = $this->settings->getGlobal()['last_content_backup'];
$file = 'backup-content-' . $latest . '.zip';
} else {
$latest = $this->settings->getGlobal()['last_files_backup'];
$file = 'backup-files-' . $latest . '.zip';
}
return response()->download(
'../content/backups/' . $file,
$file,
['Content-Type: application/zip']
);
}
}
}

View file

@ -0,0 +1,50 @@
<?php
namespace App\Http\Controllers;
use Symfony\Component\HttpFoundation\Response;
use App\Interfaces\MemberRepositoryInterface;
use Illuminate\Http\Request;
class AuthController extends Controller
{
protected $member;
public function __construct(
MemberRepositoryInterface $memberRepository
) {
$this->member = $memberRepository;
}
public function enter(Request $request): Response
{
$token = csrf_token();
$credentials = $request->validate([
'handle' => ['required'],
'password' => ['required'],
]);
if ($credentials) {
$result = $this->member->auth($request);
if ($result['status']) {
//$request->session()->regenerate();
return redirect()->intended('dashboard/start');
} else {
return back()->withErrors([
'error' => $result['message'],
]);
}
} else {
return back()->withErrors([
'error' => 'Nope. Check your crendtials, champ',
]);
}
}
public function exit(Request $request): Response
{
session()->flush();
return redirect()->intended('dashboard');
}
}

View file

@ -0,0 +1,12 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController
{
use AuthorizesRequests, ValidatesRequests;
}

View file

@ -0,0 +1,143 @@
<?php
namespace App\Http\Controllers;
use App\Interfaces\PageRepositoryInterface;
use App\Interfaces\MemberRepositoryInterface;
use App\Services\Data\ThemeService;
use App\Services\Data\SortingService;
class DashController extends Controller
{
protected PageRepositoryInterface $pages;
protected MemberRepositoryInterface $member;
protected ThemeService $themes;
protected SortingService $sort;
public function __construct(
PageRepositoryInterface $pageRepository,
MemberRepositoryInterface $memberRepo,
ThemeService $themeService,
SortingService $sortingService
) {
$this->pages = $pageRepository;
$this->member = $memberRepo;
$this->themes = $themeService;
$this->sort = $sortingService;
}
//---
// GET
//---
public function init($second, $third, $fourth)
{
switch ($second) {
case 'settings':
return $this->settings();
break;
case 'navigation':
return $this->navigation();
break;
case 'pages':
($third == null) ? $third = 'all' : $third = $third;
($fourth == null) ? $fourth = 1 : $fourth = $fourth;
return $this->book($third, $fourth);
break;
case 'page':
return $this->page($third, $fourth);
break;
case 'logout':
return $this->logout();
break;
default:
return $this->start();
break;
}
}
public function start()
{
$result = [];
if ($this->member::status()) {
$result = $this->pages->getGroup(1, 4);
}
return view('back.start', [
"status" => $this->member::status(),
"result" => $result,
"title" => "Start"
]);
}
public function book($pageFilter = 'all', $pageNum = 1)
{
$result = [];
if ($this->member::status()) {
$result = $this->pages->getGroup($pageNum, 4, $pageFilter);
}
return view('back.book', [
"status" => $this->member::status(),
"result" => $result,
"currentPage" => $pageNum,
"title" => "Pages"
]);
}
public function page($mode, $uuid)
{
$title;
$page = [];
$views = [];
$mode == 'edit' ? $page = $this->pages->getByUuid($uuid) : $page = [];
$mode == 'edit' ? $title = $page['title'] : $title = 'Add New';
$mode == 'edit' ? $views = $this->themes->getCustomViews($page['layout']) : $views[] = 'page';
return view('back.page', [
"status" => $this->member::status(),
"mode" => $mode,
"page" => $page,
"views" => $views,
"title" => $title,
]);
}
public function navigation()
{
return view('back.navigation', $this->sort->navigation());
}
public function settings()
{
return view('back.settings', $this->sort->settings());
}
//---
// POST
//---
//---
// PUT
//---
//---
// AUTH
//---
public function login()
{
if ($this->member::status()) {
return redirect('dashboard');
} else {
return view('back.login', [
"status" => $this->member::status(),
"title" => "Hi!"
]);
}
}
public function logout()
{
session()->flush();
return redirect()->intended('dashboard');
}
}

View file

@ -0,0 +1,99 @@
<?php
namespace App\Http\Controllers;
use App\Interfaces\PageRepositoryInterface;
use App\Services\Assets\AssetService;
use App\Services\Data\SettingsService;
use App\Services\Data\SortingService;
use function _\find;
class FrontController extends Controller
{
protected $settings;
protected PageRepositoryInterface $pages;
protected AssetService $assets;
protected SortingService $sort;
public function __construct(
PageRepositoryInterface $pageRepository,
SettingsService $settingsService,
AssetService $assetService,
SortingService $sortService,
) {
$this->pages = $pageRepository;
$this->settings = $settingsService;
$this->assets = $assetService;
$this->sort = $sortService;
}
//REFACTOR: there is some method overlap between index and pages, so that needs to be addressed
public function index($first = 00, $second = 00, $third = 00)
{
$global = $this->settings->getGlobal();
$currentTheme = $this->assets->getCurrentTheme();
$template;
$pageData = [];
$pages = $this->pages->getAll();
//check if configs are present
if (file_exists(env('FOLKS_PATH')) && file_exists(env('SETTINGS_PATH'))) {
if ($global['dynamicRender'] == 'true') {
if (is_numeric($first)) {
if ($first == 00 || !isset($first)) {
$page = $pages->where('id', 1)->first();
$pageData = $this->sort->page($page);
$template = $currentTheme . '.index';
} else {
$page = $this->pages->getBySlug($third);
$pageData = $this->sort->page($page);
$template = $currentTheme . '.' . $page['layout'];
}
} else {
if ($first == null || $first == '') {
$page = $pages->where('id', 1)->first();
$pageData = $this->sort->page($page);
$template = $currentTheme . '.index';
} else {
}
}
return view($template, $pageData);
} else {
if (is_file('../public/index.html')) {
return response()->file('../public/index.html');
} else {
return redirect()->intended('dashboard/start');
}
}
} else {
return view('back.init', ["status" => false, "title" => "Set Up"]);
}
}
public function page($first = 00, $second = 00, $third = 00)
{
$currentTheme = $this->assets->getCurrentTheme();
switch ($first) {
case 'archive':
case 'archives':
$template = $currentTheme . '.archive';
$pageData = $this->sort->archive();
break;
case 'tags':
$template = $currentTheme . '.tags';
$tags = $this->sort->tags(false);
$tagData = find($tags['tags'], ['tag_name' => $second]);
$pageData = [
'theme' => $currentTheme, // for theme kit
'title' => 'Pages Tagged as ' . $second,
'dynamicRender' => $tags['dynamicRender'],
'info' => $tags['info'],
'menu' => $tags['menu'],
'pages' => $tagData['pages'],
'media' => $tags['media'],
];
break;
}
return view($template, $pageData);
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Interfaces\PageRepositoryInterface;
class RouteDeleteController extends Controller
{
protected $page;
public function __construct(
PageRepositoryInterface $pageRepo
) {
$this->page = $pageRepo;
}
public function handleRequest(Request $request)
{
$path = explode('/', $request->path());
switch ($path[0]) {
case 'page':
$body = json_decode($request->getContent());
$result = $this->page->delete($body);
return response()->json($result)->header('Content-Type', 'application/json');
break;
}
}
}

View file

@ -0,0 +1,88 @@
<?php
namespace App\Http\Controllers;
use App\Interfaces\MemberRepositoryInterface;
use App\Services\Data\SettingsService;
class RouteGetController extends Controller
{
protected $dash;
protected $gate;
protected $theme;
protected $front;
protected $member;
protected $settings;
public function __construct(
DashController $dashController,
AuthController $authController,
ThemeController $themeController,
FrontController $frontController,
MemberRepositoryInterface $memberRepo,
SettingsService $settingsService,
) {
$this->dash = $dashController;
$this->gate = $authController;
$this->theme = $themeController;
$this->front = $frontController;
$this->member = $memberRepo;
$this->settings = $settingsService;
}
public function handleRequest($first = null, $second = null, $third = null, $fourth = null)
{
if (isset($first) && !is_numeric($first)) {
switch ($first) {
case 'dashboard':
if ($this->member::status()) {
return $this->dash->init($second, $third, $fourth);
} else {
return $this->dash->login();
}
break;
case 'theme':
if ($this->member::status()) {
if (isset($second)) {
return $this->theme->getView($third, $fourth);
} else {
return $this->theme->start();
}
} else {
return $this->dash->login();
}
break;
case 'tags':
case 'archives':
return $this->front->page($first, $second, $third);
break;
case 'backup':
return $this->downloadBackup($second);
break;
}
} else {
return $this->front->index($first, $second, $third);
}
}
private function downloadBackup($type)
{
if ($this->member::status()) {
$latest = '';
$file = '';
if ($type == 'content-download') {
$latest = $this->settings->getGlobal()['last_content_backup'];
$file = 'backup-content-' . $latest . '.zip';
} else {
$latest = $this->settings->getGlobal()['last_files_backup'];
$file = 'backup-files-' . $latest . '.zip';
}
return response()->download(
'../content/backups/' . $file,
$file,
['Content-Type: application/zip']
);
}
}
}

View file

@ -0,0 +1,122 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use App\Mail\SystemEmail;
use App\Interfaces\PageRepositoryInterface;
use App\Services\Upkeep\MaintenanceService;
use App\Services\Assets\FileUploadService;
use App\Interfaces\MemberRepositoryInterface;
use App\Services\Data\SettingsService;
use App\Services\Upkeep\InitService;
use App\Services\Upkeep\ResetService;
class RoutePostController extends Controller
{
protected $page;
protected $gate;
protected $maintenance;
protected $upload;
protected $settings;
protected $member;
protected $init;
protected $reset;
public function __construct(
PageRepositoryInterface $pageRepo,
AuthController $authController,
MaintenanceService $maintenanceService,
FileUploadService $fileUploadService,
SettingsService $settingsService,
MemberRepositoryInterface $memberRepo,
InitService $initService,
ResetService $resetService,
) {
$this->page = $pageRepo;
$this->gate = $authController;
$this->maintenance = $maintenanceService;
$this->upload = $fileUploadService;
$this->settings = $settingsService;
$this->member = $memberRepo;
$this->init = $initService;
$this->reset = $resetService;
}
public function handleRequest(Request $request)
{
$path = explode('/', $request->path());
switch ($path[0]) {
case 'init':
return $this->initTask($path[1], $request);
break;
case 'login':
return $this->gate->enter($request);
break;
case 'page':
$body = json_decode($request->getContent());
$result = $this->page->create($body);
return response()->json($result)->header('Content-Type', 'application/json');
break;
case 'settings':
if ($path[1] == 'mailer') {
return $this->sendNotify($request);
}
break;
case 'upload':
$result = $result = $this->upload->handleFile($request);
//update configs for specfic uploads
switch ($request['source']) {
case 'avatar-upload':
$member = [];
$member = session('member');
$member['avatar'] = $result['filePath'];
$member = (object) $member;
$this->member->update($member);
break;
case 'background-upload':
$this->settings->updateGlobalData('background', $result['filePath']);
break;
}
return $result;
break;
}
}
private function initTask($task, $request)
{
$result = [];
switch ($task) {
case 'fresh':
$result = $this->init->fresh(json_decode($request->getContent()));
break;
case 'restore':
$result = $this->init->restore($request);
break;
case 'reset':
$result = $this->reset->site($request);
break;
}
return response()->json($result)->header('Content-Type', 'application/json');
}
private function sendNotify($request)
{
$result = [];
try {
Mail::to(env('ADMIN_EMAIL'))->send(new SystemEmail($request->content));
$result = [
'type' => 'mail_good',
'message' => 'Mail Sent',
];
} catch (TransportException $e) {
$result = [
'type' => 'mail_not_good',
'message' => 'Mail Not Sent. It\'s cool. Just check mail settings in the .env',
];
}
return response()->json($result)->header('Content-Type', 'application/json');
}
}

View file

@ -0,0 +1,92 @@
<?php
namespace App\Http\Controllers;
use App\Interfaces\PageRepositoryInterface;
use App\Services\Assets\AssetService;
use App\Services\Assets\RenderService;
use App\Interfaces\MemberRepositoryInterface;
use App\Services\Data\SettingsService;
use App\Services\Upkeep\MaintenanceService;
use Illuminate\Http\Request;
class RoutePutController extends Controller
{
protected $page;
protected $assets;
protected $render;
protected $settings;
protected $member;
protected $maintenance;
public function __construct(
PageRepositoryInterface $pageRepo,
AssetService $assetService,
RenderService $renderService,
SettingsService $settingsService,
MemberRepositoryInterface $memberRepo,
MaintenanceService $maintenanceService,
) {
$this->page = $pageRepo;
$this->assets = $assetService;
$this->render = $renderService;
$this->settings = $settingsService;
$this->member = $memberRepo;
$this->maintenance = $maintenanceService;
}
public function handleRequest(Request $request)
{
$path = explode('/', $request->path());
switch ($path[0]) {
case 'page':
$body = json_decode($request->getContent());
$result = $this->page->update($body);
return response()->json($result)->header('Content-Type', 'application/json');
break;
case 'settings':
return $this->settingsTasks($request, $path[1]);
break;
case 'backup':
return $this->createBackup($request);
break;
}
}
private function createBackup($request)
{
$body = json_decode($request->getContent());
if ($body->task == 'content_backup') {
return response()->json(
$this->maintenance->createContentBackUp()
)->header('Content-Type', 'application/json');
} else {
return response()->json(
$this->maintenance->createFileBackUp()
)->header('Content-Type', 'application/json');
}
}
private function settingsTasks($request, $task)
{
$result = [];
switch ($task) {
case 'publish':
$this->assets->moveToTheme(true);
$result = $this->render->publishAll();
break;
case 'sync':
$body = json_decode($request->getContent());
//update member if needed
$this->member->update($body->member);
//sync settings
$result = $this->settings->sync($body);
break;
case 'nav-sync':
$body = json_decode($request->getContent());
$result = $this->settings->navSync($body);
break;
}
return response()->json($result)->header('Content-Type', 'application/json');
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Mail;
class SystemMailController extends Controller
{
/**
* Send notification
*/
public function sendNotification()
{
$message = "This is something important. Probably";
Mail::to(env('ADMIN_EMAIL'))->send(new SystemEmail($message));
}
}

View file

@ -0,0 +1,99 @@
<?php
namespace App\Http\Controllers;
use App\Interfaces\PageRepositoryInterface;
use App\Services\Assets\AssetService;
use App\Interfaces\MemberRepositoryInterface;
use App\Services\Data\SortingService;
use App\Services\Data\ThemeService;
class ThemeController extends Controller
{
protected PageRepositoryInterface $pages;
protected MemberRepositoryInterface $member;
protected AssetService $assets;
protected SortingService $sort;
protected ThemeService $themes;
public function __construct(
PageRepositoryInterface $pageRepository,
MemberRepositoryInterface $memberRepo,
AssetService $assetService,
SortingService $sortService,
ThemeService $themeService,
) {
$this->pages = $pageRepository;
$this->member = $memberRepo;
$this->assets = $assetService;
$this->sort = $sortService;
$this->themes = $themeService;
}
public function start()
{
if ($this->member::status()) {
return view('theme.start', [
"status" => $this->member::status(),
"title" => "Fipamo Theme Kit",
"pages" => $this->themes->getCustomViews('page')
]);
} else {
return redirect('dashboard');
}
}
public function getView($view = 'index', $id = null)
{
//move assets to theme testing dir
$this->assets->moveToTheme();
$currentTheme = $this->assets->getCurrentTheme();
$template;
$pageData = [];
switch ($view) {
case "index":
case "page":
$page;
if ($view == 'index') {
$template = $currentTheme . '.index';
$page = $this->pages->getById(0);
} else {
$template = $currentTheme . '.page';
//if coming from theme page, grabs id of latest page
if ($id == null) {
$uuid = $this->getPageUUID();
} else {
//get page by uuid
$page = $this->pages->getByUuid($id);
}
}
$pageData = $this->sort->page($page);
break;
case "tags":
$template = $currentTheme . '.tags';
$pageData = $this->sort->tags();
break;
case "archives":
case "archive":
$template = $currentTheme . '.archive';
$pageData = $this->sort->archive();
break;
default:
$template = $currentTheme . '.index';
$page = $this->pages->getBySlug('first');
$pageData = $this->sort->page($page);
}
if ($this->member::status()) {
return view($template, $pageData);
} else {
return redirect('dashboard/start');
}
}
private function getPageUUID()
{
$book = $this->pages->getAll();
$page = $book->where('layout', 'page')->first();
return $page['uuid'];
}
}

75
app/Http/Kernel.php Normal file
View file

@ -0,0 +1,75 @@
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array<int, class-string|string>
*/
protected $middleware = [
// \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];
/**
* The application's route middleware groups.
*
* @var array<string, array<int, class-string|string>>
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class . ':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
//added middleware so api has access to session data
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
],
];
/**
* The application's middleware aliases.
*
* Aliases may be used instead of class names to conveniently assign middleware to routes and groups.
*
* @var array<string, class-string|string>
*/
protected $middlewareAliases = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'precognitive' => \Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'member.check' => \App\Http\Middleware\MemberCheck::class,
'validate.key' => \App\Http\Middleware\ValidateAPIKey::class,
];
}

View file

@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Http\Request;
class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*/
protected function redirectTo(Request $request): ?string
{
return $request->expectsJson() ? null : route('login');
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends Middleware
{
/**
* The names of the cookies that should not be encrypted.
*
* @var array<int, string>
*/
protected $except = [
//
];
}

View file

@ -0,0 +1,32 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use App\Services\AuthService;
class MemberCheck
{
protected $auth;
public function __construct(
AuthService $authService,
) {
$this->auth = $authService;
}
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next)
{
if ($this->auth::status()) {
return $next($request);
} else {
return redirect('dashboard');
}
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as Middleware;
class PreventRequestsDuringMaintenance extends Middleware
{
/**
* The URIs that should be reachable while maintenance mode is enabled.
*
* @var array<int, string>
*/
protected $except = [
//
];
}

View file

@ -0,0 +1,30 @@
<?php
namespace App\Http\Middleware;
use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next, string ...$guards): Response
{
$guards = empty($guards) ? [null] : $guards;
foreach ($guards as $guard) {
if (Auth::guard($guard)->check()) {
return redirect(RouteServiceProvider::HOME);
}
}
return $next($request);
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
class TrimStrings extends Middleware
{
/**
* The names of the attributes that should not be trimmed.
*
* @var array<int, string>
*/
protected $except = [
'current_password',
'password',
'password_confirmation',
];
}

View file

@ -0,0 +1,20 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustHosts as Middleware;
class TrustHosts extends Middleware
{
/**
* Get the host patterns that should be trusted.
*
* @return array<int, string|null>
*/
public function hosts(): array
{
return [
$this->allSubdomainsOfApplicationUrl(),
];
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware
{
/**
* The trusted proxies for this application.
*
* @var array<int, string>|string|null
*/
protected $proxies;
/**
* The headers that should be used to detect proxies.
*
* @var int
*/
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB;
}

View file

@ -0,0 +1,66 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use App\Interfaces\MemberRepositoryInterface;
use App\Services\Data\SettingsService;
use function _\find;
class ValidateAPIKey
{
protected $member;
protected $settings;
public function __construct(
MemberRepositoryInterface $memberRepo,
SettingsService $settingsService,
) {
$this->member = $memberRepo;
$this->settings = $settingsService;
}
/**
* Handle an incoming request.
*
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
*/
public function handle(Request $request, Closure $next): Response
{
$response = [];
//checks to see if request is secure
if (isHttps()) {
$key = $request->header('fipamo-api-key');
$folks = $this->member->getAll();
//looks to see if API key exists
if (find($folks, ['key' => $key])) {
//final check to see if API requests are being accepted
$global = $this->settings->getGlobal();
if (isset($global['externalAPI']) && $global['externalAPI'] == "true") {
return $next($request);
} else {
$response = [
'message' => "API Auth Fail: Not Accepting Requests",
'type' => 'postError',
];
return response()->json($response)->header('Content-Type', 'application/json');
}
} else {
$response = [
'message' => "API Auth Fail: API Key Invalid",
'type' => 'postError',
];
return response()->json($response)->header('Content-Type', 'application/json');
}
} else {
$response = [
'message' => "API Auth Fail: Request must be secure (HTTPS)",
'type' => 'postError',
];
return response()->json($response)->header('Content-Type', 'application/json');
}
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Routing\Middleware\ValidateSignature as Middleware;
class ValidateSignature extends Middleware
{
/**
* The names of the query string parameters that should be ignored.
*
* @var array<int, string>
*/
protected $except = [
// 'fbclid',
// 'utm_campaign',
// 'utm_content',
// 'utm_medium',
// 'utm_source',
// 'utm_term',
];
}

View file

@ -0,0 +1,17 @@
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array<int, string>
*/
protected $except = [
//
];
}

View file

@ -0,0 +1,22 @@
<?php
namespace App\Interfaces;
interface MemberRepositoryInterface
{
public function getAll();
public function getByID($id);
public function getByHandle($handle);
public function delete($id);
public function create($member);
public function update($member);
public function auth($request);
public static function status();
}

View file

@ -0,0 +1,22 @@
<?php
namespace App\Interfaces;
interface PageRepositoryInterface
{
public function getAll();
public function getById($id);
public function getByUuid($uuid);
public function getBySlug($slug);
public function delete($uuid);
public function create($page);
public function update($page);
public function getGroup($num, $limit, $filter);
}

55
app/Mail/SystemEmail.php Normal file
View file

@ -0,0 +1,55 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Mail\Mailables\Address;
use Illuminate\Queue\SerializesModels;
class SystemEmail extends Mailable
{
use Queueable;
use SerializesModels;
/**
* Create a new message instance.
*/
public function __construct(public $notifyText)
{
//
}
/**
* Get the message envelope.
*/
public function envelope(): Envelope
{
return new Envelope(
from: new Address(env('MAIL_USERNAME'), 'System Notification'),
subject: 'Notification',
);
}
/**
* Get the message content definition.
*/
public function content(): Content
{
return new Content(
view: 'mail.notify',
);
}
/**
* Get the attachments for the message.
*
* @return array<int, \Illuminate\Mail\Mailables\Attachment>
*/
public function attachments(): array
{
return [];
}
}

45
app/Models/User.php Normal file
View file

@ -0,0 +1,45 @@
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
use HasApiTokens, HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}

View file

@ -0,0 +1,24 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
//
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace App\Providers;
// use Illuminate\Support\Facades\Gate;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
/**
* The model to policy mappings for the application.
*
* @var array<class-string, class-string>
*/
protected $policies = [
//
];
/**
* Register any authentication / authorization services.
*/
public function boot(): void
{
//
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\ServiceProvider;
class BroadcastServiceProvider extends ServiceProvider
{
/**
* Bootstrap any application services.
*/
public function boot(): void
{
Broadcast::routes();
require base_path('routes/channels.php');
}
}

View file

@ -0,0 +1,38 @@
<?php
namespace App\Providers;
use Illuminate\Auth\Events\Registered;
use Illuminate\Auth\Listeners\SendEmailVerificationNotification;
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Event;
class EventServiceProvider extends ServiceProvider
{
/**
* The event to listener mappings for the application.
*
* @var array<class-string, array<int, class-string>>
*/
protected $listen = [
Registered::class => [
SendEmailVerificationNotification::class,
],
];
/**
* Register any events for your application.
*/
public function boot(): void
{
//
}
/**
* Determine if events and listeners should be automatically discovered.
*/
public function shouldDiscoverEvents(): bool
{
return false;
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
class FipamoHelperProvider extends ServiceProvider
{
/**
* Register services.
*/
public function register(): void
{
foreach (glob(app_path() . '/Helpers/*.php') as $filename) {
require_once $filename;
}
}
/**
* Bootstrap services.
*/
public function boot(): void
{
//
}
}

View file

@ -0,0 +1,110 @@
<?php
namespace App\Providers;
use Illuminate\Support\ServiceProvider;
//Repos
use App\Repositories\PageRepository;
use App\Interfaces\PageRepositoryInterface;
use App\Repositories\MemberRepository;
use App\Interfaces\MemberRepositoryInterface;
//Asset Services
use App\Services\Assets\AssetService;
use App\Services\Assets\DocService;
use App\Services\Assets\FileUploadService;
use App\Services\Assets\RenderService;
//Data Services
use App\Services\Data\SettingsService;
use App\Services\Data\ContentService;
use App\Services\Data\PaginateService;
use App\Services\Data\ThemeService;
use App\Services\Data\SortingService;
//Upkeep Services
use App\Services\Upkeep\MaintenanceService;
use App\Services\Upkeep\InitService;
use App\Services\Upkeep\ResetService;
class FipamoServiceProvider extends ServiceProvider
{
/**
* Register services.
*/
public function register(): void
{
//services
$this->app->bind(SettingsService::class, function ($app) {
return new SettingsService(new DocService(), new ContentService());
});
$this->app->bind(ContentService::class, function ($app) {
return new ContentService();
});
$this->app->bind(ThemeService::class, function ($app) {
return new ThemeService(new SettingsService(new DocService(), new ContentService()));
});
$this->app->bind(PaginateService::class, function ($app) {
return new PaginateService(new ContentService());
});
$this->app->bind(DocService::class, function ($app) {
return new DocService();
});
$this->app->bind(FileUploadService::class, function ($app) {
return new FileUploadService();
});
$this->app->bind(RenderService::class, function ($app) {
return new RenderService(
new SortingService(
new SettingsService(new DocService(), new ContentService()),
new ContentService(),
new ThemeService(new SettingsService(new DocService(), new ContentService()))
),
new SettingsService(new DocService(), new ContentService()),
new ContentService(),
);
});
$this->app->bind(SortingService::class, function ($app) {
return new SortingService(
new SettingsService(new DocService(), new ContentService()),
new ContentService(),
new ThemeService(new SettingsService(new DocService(), new ContentService()))
);
});
$this->app->bind(AssetService::class, function ($app) {
return new AssetService(
new ThemeService(
new SettingsService(new DocService(), new ContentService())
)
);
});
$this->app->bind(MaintenanceService::class, function ($app) {
return new MaintenanceService(
new SettingsService(new DocService(), new ContentService())
);
});
$this->app->bind(InitService::class, function ($app) {
return new InitService(new DocService());
});
$this->app->bind(ResetService::class, function ($app) {
return new ResetService();
});
}
/**
* Bootstrap services.
*/
public function boot(): void
{
$this->app->bind(PageRepositoryInterface::class, PageRepository::class);
$this->app->bind(MemberRepositoryInterface::class, MemberRepository::class);
}
}

View file

@ -0,0 +1,40 @@
<?php
namespace App\Providers;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Facades\Route;
class RouteServiceProvider extends ServiceProvider
{
/**
* The path to your application's "home" route.
*
* Typically, users are redirected here after authentication.
*
* @var string
*/
public const HOME = '/home';
/**
* Define your route model bindings, pattern filters, and other route configuration.
*/
public function boot(): void
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
$this->routes(function () {
Route::middleware('api')
->prefix('api')
->group(base_path('routes/api.php'));
Route::middleware('web')
->group(base_path('routes/web.php'));
});
}
}

View file

@ -0,0 +1,123 @@
<?php
namespace App\Repositories;
use App\Services\Assets\DocService;
use App\Interfaces\MemberRepositoryInterface;
use ReallySimpleJWT\Token;
use Carbon\Carbon;
use function _\find;
use function _\findIndex;
class MemberRepository implements MemberRepositoryInterface
{
protected $folks;
protected $docs;
public function __construct(DocService $docService)
{
$this->docs = $docService;
if (file_exists(env('FOLKS_PATH'))) {
$this->folks = json_decode(file_get_contents(env('FOLKS_PATH')), true);
} else {
$this->folks = json_decode(file_get_contents(env('FIPAMO_INIT') . '/folks-template.json'), true);
}
}
public function getAll()
{
return $this->folks;
}
public function getById($id)
{
$member = find($this->folks, ['id' => $id]);
return $member;
}
public function getByHandle($handle)
{
$member = find($this->folks, ['handle' => $handle]);
return $member;
}
public function delete($id)
{
//delete member stuff
}
public function create($member)
{
//make new member
}
public function update($member)
{
$index = findIndex($this->folks, ['id' => $member->id]);
$this->folks[$index]['handle'] = $member->handle;
$this->folks[$index]['email'] = $member->email;
if (isset($member->avatar)) {
$this->folks[$index]['avatar'] = $member->avatar;
}
$this->folks[$index]['updated'] = Carbon::now();
//save new folks file
$this->docs::writeSettings($this->folks, env('FOLKS_PATH'));
//update session
session()->put('member', $this->folks[$index]);
}
public function auth($request)
{
//suth stuff
$folks = $this->folks;
$found = $this->getByHandle($request->handle);
if ($found) {
if (password_verify($request->password, $found['password'])) {
$member = [
'id' => $found['id'],
'handle' => $found['handle'],
'email' => $found['email'],
'role' => $found['role'],
'avatar' => $found['avatar'],
'key' => $found['key'],
'secret' => $found['secret'],
];
$token = Token::create(
$found['key'],
$found['secret'],
time() + 3600,
'localhost'
); //expires in an hour
$form_token = md5(uniqid(microtime(), true));
$request->session()->put('member', $member);
$request->session()->put('token', $token);
$request->session()->put('form_token', $form_token);
return ['status' => true, 'message' => 'HEY WELCOME BACK'];
//DO SESSION STUFF
} else {
return ['status' => false, 'message' => 'CHECK THAT PASSWORD'];
//RETURN ERROR
}
} else {
return ['status' => false, 'message' => 'CHECK THAT HANDLE'];
}
}
public static function status()
{
if (session('member') !== null) {
if (
Token::validate(session('token'), session('member')['secret']) &&
Token::validateExpiration(session('token'), session('member')['secret'])
) {
return true;
} else {
return false;
}
} else {
return false;
}
}
}

View file

@ -0,0 +1,184 @@
<?php
namespace App\Repositories;
use App\Interfaces\PageRepositoryInterface;
//Asset Services
use App\Services\Assets\DocService;
use App\Services\Assets\RenderService;
//Data Services
use App\Services\Data\SettingsService;
use App\Services\Data\ContentService;
use App\Services\Data\PaginateService;
use App\Services\Data\SortingService;
use Carbon\Carbon;
class PageRepository implements PageRepositoryInterface
{
protected $content;
protected $setttings;
protected $paginate;
protected $pages;
protected $docs;
protected $sort;
protected $render;
public function __construct(
ContentService $contentService,
SettingsService $settingsService,
PaginateService $paginateService,
DocService $docService,
SortingService $sortingService,
RenderService $renderService
) {
$this->content = $contentService;
$this->settings = $settingsService;
$this->paginate = $paginateService;
$this->docs = $docService;
$this->sort = $sortingService;
$this->render = $renderService;
$this->pages = $this->content->loadAllPages();
}
public function getAll()
{
return $this->pages;
}
public function getById($id)
{
$page = $this->pages->where('id', $id)->first();
//quick check to see if layout is set
$page['layout'] == '' ? $page['layout'] = 'page' : $page['layout'] = $page['layout'];
return $page;
}
public function getByUuid($uuid)
{
$page = $this->pages->where('uuid', $uuid)->first();
//quick check to see if layout is set
$page['layout'] == '' ? $page['layout'] = 'page' : $page['layout'] = $page['layout'];
return $page;
}
public function getBySlug($slug)
{
$page = $this->pages->where('slug', $slug)->first();
//quick check to see if layout is set
$page['layout'] == '' ? $page['layout'] = 'page' : $page['layout'] = $page['layout'];
return $page;
}
public function delete($page)
{
return $this->editPage($page, $this->pages->where('uuid', $page->uuid)->first(), 'delete');
}
public function create($page)
{
return $this->editPage($page, null, 'create');
}
public function update($page)
{
return $this->editPage($page, $this->pages->where('uuid', $page->uuid)->first(), 'update');
}
public function getGroup($num, $limit, $sort = "all")
{
return $this->paginate->getPage($num, $limit, $sort);
}
private function editPage($body, $page, $task)
{
$path;
$file;
$writePath;
$message;
$deleted;
if ($task != 'create') {
$path = date('Y', date($page['rawCreated'])) .
'/' .
date('m', date($page['rawCreated']));
} else {
$path = date('Y') . '/' . date('m');
}
if ($task == 'delete') {
$deleted = 'true';
$body->menu = 'false';
$body->published = 'false';
$body->featured = 'false';
//set body object with prexisting data
$body->id = $page['id'];
$body->layout = $page['layout'];
$body->slug = $page['slug'];
$body->title = $page['title'];
$body->imageList = $page['feature'];
$body->fileList = $page['files'];
$body->tags = $page['tags'];
$body->content = $page['content'];
} else {
$deleted = isset($page['deleted']) ? $page['deleted'] : 'false';
}
$created = $task != 'create' ? new Carbon($page['rawCreated']) : Carbon::now();
$updated = Carbon::now();
// grab current index from settings and update
$id = $task != 'create' ? $body->id : $this->settings->getSettings()['library_stats']['current_index'];
$uuid = $task != 'create' ? $body->uuid : createUUID();
//set variables post body for saving
$body->title = urlencode($body->title);
$body->id = $id;
$body->uuid = $uuid;
$body->path = $path;
$body->author = session('member')['handle'];
$body->created = $created->format("Y-m-d\TH:i:sP");
$body->updated = $updated->format("Y-m-d\TH:i:sP");
$body->deleted = $deleted;
//set path to save file
if ($body->layout == 'index') {
$writePath = '../content/pages/start/index.md';
} else {
$writePath = '../content/pages/' . $path . '/' . $body->slug . '.md';
}
//write file to path
$saved = $this->docs::writePages($task, $path, $writePath, $this->docs::objectToMD($body));
//handle post save result
if ($saved) {
if (
$this->settings->getGlobal()['renderOnSave'] == 'true' &&
$this->settings->getGlobal()['dynamicRender'] == 'false'
) {
$this->render->tags();
$this->render->archive();
$this->render->pages();
$message = 'Filed edited and rendered. NOICE.';
} else {
$message = 'File edited. Nice work';
}
//upadte settings if needed
$body->path = $path;
$this->settings->updateMenu($body);
$this->settings->updateTags($this->sort->tags());
// if new page added, update current index in Settings file
if ($task == 'create') {
$this->settings->updatePageIndex();
}
return [
'message' => $message,
'type' => $task == 'update' ? 'postUpdated' : 'postAdded',
'id' => $uuid,
];
} else {
return $response = [
'message' => "Uh oh. File save problem. Don't panic",
'type' => 'postError',
'id' => $uuid,
];
}
}
}

View file

@ -0,0 +1,113 @@
<?php
namespace App\Services\Assets;
use App\Services\Data\ThemeService;
class AssetService
{
protected $themeTestImagePath;
protected $themeTestCSSPath;
protected $themeTestScriptsPath;
protected $themes;
protected $currentTheme;
public function __construct(ThemeService $themeService)
{
$this->themeImagePath = '../public/assets/images/global/';
$this->themeTestImagePath = '../public/theme/images/global/';
$this->themeCSSPath = '../public/assets/css/theme/';
$this->themeTestCSSPath = '../public/theme/css/theme/';
$this->themeScriptsPath = '../public/assets/scripts/theme/';
$this->themeTestScriptsPath = '../public/theme/scripts/theme/';
$this->themes = $themeService;
$this->currentTheme = $this->themes->getCurrentTheme();
}
public function getCurrentTheme()
{
return $this->currentTheme;
}
public function moveToTheme($live = false)
{
$imagePath = '';
$cssPath = '';
$scriptPath = '';
($live) ? $imagePath = $this->themeImagePath : $imagePath = $this->themeTestImagePath;
($live) ? $cssPath = $this->themeCSSPath : $cssPath = $this->themeTestCSSPath;
($live) ? $scriptPath = $this->themeScriptsPath : $scriptPath = $this->themeTestScriptsPath;
//get current theme
foreach (
new \DirectoryIterator('../content/themes/' . $this->currentTheme . '/assets/images/global/') as $file
) {
if ($file->isDot()) {
continue;
}
//make theme directory if not present
if (!is_dir($imagePath)) {
mkdir($imagePath, 0755, true);
}
if (!is_file($imagePath . $file->getFileName())) {
copy(
'../content/themes/' .
$this->currentTheme .
'/assets/images/global/' .
$file->getFileName(),
$imagePath . $file->getFileName()
);
} else {
//image is already there, so chill
}
}
//clear test theme css and script directories
$styles = glob($cssPath . '*'); // get all file names
foreach ($styles as $file) { // iterate files
if (is_file($file)) {
unlink($file); // delete file
}
}
$scripts = glob($scriptPath . '*'); // get all file names
foreach ($scripts as $file) { // iterate files
if (is_file($file)) {
unlink($file); // delete file
}
}
//copy theme assets to public
$newcss = glob('../content/themes/' . $this->currentTheme . '/assets/css/*');
if (!is_dir($cssPath)) {
mkdir($cssPath, 0755, true);
}
foreach ($newcss as $file) { // iterate files
if (is_file($file)) {
$path = explode('/', $file);
copy($file, $cssPath . $path[6]);
} else {
// if there is a type/font folder, move it
$typePath = explode('/', $file);
$src = '../content/themes/' . $this->currentTheme . '/assets/css/' . $typePath[6];
$dst = $cssPath . $typePath[6];
if (is_dir($dst)) {
delete_directory($dst);
copy_directory($src, $dst);
} else {
copy_directory($src, $dst);
}
//rename($file, $cssPath . $typePath[6]);
}
}
$newjs = glob('../content/themes/' . $this->currentTheme . '/assets/scripts/*');
if (!is_dir($scriptPath)) {
mkdir($scriptPath, 0755, true);
}
foreach ($newjs as $file) { // iterate files
if (is_file($file)) {
$path = explode('/', $file);
copy($file, $scriptPath . $path[6]);
}
}
}
}

View file

@ -0,0 +1,185 @@
<?php
namespace App\Services\Assets;
class DocService
{
public function __construct()
{
}
public static function writePages($task, $path, $fileLocation, $fileContents)
{
try {
if ($task == 'create') {
if (!is_dir('../content/pages/' . $path)) {
//Directory does not exist, so lets create it.
mkdir('../content/pages/' . $path, 0755, true);
}
file_put_contents($fileLocation, $fileContents);
} else {
($new = fopen($fileLocation, 'w')) or die('Unable to open file!');
fwrite($new, $fileContents);
fclose($new);
}
return true;
} catch (Error $error) {
return false;
}
}
public static function updateMenu($body)
{
$settings = self::$settings;
//$menu = $settings["menu"];
$item = [
'title' => $body['title'],
'id' => $body['id'],
'uuid' => $body['uuid'],
'slug' => $body['slug'],
'path' => $body['path'],
];
if ($body['menu'] == 'true') {
if (!find($settings['menu'], ['uuid' => $item['uuid']])) {
array_push($settings['menu'], $item);
}
} else {
if (find($settings['menu'], ['uuid' => $item['uuid']])) {
pull($settings['menu'], $item);
}
}
DocTools::writeSettings('../config/settings.json', $settings);
}
public static function writeSettings($fileContents, $location = null)
{
if (is_null($location)) {
$fileLocation = env('SETTINGS_PATH');
} else {
$fileLocation = $location;
}
$message = [];
try {
if (!is_file($fileLocation)) {
file_put_contents($fileLocation, json_encode($fileContents));
} else {
($new = fopen($fileLocation, 'w')) or die('Unable to open file!');
fwrite($new, json_encode($fileContents));
fclose($new);
}$message = [
'message' => "Settings Synced. You're doing great!",
'type' => 'settingsUpdated',
];
} catch (Error $error) {
$message = [
'message' => "Settings Not Synced. We'll figure it out",
'type' => 'settingsUpdated',
];
}
return $message;
}
public static function writeHTML($location, $html, $path = null)
{
if ($path != null) {
if (!is_dir($path)) {
//Directory does not exist, so lets create it.
mkdir($path, 0755, true);
}
}
if (!is_file($location)) {
file_put_contents($location, $html);
} else {
($new = fopen($location, 'w')) or die('Unable to open file!');
fwrite($new, $html);
fclose($new);
}
}
public static function deleteFolder($path)
{
if (!empty($path) && is_dir($path)) {
$dir = new \RecursiveDirectoryIterator(
$path,
\RecursiveDirectoryIterator::SKIP_DOTS
); //upper dirs are not included,otherwise DISASTER HAPPENS :)
$files = new \RecursiveIteratorIterator(
$dir,
\RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($files as $f) {
if (is_file($f)) {
unlink($f);
} else {
$empty_dirs[] = $f;
}
}
if (!empty($empty_dirs)) {
foreach ($empty_dirs as $eachDir) {
rmdir($eachDir);
}
}
rmdir($path);
}
}
public static function objectToMD($object)
{
$markdown = "---\n" .
'id: ' .
$object->id .
"\n" .
'uuid: ' .
$object->uuid .
"\n" .
'title: ' .
"'" .
$object->title .
"'" .
"\n" .
'feature: ' .
$object->imageList .
"\n" .
'files: ' .
$object->fileList .
"\n" .
'path: ' .
$object->path .
"\n" .
'layout: ' .
$object->layout .
"\n" .
'tags: ' .
$object->tags .
"\n" .
'author: ' .
$object->author .
"\n" .
'created: ' .
$object->created .
"\n" .
'updated: ' .
$object->updated .
"\n" .
'deleted: ' .
$object->deleted .
"\n" .
'slug: ' .
$object->slug .
"\n" .
'menu: ' .
$object->menu .
"\n" .
'published: ' .
$object->published .
"\n" .
'featured: ' .
$object->featured .
"\n---\n" .
$object->content;
return $markdown;
}
}

View file

@ -0,0 +1,82 @@
<?php
namespace App\Services\Assets;
class FileUploadService
{
public function __construct()
{
}
public static function handleFile($request, $fileMode = true)
{
//$options = $request->getParsedBody();
//front end sends one by one for progress tracking, so grab first
if ($fileMode) {
$file = $request->file('upload_files')[0];
} else {
$file = $request;
}
$type = $file->extension();
$public_path = '../public';
$filesPath = '';
$path = date('Y') . '/' . date('m');
$response = [];
switch ($type) {
case 'jpeg':
case 'jpg':
case 'png':
case 'gif':
if ($fileMode) {
if (isset($request["source"])) {
if ($request["source"] == "avatar-upload") {
$filesPath = '/assets/images/user/' . $path . '/';
//Member::updateData('avi', $filesPath . $file->getClientFileName());
} else {
$filesPath = '/assets/images/user/' . $path . '/';
// Settings::updateGlobalData('background', $filesPath . '/' . $file->getClientFileName());
}
} else {
$filesPath = '/assets/images/blog/' . $path . '/';
}
} else {
$filesPath = '/assets/images/blog/' . $path . '/';
}
break;
case 'mkv':
case 'mp4':
$filesPath = '/assets/video/blog/' . $path . '/';
break;
case 'ogg':
case 'mp3':
$filesPath = '/assets/sound/blog/' . $path . '/';
break;
case 'pdf':
case 'txt':
case 'rtf':
$filesPath = '/assets/docs/blog/' . $path . '/';
break;
}
try {
// if does not exist, so lets create it.
if (!is_dir($public_path . $filesPath)) {
mkdir($public_path . $filesPath, 0755, true);
}
$filename = urlencode($file->getClientOriginalName());
$file->move($public_path . $filesPath, $filename);
} catch (RuntimeException $e) {
echo 'ERROR ' . $e->getMessage();
}
$response = [
'message' => "File Uploaded. Great!",
"filePath" => $filesPath . urlencode($file->getClientOriginalName()),
"fileName" => urlencode($file->getClientOriginalName()),
'type' => $type,
];
return $response;
}
}

View file

@ -0,0 +1,132 @@
<?php
namespace App\Services\Assets;
use Illuminate\Support\Facades\File;
use App\Services\Data\SortingService;
use App\Services\Data\SettingsService;
use App\Services\Data\ContentService;
class RenderService
{
private $sort;
private $settings;
private $contents;
private $pageInfo;
private $menu;
private $background;
private $theme;
public function __construct(
SortingService $sortingService,
SettingsService $settingsService,
ContentService $contentService
) {
$this->sort = $sortingService;
$this->settings = $settingsService;
$this->contents = $contentService;
$this->theme = $this->settings->getGlobal()['theme'];
}
public function publishAll()
{
$message = [];
$dynamicRender = $this->settings->getGlobal()['dynamicRender'];
if (isset($dynamicRender) && $dynamicRender === 'true') {
$message = [
'message' => 'Auto Rendering is already enabled!',
'type' => 'RENDER_SUCCESS',
];
} else {
try {
$this->archive();
$this->tags();
$this->pages();
$message = [
'message' => 'Site Rendered. GOOD EFFORT',
'type' => 'RENDER_SUCCESS',
];
} catch (Error $error) {
$message = [
'message' => 'Issue With Rendering. DONT PANIC',
'type' => 'RENDER_ERROR',
];
}
}
return $message;
}
public function archive()
{
$template = $this->theme . '.archive';
$pageData = $this->sort->archive(false);
$location = '../public/archives.html';
File::put(
$location,
view($template)
->with($pageData)
->render()
);
}
public function tags()
{
$data = $this->sort->tags(false);
foreach ($data['tags'] as $item) {
//$template = 'tags.twig';
$template = $this->theme . '.tags';
$pageData = [
'theme' => $this->theme, // for theme kit
'title' => 'Pages Tagged as ' . $item['tag_name'],
'dynamicRender' => $data['dynamicRender'],
'layout' => 'page',
'info' => $data['info'],
'menu' => $data['menu'],
'pages' => $item['pages'],
'media' => $data['media'],
];
$location = '../public/tags/' . $item['slug'] . '.html';
if (!is_dir('../public/tags')) {
mkdir('../public/tags', 0755, true);
}
File::put(
$location,
view($template)
->with($pageData)
->render()
);
}
}
public function pages()
{
$pages = $this->contents->loadAllPages();
foreach ($pages as $page) {
$template;
$page['layout'] == 'index' ?
$template = $this->theme . '.index' :
$template = $this->theme . '.page';
$pageData = $this->sort->page($page, false);
if (str_contains($page['layout'], 'index')) {
$location = '../public/index.html';
} else {
// if page is a menu item, render the page on public root
if ($page['menu'] == 'true') {
$location = '../public/' . $page['slug'] . '.html';
} else {
$dir = '../public/' . $page['path'];
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
$location = '../public/' . $page['path'] . '/' . $page['slug'] . '.html';
}
}
File::put($location, view($template)->with($pageData)->render());
}
}
}

View file

@ -0,0 +1,180 @@
<?php
namespace App\Services\Data;
use League\CommonMark\MarkdownConverter;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\Table\TableExtension;
use League\CommonMark\Extension\Attributes\AttributesExtension;
use League\CommonMark\Extension\FrontMatter\FrontMatterExtension;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\Strikethrough\StrikethroughExtension;
use League\CommonMark\Extension\FrontMatter\Output\RenderedContentWithFrontMatter;
use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig;
use Symfony\Component\HtmlSanitizer\HtmlSanitizer;
class ContentService
{
protected $files = [];
protected $config = [];
public function __construct()
{
$this->loadPages(env('PAGES_PATH'));
}
public function loadPages($folder)
{
$folders = glob("$folder/*", GLOB_ONLYDIR);
foreach ($folders as $folder) {
//$this->files[] = $folder . "/";
$this->loadPages($folder);
}
$files = array_filter(glob("$folder/*md"), 'is_file');
foreach ($files as $file) {
$this->files[] = $file;
}
}
public function loadAllPages()
{
$environment = new Environment($this->config);
$environment->addExtension(new CommonMarkCoreExtension());
// Add the extension
$environment->addExtension(new FrontMatterExtension());
//Add Strikethrough rendering
$environment->addExtension(new StrikethroughExtension());
//add attributes to elements in markdown
$environment->addExtension(new AttributesExtension());
//add table rendering
$environment->addExtension(new TableExtension());
// Instantiate the converter engine and start converting some Markdown!
$converter = new MarkdownConverter($environment);
$contents = [];
foreach ($this->files as $file) {
//get meta and html from file
$result = $converter->convertToHtml(file_get_contents($file));
$meta = [];
if ($result instanceof RenderedContentWithFrontMatter) {
$meta = $result->getFrontMatter();
}
//get raw markdown from file
$frontMatterExtension = new FrontMatterExtension();
$parsed = $frontMatterExtension
->getFrontMatterParser()
->parse(file_get_contents($file));
//never trust the front end. clean it up
$soap = (new HtmlSanitizerConfig())
// Allow "safe" elements and attributes. All scripts will be removed
// as well as other dangerous behaviors like CSS injection
->allowSafeElements()
->allowElement('div', ['class', 'title', 'id', 'style'])
->allowElement('img', ['src', 'alt', 'title', 'class'])
->allowElement('iframe', ['height', 'width', 'title', 'src'])
->allowElement('table')
->allowElement('li')
->allowRelativeMedias()
->allowRelativeLinks();
$laundry = new HtmlSanitizer($soap);
$scrubbed = $laundry->sanitize($result->getContent());
if (isset($meta['feature'])) {
$featureList = explode(',', $meta['feature']);
} else {
$featureList = "";
}
$docs = '';
if (isset($meta['files'])) {
$fileList = explode(',', $meta['files']);
$docs = $meta['files'];
} else {
$fileList = [];
$docs = '';
}
$media = [];
$files = [];
if ($featureList != '') {
foreach ($featureList as $file) {
$item = trim($file);
$ext = pathinfo($item, PATHINFO_EXTENSION);
if ($item != null || $item != '') {
array_push($media, ['file' => $item, 'type' => trim($ext)]);
}
}
}
if ($fileList != "") {
foreach ($fileList as $file) {
$item = trim($file);
$ext = pathinfo($item, PATHINFO_EXTENSION);
if ($item != null || $item != '') {
array_push($files, ['file' => $item, 'type' => trim($ext)]);
}
}
}
//sort attributes into page object
$page = [
'id' => $meta['id'],
'uuid' => $meta['uuid'],
'title' => $meta['title'],
'feature' => $meta['feature'],
'files' => $docs,
'path' => $meta['path'],
'layout' => $meta['layout'],
'tags' => $meta['tags'],
'author' => $meta['author'],
'created' => date('Y M D d', $meta['created']),
'updated' => date('Y M D d', $meta['updated']),
'rawCreated' => $meta['created'],
'rawUpdated' => $meta['updated'],
'createdYear' => date('Y', $meta['created']),
'createdMonth' => date('m', $meta['created']),
'deleted' => $meta['deleted'],
'menu' => $meta['menu'],
'featured' => $meta['featured'],
'published' => $meta['published'],
'slug' => $meta['slug'],
'filePath' => $file,
'content' => $parsed->getContent(),
'html' => $scrubbed,
'media' => $media,
'docs' => $files
];
//checks for duplicates
$uuid = $meta['uuid'];
$found = current(
array_filter($contents, function ($item) use ($uuid) {
return isset($item['uuid']) && $uuid == $item['uuid'];
})
);
// if uuid is not present, add it
if (!$found) {
array_push($contents, $page);
}
}
$collection = collect($contents);
$sorted = $collection->sortBy([
['id', 'desc'],
]);
$sorted->values()->all();
return $sorted;
}
public static function getAll()
{
echo("YES");
}
}

View file

@ -0,0 +1,90 @@
<?php
namespace App\Services\Data;
use function _\filter;
class PaginateService
{
protected $content;
public function __construct(ContentService $contentService)
{
$this->content = $contentService;
}
public function getPage(int $page, int $limit, string $sort = null)
{
$content = $this->content->loadAllPages();
$published = filter($content, function ($item) {
return $item['published'] == true && $item['deleted'] == false;
});
$deleted = filter($content, function ($item) {
return $item['deleted'] == true;
});
// $all = $content;
$all = filter($content, function ($item) {
return $item['deleted'] == false;
});
$filter = isset($sort) ? $sort : 'all';
switch ($filter) {
case 'published':
$filtered = $published;
break;
case 'deleted':
$filtered = $deleted;
break;
default:
$filtered = $all;
break;
}
$numOfPages = ceil(count($filtered) / ($limit + 1));
$folder = [];
if (count($filtered) != 0) {
if (count($filtered) < $limit) {
$limit = count($filtered) - 1;
}
$range = $page * $limit - $limit;
if ($range != 0) {
$range = $range + 1;
}
for ($i = 0; $i <= $limit; ++$i) {
if (isset($filtered[$i + $range])) {
array_push($folder, $filtered[$i + $range]);
} else {
// chill out
}
}
}
$prev = $page - 1;
if ($prev <= 0) {
$prev = $numOfPages;
}
$next = $page + 1;
if ($next > $numOfPages) {
$next = 1;
}
return [
'pages' => $folder,
'numOfPages' => $numOfPages,
'entryCount' => count($filtered),
'paginate' => [
'sort' => $sort,
'nextPage' => $next,
'prevPage' => $prev,
],
'stats' => [
'all' => count($all),
'published' => count($published),
'deleted' => count($deleted),
],
];
}
}

View file

@ -0,0 +1,204 @@
<?php
namespace App\Services\Data;
use Carbon\Carbon;
use App\Services\Assets\DocService;
use function _\find;
use function _\pull;
class SettingsService
{
protected $settings;
protected $tags;
protected $docs;
protected $contents;
public function __construct(DocService $docService, ContentService $contentService)
{
if (file_exists(env('TAGS_PATH'))) {
$this->tags = json_decode(file_get_contents(env('TAGS_PATH')), true);
} else {
$this->tags = [];
}
$this->docs = $docService;
$this->contents = $contentService;
}
protected function loadSettings()
{
$settings = [];
//if config files aren't avaiable, load templates
if (file_exists(env('SETTINGS_PATH'))) {
$settings = json_decode(file_get_contents(env('SETTINGS_PATH')), true);
} else {
$settings = json_decode(
file_get_contents('../content/init/settings-template.json'),
true
);
}
return $settings;
}
public function getSettings()
{
return $this->loadSettings();
}
public function getGlobal()
{
$this->settings = $this->loadSettings();
return $this->settings['global'];
}
public function getEmail()
{
$this->settings = $this->loadSettings();
return $this->settings['email'];
}
public function getMenu()
{
$this->settings = $this->loadSettings();
return $this->settings['menu'];
}
public function getTags()
{
return $this->tags;
}
public function updatePageIndex()
{
$this->settings = $this->loadSettings();
++$this->settings['library_stats']['current_index'];
$this->docs->writeSettings($this->settings);
}
public function updateGlobalData($key, $value)
{
$this->settings = $this->loadSettings();
$this->settings['global'][$key] = $value;
$this->docs->writeSettings($this->settings);
}
public function updateMenu($body)
{
$settings = $this->loadSettings();
//$menu = $settings["menu"];
$item = [
'title' => $body->title,
'id' => $body->id,
'uuid' => $body->uuid,
'slug' => $body->slug,
'path' => $body->path,
];
if ($body->menu == 'true') {
if (!find($settings['menu'], ['uuid' => $item['uuid']])) {
array_push($settings['menu'], $item);
}
} else {
if (find($settings['menu'], ['uuid' => $item['uuid']])) {
pull($settings['menu'], $item);
}
}
$this->docs->writeSettings($settings);
}
public function updateTags($update)
{
$this->docs->writeSettings($update, env('TAGS_PATH'));
}
public function sync($data)
{
$settings = $this->getSettings();
$settings['global']['base_url'] = $data->global->base_url;
$settings['global']['title'] = $data->global->title;
$settings['global']['descriptions'] = $data->global->descriptions;
$settings['global']['private'] = $data->global->private;
$settings['global']['renderOnSave'] = $data->global->renderOnSave;
$settings['global']['theme'] = $data->global->theme;
$settings['global']['externalAPI'] = $data->global->externalAPI;
$settings['global']['dynamicRender'] = $data->global->dynamicRender;
//$settings['email']['active'] = $data->email->active;
//$settings['email']['smtp'] = $data->email->smtp;
//$settings['email']['mailgun'] = $data->email->mailgun;
return $this->docs::writeSettings($settings);
}
public function navSync($data)
{
$settings = $this->loadSettings();
$pages = $this->contents->loadAllPages();
$remove = $data->remove;
$result = [];
//if remove contains id, find nav item page and set menu to false
if ($remove != null || $remove != '') {
$page = $pages->where('uuid', $remove)->first();
$page['menu'] = 'false';
$page['published']
? ($page['published'] = 'true')
: ($page['published'] = 'false');
$page['featured']
? ($page['featured'] = 'true')
: ($page['featured'] = 'false');
$page['deleted']
? ($page['deleted'] = 'true')
: ($page['deleted'] = 'false');
$updated = Carbon::now();
$created = new Carbon($page['rawCreated']);
$page['created'] = $created->format("Y-m-d\TH:i:sP");
$page['updated'] = $updated->format("Y-m-d\TH:i:sP");
if ($page['layout'] == 'index') {
$writePath = '../content/pages/start/index.md';
} else {
$writePath = '../content/pages/' . $page['path'] . '/' . $page['slug'] . '.md';
}
try {
$object = (object) $page;
$object->imageList = $page['feature'];
$object->fileList = $page['files'];
$this->docs::writePages('write', $page['path'], $writePath, $this->docs::objectToMD($object));
} catch (\Exception $e) {
$result = [
'message' => 'Page Was Not Updated. Be cool ',
'type' => 'pageUpdateError',
'error' => $e->getMessage(),
];
return $result;
}
}
$settings['menu'] = [];
$items = $data->menu;
foreach ($items as $item) {
array_push($settings['menu'], [
'title' => $item->title,
'id' => $item->id,
'uuid' => $item->uuid,
'slug' => $item->slug,
'path' => $item->path,
]);
}
try {
$this->docs->writeSettings($settings);
$result = [
'message' => 'Navigation updated. Very slick!',
'type' => 'menuUpdated',
];
} catch (\Exception $e) {
$result = [
'message' => 'Navigation Update Error. It\'ll be ok!',
'type' => 'menuUpdateError',
];
};
return $result;
}
}

View file

@ -0,0 +1,293 @@
<?php
namespace App\Services\Data;
use Carbon\Carbon;
use function _\find;
use function _\filter;
class SortingService
{
private $p_tags = [];
private $p_archive = [];
private $settings;
private $contents;
private $themes;
private $info = [];
public function __construct(
SettingsService $settingsService,
ContentService $contentService,
ThemeService $themeService,
) {
$this->settings = $settingsService;
$this->contents = $contentService;
$this->themes = $themeService;
$global = $this->settings->getGlobal();
$this->info = [
'keywords' => isset($global['keywords'])
? $global['keywords']
: 'fipamo, blog, jamstack, php, markdown, js',
'menu' => $this->settings->getMenu(),
'tags' => $this->settings->getTags(),
'description' => $global['descriptions'],
'image' => $global['base_url'] . $global['background'],
'baseURL' => $global['base_url'],
'dynamicRender' => $global['dynamicRender'],
'theme' => $global['theme'],
];
}
public function tags($debug = true)
{
$pages = $this->contents->loadAllPages();
foreach ($pages as $page) {
$temp = [];
if (isset($page['tags'])) {
$temp = explode(',', $page['tags']);
foreach ($temp as $tag) {
$label = trim($tag);
if (!find($this->p_tags, ['tag_name' => $label])) {
array_push($this->p_tags, [
'tag_name' => $label,
'slug' => safeString($label),
'pages' => $this->tagPages($label, $pages),
]);
}
}
}
}
$tagData = [];
$settingsTags = [];
if (isset($this->settings->getTags()['pages'])) {
$settingsTags = $this->settings->getTags()['pages'];
}
$tagData = [
'debug' => $debug, // for theme kit
'layout' => 'page',
'tags' => $this->p_tags,
'theme' => $this->info['theme'], // for theme kit
'title' => 'Pages Tagged as Tag',
'dynamicRender' => $this->info['dynamicRender'],
'pages' => $settingsTags,
'info' => $this->info,
'menu' => $this->settings->getMenu(),
'media' => [
['file' => $this->info['image'],
'type' => trim(pathinfo($this->info['image'], PATHINFO_EXTENSION))]
]
];
return $tagData;
}
private function tagPages($tag, $pages)
{
$tagged = [];
foreach ($pages as $page) {
if (isset($page['tags'])) {
if (strpos($page['tags'], $tag) !== false) {
array_push($tagged, [
'title' => urldecode($page['title']),
'slug' => $page['slug'],
'path' => $page['path'],
'feature' => $page['feature'],
]);
}
}
}
return $tagged;
}
public function archive($debug = true)
{
$pages = $this->contents->loadAllPages();
$years = [];
$archive = [];
foreach ($pages as $page) {
$date = explode('/', $page['path']);
if (!find($years, ['year' => trim($date[0])])) {
$findPages = filter($pages, ['createdYear' => trim($date[0])]);
array_push(
$years,
[
'year' => trim($date[0]),
'count' => count($findPages),
]
);
}
}
foreach ($years as $year) {
$sorted = [];
$filtered = filter($pages, ['createdYear' => $year['year']]);
foreach ($filtered as $obj) {
$month = date('m', date($obj['rawCreated']));
if (!find($sorted, ['month' => $month])) {
$perMonth = filter(
$pages,
[
'path' => $year['year'] . '/' . $month,
'deleted' => false,
'published' => true,
'layout' => 'page',
]
);
array_push($sorted, [
'month' => $month,
'full_month' => date('F', date($obj['rawCreated'])),
'count' => count($perMonth),
'pages' => $perMonth,
]);
}
}
array_push($this->p_archive, [
'year' => $year['year'],
'year_data' => $sorted,
]);
}
$archive_data = [];
$archiveData = [
'debug' => $debug, // for theme kit
'theme' => $this->info['theme'], // for theme kit
'layout' => 'page',
'title' => 'Archives',
'dynamicRender' => $this->info['dynamicRender'],
'archive' => $this->p_archive,
'info' => $this->info,
'menu' => $this->settings->getMenu(),
'media' => [
['file' => $this->info['image'],
'type' => trim(pathinfo($this->info['image'], PATHINFO_EXTENSION))]
],
];
return $archiveData;
}
public function page($page, $debug = true)
{
$pageOptions = [];
$tags = [];
if (isset($page['tags'])) {
$taglist = explode(',', $page['tags']);
foreach ($taglist as $tag) {
$label = trim($tag);
array_push($tags, [
'label' => $label . ' ',
'slug' => safeString($label),
]);
}
}
$meta = [
'who' => $page['author'],
'when' => $page['created'],
'tags' => $tags,
];
// if page feature isn't empty, find image from list and set it as background image
// if it is empty, just use global background
if ($page['feature'] != '' || $page['feature'] != null) {
$media = explode(',', $page['feature']);
$set = false;
foreach ($media as $file) {
$item = trim($file);
$ext = pathinfo($item, PATHINFO_EXTENSION);
if ($ext != 'mp4' && !$set) {
$this->info['image'] = $this->info['baseURL'] . $item;
$set = true;
}
}
}
$recent = [];
$featured = [];
$limit = 4;
$pages = $this->contents->loadAllPages();
foreach ($pages as $item) {
if (
!$item['deleted'] &&
$item['published'] &&
$item['menu'] != 'true'
) {
if (count($recent) < $limit) {
array_push($recent, [
'path' => $item['path'],
'slug' => $item['slug'],
'title' => urldecode($item['title']),
'feature' => $item['feature'],
]);
}
if ($item['featured'] == true) {
if (count($featured) < $limit) {
array_push($featured, [
'path' => $item['path'],
'slug' => $item['slug'],
'title' => urldecode($item['title']),
'feature' => $item['feature'],
]);
}
}
}
}
$pageData = [
"debug" => $debug,
"theme" => $this->info['theme'],
"status" => session('member') != null ? true : false,
"title" => urldecode($page['title']),
"meta" => $meta,
"menu" => $this->settings->getMenu(),
"info" => $this->info,
"media" => $page['media'],
"files" => $page['docs'],
"content" => $page['html'],
"layout" => $page['layout'],
"recent" => $recent,
"feature" => $featured,
"tags" => $meta['tags'],
"dynamicRender" => $this->info['dynamicRender'],
];
return $pageData;
}
public function navigation()
{
$pageOptions = [
'title' => 'Edit Navigation',
'status' => session('member') != '' ? true : false,
'menu' => $this->settings->getMenu(),
];
return $pageOptions;
}
public function settings()
{
$global = $this->settings->getGlobal();
$updatedContent = new Carbon($global['last_content_backup']);
$updatedFiles = new Carbon($global['last_files_backup']);
$status = session('member') != '' ? true : false;
$pageOptions = [
'title' => 'Settings',
'private' => $global['private'],
'renderOnSave' => $global['renderOnSave'],
'background' => $global['background'],
'member' => session('member'),
'ftoken' => session('form_token'),
'siteTitle' => $global['title'],
'baseUrl' => $global['base_url'],
'desc' => $global['descriptions'],
'lastContentBackup' => $updatedContent->format('Y F d H i s'),
'lastFilesBackup' => $updatedFiles->format('Y F d H i s'),
'currentTheme' => $global['theme'],
'themes' => $this->themes->getThemes(),
'apiStatus' => isset($global['externalAPI']) ? $global['externalAPI'] : 'false',
'dynamicRenderStatus' => isset($global['dynamicRender']) ? $global['dynamicRender'] : 'false',
'status' => $status,
];
return $pageOptions;
}
}

View file

@ -0,0 +1,78 @@
<?php
namespace App\Services\Data;
class ThemeService
{
protected $themes = [];
protected $settings;
public function __construct(SettingsService $settingsService)
{
$this->settings = $settingsService;
$_themes = glob(env('THEMES_PATH') . '/*', GLOB_ONLYDIR);
foreach ($_themes as $theme) {
array_push(
$this->themes,
json_decode(file_get_contents($theme . '/theme.json'), true)
);
}
}
public function getCurrentTheme()
{
return $this->settings->getGlobal()['theme'];
}
public function getThemes()
{
return $this->themes;
}
public function getCustomViews($layout)
{
$views = [];
if (str_contains($layout, 'index')) {
$views = $this->findCustomIndex();
} else {
$views = $this->findCustomViews();
}
return $views;
}
public function findCustomIndex()
{
$currentTheme = $this->settings->getGlobal()['theme'];
$folder = '../content/themes/' . $currentTheme;
$files = array_filter(glob("$folder/*blade.php"), 'is_file');
$views = [];
foreach ($files as $file) {
$path = explode('/', $file);
$fileName = $path[4];
if (str_contains($fileName, 'index')) {
$page = explode('.', $fileName);
$views[] = $page[0];
}
}
return $views;
}
public function findCustomViews()
{
$currentTheme = $this->settings->getGlobal()['theme'];
$folder = '../content/themes/' . $currentTheme;
$files = array_filter(glob("$folder/*blade.php"), 'is_file');
$views = [];
foreach ($files as $file) {
$path = explode('/', $file);
$fileName = $path[4];
if (str_contains($fileName, 'page')) {
$page = explode('.', $fileName);
$views[] = $page[0];
}
}
return $views;
}
}

View file

@ -0,0 +1,286 @@
<?php
namespace App\Services\Upkeep;
use ReallySimpleJWT\Token;
use ReallySimpleJWT\Exception\EncodeException;
use App\Services\Assets\DocService;
use Carbon\Carbon;
use function _\find;
class InitService
{
protected $docs;
protected $filePaths = [];
public function __construct(DocService $docService)
{
$this->docs = $docService;
$this->filePaths = ['../public/assets/images/blog',
'../public/assets/images/user', '../public/assets/video/blog',
'../public/assets/docs/blog', '../public/assets/sound/blog'];
}
private static function validSecret($length)
{
$alphanum = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
$special = '*&!@%^#$';
$alphabet = $alphanum . $special;
$random = openssl_random_pseudo_bytes($length);
$alphabet_length = strlen($alphabet);
$string = '';
for ($i = 0; $i < $length; ++$i) {
$string .= $alphabet[ord($random[$i]) % $alphabet_length];
}
//secret needs to be a valid token
if ($length == 12) {
try {
$secret = Token::create(12, $string, time() + 3600, 'localhost');
return $string;
} catch (EncodeException $e) {
//bad secret, so try agiain
return self::validSecret(12);
}
if (Token::validate($key, $string)) {
return $string;
} else {
return self::validSecret(12);
}
}
}
public function fresh($body)
{
//grab template files
//TODO: Remove hardcoded link and set up init path in settings
$newFolks = json_decode(
file_get_contents(env('FIPAMO_INIT') . '/folks-template.json'),
true
);
$newSettings = json_decode(
file_get_contents(env('FIPAMO_INIT') . '/settings-template.json'),
true
);
//get form values
//$body = $request->getParsedBody();
$handle = $body->new_member_handle;
$email = $body->new_member_email;
$pass = $body->new_member_pass;
$title = $body->new_member_title;
$now = Carbon::now();
//setup folks config
$hash = password_hash($pass, PASSWORD_DEFAULT);
$newFolks[0]['id'] = 0;
$newFolks[0]['handle'] = $handle;
$newFolks[0]['email'] = $email;
$newFolks[0]['password'] = $hash;
$newFolks[0]['key'] = password_hash($email, PASSWORD_DEFAULT);
$newFolks[0]['secret'] = self::validSecret(12);
$newFolks[0]['role'] = 'hnic';
$newFolks[0]['created'] = $now->format("Y-m-d\TH:i:sP");
$newFolks[0]['updated'] = $now->format("Y-m-d\TH:i:sP");
//set up settings config
$newSettings['global']['title'] = $title;
//create index file
//TODO: upate path attribute to use env variable
$index = [
'id' => 0,
'uuid' => createUUID(),
'title' => 'FIRST!',
'imageList' => '/assets/images/global/default-bg.jpg',
'fileList' => '',
'path' => 'content/pages/start',
'layout' => 'index',
'tags' => 'start, welcome',
'author' => $handle,
'created' => $now->format("Y-m-d\TH:i:sP"),
'updated' => $now->format("Y-m-d\TH:i:sP"),
'deleted' => 'false',
'slug' => 'first',
'menu' => 'false',
'featured' => 'false',
'published' => 'true',
'content' => "# F**k Yes \n\nIf you're seeing this, you're up and running. NICE WORK!\n\nFrom here, feel free to start dropping pages to your heart's content.\n\nFor some tips about using Fipamo, check out the ![docs](https://code.playvicio.us/Are0h/Fipamo/wiki/02-Usage)\n\nAll good? Feel free to edit this page to whatever you want!\n\nYOU'RE THE CAPTAIN NOW.",
];
//once all files created, write down
mkdir(env('FIPAMO_CONFIG'), 0755, true);
$this->docs->writeSettings($newSettings, env('FIPAMO_CONFIG') . '/settings.json');
$this->docs->writeSettings($newFolks, env('FIPAMO_CONFIG') . '/folks.json');
$this->docs->writeSettings([], env('FIPAMO_CONFIG') . '/tags.json');
$object = (object) $index;
$this->docs->writePages(
'create',
'start',
env('PAGES_PATH') . '/start/index.md',
$this->docs::objectToMD($object)
);
$result = ['type' => 'blogInitGood', 'message' => 'Site Created'];
return $result;
}
public function restore($request)
{
//content required, so check it
$result = [];
$contentArchive = $request->file('backup-content-upload');
$fileArchive = $request->file('backup-files-upload');
if ($contentArchive == null || $contentArchive == '') {
return $result = [
'type' => 'requestLame',
'message' => 'Content Archive EMPTY',
];
}
$result = $this->restoreContent($contentArchive, $request);
if ($result['type'] == 'requestLame') {
return $result;
}
//file upload is optional, so if it's present, restore it
if ($fileArchive != null || $fileArchive != '') {
$result = $this->restoreFiles($fileArchive);
}
return $result;
}
private function restoreContent($contentUpload, $request)
{
$contentUpload->move(env('FIPAMO_DIR') . '/', $contentUpload->getClientOriginalName());
$contentZip = new \ZipArchive();
$result = [];
$tempDir = env('FIPAMO_DIR') . '/_temp';
if ($contentZip->open(env('FIPAMO_DIR') . '/' . $contentUpload->getClientOriginalName()) === true) {
$folks = json_decode($contentZip->getFromName('config/folks.json'), true);
$found = find($folks, ['handle' => $request->restore_member_handle]);
if ($found) {
if (password_verify($request->restore_member_pass, $found['password'])) {
//restore assets from previous site
if ($request->restore_former_url != '' || $request->restore_former_url != null) {
$this->moveAssets($contentZip, $request->restore_former_url);
}
$newFolks = [];
if (!isset($found['secret'])) {
$found['secret'] = self::validSecret(12);
}
array_push($newFolks, $found);
//make temp folder and dump file in there
mkdir($tempDir, 0755, true);
$contentZip->extractTo($tempDir);
//load up old config file
$newConfig = json_decode(
file_get_contents($tempDir . '/config/settings.json'),
true
);
//check for key, add if not there
if (!isset($newConfig['global']['externalAPI'])) {
$newConfig['global']['externalAPI'] = 'false';
}
//make dir and write new config files
if (!is_dir(env('FIPAMO_CONFIG'))) {
mkdir(env('FIPAMO_CONFIG'), 0755, true);
}
$this->docs->writeSettings($newConfig, env('FIPAMO_CONFIG') . '/settings.json');
$this->docs->writeSettings($newFolks, env('FIPAMO_CONFIG') . '/folks.json');
rename($tempDir . '/config/tags.json', env('FIPAMO_CONFIG') . '/tags.json');
//move saved markdown pages
rename($tempDir . '/content/pages/', env('PAGES_PATH'));
//clean up temp dir and zip file
$this->docs::deleteFolder($tempDir);
$contentZip->close();
unlink(env('FIPAMO_DIR') . '/' . $contentUpload->getClientOriginalName());
$result = [
'type' => 'requestGood',
'message' => 'Content Restored! Redirecting',
];
} else {
$result = [
'type' => 'requestLame',
'message' => 'Check that password, champ.',
];
}
} else {
$result = [
'type' => 'requestLame',
'message' => 'Uh Oh. Check that handle',
];
}
};
return $result;
}
private function restoreFiles($filesUpload)
{
$filesUpload->move(env('FIPAMO_DIR') . '/', $filesUpload->getClientOriginalName());
$filesZip = new \ZipArchive();
$tempDir = env('FIPAMO_DIR') . '/_file_temp';
$result = [];
if ($filesZip->open(env('FIPAMO_DIR') . '/' . $filesUpload->getClientOriginalName()) === true) {
$filesZip->extractTo($tempDir);
//clear and move dir if present
foreach ($this->filePaths as $path) {
delete_directory($path, false);
//non image directories don't exist, so they need to be created
$pathing = explode("/", $path);
if ($pathing[3] != 'images') {
if (!is_dir('../public/assets/' . $pathing[3])) {
mkdir('../public/assets/' . $pathing[3]);
}
}
$tempPath = $tempDir . '/' . substr($path, 3);
if (is_dir($tempPath)) {
rename($tempPath, $path);
}
}
$result = [
'type' => 'requestGood',
'message' => 'Files & Content Restored! Redirecting',
];
}
delete_directory($tempDir);
$filesZip->close();
unlink(env('FIPAMO_DIR') . '/' . $filesUpload->getClientOriginalName());
return $result;
}
private function moveAssets($zip, $url)
{
$assetFail = 0;
$assetList = [];
array_push($assetList, json_decode($zip->getFromName('assets/blog_images.json'), true));
array_push($assetList, json_decode($zip->getFromName('assets/user_images.json'), true));
array_push($assetList, json_decode($zip->getFromName('assets/blog_docs.json'), true));
array_push($assetList, json_decode($zip->getFromName('assets/blog_videos.json'), true));
foreach ($assetList as $list) {
foreach ($list as $asset) {
$path = explode('/', $asset['path']);
$type = $path[3];
$section = $path[4];
$year = $path[5];
$month = $path[6];
$blogDir = '../public/assets/' . $type . '/' . $section . '/' . $year . '/' . $month;
if (!is_dir($blogDir)) {
mkdir($blogDir, 0755, true);
}
$externalPath = '/assets/' . $type . '/' . $section . '/' . $year . '/' . $month;
$asset_url = $url . $externalPath . '/' . $asset['file'];
try {
file_put_contents(
$asset['path'] . '/' . $asset['file'],
file_get_contents($asset_url)
);
} catch (\Throwable $e) {
$assetFail++;
}
}
}
}
}

View file

@ -0,0 +1,162 @@
<?php
namespace App\Services\Upkeep;
use App\Services\Data\SettingsService;
use Carbon\Carbon;
class MaintenanceService
{
protected $settings;
protected $filePaths = [];
public function __construct(SettingsService $settingsService)
{
$this->settings = $settingsService;
$this->filePaths = ['../public/assets/images/blog',
'../public/assets/images/user', '../public/assets/docs/blog',
'../public/assets/video/blog', '../public/assets/sound/blog'];
}
public function createContentBackUp()
{
//make sure back directory is there
$stamp = Carbon::now()->format("YmdHis");
if (!is_dir(env('FIPAMO_BACKUPS'))) {
mkdir(env('FIPAMO_BACKUPS'), 0755, true);
}
//creat backup zip
$zip = new \ZipArchive();
$zip->open(
env('FIPAMO_BACKUPS') . '/backup-content-' . $stamp . '.zip',
\ZipArchive::CREATE | \ZipArchive::OVERWRITE
);
//gather data and path info for md pages
$pagePath = env('PAGES_PATH');
$yearPaths = glob($pagePath . '/*', GLOB_ONLYDIR);
foreach ($yearPaths as $years) {
$year = explode('/', $years);
//grap the index and save it
if (trim($year[3]) == 'start') {
$options = [
'add_path' => 'content/pages/' . $year[3] . '/',
'remove_all_path' => true,
];
$zip->addGlob($years . '/*.md', GLOB_BRACE, $options);
}
$monthsPath = glob($pagePath . '/' . $year[3] . '/*', GLOB_ONLYDIR);
foreach ($monthsPath as $months) {
$month = explode('/', $months);
//once info is collected, add md pages to zip
$options = [
'add_path' => 'content/pages/' . $year[3] . '/' . $month[4] . '/',
'remove_all_path' => true,
];
$zip->addGlob($months . '/*.md', GLOB_BRACE, $options);
}
}
//gather paths for blog images
$blogImages = [];
$dir = new \RecursiveDirectoryIterator('../public/assets/images/blog');
$flat = new \RecursiveIteratorIterator($dir);
$files = new \RegexIterator($flat, '/\.png|jpg|gif$/i');
foreach ($files as $file) {
$blogImages[] = ['path' => $file->getPath(), 'file' => $file->getFilename()];
};
//gather paths for user images
$userImages = [];
if (is_dir('../public/assets/images/user')) {
$dir = new \RecursiveDirectoryIterator('../public/assets/images/user');
$flat = new \RecursiveIteratorIterator($dir);
$files = new \RegexIterator($flat, '/\.png|jpg|gif$/i');
foreach ($files as $file) {
$userImages[] = ['path' => $file->getPath(), 'file' => $file->getFilename()];
};
}
//gather paths for blog documents
$blogDocs = [];
if (is_dir('../public/assets/docs/blog')) {
$dir = new \RecursiveDirectoryIterator('../public/assets/docs/blog');
$flat = new \RecursiveIteratorIterator($dir);
$files = new \RegexIterator($flat, '/\.txt|pdf|rtf$/i');
foreach ($files as $file) {
$blogDocs[] = ['path' => $file->getPath(), 'file' => $file->getFilename()];
};
}
//gather paths for blog videos
$blogVids = [];
if (is_dir('../public/assets/video/blog')) {
$dir = new \RecursiveDirectoryIterator('../public/assets/video/blog');
$flat = new \RecursiveIteratorIterator($dir);
$files = new \RegexIterator($flat, '/\.mp4$/i');
foreach ($files as $file) {
$blogVids[] = ['path' => $file->getPath(), 'file' => $file->getFilename()];
};
}
//add directory for settings and save them
$zip->addEmptyDir('config');
$zip->addEmptyDir('assets');
$zip->addFile(env('SETTINGS_PATH'), 'config/settings.json');
$zip->addFile(env('FOLKS_PATH'), 'config/folks.json');
$zip->addFile(env('TAGS_PATH'), 'config/tags.json');
// create temp files for image lists
file_put_contents(env('FIPAMO_BACKUPS') . '/blog_images_temp.json', json_encode($blogImages));
file_put_contents(env('FIPAMO_BACKUPS') . '/user_images_temp.json', json_encode($userImages));
file_put_contents(env('FIPAMO_BACKUPS') . '/blog_docs_temp.json', json_encode($blogDocs));
file_put_contents(env('FIPAMO_BACKUPS') . '/blog_vids_temp.json', json_encode($blogVids));
//add to zip
$zip->addFile(env('FIPAMO_BACKUPS') . '/blog_images_temp.json', 'assets/blog_images.json');
$zip->addFile(env('FIPAMO_BACKUPS') . '/user_images_temp.json', 'assets/user_images.json');
$zip->addFile(env('FIPAMO_BACKUPS') . '/blog_docs_temp.json', 'assets/blog_docs.json');
$zip->addFile(env('FIPAMO_BACKUPS') . '/blog_vids_temp.json', 'assets/blog_videos.json');
//save zip file
$zip->close();
//clean up temp files
unlink(env('FIPAMO_BACKUPS') . '/blog_images_temp.json');
unlink(env('FIPAMO_BACKUPS') . '/user_images_temp.json');
unlink(env('FIPAMO_BACKUPS') . '/blog_docs_temp.json');
unlink(env('FIPAMO_BACKUPS') . '/blog_vids_temp.json');
//update settings file with latest back up date
$this->settings->updateGlobalData('last_content_backup', $stamp);
return ['message' => "Content backup created. THIS IS A SAFE SPACE!"];
}
public function createFileBackUp()
{
$stamp = Carbon::now()->format("YmdHis");
$zip = new \ZipArchive();
$zip->open(
env('FIPAMO_BACKUPS') . '/backup-files-' . $stamp . '.zip',
\ZipArchive::CREATE | \ZipArchive::OVERWRITE
);
//gather data and path info for blog images
foreach ($this->filePaths as $path) {
$yearPaths = glob($path . '/*', GLOB_ONLYDIR);
foreach ($yearPaths as $years) {
$year = explode('/', $years);
$monthsPath = glob($path . '/' . $year[5] . '/*', GLOB_ONLYDIR);
foreach ($monthsPath as $months) {
$month = explode('/', $months);
//once info is collected, add images pages to zip
$options = [
'add_path' => substr($path, 3) . '/' . $year[5] . '/' . $month[6] . '/',
'remove_all_path' => true,
];
$zip->addGlob($months . '/*.*', GLOB_BRACE, $options);
}
}
}
$zip->close();
$this->settings->updateGlobalData('last_files_backup', $stamp);
return ['message' => "Files are backed up. Breath Easy!"];
}
}

View file

@ -0,0 +1,71 @@
<?php
namespace App\Services\Upkeep;
class ResetService
{
private $protectedItems = ['assets', '.htaccess', 'index.php', 'robots.txt'];
public function __construct()
{
}
public function site()
{
$response = [];
try {
$this->clearPublicAssets();
$this->clearPublicRoot();
$this->clearContent();
session()->flush();
$response = [
'message' => "PUBLIC CLEARED",
'type' => "COOL",
];
} catch (\Throwable $e) {
$response = [
'message' => "RESET NOT COMPLETED",
'error' => $e,
'type' => "ERROR",
];
}
return $response;
}
private function clearContent()
{
$contentDir = env('FIPAMO_DIR');
delete_directory($contentDir . '/pages', false);
delete_directory($contentDir . '/config', false);
}
private function clearPublicAssets()
{
delete_directory('../public/assets/docs', false);
delete_directory('../public/assets/video', false);
delete_directory('../public/assets/sound', false);
delete_directory('../public/assets/css/theme', false);
delete_directory('../public/assets/scripts/theme', false);
delete_directory('../public/assets/images/blog', false);
delete_directory('../public/assets/images/user', false);
}
private function clearPublicRoot()
{
$publicItems = glob('../public/*');
$response = [];
foreach ($publicItems as $path) {
$item = explode('/', $path);
if (in_array($item[2], $this->protectedItems)) {
//protected item do nothing
} else {
if (is_file($path)) {
unlink($path);
} else {
delete_directory($path);
}
}
}
}
}

53
artisan Executable file
View file

@ -0,0 +1,53 @@
#!/usr/bin/env php
<?php
define('LARAVEL_START', microtime(true));
/*
|--------------------------------------------------------------------------
| Register The Auto Loader
|--------------------------------------------------------------------------
|
| Composer provides a convenient, automatically generated class loader
| for our application. We just need to utilize it! We'll require it
| into the script here so that we do not have to worry about the
| loading of any of our classes manually. It's great to relax.
|
*/
require __DIR__.'/vendor/autoload.php';
$app = require_once __DIR__.'/bootstrap/app.php';
/*
|--------------------------------------------------------------------------
| Run The Artisan Application
|--------------------------------------------------------------------------
|
| When we run the console application, the current CLI command will be
| executed in this console and the response sent back to a terminal
| or another output device for the developers. Here goes nothing!
|
*/
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
$status = $kernel->handle(
$input = new Symfony\Component\Console\Input\ArgvInput,
new Symfony\Component\Console\Output\ConsoleOutput
);
/*
|--------------------------------------------------------------------------
| Shutdown The Application
|--------------------------------------------------------------------------
|
| Once Artisan has finished running, we will fire off the shutdown events
| so that any final work may be done by the application before we shut
| down the process. This is the last thing to happen to the request.
|
*/
$kernel->terminate($input, $status);
exit($status);

55
bootstrap/app.php Normal file
View file

@ -0,0 +1,55 @@
<?php
/*
|--------------------------------------------------------------------------
| Create The Application
|--------------------------------------------------------------------------
|
| The first thing we will do is create a new Laravel application instance
| which serves as the "glue" for all the components of Laravel, and is
| the IoC container for the system binding all of the various parts.
|
*/
$app = new Illuminate\Foundation\Application(
$_ENV['APP_BASE_PATH'] ?? dirname(__DIR__)
);
/*
|--------------------------------------------------------------------------
| Bind Important Interfaces
|--------------------------------------------------------------------------
|
| Next, we need to bind some important interfaces into the container so
| we will be able to resolve them when needed. The kernels serve the
| incoming requests to this application from both the web and CLI.
|
*/
$app->singleton(
Illuminate\Contracts\Http\Kernel::class,
App\Http\Kernel::class
);
$app->singleton(
Illuminate\Contracts\Console\Kernel::class,
App\Console\Kernel::class
);
$app->singleton(
Illuminate\Contracts\Debug\ExceptionHandler::class,
App\Exceptions\Handler::class
);
/*
|--------------------------------------------------------------------------
| Return The Application
|--------------------------------------------------------------------------
|
| This script returns the application instance. The instance is given to
| the calling script so we can separate the building of the instances
| from the actual running of the application and sending responses.
|
*/
return $app;

2
bootstrap/cache/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*
!.gitignore

View file

@ -1,235 +0,0 @@
import * as DataEvent from '../../../src/com/events/DataEvent';
import mdparser from 'markdown-yaml-metadata-parser';
const uuidv4 = require('uuid/v4');
const express = require('express');
const router = express.Router();
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const fs = require('fs-extra');
const _ = require('lodash');
const crypto = require('crypto'); // for setting up new accounts
const secret_key = '58d5aeec3c604e2837aef70bc1606f35131ab8fea9731925558f5acfaa00da60';
const moment = require('moment');
/**
* Get Auth Status
*/
router.get('/', function (req, res) {
var token = req.headers['x-access-token'];
if (!token) return res.status(401).send({ auth: false, message: 'No token provided.' });
jwt.verify(token, 'super-secret-string', function (err, decoded) {
if (err)
return res.status(500).send({ auth: false, message: 'Failed to authenticate token.' });
res.status(200).send(decoded);
});
});
/**
* Get Auth Status
*/
router.get('/status', function (req, res) {
if (req.session.user) {
let session = req.session;
res.json({
type: DataEvent.API_REQUEST_GOOD,
message: 'Auth is Good',
token: session.hashToken
});
} else {
res.json({
type: DataEvent.API_REQUEST_LAME,
message: 'NOT AUTHORIZED'
});
}
});
/**
* Login Member and return token
*/
router.post('/login', function (req, res) {
fs.readJson('site/folks.json').then(folks => {
let found = _.find(folks, { handle: req.body.handle });
if (found) {
if (!isValidPassword(found, req.body.password)) {
res.json({
type: DataEvent.REQUEST_LAME,
message: 'CHECK YOUR PASSWORD'
});
}
let token = jwt.sign({ id: found.id }, found.key, {
expiresIn: 86400 // expires in 24 hours
});
let session = req.session;
session.user = found;
session.token = token;
session.hashToken = hashToken(token);
res.json({
type: DataEvent.REQUEST_GOOD,
message: 'Welcome Back',
token: session.hashToken
});
} else {
res.json({
type: DataEvent.REQUEST_LAME,
message: 'Need to see some id, champ.'
});
}
});
});
/**
* Initial Site Setup
*/
router.post('/init', function (req, res) {
let body = req.body;
let re = /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/;
// check email
if (!re.test(body.new_member_email)) {
res.json({
type: DataEvent.API_INIT_LAME,
message: 'Need a valid email address'
});
}
//check handle is being passed
if (body.new_member_handle === null || body.new_member_handle === '') {
res.json({
type: DataEvent.API_INIT_LAME,
message: 'No handle. Kinda need that.'
});
}
// check password match
if (
body.new_member_pass !== body.new_member_pass2 ||
body.new_member_pass === '' ||
body.new_member_pass2 === ''
) {
res.json({
type: DataEvent.API_INIT_LAME,
message: 'Passwords do not match.'
});
}
if (body.new_member_title === null || body.new_member_title === '') {
res.json({
type: DataEvent.API_INIT_LAME,
message: 'No title. Gotta call it something.'
});
}
let key = crypto
.createHash('sha256')
.update(body.new_member_pass + secret_key)
.digest('hex');
// set up config files
fs.readJson('site/init/settings-template.json').then(fresh => {
fresh.global.title = body.new_member_title;
fs.writeJSON('site/settings.json', fresh);
});
fs.readJson('site/init/folks-template.json').then(folks => {
folks[0].id = 1;
folks[0].handle = body.new_member_handle;
folks[0].email = body.new_member_email;
folks[0].password = bcrypt.hashSync(body.new_member_pass, bcrypt.genSaltSync(10), null);
folks[0].key = key;
folks[0].role = 'hnic';
folks[0].created = moment(Date.now()).format();
folks[0].updated = moment(Date.now()).format();
fs.writeJSON('site/folks.json', folks);
});
fs.writeJson('site/tags.json', { tags: [] });
//set up index file as first page
fs.readFile('site/init/index-template.md', { encoding: 'utf8' }).then(file => {
let index = mdparser(file);
let data = index.metadata;
data.uuid = uuidv4();
data.path = moment().format('YYYY') + '/' + moment().format('MM');
data.author = body.new_member_handle;
data.created = moment(Date.now()).format();
data.updated = moment(Date.now()).format();
var init =
'---\n' +
'id: ' +
data.id +
'\n' +
'uuid: ' +
data.uuid +
'\n' +
'title: ' +
data.title +
'\n' +
'feature: ' +
data.feature +
'\n' +
'path: ' +
moment(Date.now()).format('YYYY') +
'/' +
moment(Date.now()).format('MM') +
'\n' +
'layout: ' +
'index' +
'\n' +
'tags: ' +
data.tags +
'\n' +
'author: ' +
body.new_member_handle +
'\n' +
'created: ' +
moment(Date.now()).format() +
'\n' +
'updated: ' +
moment(Date.now()).format() +
'\n' +
'deleted: ' +
'false' +
'\n' +
'menu: ' +
data.menu +
'\n' +
'featured: ' +
data.featured +
'\n' +
'published: ' +
data.published +
'\n' +
'slug: ' +
data.slug +
'\n' +
'---\n' +
index.content;
fs.ensureDir('content/pages/').then(() => {
fs.writeFile('content/pages/index.md', init)
.then(() => {
//console.log('index file created');
})
.catch(() => {
//console.log('ERROR', err);
});
});
});
res.json({
type: DataEvent.API_INIT_GOOD,
message: 'All Set Up'
});
});
//router.post('/logout', function(req, res) {});
module.exports = router;
function isValidPassword(user, password) {
return bcrypt.compareSync(password, user.password);
}
function hashToken(token) {
return bcrypt.hashSync(token, bcrypt.genSaltSync(10), null);
}

View file

@ -1,107 +0,0 @@
import * as DataEvent from '../../../src/com/events/DataEvent';
import Auth from '../../data/Auth';
import Utils from '../../data/Utils';
const express = require('express');
const router = express.Router();
const multer = require('multer');
const auth = new Auth();
const utils = new Utils();
var backup_upload = multer().array('backup_upload');
var backup_restore = multer().any();
/***
CREATE BACK UP
*/
router.post('/create', (req, res) => {
auth.authCheck(req)
.then(() => {
utils
.createBackup()
.then(() => {
res.json({
type: DataEvent.API_BACKUP_CREATE,
message: "You're backed up. Hi fives"
});
})
.catch(err => {
res.json({
type: err.type,
message: err.message
});
});
})
.catch(err => {
res.json({
type: err.type,
message: err.message
});
});
});
/***
RETRIEVE BACKUP
*/
router.get('/download', (req, res) => {
if (req.session.user) {
var filePath = 'content/backup.zip'; // Or format the path using the `id` rest param
var fileName = 'backup.zip'; // The default name the browser will use
res.download(filePath, fileName);
} else {
res.json({
type: DataEvent.REQUEST_LAME,
message: "You're not logged in, champ"
});
}
//Move to route?
});
/***
RESTORE BACKUP
*/
router.post('/restore', backup_upload, (req, res) => {
auth.authCheck(req)
.then(() => {
utils
.restoreBackup(req.files[0])
.then(() => {
res.json({
type: DataEvent.API_BACKUP_RESTORE,
message: 'Settings, files and pages restored. Nice work.'
});
})
.catch(err => {
res.json({
type: err.type,
message: 'Backup not restored. Uh oh.'
});
});
})
.catch(err => {
res.json({
type: err.type,
message: err.message
});
});
});
router.post('/init-restore', backup_restore, (req, res) => {
utils
.verifyBackup(req.files[0], req.body)
.then(response => {
res.json({
type: response.type,
message: response.message
});
})
.catch(err => {
res.json({
type: err.type,
message: err.message
});
});
});
module.exports = router;

View file

@ -1,83 +0,0 @@
import Settings, { SETTINGS_FILE } from '../../data/Settings';
import Auth from '../../data/Auth';
var express = require('express');
var router = express.Router();
var nodemailer = require('nodemailer');
var mg = require('nodemailer-mailgun-transport');
const pug = require('pug');
const settings = new Settings();
const auth = new Auth();
router.post('/', function (req, res) {
auth.authCheck(req)
.then(() => {
settings
.load(SETTINGS_FILE)
.then(settings => {
let transport = '';
var auth = '';
switch (settings.email.active) {
case 'option-smtp':
auth = {
host: settings.email.smtp.domain,
port: 587,
secure: false,
auth: {
type: 'login',
user: settings.email.smtp,
pass: settings.email.smtp.password
}
};
transport = nodemailer.createTransport(auth);
break;
case 'option-mg':
auth = {
auth: {
api_key: settings.email.mailgun.key,
domain: settings.email.mailgun.domain
}
};
transport = nodemailer.createTransport(mg(auth));
break;
}
let render = pug.compileFile('brain/views/email/base.pug');
let html = render({
title: settings.global.title,
header: 'a note from ' + settings.global.title,
content: req.body.content,
footer: 'powered by fipamo'
});
transport.sendMail(
{
from: 'control@playvico.us',
to: req.session.user.email, // An array if you have multiple recipients.
subject: 'Hey beautiful',
//You can use "html:" to send HTML email content. It's magic!
html: html
//You can use "text:" to send plain-text content. It's oldschool!
//text: 'Mailgun rocks, pow pow!'
},
function (err, info) {
if (err) {
res.json({
message: 'MAIL ERROR',
desc: err
});
} else {
//console.log(info);
res.json({
message: 'MAIL SENT',
desc: info
});
}
}
);
})
.catch(() => {
//console.error(err);
});
})
.catch(err => {
res.json(err);
});
});
module.exports = router;

View file

@ -1,268 +0,0 @@
import Book from '../../data/Book';
import Auth from '../../data/Auth';
import Settings, { SETTINGS_FILE } from '../../data/Settings';
import * as DataEvent from '../../../src/com/events/DataEvent';
import Render from '../../data/Render';
const express = require('express');
const router = express.Router();
const multer = require('multer');
const fs = require('fs-extra');
const moment = require('moment');
const book = new Book();
const auth = new Auth();
const settings = new Settings();
const render = new Render();
const _ = require('lodash');
const uploadPath =
'./public/assets/images/blog/' + moment().format('YYYY') + '/' + moment().format('MM');
var storage = multer.diskStorage({
destination: function (req, file, cb) {
fs.ensureDir(uploadPath, () => {
// dir has now been created, including the directory it is to be placed in
cb(null, uploadPath);
});
},
filename: function (req, file, cb) {
var splice = file.originalname.split(':');
cb(null, splice[0]);
}
});
var feature_upload = multer({
storage: storage
}).array('feature_image');
var post_upload = multer({
storage: storage
}).array('post_image');
/**
* Retrieves a page of a published entries
* @public
*/
router.get('/published/:pageNum?', (req, res) => {
//console.log('PAGE NUM', req.params.pageNum);
let pageNum = req.params.pageNum;
if (pageNum === null || pageNum === '' || !pageNum) pageNum = 1;
let pages = [];
book.getPage().then(result => {
result.sort((a, b) => parseFloat(b.metadata.id) - parseFloat(a.metadata.id));
let displayed = _.filter(result, page => {
return (
page.metadata.deleted === false &&
page.metadata.published === true &&
page.metadata.layout != 'index'
);
});
var pageLimit = 6;
var count = Math.ceil(displayed.length / pageLimit);
if (pageNum > count || isNaN(pageNum))
res.json({ type: DataEvent.REQUEST_LAME, message: "That page doesn't exist, champ." });
var rangeIndex = pageNum * pageLimit - pageLimit;
let meta = [];
for (let index = 0; index < pageLimit; index++) {
const page = displayed[index + rangeIndex];
try {
if (
page.metadata.id != null &&
page.metadata.deleted === false &&
page.metadata.published === true
) {
let entry = page.metadata;
entry.content = page.content;
//console.log('ENTRY', entry);
pages.push({
page: entry,
displayDate: moment(page.metadata.created).fromNow()
});
}
} catch (e) {
//console.log("NO POST", e)
}
}
meta.push({ currentPage: pageNum, totalPages: count });
let data = { pages: pages, meta: meta };
res.json({
type: DataEvent.REQUEST_GOOD,
message: 'This is Page ' + pageNum + ' of ' + count,
data: data
});
});
});
/**
* Retrieves single entry
* @public
*/
router.get('/single/:id', (req, res) => {
let id = req.params.id;
if (id === null || id === '')
res.json({ type: DataEvent.REQUEST_LAME, message: " Nah, this isn't here." });
book.getPage(id)
.then(page => {
let entry = page.metadata;
entry.content = page.content;
res.json({
type: DataEvent.REQUEST_GOOD,
message: 'Found it. Here you go.',
data: entry
});
})
.catch(err => {
res.json({
type: DataEvent.REQUEST_LAME,
message: "This doesn't seem to be here, homie.",
err: err.message
});
});
});
/**
* Add/Update Page
*/
router.post('/write/:task?', feature_upload, (req, res) => {
auth.authCheck(req)
.then(() => {
let body = _.mapValues(req.body);
let feature = '';
let task = '';
req.params.task === 'new'
? (task = DataEvent.API_PAGE_CREATE)
: (task = DataEvent.API_PAGE_WRITE);
if (req.files.length > 0) {
var path = req.files[0].path;
//console.log('NEW FEATURE URL', path);
feature = '/' + path.substring(7, path.length);
} else {
var url = body.feature_image;
//switch this to the new feature path edit
if (url != null || url != undefined || url != '') {
let chunks = url.split('/');
let strip = chunks[0] + '/' + chunks[1] + chunks[2];
feature = url.substr(strip.length + 1, url.length);
} else {
feature = '';
}
}
body.feature = feature;
body.deleted = false;
//if title changes, get rid of a pages with old title
if (body.current_title !== body.slug) {
let path =
moment(body.created).format('YYYY') + '/' + moment(body.created).format('MM');
//remove html page
fs.unlink('public/' + path + '/' + body.current_title + '.html')
.then()
.catch(() => {
//console.log('HTML ERROR', err);
});
//remove markdown
fs.unlink('content/pages/' + path + '/' + body.current_title + '.md')
.then()
.catch(() => {
//console.log('MD ERROR', err);
});
}
book.editPage(body, body.page_uuid, task, req.session.user)
.then(result => {
if (result.type === DataEvent.PAGE_ADDED) {
settings.updatePageIndex();
}
//load all page data and render if render on save flag is set in settings file
getBookData()
.then(result => {
if (result.settings.global.renderOnSave === 'true') {
render
.publishAll(
result.pages,
result.settings.global.theme,
req.session.user.handle
)
.then(response => {
res.json({
type: response.type,
message: response.message
});
})
.catch(err => {
res.json({
type: DataEvent.PAGES_NOT_RENDERED,
message: 'Uh oh. Pages not rendered, sport',
error: err
});
});
} else {
//console.log('DONT RENDER PAGES');
}
})
.catch(() => {
//console.log();
});
res.json(result);
})
.catch(err => {
res.json(err);
});
})
.catch(err => {
res.json(err);
});
});
/**
* Soft deletes Page
*/
router.post('/delete', (req, res) => {
auth.authCheck(req)
.then(() => {
book.editPage([], req.body.id, DataEvent.API_PAGE_DELETE, req.session.user)
.then(result => {
//remove item from menu in settings
res.json(result);
})
.catch(err => {
res.json(err);
});
})
.catch(err => {
res.json(err);
});
});
/**
* Uploads image from a Page content
*/
router.post('/add-post-image', post_upload, function (req, res) {
//console.log(req.body);
var image = req.files[0].path;
return res.json({
type: DataEvent.POST_IMAGE_ADDED,
message: 'Added Image',
url: '/' + image.substr(7, image.length)
});
});
module.exports = router;
function getBookData() {
return new Promise((resolve, reject) => {
let getSettings = settings.load(SETTINGS_FILE);
let getBook = book.getPage();
Promise.all([getSettings, getBook])
.then(result => {
const [settings, pages] = result;
let data = { settings: settings, pages: pages };
resolve(data);
})
.catch(err => {
reject(err);
});
});
}

View file

@ -1,250 +0,0 @@
import * as DataEvent from '../../../src/com/events/DataEvent';
import Auth from '../../data/Auth';
import Render from '../../data/Render';
import Settings, { SETTINGS_FILE, SETTINGS_FOLKS } from '../../data/Settings';
import Navigation from '../../data/Navigation';
import Book from '../../data/Book';
const express = require('express');
const router = express.Router();
const multer = require('multer');
const fs = require('fs-extra');
const moment = require('moment');
const _ = require('lodash');
const auth = new Auth();
const render = new Render();
const book = new Book();
const settings = new Settings();
const nav = new Navigation();
const uploadPath =
'./public/assets/images/user/' + moment().format('YYYY') + '/' + moment().format('MM');
var storage = multer.diskStorage({
destination: function (req, file, cb) {
fs.ensureDir(uploadPath, () => {
// dir has now been created, including the directory it is to be placed in
cb(null, uploadPath);
});
},
filename: function (req, file, cb) {
var splice = file.originalname.split(':');
cb(null, splice[0]);
}
});
var avatar_upload = multer({
storage: storage
}).array('avatar_upload');
var background_upload = multer({
storage: storage
}).array('background_upload');
//** SYNC POSTS */
router.post('/sync', (req, res) => {
auth.authCheck(req)
.then(() => {
settings
.sync(req, res)
.then(() => {
res.json({
type: DataEvent.SETTINGS_UPDATED,
message: 'Settings Saved'
});
})
.catch(err => {
res.json({
type: DataEvent.REQUEST_LAME,
error: err.message,
message: "Uh oh. Settings didn't take, sport"
});
});
})
.catch(err => {
res.json({
type: err.type,
message: err.message
});
});
});
router.post('/nav-sync', (req, res) => {
auth.authCheck(req)
.then(() => {
// find removed menu item page and set menu to false
book.getPage(req.body.remove).then(page => {
let body = page.metadata;
body.content = page.content;
body.menu = false;
book.editPage(body, body.uuid, DataEvent.API_PAGE_WRITE, req.session.user);
});
nav.sync(req.body)
.then(response => {
res.json({
type: response.type,
message: response.message
});
})
.catch(err => {
res.json({
type: DataEvent.REQUEST_LAME,
message: err
});
});
})
.catch(err => {
res.json({
type: err.type,
message: err.message
});
});
});
router.post('/publish-pages', (req, res) => {
auth.authCheck(req)
.then(() => {
getBookData()
.then(result => {
render
.publishAll(
result.pages,
result.settings.global.theme,
req.session.user.handle
)
.then(response => {
res.json({
type: response.type,
message: response.message
});
})
.catch(err => {
res.json({
type: DataEvent.PAGES_NOT_RENDERED,
message: 'Uh oh. Pages not rendered, sport',
error: err
});
});
})
.catch(err => {
res.json({
type: DataEvent.PAGES_NOT_RENDERED,
message: 'Uh oh. Pages not rendered, sport',
error: err
});
});
})
.catch(err => {
res.json({
type: err.type,
message: err.message
});
});
});
/***
UPLOAD AVATAR
*/
router.post('/add-avatar', avatar_upload, (req, res) => {
if (req.session.user) {
let user = req.session.user;
settings
.load(SETTINGS_FOLKS)
.then(folks => {
let found = _.find(folks, { handle: user.handle });
if (found) {
var index = found.id - 1;
var path = req.files[0].path;
var image = path.substr(7, path.length);
folks[index].avi = '/' + image;
fs.writeJson('site/folks.json', folks);
user.avi = '/' + image;
res.json({
type: DataEvent.AVATAR_UPLOADED,
message: 'Changed avi. You look great.',
url: '/' + image
});
}
})
.catch(() => {
res.json({
type: DataEvent.REQUEST_LAME,
message: 'Members Not found'
});
});
} else {
res.json({
type: DataEvent.REQUEST_LAME,
message: "You're not logged in, champ"
});
}
});
/***
UPLOAD FEATURE BACKGROUND
*/
router.post('/add-feature-background', background_upload, (req, res) => {
if (req.session.user) {
settings
.load(SETTINGS_FILE)
.then(settings => {
var path = req.files[0].path;
var image = path.substr(7, path.length);
settings.global.background = '/' + image;
fs.writeJson('site/settings.json', settings);
res.json({
type: DataEvent.SITE_BACKGROUND_UPLOADED,
message: 'Background Uploaded',
url: '/' + image
});
})
.catch(() => {
//console.log('ERROR', err);
});
} else {
res.json({
type: DataEvent.REQUEST_LAME,
message: "You're not logged in, champ"
});
}
});
router.post('/reindex', (req, res) => {
auth.authCheck(req)
.then(() => {
book.reindexPages(req)
.then(response => {
//reset settings index
settings.resetLibraryIndex(response.count + 1);
//return success to front end
res.json(response);
})
.catch(err => {
res.json({
type: err.type,
message: err.message
});
});
})
.catch(err => {
res.json({
type: err.type,
message: err.message
});
});
});
module.exports = router;
function getBookData() {
return new Promise((resolve, reject) => {
let getSettings = settings.load(SETTINGS_FILE);
let getBook = book.getPage();
Promise.all([getSettings, getBook])
.then(result => {
const [settings, pages] = result;
let data = { settings: settings, pages: pages };
resolve(data);
})
.catch(err => {
reject(err);
});
});
}

View file

@ -1,94 +0,0 @@
var express = require('express');
var path = require('path');
//var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var session = require('express-session');
var MemoryStore = require('memorystore')(session);
var flash = require('connect-flash');
var app = express();
// favicon stuff
//app.use(favicon(path.join(__dirname, 'favicons', 'favicon.ico')));
// view engine setup
app.set('views', path.join(__dirname, './views'));
app.set('view engine', 'pug');
app.use(logger('dev'));
app.use(bodyParser.json({ limit: '50mb' }));
app.use(
bodyParser.urlencoded({
extended: false,
limit: '50mb'
})
);
app.use(cookieParser());
app.use(express.static(path.join(__dirname, '../public'), { extensions: ['html'] }));
app.use(
session({
store: new MemoryStore({
checkPeriod: 86400000 // prune expired entries every 24h
}),
secret: '1KqZ18W8KskE1iSw',
saveUninitialized: false,
resave: false,
cookie: {
maxAge: 608800000
}
})
);
app.use(flash());
//sections
//var front = require('./routes/front/index')(session);
var dash = require('./routes/dash/index');
var page = require('./routes/dash/pages');
var settings = require('./routes/dash/settings');
var nav = require('./routes/dash/nav');
//api
var pages = require('./api/v1/pages');
var setting = require('./api/v1/settings');
var mailer = require('./api/v1/mailer');
var auth = require('./api/v1/auth');
var backup = require('./api/v1/backup');
// API PATHS
app.use('/api/v1/page', pages);
app.use('/api/v1/settings', setting);
app.use('/api/v1/auth', auth);
app.use('/api/v1/mailer', mailer);
app.use('/api/v1/backup', backup);
// PAGES
app.use('/@/dashboard', dash);
app.use('/@/dashboard/page', page);
app.use('/@/dashboard/settings', settings);
app.use('/@/dashboard/navigation', nav);
// catch 404 and forward to error handler
app.use(function (req, res, next) {
var err = new Error('Not Found');
err.status = 404;
next(err);
});
// error handlers
// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
app.use(function (err, req, res) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: err
});
});
}
// production error handler
// no stacktraces leaked to user
app.use(function (err, req, res) {
res.status(err.status || 500);
res.render('error', {
message: err.message,
error: {}
});
});
module.exports = app;

View file

@ -1,105 +0,0 @@
import * as DataEvent from '../../src/com/events/DataEvent';
const bCrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const _ = require('lodash');
export default class Auth {
//--------------------------
// constructor
//--------------------------
constructor() {}
//--------------------------
// methods
//--------------------------
start() {}
/**
* Makes sure access token is legit
* @parameter req
*/
authCheck(req) {
let self = this;
return new Promise((resolve, reject) => {
let hash = req.headers['x-access-token'];
let response = [];
//check to see if user is logged in
if (!req.session.user) {
response = {
status: false,
type: DataEvent.API_REQUEST_LAME,
message: "You're not logged in, champ."
};
reject(response);
}
//Checks if token is a proper hash, if not reject
if (!self.isTokenValid(req.session.token, hash)) {
response = {
status: false,
type: DataEvent.API_REQUEST_LAME,
message: 'No Token Present. Auth Blocked'
};
reject(response);
//res.json();
} else {
var member = req.session.user;
jwt.verify(req.session.token, member.key, function (err, decoded) {
if (err) {
response = {
status: false,
type: DataEvent.API_REQUEST_LAME,
message: 'Invalid Token. Auth Blocked'
};
reject(response);
}
response = {
status: true,
type: DataEvent.API_REQUEST_GOOD,
message: 'Token Verified',
token: decoded
};
resolve(response);
});
}
});
}
verifyCredentials(config, credentials) {
return new Promise((resolve, reject) => {
var found = _.find(config, { handle: credentials.handle });
var response;
if (found) {
if (!this.isValidPassword(found, credentials.pass)) {
response = {
type: DataEvent.REQUEST_LAME,
message: 'CHECK YOUR PASSWORD'
};
reject(response);
}
response = { type: DataEvent.REQUEST_GOOD, message: 'Backup Verified. Restoring' };
resolve(response);
} else {
response = { type: DataEvent.REQUEST_LAME, message: 'Handle not found, boss' };
reject(response);
}
});
}
isValidPassword(user, password) {
return bCrypt.compareSync(password, user.password);
}
/**
* Checks to make sure received token matches
* @parameter token: created token
* @parameter hashedToken: encrypted token
*/
isTokenValid(token, hashedToken) {
return bCrypt.compareSync(token, hashedToken);
}
//--------------------------
// event handlers
//--------------------------
}

View file

@ -1,273 +0,0 @@
import fh from 'filehound';
import fs from 'fs-extra';
import metadataParser from 'markdown-yaml-metadata-parser';
import _ from 'lodash';
import * as DataEvent from '../../src/com/events/DataEvent';
import Navigation from './Navigation';
import Utils from './Utils';
const moment = require('moment');
const nav = new Navigation();
const utils = new Utils();
/**
* Class for handling blog content pages
*/
export default class Book {
//--------------------------
// constructor
//--------------------------
constructor() {}
//--------------------------
// methods
//--------------------------
start() {}
/**
* Retrieves single page or pages
* @parameter id: optional id if requesting a single Page
*/
getPage(id) {
return new Promise((resolve, reject) => {
fh.create()
.paths('content/pages')
.ext('md')
.find()
.then(files => {
let pages = [];
for (let index = 0; index < files.length; index++) {
fs.readFile(files[index], { encoding: 'utf8' }, (err, file) => {
pages.push(metadataParser(file));
});
}
if (id === null || id === null || id === undefined) {
setTimeout(() => {
//TODO: Duct tape solution until something better created
utils.organizeTags(pages);
utils.organizeArchive(pages);
resolve(pages);
}, 100);
} else {
setTimeout(() => {
//TODO: Duct tape solution until something better created
//make check against menu to see if page should be marked as menu item
//if it doesn't exist in menu change, edit page to
let page = _.find(pages, list => {
return list.metadata.uuid === id;
});
resolve(page);
}, 100);
}
})
.catch(err => {
reject(err);
});
});
}
/**
* Edits single page based on id and task
* @parameter body: object that contains all page information
* @parameter id: identifier for page being edited
* @parameter task: type of task being performed - listed in DataEvents Class /src/com/events
* @parameter user: object contain user information
*/
editPage(body, id, task, user) {
return new Promise((resolve, reject) => {
let self = this;
let response = [];
switch (task) {
case DataEvent.API_PAGE_CREATE:
case DataEvent.API_PAGE_WRITE:
var layout = 'page';
var path = '';
fs.ensureDir(
'content/pages/' +
moment(body.created).format('YYYY') +
'/' +
moment(body.created).format('MM') +
'/'
).then(() => {
if (body.menu === 'true') {
body.path =
moment(body.created).format('YYYY') +
'/' +
moment(body.created).format('MM');
nav.editMenu(DataEvent.MENU_ADD_ITEM, body, user);
} else {
nav.editMenu(DataEvent.MENU_DELETE_ITEM, body, user);
}
if (body.layout !== 'page') layout = body.layout;
if (body.layout === null || body.layout === 'null') layout = 'page';
var pageWrite =
'---\n' +
'id: ' +
body.id +
'\n' +
'uuid: ' +
body.uuid +
'\n' +
'title: ' +
body.title +
'\n' +
'feature: ' +
body.feature +
'\n' +
'path: ' +
moment(body.created).format('YYYY') +
'/' +
moment(body.created).format('MM') +
'\n' +
'layout: ' +
layout +
'\n' +
'tags: ' +
body.tags +
'\n' +
'author: ' +
user.handle +
'\n' +
'created: ' +
moment(body.created).format() +
'\n' +
'updated: ' +
moment(Date.now()).format() +
'\n' +
'deleted: ' +
body.deleted +
'\n' +
'menu: ' +
body.menu +
'\n' +
'featured: ' +
body.featured +
'\n' +
'published: ' +
body.published +
'\n' +
'slug: ' +
body.slug +
'\n' +
'---\n' +
body.content;
layout === 'index'
? (path = 'content/pages/index.md')
: (path =
'content/pages/' +
moment(body.created).format('YYYY') +
'/' +
moment(body.created).format('MM') +
'/' +
body.slug +
'.md');
fs.writeFile(path, pageWrite, err => {
// throws an error, you could also catch it here
if (err) {
response = { type: DataEvent.PAGE_ERROR, message: err };
reject(response);
}
// success case, the file was saved
if (task === DataEvent.API_PAGE_CREATE) {
// if new file, update settings index and page count
response = {
type: DataEvent.PAGE_ADDED,
message: 'New Page Created',
id: body.uuid
};
resolve(response);
} else {
response = {
type: DataEvent.PAGE_UPDATED,
message: 'Page saved. Nice Work'
};
resolve(response);
}
});
});
break;
case DataEvent.API_PAGE_DELETE:
this.getPage(id)
.then(page => {
let body = _.mapValues(page.metadata);
body.content = page.content;
body.deleted = moment(Date.now()).format();
body.menu = false;
self.editPage(body, body.uuid, DataEvent.API_PAGE_WRITE, user)
.then(() => {
let item = {
title: body.title,
id: body.id,
slug: body.slug,
uuid: body.uuid
};
nav.editMenu(DataEvent.MENU_DELETE_ITEM, item);
response = {
type: DataEvent.PAGE_DELETED,
message: 'Page deleted, sport',
data: { uuid: body.uuid }
};
resolve(response);
})
.catch(err => {
response = { type: DataEvent.PAGE_ERROR, message: err };
reject(response);
});
})
.catch(err => {
response = { type: DataEvent.PAGE_ERROR, message: err };
reject(response);
});
break;
}
});
}
reindexPages(req) {
var response = '';
var self = this;
return new Promise((resolve, reject) => {
self.getPage()
.then(pages => {
let sorted = [];
for (let i = 0; i < pages.length; i++) {
let body = pages[i].metadata;
body.content = pages[i].content;
sorted.push(body);
}
//resorts pages by date created
let byDate = _.sortBy(sorted, page => {
return page.created;
});
//reassigns id sequentially based on sorted pages
for (let index = 0; index < byDate.length; index++) {
byDate[index].id = index;
self.editPage(
byDate[index],
index,
DataEvent.API_PAGE_WRITE,
req.session.user
);
}
response = {
type: DataEvent.API_REINDEX_PAGES,
message: 'Pages re-sorted. Easy peasy.',
count: byDate.length
};
resolve(response);
})
.catch(err => {
response = { type: DataEvent.PAGE_ERROR, message: err };
reject(response);
});
});
}
//--------------------------
// event handlers
//--------------------------
}

View file

@ -1,75 +0,0 @@
import fs from 'fs-extra';
import _ from 'lodash';
import * as DataEvent from '../../src/com/events/DataEvent';
import Settings, { SETTINGS_FILE } from './Settings';
const settings = new Settings();
export default class Navigation {
//--------------------------
// constructor
//--------------------------
constructor() {}
//--------------------------
// methods
//--------------------------
sync(body) {
return new Promise((resolve, reject) => {
let response = [];
settings
.load(SETTINGS_FILE)
.then(settings => {
let payload = body;
settings.menu = payload.nav;
fs.writeJson('site/settings.json', settings)
.then(() => {
response = {
type: DataEvent.SETTINGS_UPDATED,
message: 'Menu order saved, champ'
};
resolve(response);
})
.catch(err => {
response = {
type: DataEvent.REQUEST_LAME,
message: err
};
reject(response);
});
})
.catch(err => {
response = {
type: DataEvent.REQUEST_LAME,
message: err
};
reject(response);
});
});
}
editMenu(task, item) {
settings.load(SETTINGS_FILE).then(settings => {
switch (task) {
case DataEvent.MENU_ADD_ITEM:
settings.menu.push({
title: item.title,
id: item.id,
slug: item.slug,
uuid: item.uuid,
path: item.path
});
break;
case DataEvent.MENU_DELETE_ITEM:
settings.menu = _.remove(settings.menu, m => {
return m.uuid != item.uuid;
});
break;
}
fs.writeJSON(SETTINGS_FILE, settings);
});
}
//--------------------------
// event handlers
//--------------------------
}

View file

@ -1,304 +0,0 @@
import * as DataEvent from '../../src/com/events/DataEvent';
import StringUtils from '../../src/com/utils/StringUtils';
import Settings, { SETTINGS_FILE, SETTINGS_TAG } from './Settings';
import fs from 'fs-extra';
import sanitize from 'sanitize-html';
import Utils from './Utils';
const pug = require('pug');
const md = require('markdown-it')('commonmark');
const _ = require('lodash');
const moment = require('moment');
const settings = new Settings();
export default class Render {
//--------------------------
// constructor
//--------------------------
constructor() {}
//--------------------------
// methods
//--------------------------
start() {}
/**
* Renders all pages from markdown to html
* @parameter pages: payload for site pages
* @parameter theme: current theme being used as defined in settings
*/
publishAll(pages, theme, author) {
return new Promise((resolve, reject) => {
settings
.load(SETTINGS_FILE)
.then(config => {
let response = [];
let count = _.filter(pages, page => {
return page.metadata.deleted === false && page.metadata.published === true;
}).length;
let rendered = 0;
let display_count = 0;
let recent = [];
let featured = _.filter(pages, page => {
return (
page.metadata.deleted === false &&
page.metadata.published === true &&
page.metadata.featured === true &&
page.metadata.layout !== 'index'
);
});
for (let index = 0; index < pages.length; index++) {
pages.sort((a, b) => parseFloat(b.metadata.id) - parseFloat(a.metadata.id));
const page = pages[index];
if (page.metadata.deleted === false && page.metadata.published === true) {
if (page.metadata.layout != 'index') {
if (recent.length < config.global.display_limit) {
recent.push({
title: page.metadata.title,
slug: page.metadata.slug,
feature: page.metadata.feature,
created: moment(page.metadata.created).fromNow(),
path: page.metadata.path
});
display_count = ++display_count;
}
}
let writeFile, template;
let path =
'public/' +
moment(page.metadata.created).format('YYYY') +
'/' +
moment(page.metadata.created).format('MM') +
'/';
if (page.metadata.layout === 'index') {
template = 'content/themes/' + theme + '/index.pug';
writeFile = 'public/index.html';
} else {
writeFile = path + page.metadata.slug + '.html';
template = 'content/themes/' + theme + '/page.pug';
}
let buffed = sanitize(page.content, {
allowedTags: ['del', 'a', 'iframe', 'img'],
allowedAttributes: {
a: ['href', 'name', 'target'],
img: ['src'],
iframe: [
'height',
'width',
'src',
'frameborder',
'allow',
'allowfullscreen'
]
}
});
let bag = page.metadata.tags.split(',');
let tags = [];
for (let index = 0; index < bag.length; index++) {
let tag = bag[index].trim();
tags.push({
label: bag[index],
slug: new StringUtils().cleanString(tag)
});
}
buffed = new StringUtils().decodeHTML(buffed);
let html = md.render(buffed, { html: true, xhtmlOut: true });
//add open graph meta variables
let file = pug.renderFile(template, {
title: page.metadata.title,
default_bg: page.metadata.feature,
image: page.metadata.feature,
keywords: page.metadata.tags,
content: html,
tags: tags,
menu: config.menu,
recent_posts: recent,
featured_posts: featured,
meta: {
who: author,
when: moment(page.metadata.created).fromNow(),
tags: tags
},
welcome_message: page.metadata.title
});
fs.ensureDir(path).then(() => {
fs.writeFile(writeFile, file, err => {
// throws an error, you could also catch it here
if (err) {
response = {
type: DataEvent.PAGES_NOT_RENDERED,
message: err
};
reject(response);
}
// success case, the file was saved
});
});
rendered = ++rendered;
if (rendered === count) {
response = {
type: DataEvent.PAGES_RENDERED,
message: 'All Pages Rendered. Sweet.'
};
//move theme assets to public when pages are rendered
new Utils().moveAssets();
resolve(response);
}
} else {
if (count === 0) {
response = {
type: DataEvent.PAGES_RENDERED,
message: 'No page rendering needed'
};
resolve(response);
}
//check to see if deleted pages have been renderered and delete them
if (page.metadata.layout !== 'index') {
fs.unlink(
'public/' +
page.metadata.path +
'/' +
page.metadata.slug +
'.html'
)
.then()
.catch(() => {
//console.log('ERROR', err);
});
}
}
}
})
.catch(err => {
//console.log('ERROR', err);
reject(err);
});
});
}
/**
* Method to extract, group and render tags in page
* @parameter pages: payload for site pages
*/
publishTags(pages) {
let self = this;
return new Promise((resolve, reject) => {
self.loadRenderData()
.then(result => {
let tags = result.tags.tags;
let renderList = [];
for (let index = 0; index < tags.length; index++) {
let tag = tags[index];
//console.log('**TAG**', tag.tag_name);
var pageList = [];
for (let i = 0; i < pages.length; i++) {
let page = pages[i];
//TODO: filter for deleted and unpublished pages
if (
page.metadata.deleted === false &&
page.metadata.published === true
) {
if (_.includes(page.metadata.tags, tag.tag_name)) {
pageList.push({
title: page.metadata.title,
slug: page.metadata.slug,
path: page.metadata.path
});
}
}
}
renderList.push({ tag: tag.tag_name, tag_list: pageList, slug: tag.slug });
}
let response = [];
for (let index = 0; index < renderList.length; index++) {
let item = renderList[index];
let file = pug.renderFile(
'content/themes/' + result.settings.global.theme + '/tags.pug',
{
title: item.tag,
default_bg: result.settings.global.background,
content_tags: 'THESE ARE TAGS',
tag_list: item.tag_list,
menu: result.settings.menu
}
);
fs.ensureDir('public/tags', () => {
fs.writeFile('public/tags/' + item.slug + '.html', file, err => {
// throws an error, you could also catch it here
if (err) {
response = {
type: DataEvent.TAG_PAGES_NOT_RENDERED,
message: err
};
reject(response);
}
// success case, the file was saved
response = {
type: DataEvent.TAG_PAGES_RENDERED,
message: 'Tag Pages ready to go. Good job.'
};
resolve(response);
});
});
}
})
.catch(err => {
reject(err);
});
});
}
/**
* Method to build page that lists all active pages, organized by year and month
* @parameter pages: payload for site pages
*/
publishArchive(archive) {
settings
.load(SETTINGS_FILE)
.then(settings => {
let file = pug.renderFile(
'content/themes/' + settings.global.theme + '/archive.pug',
{
title: 'ARCHIVES',
default_bg: settings.global.background,
content_tags: 'COLD STORAGE',
archives: archive,
menu: settings.menu
}
);
fs.writeFile('public/archives.html', file, err => {
// throws an error, you could also catch it here
if (err) {
//console.log('ERROR', err);
//response = { type: DataEvent.TAG_PAGES_NOT_RENDERED, message: err };
}
// success case, the file was saved
});
})
.catch(() => {
//console.log(err);
});
}
loadRenderData() {
return new Promise((resolve, reject) => {
let getSettings = settings.load(SETTINGS_FILE);
let getTags = settings.load(SETTINGS_TAG);
Promise.all([getSettings, getTags])
.then(result => {
const [settings, tags] = result;
let data = { settings: settings, tags: tags };
resolve(data);
})
.catch(err => {
reject(err);
});
});
}
//--------------------------
// event handlers
//--------------------------
}

View file

@ -1,184 +0,0 @@
import * as DataEvent from '../../src/com/events/DataEvent';
import fs from 'fs-extra';
const _ = require('lodash');
export const SETTINGS_FILE = 'site/settings.json';
export const SETTINGS_FOLKS = 'site/folks.json';
export const SETTINGS_TAG = 'site/tags.json';
export default class Settings {
//--------------------------
// constructor
//--------------------------
constructor() {}
//--------------------------
// methods
//--------------------------
sync(req) {
let self = this;
return new Promise((resolve, reject) => {
self.loadConfigData()
.then(result => {
let payload = req.body;
//so payload matches loaded config
payload.global.display_limit = result.settings.global.display_limit;
payload.global.port = result.settings.global.port;
payload.global.last_backup = result.settings.global.last_backup;
let user = req.session.user;
let found = _.find(result.folks, { id: user.id });
let needToUpdate = false;
let response = [];
if (found) {
let index = found.id - 1;
if (
result.folks[index].handle != payload.member.handle ||
result.folks[index].email != payload.member.email
) {
user.handle = payload.member.handle;
user.email = payload.member.email;
result.folks[index].handle = payload.member.handle;
result.folks[index].email = payload.member.email;
fs.writeJson('site/folks.json', result.folks);
} else {
//no need to save
}
} else {
let response = {
type: DataEvent.REQUEST_LAME,
message: "You're not logged in, champ"
};
reject(response);
}
if (!_.isEqual(result.settings.global, payload.global)) {
let bg = payload.global.background;
let chunks = bg.split('/');
let strip = chunks[0] + '/' + chunks[1] + chunks[2];
payload.global.background = bg.substr(strip.length + 1, bg.length);
result.settings.global = payload.global;
needToUpdate = true;
} else {
//no need to save
}
if (!_.isEqual(result.settings.email, payload.email)) {
result.settings.email = payload.email;
needToUpdate = true;
} else {
//no need to save
}
if (needToUpdate) {
fs.writeJson('site/settings.json', result.settings)
.then(() => {
response = {
type: DataEvent.SETTINGS_UPDATED,
message: 'Settings Saved'
};
resolve(response);
})
.catch(() => {
//console.error(err);
});
} else {
//no need to update
}
})
.catch(err => {
reject(err);
});
});
}
saveTags(tags) {
let self = this;
return new Promise((resolve, reject) => {
self.load(SETTINGS_TAG)
.then(config => {
if (!_.isEqual(config.tags, tags)) {
config.tags = tags;
fs.writeJson('site/tags.json', config)
.then(() => {
let response = {
type: DataEvent.SETTINGS_UPDATED,
message: 'Settings Saved'
};
resolve(response);
})
.catch(err => {
reject(err);
});
} else {
let response = {
type: DataEvent.SETTINGS_NOT_UPDATED,
message: 'Settings Already Saved'
};
resolve(response);
}
})
.catch(err => {
reject(err);
});
});
}
updatePageIndex() {
fs.readJSON('site/settings.json').then(settings => {
settings.library_stats.current_index = ++settings.library_stats.current_index;
setTimeout(() => {
//TODO: Duct tape solution until something better created
fs.writeJSON('site/settings.json', settings)
.then(() => {
//console.log('ALL TO THE GOOD');
})
.catch(() => {
//.log('ERR', err);
});
}, 100);
});
}
resetLibraryIndex(index) {
fs.readJSON('site/settings.json').then(settings => {
settings.library_stats.current_index = index;
setTimeout(() => {
//TODO: Duct tape solution until something better created
fs.writeJSON('site/settings.json', settings)
.then(() => {
//console.log('ALL TO THE GOOD');
})
.catch(() => {
//.log('ERR', err);
});
}, 100);
});
}
load(fileToLoad) {
return new Promise((resolve, reject) => {
fs.readJSON(fileToLoad)
.then(file => {
resolve(file);
})
.catch(err => {
reject(err);
});
});
}
loadConfigData() {
return new Promise((resolve, reject) => {
let getSettings = this.load(SETTINGS_FILE);
let getFolks = this.load(SETTINGS_FOLKS);
Promise.all([getSettings, getFolks])
.then(result => {
const [settings, folks] = result;
let data = { settings: settings, folks: folks };
resolve(data);
})
.catch(err => {
reject(err);
});
});
}
//--------------------------
// event handlers
//--------------------------
}

View file

@ -1,243 +0,0 @@
import Settings, { SETTINGS_FILE } from './Settings';
import Render from './Render';
import StringUtils from '../../src/com/utils/StringUtils';
import _ from 'lodash';
import Auth from '../data/Auth';
const settings = new Settings();
const render = new Render();
const stringUtils = new StringUtils();
const moment = require('moment');
const fs = require('fs-extra');
const AdmZip = require('adm-zip');
const auth = new Auth();
export default class Utils {
constructor() {}
/**
* Retrieves single page or pages
* @parameter pages: payload of pages
*/
organizeTags(pages) {
let tags = [];
for (let index = 0; index < pages.length; index++) {
const page = pages[index];
let temp = [];
temp = page.metadata.tags.split(',');
for (let i = 0; i < temp.length; i++) {
let label = temp[i].trim();
if (!_.find(tags, { tag_name: label })) {
tags.push({
tag_name: label,
slug: stringUtils.cleanString(label),
count: 1
});
} else {
_.find(tags, { tag_name: label }).count++;
}
}
}
tags = _.orderBy(tags, ['tag_name'], ['asc']);
settings.saveTags(tags).then(() => {
render
.publishTags(pages)
.then(() => {
//console.log(response);
})
.catch(() => {
//console.log(err);
});
});
}
organizeArchive(pages) {
let years = [];
let archive = [];
for (let index = 0; index < pages.length; index++) {
let page = pages[index].metadata;
if (page.layout !== 'index') {
let year = moment(page.created).format('YYYY');
if (!_.find(years, { year: year })) {
years.push({ year: year, count: 1 });
} else {
_.find(years, { year: year }).count++;
}
}
}
years.sort((a, b) => parseFloat(b.year) - parseFloat(a.year));
for (let index = 0; index < years.length; index++) {
let item = years[index];
let sorted = [];
let filtered = _.filter(pages, page => {
return moment(page.metadata.created).format('YYYY') === item.year;
});
for (let index = 0; index < filtered.length; index++) {
let obj = filtered[index].metadata;
let month = moment(obj.created).format('MM');
if (!_.find(sorted, { month: month })) {
sorted.push({
month: month,
full_month: moment(obj.created).format('MMMM'),
count: 1,
pages: _.filter(pages, page => {
return (
moment(page.metadata.created).format('YYYY') === item.year &&
moment(page.metadata.created).format('MM') === month &&
page.metadata.deleted === false &&
page.metadata.published === true &&
page.metadata.layout !== 'index'
);
})
});
} else {
_.find(sorted, { month: month }).count++;
}
}
archive.push({ year: item.year, year_data: sorted });
}
render.publishArchive(archive);
}
moveAssets() {
settings
.load(SETTINGS_FILE)
.then(settings => {
//move css assets to public directory
fs.copy(
'content/themes/' + settings.global.theme + '/assets/css',
'public/assets/css',
function (err) {
if (err) {
//console.log('An error occured while copying the folder.', err);
//return console.error(err);
}
//console.log('Copy completed!');
}
);
//move js assets to public directory
fs.copy(
'content/themes/' + settings.global.theme + '/assets/scripts',
'public/assets/scripts',
function (err) {
if (err) {
//console.log('An error occured while copying the folder.', err);
//return console.error(err);
}
//console.log('Copy completed!');
}
);
//TODO: Add method to move new logo to public from theme upload
})
.catch(() => {
//console.log('ERROR', err);
});
}
createBackup() {
//let self = this;
var response;
return new Promise(resolve => {
var zip = new AdmZip();
zip.addLocalFolder('public/assets/images/blog', 'public/assets/images/blog');
zip.addLocalFolder('content/pages', 'content/pages/');
zip.addLocalFile('site/folks.json', 'settings/');
zip.addLocalFile('site/settings.json', 'settings/');
zip.addLocalFile('site/tags.json', 'settings/');
zip.writeZip('content/backup.zip');
fs.readJSON('site/settings.json').then(settings => {
settings.global.last_backup = moment(Date.now()).format();
fs.writeJSON('site/settings.json', settings);
});
response = {
type: '',
message: 'BACKUP CREATED'
};
resolve(response);
});
}
restoreBackup(file) {
var response;
return new Promise((resolve, reject) => {
var zip = new AdmZip(file.buffer);
try {
zip.extractEntryTo('settings/settings.json', 'site', false, true);
zip.extractEntryTo('settings/folks.json', 'site', false, true);
zip.extractEntryTo('settings/tags.json', 'site', false, true);
zip.getEntries().forEach(function (entry) {
var entryName = entry.entryName;
var list = entryName.split('/');
if (list[0] === 'public') {
if (list[6]) {
zip.extractEntryTo(
entryName,
'public/assets/images/blog/' + list[4] + '/' + list[5],
false,
true
);
}
}
if (list[0] === 'content') {
if (list[4]) {
zip.extractEntryTo(
entryName,
'content/pages/' + list[2] + '/' + list[3],
false,
true
);
}
zip.extractEntryTo('content/pages/index.md', 'content/pages', false, true);
}
});
resolve();
} catch (error) {
response = {
type: error,
message: 'ERROR READING BACKUP'
};
reject(response);
}
});
}
verifyBackup(file, body) {
var response;
var zip = new AdmZip(file.buffer);
var credentials = { handle: body.restore_member_handle, pass: body.restore_member_pass };
var self = this;
return new Promise((resolve, reject) => {
try {
let folks = JSON.parse(zip.readAsText('settings/folks.json'));
auth.verifyCredentials(folks, credentials)
.then(() => {
//resolve(r);
self.restoreBackup(file)
.then(() => {
response = {
type: '',
message: 'RESTORE COMPLETE'
};
resolve(response);
})
.catch(err => {
response = {
type: err,
message: 'ERROR RESTORING BACKUP'
};
});
})
.catch(err => {
reject(err);
});
} catch (error) {
response = {
type: 'error',
message: 'ERROR READING BACKUP FILE'
};
reject(response);
}
});
}
}

View file

@ -1,62 +0,0 @@
import Book from '../../data/Book';
import Settings, { SETTINGS_FILE } from '../../data/Settings';
const express = require('express');
const moment = require('moment');
const router = express.Router();
const book = new Book();
const settings = new Settings();
const indexLimit = 5;
//--------------------------
// Index
//--------------------------
router.get('/', function (req, res) {
settings
.load(SETTINGS_FILE)
.then(config => {
book.getPage().then(result => {
result.sort((a, b) => parseFloat(b.metadata.id) - parseFloat(a.metadata.id));
let indexPages = [];
let indexCount = 0;
result.forEach(page => {
if (
typeof page.metadata.deleted === 'undefined' ||
page.metadata.deleted === false
) {
if (indexCount === indexLimit) return;
indexPages.push({
page: page,
date: moment(page.metadata.created).fromNow()
});
++indexCount;
}
});
let pageData = [];
if (req.session.user) {
pageData = { title: config.global.title, status: true, pages: indexPages };
} else {
pageData = { title: config.global.title, status: false, pages: indexPages };
}
res.render('index', pageData);
});
})
.catch(err => {
if (err.code === 'ENOENT') {
let setupData = { title: 'Fipamo Set up' };
res.render('init', setupData);
} else {
res.render('error', { error: err });
}
});
});
//--------------------------
// Logout
//--------------------------
router.get('/logout', (req, res) => {
//req.logout();
req.session.destroy();
res.redirect('/@/dashboard');
});
module.exports = router;

View file

@ -1,79 +0,0 @@
import Book from '../../../brain/data/Book';
import Settings, { SETTINGS_FILE } from '../../data/Settings';
const express = require('express');
const router = express.Router();
const _ = require('lodash');
//const settings = require('../../../site/settings.json');
const book = new Book();
const settings = new Settings();
//--------------------------
// SETTINGS
//--------------------------
router.get('/', function (req, res) {
if (req.session.user) {
settings
.load(SETTINGS_FILE)
.then(settings => {
var nav = [];
book.getPage()
.then(pages => {
if (settings.menu.length === 0) {
for (let index = 0; index < pages.length; index++) {
let item = pages[index].metadata;
if (item.menu) {
nav.push({
id: item.id,
uuid: item.uuid,
title: item.title,
slug: item.slug,
path: item.path
});
}
}
} else {
let newpages = [];
nav = settings.menu;
for (let index = 0; index < pages.length; index++) {
let item = pages[index].metadata;
if (item.menu)
newpages.push({
id: item.id,
uuid: item.uuid,
title: item.title,
slug: item.slug,
path: item.path
});
}
for (let i = 0; i < newpages.length; i++) {
if (_.find(nav, { uuid: newpages[i].uuid })) {
//menu item already exists
} else {
nav.push({
id: newpages[i].id,
uuid: newpages[i].uuid,
title: newpages[i].title,
slug: newpages[i].slug,
path: newpages[i].path
});
}
}
}
res.render('navigation', {
menu: nav,
welcome: 'Edit Navigation',
status: true,
title: 'Dashboard | Navigation'
});
})
.catch(err => {
res.render('error', { error: err });
});
})
.catch(err => {
res.render('error', { error: err });
});
} else {
res.redirect('/@/dashboard');
}
});
module.exports = router;

View file

@ -1,172 +0,0 @@
import Book from '../../data/Book';
const express = require('express');
const router = express.Router();
const moment = require('moment');
const book = new Book();
const uuidv4 = require('uuid/v4');
const fs = require('fs-extra');
//--------------------------
// POSTS
//--------------------------
router.get('/list/:filter?/:page?', function (req, res) {
var pageNum = req.params.page;
var filter = req.params.filter;
if (pageNum == '' || pageNum == null) pageNum = 1;
if (filter == '' || filter == null) filter = 'all';
if (req.session.user) {
book.getPage()
.then(pages => {
pages.sort((a, b) => parseFloat(b.metadata.id) - parseFloat(a.metadata.id));
let all = [];
let deleted = [];
let published = [];
let menu = [];
let featured = [];
for (let index = 0; index < pages.length; index++) {
let item = pages[index].metadata;
if (typeof item.deleted === 'undefined' || item.deleted === false) {
all.push({
page: pages[index].metadata,
date: moment(pages[index].metadata.created).fromNow()
});
if (item.published == true)
published.push({
page: pages[index].metadata,
date: moment(pages[index].metadata.created).fromNow()
});
if (item.menu == true)
menu.push({
page: pages[index].metadata,
date: moment(pages[index].metadata.created).fromNow()
});
if (item.featured == true)
featured.push({
page: pages[index].metadata,
date: moment(pages[index].metadata.created).fromNow()
});
} else {
deleted.push({
page: pages[index].metadata,
date: moment(pages[index].metadata.created).fromNow()
});
}
}
var filtered;
switch (filter) {
case 'published':
filtered = published;
break;
case 'deleted':
filtered = deleted;
break;
default:
filtered = all;
break;
}
var count = Math.ceil(filtered.length / 6);
var pageItems = [];
var itemLimit = 6;
var rangeStart = pageNum * itemLimit - itemLimit;
for (var i = 0; i < itemLimit; i++) {
try {
if (filtered[i + rangeStart].page.id != null) {
pageItems.push({
page: filtered[i + rangeStart].page,
date: moment(filtered[i + rangeStart].page.created).fromNow()
});
}
} catch (e) {
//console.log("NO POST", e)
}
}
res.render('book-index', {
title: 'Dashbord | Book',
welcome: 'Your pages',
items: pageItems,
page_info: {
all: all.length,
deleted: deleted.length,
published: published.length,
pages: pages.length,
featured: featured.length
},
page_index: pageNum,
page_count: count,
postFilter: filter,
status: true
});
})
.then(() => {
//console.log(value);
})
.catch(err => {
res.render('error', { error: err });
});
} else {
res.redirect('/@/dashboard');
}
});
//--------------------------
// BLOG POST ADD DISPLAY
//--------------------------
router.get('/add/new', function (req, res) {
if (req.session.user) {
//need to grab a few copy of settings for the lastest index
fs.readJSON('site/settings.json')
.then(config => {
res.render('page-edit', {
id: config.library_stats.current_index,
uuid: uuidv4(),
title: 'Add New Page',
user_status: true,
welcome: 'Add New Page',
date: moment(Date.now()).format('YYYY MMM DD'),
page: [],
rawDate: moment(Date.now()).format(),
status: ['false', 'false', 'false'],
edit: false
});
})
.catch(err => {
res.render('error', { error: err });
});
} else {
res.redirect('/@/dashboard');
}
});
//--------------------------
// BLOG POST EDIT DISPLAY
//--------------------------
router.get('/edit/:id', function (req, res) {
var id = req.params.id;
if (req.session.user) {
book.getPage(id)
.then(page => {
res.render('page-edit', {
id: page.metadata.id,
uuid: page.metadata.uuid,
title: 'Edit Page',
welcome: 'Edit Page',
page: page.metadata,
date: moment(page.metadata.created).format('YYYY MMM DD HH:mm'),
layout: page.metadata.layout,
rawDate: page.metadata.created,
content: page.content,
feature: page.metadata.feature,
status: [
String(page.metadata.menu),
String(page.metadata.featured),
String(page.metadata.published)
],
edit: true
});
})
.catch(err => {
res.render('error', { error: err });
});
} else {
res.redirect('/@/dashboard');
}
});
module.exports = router;

View file

@ -1,94 +0,0 @@
import Settings, { SETTINGS_FILE } from '../../data/Settings';
const express = require('express');
const router = express.Router();
const FileHound = require('filehound');
const fs = require('fs-extra');
const settings = new Settings();
const moment = require('moment');
var config = [];
//--------------------------
// SETTINGS
//--------------------------
router.get('/', function (req, res) {
settings
.load(SETTINGS_FILE)
.then(obj => {
config = obj;
})
.catch(err => {
res.render('error', { error: err });
});
loadThemes()
.then(themes => {
if (req.session.user) {
let memberInfo = [];
let user = req.session.user;
memberInfo.push({
handle: user.handle,
email: user.email,
avi: user.avi
});
themes.sort(function (a, b) {
var textA = a.theme.name.toUpperCase();
var textB = b.theme.name.toUpperCase();
return textA < textB ? -1 : textA > textB ? 1 : 0;
});
res.render('settings', {
title: 'Dashboard | Settings',
welcome: 'Your Settings',
status: true,
themes: themes,
settings: config,
last_backup: moment(config.global.last_backup).fromNow(),
member: memberInfo[0]
});
} else {
res.redirect('/@/dashboard');
}
})
.catch(err => {
res.render('error', { error: err });
});
});
module.exports = router;
function loadThemes() {
return new Promise((resolve, reject) => {
settings
.load(SETTINGS_FILE)
.then(settings => {
FileHound.create()
.paths('content/themes')
.ext('json')
.find()
.then(files => {
let themes = [];
for (let index = 0; index < files.length; index++) {
fs.readJSON(files[index], (err, theme) => {
if (theme.name == settings.global.theme) {
themes.push({
theme: theme,
current: 'true'
});
} else {
themes.push({
theme: theme,
current: 'false'
});
}
});
}
setTimeout(() => {
resolve(themes);
}, 200);
})
.catch(err => {
reject(err);
});
})
.catch(err => {
reject(err);
});
});
}

View file

@ -1,57 +0,0 @@
extends frame
block main-content
#post-index
#post-index-wrapper
#post-index-menu
- if(postFilter=='all')
a.current-filter(href="/@/dashboard/page/list/all")= "All Pages ("+page_info.all+")"
- else
a(href="/@/dashboard/page/list/all")= "All Pages ("+page_info.all+")"
| .
- if(postFilter=='published')
a.current-filter(href="/@/dashboard/page/list/published")= "Published ("+page_info.published+")"
- else
a(href="/@/dashboard/page/list/published")= "Published ("+page_info.published+")"
| .
- if(postFilter=='deleted')
a.current-filter(href="/@/dashboard/page/list/deleted")= "Deleted ("+page_info.deleted+")"
- else
a(href="/@/dashboard/page/list/deleted")= "Deleted ("+page_info.deleted+")"
a.add-new-post(href="/@/dashboard/page/add/new") +
label Create New Post
#posts-list
- var index = 0;
- for ( index; index < items.length; index++)
a.page-link(href="/@/dashboard/page/edit/"+items[index].page.uuid id=items[index].uuid)
div.page-bg(style="background: #fc6399 url("+items[index].page.feature+") no-repeat center center / cover")
#meta
- var menu = String(items[index].page.menu)
- var published = String(items[index].page.published)
- var featured = String(items[index].page.featured)
span= items[index].date
label= items[index].page.title
br
#options
span.meta-options(data-active=menu) MENU ITEM
span.meta-options(data-active=published) PUBLISHED
span.meta-options(data-active=featured) FEATURED
- var next = parseInt(page_index, 10) + 1
- var prev = parseInt(page_index, 10) - 1
- if(next > page_count) next = 1
- if(prev <= 0) prev = page_count
- if(page_count > 1)
br
.paginate
a.page-btns(href="/@/dashboard/page/list/"+postFilter+"/"+prev)
svg(viewBox="0 0 20 20" class="icons")
use(xlink:href='/assets/images/global/sprite.svg#entypo-chevron-left')
span.count= "PAGE "+page_index+" OF "+page_count
a.page-btns(href="/@/dashboard/page/list/"+postFilter+"/"+next)
svg(viewBox="0 0 20 20" class="icons")
use(xlink:href='/assets/images/global/sprite.svg#entypo-chevron-right')

View file

@ -1,94 +0,0 @@
doctype strict
head
meta(http-equiv='Content-Type' content='text/html; charset=utf-8')
meta(name='viewport' content='width=device-width, initial-scale=1.0')
title #{title}
style(type='text/css').
/* reset */
#outlook a {
padding: 0;
}
/* Force Outlook to provide a "view in browser" menu link. */
.ExternalClass {
width: 100%;
}
/* Force Hotmail to display emails at full width */
.ExternalClass,
.ExternalClass p,
.ExternalClass span,
.ExternalClass font,
.ExternalClass td,
.ExternalClass div {
line-height: 100%;
}
/* Forces Hotmail to display normal line spacing. More on that: http://www.emailonacid.com/forum/viewthread/43/ */
p {
margin: 0;
padding: 0;
font-size: 0px;
line-height: 0px;
}
/* squash Exact Target injected paragraphs */
table td {
border-collapse: collapse;
}
/* Outlook 07, 10 padding issue fix */
table {
border-collapse: collapse;
mso-table-lspace: 0pt;
mso-table-rspace: 0pt;
}
/* remove spacing around Outlook 07, 10 tables */
/* bring inline */
img {
display: block;
outline: none;
text-decoration: none;
-ms-interpolation-mode: bicubic;
}
a img {
border: none;
}
a {
text-decoration: none;
color: #000001;
}
/* text link */
a.phone {
text-decoration: none;
color: #000001 !important;
pointer-events: auto;
cursor: default;
}
/* phone link, use as wrapper on phone numbers */
span {
font-size: 13px;
line-height: 17px;
font-family: monospace;
color: #000001;
}
//if gte mso 9
style.
/* Target Outlook 2007 and 2010 */
// body wrapper
table(cellpadding='0' cellspacing='0' border='0' style='margin:0; padding:0; width:100%; line-height: 100% !important;')
tr
td(valign='top')
// edge wrapper
table(cellpadding='0' cellspacing='0' border='0' align='center' width='600' style='background: #374857;')
tr
td(valign='top' style='vertical-align: top;')
// /////////////////////////////////////////////////////
table(cellpadding='0' cellspacing='0' border='0' align='center' style='width:100%')
tr
td(valign='top' style='vertical-align: top;text-align: center; padding: 10px')
span(style='font-family: Arial,Helvetica Neue,Helvetica,sans-serif; color:#f5ab35; font-size:20px; font-weight: bold;')
| #{header}
tr
td(valign='top' style='vertical-align: top; background: #161d23; padding:10px;')
span(style='font-family: Arial,Helvetica Neue,Helvetica,sans-serif; color:#cecece; font-size:16px;')
| #{content}
tr
td(valign='top' style='vertical-align: top; padding: 10px;')
span(style='font-family: Arial,Helvetica Neue,Helvetica,sans-serif; color:#b2cce5; font-size:12px;')
| #{footer}

View file

@ -1,8 +0,0 @@
extends frame
block main-content
#error-index
br
label#message Ok, so this is... awkward
br
label#error= error

View file

@ -1,42 +0,0 @@
doctype html
html(xmlns='http://www.w3.org/1999/xhtml', lang='en', xml:lang="en")
head
title= title
meta(content="text/html;charset=utf-8", http-equiv="Content-Type")
meta(meta content="utf-8", http-equiv="encoding")
meta(name='viewport', content='width=device-width, initial-scale=1.0')
meta(name="keywords" content="")
meta(name="description" content="")
meta(http-equiv="content-type", content="text/html; charset=utf-8")
//meta(property="og:image" content="https://thetwelfth.house/base-assets/images/current.png")
//meta(name="twitter:image" content="https://thetwelfth.house/base-assets/images/current.png")
link(rel='stylesheet', href="/assets/css/dash.css", type='text/css')
body
#notifications.notifications
#notifyMessage.notifyMessage
.notify-icon#notify-good
svg(viewBox="0 0 20 20" class="icons")
use#submit-update(xlink:href='/assets/images/global/sprite.svg#entypo-emoji-flirt')
.notify-icon#notify-lame
svg(viewBox="0 0 20 20" class="icons")
use#submit-update(xlink:href='/assets/images/global/sprite.svg#entypo-emoji-sad')
.notify-icon#notify-working
#notify-working-box
svg(viewBox="0 0 20 20" class="icons")
use#submit-update(xlink:href='/assets/images/global/sprite.svg#entypo-cog')
p#message-text This is a message
.main-container#main-content
section#dash-index-content
header#header
#wrapper
#left
a(href="/@/dashboard")
img#the-logo(src="/assets/images/global/the-logo.svg")
#right
-if(status)
include partials/dash-nav
block main-content
//script(src='/assets/scripts/dashkit.min.js' type="text/javascript")
script(src='/assets/scripts/dash.min.js' type="text/javascript")

View file

@ -1,9 +0,0 @@
extends frame
block main-content
#dash-index
#dash-index-wrapper
-if(!status)
include partials/login
-else
include partials/front

View file

@ -1,52 +0,0 @@
extends frame
block main-content
#dash-index
#dash-index-wrapper
.dash-init#dash-init
br
form#init-form
h1 What up.
p Just fill these in and it'll get you started.
label What's your handle?
br
input.large(type='text', name='new_member_handle' id='new_member_handle', placeholder="What\'s your handle?")
br
label Let's get that email
br
input.large(type='text', name='new_member_email' id='new_member_email', placeholder="Email Please")
br
label Let's get a password
br
input.large(type='password', name='new_member_pass' id='new_member_pass', placeholder="Password Please")
br
label And let's confirm that password
br
input.large(type='password', name='new_member_pass2' id='new_member_pass2', placeholder="Password Confirm")
br
label And finally, a title
br
input.large(type='text', name='new_member_title' id='new_member_title', placeholder="Site Title Please")
br
button#init-blog(data-action='blog-init' type='submit') SET IT UP
.option
button.init-option#init-switch-restore OR RESTORE FROM BACKUP
.dash-restore#dash-restore
form#init-restore
h1 Restore backup.
p Let's verify your backup
label What's your handle?
br
input.large(type='text', name='restore_member_handle' id='restore_member_handle', placeholder="What\'s your handle?")
br
label Let's get a password
br
input.large(type='password', name='restore_member_pass' id='restore_member_pass', placeholder="Password Please")
br
label Backup File
br
input(id="backup-upload" type="file" name="backup-upload")
br
button#blog-restore(data-action='blog-restore' type='submit') RESTORE
.option
button.init-option#init-switch-fresh OR INSTALL FRESH SITE

View file

@ -1,16 +0,0 @@
extends frame
block main-content
#nav-index
#nav-index-wrapper
#nav-pages
- var index = 0;
- for ( index; index < menu.length; index++)
.nav-item(id=menu[index].id, data-slug=menu[index].slug, data-uuid=menu[index].uuid, data-path=menu[index].path)
svg#item-arrows(viewBox="0 0 20 20" class="icons")
use(xlink:href='/assets/images/global/sprite.svg#entypo-select-arrows')
label
= menu[index].title
#nav-btns
button.nav-btn#edit-item(data-id=menu[index].uuid) EDIT
button.nav-btn#remove-item(data-id=menu[index].id, data-uuid=menu[index].uuid) REMOVE

View file

@ -1,57 +0,0 @@
extends frame
block main-content
#post-edit-index(data-index=id data-uuid=uuid data-layout=layout data-slug=page.slug)
#post-edit-index-wrapper
//h2 EDIT
=post_title
#post-feature
//label FEATURE IMAGE
if(post_feature == 'null')
#featured-image-drop
| DRAG AND DROP IMAGE OR
label(for="featured-image-upload") CLICK TO CHOOSE
else
#featured-new-image-btn
button#new-feature-upload
svg#new-feature-upload(viewBox="0 0 20 20" class="icons")
use(xlink:href='/assets/images/global/sprite.svg#entypo-image-inverted')
#featured-image-drop
img#featured-image(src=page.feature)
#post-header
#post-header-wrapper.columns
#post-title.column
label TITLE
textarea(id="post_title" type='text', name='post_title' class='post-edit', placeholder='title', required, autofocus)
=page.title
label CREATED
br
span(id="post-date" type="text" value=date data-raw=rawDate)= date
//#calendar-icon
svg(viewBox="0 0 20 20" class="icons")
use(xlink:href='/assets/images/global/sprite.svg#entypo-calendar')
//input(id="post-date" type="text" value=date data-raw=rawDate)
#post-meta.column
label TAGS
textarea(id='post_tags' type='text', name='post_tags' class='form-control', placeholder='tags [comma seperated]', autofocus)
=page.tags
// file inputs for image uploads
label OPTIONS
br
include partials/options
input(id="featured-image-upload" type="file" name="featured-image-upload")
input(id="post-image-upload" type="file" name="post-image-upload")
#edit-post
include partials/editor
#edit-post-wrapper
pre
code#edit-post-text(contenteditable="true") !{content}

View file

@ -1,7 +0,0 @@
#dash-menu
a#settings(href="/@/dashboard/settings") Settings
| .
a#navigation(href="/@/dashboard/navigation") Navigation
| .
a#navigation(href="/@/dashboard/logout") Log Out

View file

@ -1,37 +0,0 @@
#edit-control
button#edit-bold.content-editor-btn-text.editor-button(title="bold")
| B
button#edit-italic.content-editor-btn-text.editor-button(title="italics")
| I
button#edit-strikethrough.content-editor-btn-text.editor-button(title="strikethrough")
| S
button#edit-link.content-editor-btn-icon.editor-button(title="insert link")
svg#edit-link(viewBox="0 0 20 20" class="icons")
use#edit-link(xlink:href='/assets/images/global/sprite.svg#entypo-link')
button#edit-header1.content-editor-btn-text.editor-button(title="header 1")
| H1
button#edit-header2.content-editor-btn-text.editor-button(title="header 2")
| H2
button#edit-header3.content-editor-btn-text.editor-button(title="header 3")
| H3
button#edit-image.content-editor-btn-icon.editor-button(title='insert image')
svg#edit-image(viewBox="0 0 20 20" class="icons")
use#edit-image(xlink:href='/assets/images/global/sprite.svg#entypo-image')
if(edit)
button#edit-update.post-sumbit-btn.submit-start.editor-button(data-action='blog-update' data-id=page.id type='submit')
svg#submit-update(viewBox="0 0 20 20" class="icons")
use#submit-update(xlink:href='/assets/images/global/sprite.svg#entypo-save' data-action='blog-update' data-id=page.id)
svg#submit-good.icon-hide(viewBox="0 0 20 20" class="icons")
use(xlink:href='/assets/images/global/sprite.svg#entypo-thumbs-up')
svg#submit-error.icon-hide(viewBox="0 0 20 20" class="icons")
use(xlink:href='/assets/images/global/sprite.svg#entypo-thumbs-down')
button#edit-delete.content-editor-btn-icon.editor-button.submit-delete(for="post-delete" title='delete post')
svg#edit-delete(viewBox="0 0 20 20" class="icons")
use#edit-delete(xlink:href='/assets/images/global/sprite.svg#entypo-cross')
else
button#edit-save.post-sumbit-btn.submit-start.editor-button(data-action='blog-add' type='submit')
svg#submit-save(viewBox="0 0 20 20" class="icons")
use#submit-save(xlink:href='/assets/images/global/sprite.svg#entypo-plus' data-action='blog-add')

View file

@ -1,30 +0,0 @@
#dash-recent
#recent-list
.recent-header
h3 Recent
.index-menu
a(href='/@/dashboard/page/list') View Pages
| .
a(href='/@/dashboard/page/add/new') Create Page
br
- var index = 0;
- var cap = 5; // number of posts to display, get rid of this and put it in the config
if(pages.length == 0)
label this is empty
else
- for ( index; index < pages.length; index++)
a.post-link(href="/@/dashboard/page/edit/"+pages[index].page.metadata.uuid id=pages[index].page.metadata.uuid style="background:url("+pages[index].page.metadata.feature+") no-repeat center center / cover")
div
label= pages[index].page.metadata.title
#options
- var menu = String(pages[index].page.metadata.menu)
- var published = String(pages[index].page.metadata.published)
- var featured = String(pages[index].page.metadata.featured)
span.item-options(data-active=menu) MENU ITEM
span.item-options(data-active=published) PUBLISHED
span.item-options(data-active=featured) FEATURED
span= pages[index].date

View file

@ -1,7 +0,0 @@
#dash-login
.dash-form#dash-form
form(id="login" class='login', name="login" action="/@/dashboard/login" method="POST")
input(type='text', name='handle' class='form-control', placeholder='handle', required, autofocus)
input(type='password', name='password' class='form-control', placeholder='password', required)
button(id="login-btn", class='login-btn', type='submit') SUBMIT YOUR STUFF

Some files were not shown because too many files have changed in this diff Show more