diff --git a/db/.gitkeep b/db/.gitkeep
new file mode 100644
index 0000000..e69de29
diff --git a/db/seed/posts.json b/db/seed/posts.json
new file mode 100644
index 0000000..a16ac35
--- /dev/null
+++ b/db/seed/posts.json
@@ -0,0 +1,29 @@
+{
+ "projects": [
+ {
+ "name": "Blog",
+ "readableIdentifier": "blog",
+ "posts": [
+ {
+ "title": "Test Post",
+ "content": "# Hello \nthis is some **test** markdown and we make some edits\n",
+ "description": "A new post to test the blog system",
+ "project_id": 1,
+ "user_id": 1,
+ "buckets": [
+ {
+ "id": "788dfc19-55ba-482c-8124-277702296dfb",
+ "attachments": [
+ {
+ "path": "FB_IMG_1716665756868.jpg"
+ },{
+ "path": "patchnotes.jpg"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/db/seed/posts/FB_IMG_1716665756868.jpg b/db/seed/posts/FB_IMG_1716665756868.jpg
new file mode 100644
index 0000000..3fb6d8e
Binary files /dev/null and b/db/seed/posts/FB_IMG_1716665756868.jpg differ
diff --git a/db/seed/posts/patchnotes.png b/db/seed/posts/patchnotes.png
new file mode 100644
index 0000000..716a0fe
Binary files /dev/null and b/db/seed/posts/patchnotes.png differ
diff --git a/db/seed/users.json b/db/seed/users.json
new file mode 100644
index 0000000..71ad0ef
--- /dev/null
+++ b/db/seed/users.json
@@ -0,0 +1,21 @@
+{
+ "users": [{
+ "username": "admin",
+ "password": "changeme",
+ "perms": {
+ "isAdmin": true
+ }
+ },{
+ "username": "notadmin",
+ "password": "changeme",
+ "perms": {
+ "isAdmin": false
+ }
+ },{
+ "username": "theodore",
+ "password": "changeme",
+ "perms": {
+ "isAdmin": false
+ }
+ }]
+}
\ No newline at end of file
diff --git a/src/app/api/setupDB/route.ts b/src/app/api/setupDB/route.ts
index bb1bfe6..1063b23 100644
--- a/src/app/api/setupDB/route.ts
+++ b/src/app/api/setupDB/route.ts
@@ -5,38 +5,109 @@ import { cookies } from "next/headers";
import { APIError} from "@/util/api/error"
import { UserAuth, parseBasicAuth, getAssociatedUser } from "@/util/api/user"
import { Attachment, Auth, Bucket, DBState, Post, PostTag, Project, Tag, User, UserPerms,dbSync,sequelize} from "@/models";
-import Sequelize, { DataTypes } from "@sequelize/core";
+import Sequelize, { CreationAttributes, DataTypes } from "@sequelize/core";
import { SqliteColumnsDescription, SqliteDialect, SqliteQueryInterface } from "@sequelize/sqlite3";
import { hashPassword } from "@/util/Auth";
+import { copyFile, readFileSync } from "fs";
+import path from "path";
+import { Attributes } from '@sequelize/core';
+import { DeepPartial } from "@/util/DeepPartial";
+import { UUID } from "crypto";
+async function seedUsers(qif: SqliteQueryInterface){
+ const fp = path.resolve('./db/seed/users.json');
+ const json: {users: CreationAttributes[]} = JSON.parse(Buffer.from(readFileSync(fp).valueOf()).toString());
-async function seedDatabase(queryInterface:SqliteQueryInterface){
+ const users = json.users.map(async user=>{
+ user.password = await hashPassword(user.password);
+ return user;
+ })
- const password = await hashPassword('changeme');
+ const dbUsers = await User.bulkCreate(await Promise.all(users), {include: User.associations.perms})
+}
+
+async function seedPosts(qif: SqliteQueryInterface){
+ const fp = path.resolve('./db/seed/posts.json');
+ const json: {users: CreationAttributes[]} = JSON.parse(Buffer.from(readFileSync(fp).valueOf()).toString());
+ const projects =[
+ {
+ "name": "Blog",
+ "readableIdentifier": "blog",
+ "posts": [
+ {
+ "title": "Test Post",
+ "content": "# Hello \nthis is some **test** markdown and we make some edits\n",
+ "description": "A new post to test the blog system",
+ "project_id": 1,
+ "user_id": 1,
+ "buckets": [
+ {
+ "id": "788dfc19-55ba-482c-8124-277702296dfb",
+ "attachments": [
+ {
+ "filename": "FB_IMG_1716665756868.jpg"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+ }]
+ projects.map(project=>{
+ Project.create({name: project.name, readableIdentifier: project.readableIdentifier});
+ project.posts.map(async post=>{
+ const pst = await Post.create({title:post.title, content:post.content, description:post.description, project_id: post.project_id, user_id: post.user_id});
+ post.buckets.map(async bucket=>{
+ pst.createBucket({id:bucket.id as UUID});
+ bucket.attachments.map(attachment=>{
+ Attachment.create({bucket_id:bucket.id as UUID, filename: attachment.filename}).then((a)=>{
+ copyFile(
+ path.resolve('.','db','seed','post',a.filename),
+ path.resolve('.','bucket',bucket.id,a.filename),
+ ()=>{
+
+ }
+ )
+ })
+ })
+ })
+ })
+ })
+
+
+
+
const project = await Project.findOne({where: {
readableIdentifier: 'blog'
}}).then(e=> e ? e : Project.create({name:'General Blog',readableIdentifier:'blog'}));
- const user = await User.findOne({where: {
- username: 'admin'
- }}).then(e=> e ? e : User.create({username: 'admin', password: password, perms: {isAdmin: true}}, {include: User.associations.perms}));
- await Post.create({
- title: 'Test Post',
- content: `
- # Hello
- this is some **test** markdown
- `,
- project_id: project.id,
- user_id: user.id
- })
- await Post.create({
- title: 'Test Post 2',
- content: `
- # Hello
- this is amother post with some **test** markdown
- `,
- project_id: project.id,
- user_id: user.id
- })
+}
+
+
+
+
+
+async function seedDatabase(qif:SqliteQueryInterface){
+ await seedUsers(qif);
+ await seedPosts(qif)
+
+ // await Post.create({
+ // title: 'Test Post',
+ // content: `
+ // # Hello
+ // this is some **test** markdown
+ // `,
+ // project_id: project.id,
+ // user_id: user.id
+ // })
+ // await Post.create({
+ // title: 'Test Post 2',
+ // content: `
+ // # Hello
+ // this is amother post with some **test** markdown
+ // `,
+ // project_id: project.id,
+ // user_id: user.id
+ // })
}
async function trySetup(request:Request){
diff --git a/src/app/attachment/[...slug]/route.ts b/src/app/attachment/[...slug]/route.ts
index 299aabd..441a789 100644
--- a/src/app/attachment/[...slug]/route.ts
+++ b/src/app/attachment/[...slug]/route.ts
@@ -1,8 +1,30 @@
+import { Attachment, Bucket, dbSync } from "@/models";
import { open, openSync, readFileSync } from "fs";
import { NextRequest, NextResponse } from "next/server";
import path from "path";
-export async function GET(req:NextRequest, { params }: {params:{slug: string}}){
+export async function GET(req:NextRequest, { params }: {params:{slug: string[]}}){
+ await dbSync;
+ const filename: `${string}${'.'}${string}` = params.slug[params.slug.length-1] as `${string}${'.'}${string}`;
+ const bucket = await Bucket.findOne({
+ where: {
+ id:params.slug[0],
+ },
+ include: [{
+ association: Bucket.associations.attachments,
+ where: {
+ filename: params.slug[params.slug.length-1]
+ }
+ }]
+ },)
+ console.log(params.slug);
+ if(!bucket || !bucket.attachments || !bucket.attachments[0] || bucket.attachments[0].filename != params.slug[params.slug.length-1]){
+ const headers = new Headers();
+ headers.set("Content-Type", "application/json");
+ return new NextResponse(JSON.stringify({
+ error:"Access Denied"
+ }),{headers:headers});
+ }
const fp = path.resolve('.',`bucket`,...params.slug);
return new Response(readFileSync(fp));
}
\ No newline at end of file
diff --git a/src/app/lib/actions/entityManagement/postActions.ts b/src/app/lib/actions/entityManagement/postActions.ts
index 3eda9ff..ed881fb 100644
--- a/src/app/lib/actions/entityManagement/postActions.ts
+++ b/src/app/lib/actions/entityManagement/postActions.ts
@@ -70,4 +70,13 @@ export async function updatePost(postAttributes: Partial>): Pro
if(! await userIsAdmin()) return {error:"Unauthorized, not updating Post."}
const post = await Post.update(postAttributes, {where:{id:postAttributes.id}});
return {result:JSON.parse(JSON.stringify(post))};
-}
\ No newline at end of file
+}
+
+export type PostServerActions = {
+ deletePost: (id: number) => Promise>;
+ getPosts: () => Promise>;
+ getProjects: () => Promise[]>>;
+ savePost: (
+ data: Partial>
+ ) => Promise[]>>;
+};
diff --git a/src/components/client/admin/PostEditor.tsx b/src/components/client/admin/PostEditor.tsx
index 7923ad6..3e27c66 100644
--- a/src/components/client/admin/PostEditor.tsx
+++ b/src/components/client/admin/PostEditor.tsx
@@ -2,6 +2,7 @@ import { GetPostsAttributes } from "@/app/lib/actions/entityManagement/postActio
import { Post, Project, Bucket, PostBucket, Attachment } from "@/models";
import { Attributes } from "@sequelize/core";
import { UUID } from "crypto";
+import { EntityEditorTextArea } from '../input/EntityEditorTextArea';
import {
ChangeEventHandler,
MouseEventHandler,
@@ -49,23 +50,6 @@ export default function PostEditor({
let [postProjectIDState, setPostProjectIDState] = useState(
editorPost.project.id
);
- let textbox: any = useRef(undefined);
-
- // Autosize the text area
- function textAreaAutoSize(
- textbox: MutableRefObject
- ): void {
- if (!textbox.current || !textbox.current.style) return;
- textbox.current.style.height = "fit-content";
- textbox.current.style.height = `${textbox.current.scrollHeight}px`;
- }
- useLayoutEffect(() => textAreaAutoSize(textbox));
-
- // Handle user input on the text area by updating state and autosizing the textfield
- const onTextAreaChange: ChangeEventHandler = (e) => {
- setPostContentState(e.target.value); // Update State
- textAreaAutoSize(textbox); // Autosize the text area
- };
// Handle changing the selected project using the dropdown select
const projectSelectionChange: ChangeEventHandler = (e) =>
@@ -102,14 +86,14 @@ export default function PostEditor({
Content
-
+
+
Project
diff --git a/src/components/client/admin/PostTable.tsx b/src/components/client/admin/PostTable.tsx
index 09971d9..e3f39e4 100644
--- a/src/components/client/admin/PostTable.tsx
+++ b/src/components/client/admin/PostTable.tsx
@@ -1,38 +1,28 @@
"use client";
-import React, { ReactNode } from "react";
+import React from "react";
import EntityManagementTable from "../EntityManagementTable";
import toast from "react-hot-toast";
import { EditorRenderer, EditorState, PostTableCallbacks } from "./PostEditor";
-import { Attributes } from "@sequelize/core";
-import { useState } from "react";
-import { Project, Post, Bucket } from "@/models";
-import { ActionResult } from "@/app/lib/actions/ActionResult";
+import { Attributes, InferAttributes } from "@sequelize/core";
+import { Project, Post, Bucket, User, PostAttributesWithBuckets } from "@/models";
import { handleActionResult } from "@/app/lib/actions/clientActionHandler";
import {
getPostsWithBucketsAndProject,
GetPostsAttributes,
+ PostServerActions,
} from "@/app/lib/actions/entityManagement/postActions";
+import { PostViewProps } from "@/views/admin/ClientPostView";
+import { aifa } from "@/util/Utils";
+import { StateHook } from "../../../util/types/StateHook";
-export type PostTableServerActions = {
- deletePost: (id: number) => Promise>;
- getPosts: () => Promise>;
- getProjects: () => Promise[]>>;
- savePost: (
- data: Partial>
- ) => Promise[]>>;
- // uploadAttachment: ()=> Promise>
-};
+export type PostTableStateProps = {
+ posts:StateHook,
+ editor:StateHook
+}
-type PostTableProps = {
- children?: ReactNode;
- headings: Array;
- posts: GetPostsAttributes[];
- projects: Attributes[];
- actions: PostTableServerActions;
-};
+export type PostTableProps = PostViewProps & {state:PostTableStateProps};
-const aifa = (a: ReactNode, b: ReactNode) => (a ? a : b);
export default function PostTable({
children,
@@ -40,26 +30,20 @@ export default function PostTable({
posts,
projects,
actions,
+ state,
}: PostTableProps) {
- // Init editor state. Make sure it is not opened by default
- const initEditorState: EditorState = {
- isEditorOpen: false,
- editorPost: posts[0],
- };
- // Set up required state hooks
- const [postsState, setPostsState] = useState(posts);
- const [editorState, setEditorState] = useState(initEditorState);
+
// Define editor controls
const editorControls = {
closeEditor: () => {
- setEditorState({
+ state.editor.setState({
isEditorOpen: false,
editorPost: posts[0],
});
},
showEditor: (entry: GetPostsAttributes) => {
- setEditorState({
+ state.editor.setState({
isEditorOpen: true,
editorPost: entry,
});
@@ -72,8 +56,8 @@ export default function PostTable({
actions.deletePost(entry.id).then((actionResult) => {
const result = handleActionResult(actionResult);
if (!result) return;
- postsState.splice(postsState.indexOf(entry), 1);
- setPostsState([...postsState]);
+ state.posts.state.splice(state.posts.state.indexOf(entry), 1);
+ state.posts.setState([...state.posts.state]);
toast.success("Removed Post:" + entry.id);
});
},
@@ -86,7 +70,7 @@ export default function PostTable({
getPostsServerActionResult
);
// Set Posts state
- if (result) setPostsState(result);
+ if (result) state.posts.setState(result);
});
},
savePost: (e: Partial>) => {
@@ -101,26 +85,19 @@ export default function PostTable({
.then(getPostsWithBucketsAndProject)
.then((res) => {
const result = handleActionResult(res);
- if (result) setPostsState(result);
+ if (result) state.posts.setState(result);
})
.then(editorControls.closeEditor)
.then(postActions.refetch);
},
};
- const callbacks: PostTableCallbacks = {
- savePost: postActions.savePost,
- closeEditor: editorControls.closeEditor,
- refetch: postActions.refetch,
- uploadAttachment: () => {},
- };
-
return (
- {postsState.map((post: GetPostsAttributes) => (
+ {state.posts.state.map((post: GetPostsAttributes) => (
@@ -177,8 +154,13 @@ export default function PostTable({
headings={headings}
editorPost={post}
editorControls={editorControls}
- editorState={editorState}
- callbacks={callbacks}
+ editorState={state.editor.state}
+ callbacks={{
+ savePost: postActions.savePost,
+ closeEditor: editorControls.closeEditor,
+ refetch: postActions.refetch,
+ uploadAttachment: () => {},
+ }}
projects={projects}
/>
diff --git a/src/components/client/input/EntityEditorTextArea.tsx b/src/components/client/input/EntityEditorTextArea.tsx
new file mode 100644
index 0000000..41dd33f
--- /dev/null
+++ b/src/components/client/input/EntityEditorTextArea.tsx
@@ -0,0 +1,34 @@
+'use client'
+
+import { useRef, MutableRefObject, useLayoutEffect, ChangeEventHandler, useState } from "react";
+import { StateHook } from '../../../util/types/StateHook';
+
+export function EntityEditorTextArea({contentHook, className}: {contentHook:StateHook, className:string}){
+
+ let textbox: any = useRef(undefined);
+
+ // Autosize the text area
+ function textAreaAutoSize(
+ textbox: MutableRefObject
+ ): void {
+ if (!textbox.current || !textbox.current.style) return;
+ textbox.current.style.height = "fit-content";
+ textbox.current.style.height = `${textbox.current.scrollHeight}px`;
+ }
+ useLayoutEffect(() => textAreaAutoSize(textbox));
+
+ // Handle user input on the text area by updating state and autosizing the textfield
+ const onTextAreaChange: ChangeEventHandler = (e) => {
+ contentHook?.setState(e.target.value); // Update State
+ textAreaAutoSize(textbox); // Autosize the text area
+ };
+
+ return
+}
\ No newline at end of file
diff --git a/src/models/User.ts b/src/models/User.ts
index 57d70f6..362ac60 100644
--- a/src/models/User.ts
+++ b/src/models/User.ts
@@ -29,6 +29,7 @@ export class User extends Model, InferCreationAttributes;
@Attribute(DataTypes.STRING)
+ @Unique()
declare username: string;
@Attribute(DataTypes.STRING)
declare password: string;
diff --git a/src/util/Utils.ts b/src/util/Utils.ts
index 4088702..4f3cf09 100644
--- a/src/util/Utils.ts
+++ b/src/util/Utils.ts
@@ -1,4 +1,5 @@
'server only'
+import { ReactNode } from "react";
import Gens from "./gens";
// import Auth from "./Auth";
@@ -28,4 +29,5 @@ function truncateString(str:string = '', num:number = 255) {
}
-export { Gens, constructAPIUrl, constructUrl, truncateString }
\ No newline at end of file
+export { Gens, constructAPIUrl, constructUrl, truncateString }
+export const aifa = (a: ReactNode, b: ReactNode) => (a ? a : b);
diff --git a/src/util/api/user.ts b/src/util/api/user.ts
index a56f4de..34343ec 100644
--- a/src/util/api/user.ts
+++ b/src/util/api/user.ts
@@ -3,7 +3,7 @@
import { validatePassword } from "@/util/Auth";
import { APIError } from "@/util/api/error";
-import { User } from "@/model/User";
+import { User } from "@/models";
export function parseBasicAuth(authb64:string):UserAuth
{
diff --git a/src/util/types/StateHook.tsx b/src/util/types/StateHook.tsx
new file mode 100644
index 0000000..c7350d8
--- /dev/null
+++ b/src/util/types/StateHook.tsx
@@ -0,0 +1,9 @@
+import { Dispatch, SetStateAction } from "react";
+
+export type StateHook = {
+ state: T;
+ setState: Dispatch>;
+};
+export type StateHookArray = [
+ T,Dispatch>
+]
\ No newline at end of file
diff --git a/src/views/admin/ClientPostView.tsx b/src/views/admin/ClientPostView.tsx
new file mode 100644
index 0000000..8509695
--- /dev/null
+++ b/src/views/admin/ClientPostView.tsx
@@ -0,0 +1,62 @@
+"use client";
+
+import PostTable, {
+ PostTableStateProps,
+} from "@/components/client/admin/PostTable";
+import { PostTableProps } from "../../components/client/admin/PostTable";
+import {
+ GetPostsAttributes,
+ PostServerActions,
+} from "@/app/lib/actions/entityManagement/postActions";
+import { EditorState } from "@/components/client/admin/PostEditor";
+import { Project } from "@/models";
+import { Dispatch, ReactNode, SetStateAction, useState } from "react";
+import { Attributes } from "@sequelize/core";
+import { StateHookArray } from "@/util/types/StateHook";
+
+export type PostViewProps = {
+ children?: ReactNode;
+ headings: Array;
+ posts: GetPostsAttributes[];
+ projects: Attributes[];
+ actions: PostServerActions;
+};
+
+export function ClientPostView({
+ children,
+ headings,
+ posts,
+ projects,
+ actions,
+}: PostViewProps) {
+ // Init editor state. Make sure it is not opened by default
+ const initEditorState: EditorState = {
+ isEditorOpen: false,
+ editorPost: posts[0],
+ };
+ // Set up required state hooks
+ const [postsState, setPostsState]: StateHookArray = useState(posts);
+ const [editorState, setEditorState]: StateHookArray = useState(initEditorState);
+ const state: PostTableStateProps = {
+ posts: {
+ state: postsState,
+ setState: setPostsState,
+ },
+ editor: {
+ state: editorState,
+ setState: setEditorState,
+ },
+ };
+
+ return (
+ <>
+
+ >
+ );
+}
diff --git a/src/views/admin/PostView.tsx b/src/views/admin/PostView.tsx
index 4182cad..8df5241 100644
--- a/src/views/admin/PostView.tsx
+++ b/src/views/admin/PostView.tsx
@@ -2,25 +2,29 @@
cache: "no-store";
import { ReactNode, useEffect } from "react";
-import PostTable, {
- PostTableServerActions,
-} from "@/components/client/admin/PostTable";
+import PostTable from "@/components/client/admin/PostTable";
import {
deletePost,
getPostsWithBucketsAndProject,
- GetPostsAttributes,
updatePost,
+ GetPostsAttributes,
+ PostServerActions,
} from "@/app/lib/actions/entityManagement/postActions";
import { getProjects } from "@/app/lib/actions/entityManagement/projectActions";
import { Bucket, Project, Post, dbSync, Attachment } from "@/models";
import { handleActionResult } from "../../app/lib/actions/clientActionHandler";
+import { ClientPostView } from "./ClientPostView";
+import { readFileSync } from "fs";
+import path from "path";
type Props = {
children?: ReactNode;
};
-async function getHeadings() {
- let headings: string[] = [
+export default async function PostView(props: Props) {
+ const sync = await dbSync;
+
+ const headings:string[] = [
"#",
"Title",
"Content",
@@ -29,15 +33,7 @@ async function getHeadings() {
"UpdatedAt",
];
- return headings;
-}
-
-export default async function PostView(props: Props) {
- const sync = await dbSync;
-
- const headings = await getHeadings();
-
- const actions: PostTableServerActions = {
+ const actions: PostServerActions = {
deletePost: deletePost,
getPosts: getPostsWithBucketsAndProject,
getProjects: getProjects,
@@ -55,12 +51,12 @@ export default async function PostView(props: Props) {
return (
);
diff --git a/src/views/admin/ProjectView.tsx b/src/views/admin/ProjectView.tsx
index 192c765..b150399 100644
--- a/src/views/admin/ProjectView.tsx
+++ b/src/views/admin/ProjectView.tsx
@@ -1,64 +1,70 @@
-cache: 'no-store'
+cache: "no-store";
import { tryFetchPosts } from "@/app/api/post/route";
import { constructAPIUrl } from "@/util/Utils";
import { ReactNode, useEffect } from "react";
import EntityManagementTable from "../../components/client/EntityManagementTable";
import PostTable from "@/components/client/admin/PostTable";
-import { deletePost, getPostsWithBucketsAndProject, updatePost } from "@/app/lib/actions/entityManagement/postActions";
+import {
+ deletePost,
+ getPostsWithBucketsAndProject,
+ updatePost,
+} from "@/app/lib/actions/entityManagement/postActions";
import { getProjects } from "@/app/lib/actions/entityManagement/projectActions";
import { Bucket, Project, Post, dbSync, Attachment } from "@/models";
import { tryCreateAttachment } from "@/app/api/attachment/route";
-import { Attributes } from '@sequelize/core';
-import * as util from 'util'
+import { Attributes } from "@sequelize/core";
+import * as util from "util";
type Props = {
- children?:ReactNode
+ children?: ReactNode;
+};
+
+async function getHeadings() {
+ let headings: string[] = [];
+ for (const key in Project.getAttributes()) {
+ headings.push(key);
+ }
+ return headings;
}
-async function getHeadings(){
- let headings:string[] = []
- for (const key in Project.getAttributes())
- {
- headings.push(key)
- }
- return headings
-}
+export default async function ProjectView(props: Props) {
+ const sync = await dbSync;
-export default async function ProjectView(props:Props){
- const sync = await dbSync;
-
- // const headings = [
- // '#',
- // 'Title',
- // 'Content',
- // 'Project',
- // 'Date Created',
- // 'Date Modified',
- // 'Edit',
- // 'Delete',
- // ]
- // const actions = {
- // deletePost: deletePost,
- // getPosts:getPostsWithBuckets,
- // getProjects:getProjects,
- // savePost:updatePost,
- // // uploadAttachment:tryCreateAttachment
- // };
-
- const posts:Post[] = await Post.findAll({include: {model: Bucket, include: {model: Attachment}}}).then(posts=>posts.map((e)=>JSON.parse(JSON.stringify(e))));
- const projects = await Project.findAll().then(projects=>projects.map((e)=>JSON.parse(JSON.stringify(e))));
- const headings = await getHeadings();
- return <>
-
-
- <>>
- {/*
+ projects.map((e) => JSON.parse(JSON.stringify(e)))
+ );
+ return (
+ <>
+
- >;
-}
\ No newline at end of file
+
+
+ >
+ );
+}
|