mirror of
https://github.com/AtHeartEngineer/FreedInk.git
synced 2026-01-09 19:37:54 -05:00
crude working version without a review process complete
This commit is contained in:
100
db_definition.sql
Normal file
100
db_definition.sql
Normal file
@@ -0,0 +1,100 @@
|
||||
create table
|
||||
public."Users" (
|
||||
id bigint generated by default as identity not null,
|
||||
created_at timestamp with time zone not null default now(),
|
||||
address text not null,
|
||||
username text null,
|
||||
idc text null,
|
||||
public_key text null,
|
||||
constraint Users_pkey primary key (id),
|
||||
constraint Users_address_key unique (address),
|
||||
constraint Users_username_key unique (username)
|
||||
) tablespace pg_default;
|
||||
|
||||
create table
|
||||
public."Blogs" (
|
||||
id uuid not null default gen_random_uuid (),
|
||||
created_at timestamp with time zone not null default now(),
|
||||
title text not null,
|
||||
description text null,
|
||||
slug text not null,
|
||||
constraint Blogs_pkey primary key (id),
|
||||
constraint Blogs_slug_key unique (slug),
|
||||
constraint Blogs_title_key unique (title),
|
||||
constraint Blogs_slug_check check ((length(slug) < 500))
|
||||
) tablespace pg_default;
|
||||
|
||||
create table
|
||||
public."BlogPosts" (
|
||||
id bigint generated by default as identity not null,
|
||||
created_at timestamp with time zone not null default now(),
|
||||
blog_id uuid not null,
|
||||
current_version bigint null,
|
||||
status public.post_status not null default 'draft'::post_status,
|
||||
constraint BlogPosts_pkey primary key (id),
|
||||
constraint BlogPosts_blog_id_fkey foreign key (blog_id) references "Blogs" (id) on update cascade on delete cascade,
|
||||
constraint BlogPosts_current_version_fkey foreign key (current_version) references "BlogPostVersions" (id) on update cascade on delete cascade
|
||||
) tablespace pg_default;
|
||||
|
||||
create table
|
||||
public."BlogPostVersions" (
|
||||
id bigint generated by default as identity not null,
|
||||
created_at timestamp with time zone not null default now(),
|
||||
post_id bigint not null,
|
||||
title text not null,
|
||||
content text not null,
|
||||
version text not null,
|
||||
proof json not null,
|
||||
slug text not null,
|
||||
published_at timestamp with time zone null,
|
||||
status public.post_status not null default 'draft'::post_status,
|
||||
constraint BlogPostVersions_pkey primary key (id),
|
||||
constraint BlogPostVersions_post_id_fkey foreign key (post_id) references "BlogPosts" (id) on update cascade on delete cascade
|
||||
) tablespace pg_default;
|
||||
|
||||
create table
|
||||
public."BlogPostVersionReview" (
|
||||
id bigint generated by default as identity not null,
|
||||
created_at timestamp with time zone not null default now(),
|
||||
post_version_id bigint not null,
|
||||
approved boolean not null default false,
|
||||
proof json not null,
|
||||
comment text null,
|
||||
constraint BlogPostVersionReview_pkey primary key (id),
|
||||
constraint BlogPostVersionReview_post_version_id_fkey foreign key (post_version_id) references "BlogPostVersions" (id) on update cascade on delete cascade
|
||||
) tablespace pg_default;
|
||||
|
||||
create table
|
||||
public."BlogOwners" (
|
||||
id bigint generated by default as identity not null,
|
||||
created_at timestamp with time zone not null default now(),
|
||||
blog_id uuid not null,
|
||||
owner_id bigint not null,
|
||||
constraint BlogOwners_pkey primary key (id),
|
||||
constraint BlogOwners_blog_id_fkey foreign key (blog_id) references "Blogs" (id) on update cascade on delete cascade,
|
||||
constraint BlogOwners_owner_id_fkey foreign key (owner_id) references "Users" (id) on update cascade on delete cascade
|
||||
) tablespace pg_default;
|
||||
|
||||
create table
|
||||
public."BlogAuthors" (
|
||||
id bigint generated by default as identity not null,
|
||||
created_at timestamp with time zone not null default now(),
|
||||
blog_id uuid not null,
|
||||
author_id bigint not null,
|
||||
constraint BlogAuthors_pkey primary key (id),
|
||||
constraint BlogAuthors_author_id_fkey foreign key (author_id) references "Users" (id) on update cascade on delete cascade,
|
||||
constraint BlogAuthors_blog_id_fkey foreign key (blog_id) references "Blogs" (id) on update cascade on delete cascade
|
||||
) tablespace pg_default;
|
||||
|
||||
create table
|
||||
public."BlogReviewers" (
|
||||
id bigint generated by default as identity not null,
|
||||
created_at timestamp with time zone not null default now(),
|
||||
blog_id uuid not null,
|
||||
reviewer_id bigint not null,
|
||||
constraint BlogReviewers_pkey primary key (id),
|
||||
constraint BlogReviewers_blog_id_fkey foreign key (blog_id) references "Blogs" (id) on update cascade on delete cascade,
|
||||
constraint BlogReviewers_reviewer_id_fkey foreign key (reviewer_id) references "Users" (id) on update cascade on delete cascade
|
||||
) tablespace pg_default;
|
||||
|
||||
public enum post_status (draft, under_review, published, rejected)
|
||||
15
package-lock.json
generated
15
package-lock.json
generated
@@ -9,8 +9,11 @@
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@semaphore-protocol/core": "^4.0.3",
|
||||
"@semaphore-protocol/group": "^4.0.3",
|
||||
"@semaphore-protocol/proof": "^4.0.3",
|
||||
"@supabase/supabase-js": "^2.45.3",
|
||||
"ethers": "^6.13.2",
|
||||
"poseidon-lite": "^0.3.0",
|
||||
"siwe": "^2.3.2",
|
||||
"uuid": "^10.0.0"
|
||||
},
|
||||
@@ -1072,6 +1075,12 @@
|
||||
"ieee754": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@semaphore-protocol/identity/node_modules/poseidon-lite": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/poseidon-lite/-/poseidon-lite-0.2.0.tgz",
|
||||
"integrity": "sha512-vivDZnGmz8W4G/GzVA72PXkfYStjilu83rjjUfpL4PueKcC8nfX6hCPh2XhoC5FBgC6y0TA3YuUeUo5YCcNoig==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@semaphore-protocol/proof": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@semaphore-protocol/proof/-/proof-4.0.3.tgz",
|
||||
@@ -4637,9 +4646,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/poseidon-lite": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/poseidon-lite/-/poseidon-lite-0.2.0.tgz",
|
||||
"integrity": "sha512-vivDZnGmz8W4G/GzVA72PXkfYStjilu83rjjUfpL4PueKcC8nfX6hCPh2XhoC5FBgC6y0TA3YuUeUo5YCcNoig==",
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/poseidon-lite/-/poseidon-lite-0.3.0.tgz",
|
||||
"integrity": "sha512-ilJj4MIve4uBEG7SrtPqUUNkvpJ/pLVbndxa0WvebcQqeIhe+h72JR4g0EvwchUzm9sOQDlOjiDNmRAgxNZl4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/possible-typed-array-names": {
|
||||
|
||||
@@ -33,8 +33,11 @@
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@semaphore-protocol/core": "^4.0.3",
|
||||
"@semaphore-protocol/group": "^4.0.3",
|
||||
"@semaphore-protocol/proof": "^4.0.3",
|
||||
"@supabase/supabase-js": "^2.45.3",
|
||||
"ethers": "^6.13.2",
|
||||
"poseidon-lite": "^0.3.0",
|
||||
"siwe": "^2.3.2",
|
||||
"uuid": "^10.0.0"
|
||||
}
|
||||
|
||||
@@ -11,15 +11,18 @@ export const handle: Handle = async ({ event, resolve }) => {
|
||||
const sessionData = getSession(sessionId);
|
||||
if (sessionData) {
|
||||
event.locals.siwe = sessionData.siwe; // Store session data in locals for use in the app
|
||||
console.log('Session data successfully retrieved and set in locals:', event.locals.siwe);
|
||||
console.debug(
|
||||
'Session data successfully retrieved and set in locals',
|
||||
event.locals.siwe.address
|
||||
);
|
||||
} else {
|
||||
console.log('Session ID found in cookie but no session data found in store.');
|
||||
console.debug('Session ID found in cookie but no session data found in store.', sessionId);
|
||||
}
|
||||
} else {
|
||||
// If no session ID is found, create a new one and set it in the cookies
|
||||
sessionId = uuid();
|
||||
event.cookies.set('session_id', sessionId, { path: '/' });
|
||||
console.log('New session ID created and set in cookie:', sessionId);
|
||||
console.debug('New session ID created and set in cookie:', sessionId);
|
||||
}
|
||||
|
||||
// Proceed with request handling
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
if (data.new_user) {
|
||||
goto('/signup/profile');
|
||||
} else {
|
||||
window.location.href = '/dashboard';
|
||||
window.location.href = '/admin';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,22 +1,156 @@
|
||||
import { supabase } from '$lib/supabaseClient';
|
||||
import { sluggify } from '$lib/utils';
|
||||
import { getUserByAddress } from './users';
|
||||
|
||||
export const getAllBlogs = async () => {
|
||||
export async function getAllBlogs() {
|
||||
console.debug('getAllBlogs');
|
||||
const { data } = await supabase.from('Blogs').select();
|
||||
return {
|
||||
Blogs: data ?? []
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export const getBlogBySlug = async (slug: string) => {
|
||||
export async function getBlogBySlug(slug: string) {
|
||||
console.debug('getBlogBySlug', slug);
|
||||
const { data: blogData, error: blogError } = await supabase
|
||||
.from('Blogs')
|
||||
.select()
|
||||
.eq('slug', slug)
|
||||
.single();
|
||||
|
||||
if (blogError) {
|
||||
console.error('Error fetching blog:', blogError);
|
||||
return null;
|
||||
}
|
||||
return blogData;
|
||||
};
|
||||
}
|
||||
|
||||
export async function getOwnedBlogsByAddress(address: string) {
|
||||
console.debug('getOwnedBlogsByAddress', address);
|
||||
const userData = await getUserByAddress(address);
|
||||
const userId = userData.id;
|
||||
|
||||
const { data: blogs, error: blogError } = await supabase
|
||||
.from('BlogOwners')
|
||||
.select('blog_id, Blogs (title, slug)')
|
||||
.eq('owner_id', userId);
|
||||
|
||||
if (blogError) {
|
||||
console.error('Error fetching owned blogs:', blogError);
|
||||
return [];
|
||||
}
|
||||
return blogs.map((blog) => blog.Blogs);
|
||||
}
|
||||
|
||||
export async function getAuthoredBlogsByAddress(address: string) {
|
||||
console.debug('getAuthoredBlogsByAddress', address);
|
||||
const userData = await getUserByAddress(address);
|
||||
const userId = userData.id;
|
||||
|
||||
const { data: blogs, error: blogError } = await supabase
|
||||
.from('BlogAuthors')
|
||||
.select('blog_id, Blogs (title, slug)')
|
||||
.eq('author_id', userId);
|
||||
|
||||
if (blogError) {
|
||||
console.error('Error fetching owned blogs:', blogError);
|
||||
return [];
|
||||
}
|
||||
return blogs.map((blog) => blog.Blogs);
|
||||
}
|
||||
|
||||
export async function getReviewedBlogsByAddress(address: string) {
|
||||
console.debug('getReviewedBlogsByAddress', address);
|
||||
const userData = await getUserByAddress(address);
|
||||
const userId = userData.id;
|
||||
|
||||
const { data: blogs, error: blogError } = await supabase
|
||||
.from('BlogReviewers')
|
||||
.select('blog_id, Blogs (title, slug)')
|
||||
.eq('reviewer_id', userId);
|
||||
|
||||
if (blogError) {
|
||||
console.error('Error fetching owned blogs:', blogError);
|
||||
return [];
|
||||
}
|
||||
return blogs.map((blog) => blog.Blogs);
|
||||
}
|
||||
|
||||
export async function createBlog(address: string, title: string, description: string) {
|
||||
console.debug('createBlog', address, title);
|
||||
const userData = await getUserByAddress(address);
|
||||
const userId = userData.id;
|
||||
const slug = sluggify(title);
|
||||
|
||||
const { data: blogData, error: blogError } = await supabase
|
||||
.from('Blogs')
|
||||
.insert([{ title, description, slug }])
|
||||
.select('id')
|
||||
.single();
|
||||
|
||||
if (blogError) {
|
||||
console.error('Error creating blog:', blogError);
|
||||
return { success: false, message: 'Error creating blog.' };
|
||||
}
|
||||
|
||||
const blogId = blogData.id;
|
||||
|
||||
const { error: ownerError } = await supabase
|
||||
.from('BlogOwners')
|
||||
.insert([{ blog_id: blogId, owner_id: userId }]);
|
||||
|
||||
if (ownerError) {
|
||||
console.error('Error setting blog owner:', ownerError);
|
||||
return { success: false, message: 'Error setting blog owner.' };
|
||||
}
|
||||
|
||||
return { success: true, message: 'Blog created and owner set successfully.', slug };
|
||||
}
|
||||
|
||||
export async function getAuthorIDCs(blog_id: string) {
|
||||
try {
|
||||
// Fetch owners
|
||||
const { data: owners, error: ownersError } = await supabase
|
||||
.from('BlogOwners')
|
||||
.select('owner_id')
|
||||
.eq('blog_id', blog_id);
|
||||
|
||||
if (ownersError) throw new Error(`Error fetching owners: ${ownersError.message}`);
|
||||
|
||||
// Fetch authors
|
||||
const { data: authors, error: authorsError } = await supabase
|
||||
.from('BlogAuthors')
|
||||
.select('author_id')
|
||||
.eq('blog_id', blog_id);
|
||||
|
||||
if (authorsError) throw new Error(`Error fetching authors: ${authorsError.message}`);
|
||||
|
||||
// Fetch reviewers
|
||||
const { data: reviewers, error: reviewersError } = await supabase
|
||||
.from('BlogReviewers')
|
||||
.select('reviewer_id')
|
||||
.eq('blog_id', blog_id);
|
||||
|
||||
if (reviewersError) throw new Error(`Error fetching reviewers: ${reviewersError.message}`);
|
||||
|
||||
// Combine all the user IDs
|
||||
const userIds = [
|
||||
...owners.map((owner) => owner.owner_id),
|
||||
...authors.map((author) => author.author_id),
|
||||
...reviewers.map((reviewer) => reviewer.reviewer_id)
|
||||
];
|
||||
|
||||
// Fetch the users based on the combined IDs
|
||||
const { data: users, error: usersError } = await supabase
|
||||
.from('Users')
|
||||
.select('idc, created_at')
|
||||
.in('id', userIds)
|
||||
.order('created_at', { ascending: true });
|
||||
|
||||
if (usersError) throw new Error(`Error fetching users: ${usersError.message}`);
|
||||
|
||||
return users.map((user) => user.idc);
|
||||
} catch (error) {
|
||||
console.error('Error fetching IDCs:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,102 +1,272 @@
|
||||
import { supabase } from '$lib/supabaseClient';
|
||||
import { getBlogBySlug } from './blogs';
|
||||
import { getAuthorIDCs, getBlogBySlug } from './blogs';
|
||||
import { getBlogAuthors, getBlogOwners } from './roles';
|
||||
import { getUserByAddress } from './users';
|
||||
import { getBlogReviewers } from '$lib/db/roles';
|
||||
import { sluggify } from '$lib/utils';
|
||||
import { verifyProof, type SemaphoreProof } from '@semaphore-protocol/proof';
|
||||
import { Group } from '@semaphore-protocol/group';
|
||||
|
||||
/**
|
||||
* Fetch all posts for a specific blog based on the blog's slug.
|
||||
* @param blog_slug The slug of the blog.
|
||||
* @returns Blog details and an array of blog posts.
|
||||
*/
|
||||
export async function getBlogPosts(blog_slug: string) {
|
||||
console.log('slug:', blog_slug);
|
||||
// First, retrieve the blog's ID using the slug
|
||||
const blogData = await getBlogBySlug(blog_slug);
|
||||
console.debug('getBlogPosts', blog_slug);
|
||||
try {
|
||||
// Retrieve the blog's ID and details using the slug
|
||||
const blogData = await getBlogBySlug(blog_slug);
|
||||
|
||||
// Now, retrieve the blog posts for the fetched blog ID
|
||||
const { data: postsData, error: postsError } = await supabase
|
||||
.from('BlogPosts')
|
||||
.select(
|
||||
if (!blogData) {
|
||||
console.error(`Blog with slug ${blog_slug} not found.`);
|
||||
return null;
|
||||
}
|
||||
console.log('getting all authors');
|
||||
const authors = await getBlogAuthors(blogData.id);
|
||||
const reviewers = await getBlogReviewers(blogData.id, '', true);
|
||||
const owners = await getBlogOwners(blogData.id, '', true);
|
||||
const all_authors = [
|
||||
...(authors?.authors ?? []).map((author) => author.username),
|
||||
...(reviewers?.reviewers ?? []).map((reviewer) => reviewer.username),
|
||||
...(owners?.owners ?? []).map((owner) => owner.username)
|
||||
].sort();
|
||||
console.log(authors, reviewers, owners, all_authors);
|
||||
|
||||
// Fetch blog posts for the specified blog ID
|
||||
const { data: postsData, error: postsError } = await supabase
|
||||
.from('BlogPosts')
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
created_at,
|
||||
blog_id,
|
||||
status,
|
||||
current_version,
|
||||
BlogPostVersions:BlogPosts_current_version_fkey (
|
||||
title,
|
||||
content,
|
||||
slug
|
||||
)
|
||||
`
|
||||
id,
|
||||
created_at,
|
||||
blog_id,
|
||||
status,
|
||||
current_version,
|
||||
BlogPostVersions:BlogPosts_current_version_fkey (
|
||||
title,
|
||||
content,
|
||||
slug
|
||||
)
|
||||
`
|
||||
)
|
||||
.eq('blog_id', blogData?.id)
|
||||
.order('created_at', { ascending: true });
|
||||
.eq('blog_id', blogData.id)
|
||||
.order('created_at', { ascending: true });
|
||||
|
||||
if (postsError) {
|
||||
console.error('Error fetching blog posts:', postsError);
|
||||
if (postsError) throw postsError;
|
||||
|
||||
// Flatten the posts to simplify data structure
|
||||
const flattenedPosts = postsData.map((post) => ({
|
||||
title: post.BlogPostVersions?.title,
|
||||
content: post.BlogPostVersions?.content,
|
||||
slug: post.BlogPostVersions?.slug,
|
||||
status: post.status
|
||||
}));
|
||||
|
||||
return {
|
||||
Blog: {
|
||||
title: blogData.title,
|
||||
slug: blog_slug,
|
||||
description: blogData.description,
|
||||
authors: all_authors
|
||||
},
|
||||
Posts: flattenedPosts ?? []
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching blog posts:', error);
|
||||
return null;
|
||||
}
|
||||
|
||||
const flattenedPosts = postsData.map((post) => ({
|
||||
title: post.BlogPostVersions?.title,
|
||||
content: post.BlogPostVersions?.content,
|
||||
slug: post.BlogPostVersions?.slug,
|
||||
status: post.status
|
||||
}));
|
||||
//.filter((post) => post.status === 'published');
|
||||
|
||||
return {
|
||||
Blog: { title: blogData?.title, slug: blog_slug, description: blogData?.description },
|
||||
Posts: flattenedPosts ?? []
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single blog post by the blog's slug and the post's slug.
|
||||
* @param blog_slug The slug of the blog.
|
||||
* @param post_slug The slug of the blog post.
|
||||
* @returns Blog details and the specific blog post.
|
||||
*/
|
||||
export async function getBlogPost(blog_slug: string, post_slug: string) {
|
||||
// Step 1: Retrieve the blog's ID using the blog slug
|
||||
const { data: blogData, error: blogError } = await supabase
|
||||
.from('Blogs')
|
||||
.select('id, title')
|
||||
.eq('slug', blog_slug)
|
||||
.single(); // Expecting a single blog with the given slug
|
||||
try {
|
||||
// Retrieve the blog's ID and title using the blog slug
|
||||
const { data: blogData, error: blogError } = await supabase
|
||||
.from('Blogs')
|
||||
.select('id, title')
|
||||
.eq('slug', blog_slug)
|
||||
.single();
|
||||
|
||||
if (blogError) {
|
||||
console.error('Error fetching blog:', blogError);
|
||||
if (blogError) throw blogError;
|
||||
if (!blogData) {
|
||||
console.error(`Blog with slug ${blog_slug} not found.`);
|
||||
return null;
|
||||
}
|
||||
// Retrieve the post details using the blog and post slugs
|
||||
const { data: postData, error: postError } = await supabase
|
||||
.from('BlogPosts')
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
created_at,
|
||||
blog_id,
|
||||
status,
|
||||
BlogPostVersions:BlogPosts_current_version_fkey (
|
||||
title,
|
||||
content,
|
||||
slug
|
||||
)
|
||||
`
|
||||
)
|
||||
.eq('blog_id', blogData.id)
|
||||
.eq('BlogPostVersions.slug', post_slug)
|
||||
.single();
|
||||
|
||||
if (postError) throw postError;
|
||||
|
||||
// Flatten the post data
|
||||
const post = {
|
||||
id: postData.id,
|
||||
created_at: postData.created_at,
|
||||
blog_id: postData.blog_id,
|
||||
status: postData.status,
|
||||
title: postData.BlogPostVersions?.title,
|
||||
content: postData.BlogPostVersions?.content,
|
||||
slug: postData.BlogPostVersions?.slug
|
||||
};
|
||||
|
||||
// Return the blog and post details
|
||||
return {
|
||||
Blog: { title: blogData.title, slug: blog_slug },
|
||||
Post: post
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching blog post:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Retrieve the specific blog post by post slug and blog ID
|
||||
export async function getUnderReviewPostsByReviewer(address: string) {
|
||||
try {
|
||||
const userData = await getUserByAddress(address);
|
||||
|
||||
const reviewerId = userData.id;
|
||||
|
||||
// Step 2: Fetch the blog IDs where the user is a reviewer
|
||||
const { data: blogReviewerData, error: blogReviewerError } = await supabase
|
||||
.from('BlogReviewers')
|
||||
.select('blog_id')
|
||||
.eq('reviewer_id', reviewerId);
|
||||
|
||||
if (blogReviewerError || !blogReviewerData.length) {
|
||||
throw new Error(`No blogs found where the address ${address} is a reviewer.`);
|
||||
}
|
||||
|
||||
const blogIds = blogReviewerData.map((entry) => entry.blog_id);
|
||||
|
||||
// Step 3: Fetch all blog posts that are in 'under_review' status and belong to the blogs where the user is a reviewer
|
||||
const { data: postsData, error: postsError } = await supabase
|
||||
.from('BlogPosts')
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
created_at,
|
||||
blog_id,
|
||||
status,
|
||||
BlogPostVersions!inner (
|
||||
id,
|
||||
title,
|
||||
content,
|
||||
version,
|
||||
slug,
|
||||
status
|
||||
)
|
||||
`
|
||||
)
|
||||
.in('blog_id', blogIds)
|
||||
.eq('status', 'under_review') // BlogPost must be under review
|
||||
.eq('BlogPostVersions.status', 'under_review') // Fetch the BlogPostVersion that is under review
|
||||
.order('created_at', { ascending: true });
|
||||
|
||||
if (postsError) {
|
||||
throw new Error(`Error fetching under-review blog posts: ${postsError.message}`);
|
||||
}
|
||||
|
||||
// Step 4: Flatten the data for ease of use in the frontend
|
||||
const flattenedPosts = postsData.map((post) => ({
|
||||
id: post.id,
|
||||
created_at: post.created_at,
|
||||
blog_id: post.blog_id,
|
||||
status: post.status,
|
||||
version: {
|
||||
id: post.BlogPostVersions.id,
|
||||
title: post.BlogPostVersions.title,
|
||||
content: post.BlogPostVersions.content,
|
||||
slug: post.BlogPostVersions.slug,
|
||||
status: post.BlogPostVersions.status
|
||||
}
|
||||
}));
|
||||
|
||||
return flattenedPosts;
|
||||
} catch (error) {
|
||||
console.error('Error fetching under-review posts by reviewer:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createBlogPost(
|
||||
blog_id: string,
|
||||
title: string,
|
||||
content: string,
|
||||
proof: object = {}
|
||||
) {
|
||||
const slug = sluggify(title);
|
||||
const verified = await verifyProof(proof as SemaphoreProof);
|
||||
console.warn('verified', verified);
|
||||
const { data: postData, error: postError } = await supabase
|
||||
.from('BlogPosts')
|
||||
.select(
|
||||
`
|
||||
id,
|
||||
created_at,
|
||||
blog_id,
|
||||
status,
|
||||
BlogPostVersions:BlogPosts_current_version_fkey (
|
||||
title,
|
||||
content,
|
||||
slug
|
||||
)
|
||||
`
|
||||
)
|
||||
.eq('blog_id', blogData.id)
|
||||
.eq('BlogPostVersions.slug', post_slug) // Filter by post slug
|
||||
.single(); // Expecting a single post with the given slug
|
||||
.insert([
|
||||
{
|
||||
blog_id: blog_id,
|
||||
status: 'under_review'
|
||||
}
|
||||
])
|
||||
.select();
|
||||
|
||||
if (postError) {
|
||||
console.error('Error fetching blog post:', postError);
|
||||
console.error('Error creating blog post:', postError);
|
||||
return null;
|
||||
}
|
||||
console.debug('Create post but havent created post version yet');
|
||||
|
||||
const post_id = postData[0].id;
|
||||
|
||||
const { data: versionData, error: versionError } = await supabase
|
||||
.from('BlogPostVersions')
|
||||
.insert([
|
||||
{
|
||||
post_id: post_id,
|
||||
title: title,
|
||||
content: content,
|
||||
version: 1,
|
||||
slug: slug,
|
||||
proof: proof,
|
||||
status: 'under_review'
|
||||
}
|
||||
])
|
||||
.select();
|
||||
|
||||
if (versionError) {
|
||||
console.error('Error creating blog post version:', versionError);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Step 3: Flatten the data to remove the relationship object
|
||||
const post = {
|
||||
id: postData.id,
|
||||
created_at: postData.created_at,
|
||||
blog_id: postData.blog_id,
|
||||
status: postData.status,
|
||||
title: postData.BlogPostVersions?.title,
|
||||
content: postData.BlogPostVersions?.content,
|
||||
slug: postData.BlogPostVersions?.slug
|
||||
};
|
||||
const { error: postUpdateError } = await supabase
|
||||
.from('BlogPosts')
|
||||
.update({ current_version: versionData[0].id })
|
||||
.eq('id', post_id)
|
||||
.select();
|
||||
|
||||
// Step 4: Return the blog and post details
|
||||
return {
|
||||
Blog: { title: blogData.title, slug: blog_slug },
|
||||
Post: post
|
||||
};
|
||||
if (postUpdateError) {
|
||||
console.error('Error updating blog post:', postUpdateError);
|
||||
return null;
|
||||
}
|
||||
|
||||
return { post: postData[0], version: versionData[0] };
|
||||
}
|
||||
|
||||
300
src/lib/db/roles.ts
Normal file
300
src/lib/db/roles.ts
Normal file
@@ -0,0 +1,300 @@
|
||||
import { supabase } from '$lib/supabaseClient';
|
||||
import { getUserByAddress } from './users';
|
||||
|
||||
export async function isOwner(blog_id: string, owner_address: string): Promise<boolean> {
|
||||
console.debug('isOwner?', owner_address);
|
||||
const user = await getUserByAddress(owner_address);
|
||||
const { data } = await supabase
|
||||
.from('BlogOwners')
|
||||
.select('id')
|
||||
.eq('blog_id', blog_id)
|
||||
.eq('owner_id', user.id);
|
||||
return data ? data.length > 0 : false;
|
||||
}
|
||||
|
||||
export async function isAuthor(blog_id: string, author_address: string): Promise<boolean> {
|
||||
console.debug('isAuthor?', author_address);
|
||||
const user = await getUserByAddress(author_address);
|
||||
const { data } = await supabase
|
||||
.from('BlogAuthors')
|
||||
.select('id')
|
||||
.eq('blog_id', blog_id)
|
||||
.eq('author_id', user.id);
|
||||
return data ? data.length > 0 : false;
|
||||
}
|
||||
|
||||
export async function isReviewer(blog_id: string, reviewer_address: string): Promise<boolean> {
|
||||
console.debug('isReviewer?', blog_id, reviewer_address);
|
||||
const user = await getUserByAddress(reviewer_address);
|
||||
const { data } = await supabase
|
||||
.from('BlogReviewers')
|
||||
.select('id')
|
||||
.eq('blog_id', blog_id)
|
||||
.eq('reviewer_id', user.id);
|
||||
return data ? data.length > 0 : false;
|
||||
}
|
||||
|
||||
async function isFirstOwner(blog_id: string, owner_address: string): Promise<boolean> {
|
||||
console.debug('isFirstOwner?', owner_address);
|
||||
const user = await getUserByAddress(owner_address);
|
||||
const { data, error } = await supabase
|
||||
.from('BlogOwners')
|
||||
.select('owner_id')
|
||||
.eq('blog_id', blog_id)
|
||||
.order('created_at', { ascending: true })
|
||||
.limit(1)
|
||||
.single();
|
||||
|
||||
if (error || !data) {
|
||||
console.error('Error fetching first owner or no owner found:', error);
|
||||
return false;
|
||||
}
|
||||
|
||||
return data.owner_id === user.id;
|
||||
}
|
||||
|
||||
export async function addOwner(blog_id: string, owner_address: string, new_owner_address: string) {
|
||||
console.debug('addOwner', blog_id, owner_address, new_owner_address);
|
||||
|
||||
const isCurrentOwner = await isFirstOwner(blog_id, owner_address);
|
||||
const new_user = await getUserByAddress(new_owner_address);
|
||||
if (!isCurrentOwner) {
|
||||
throw new Error('Only the original blog owner can add new owners.');
|
||||
}
|
||||
|
||||
await removeAuthor(blog_id, owner_address, new_owner_address);
|
||||
await removeReviewer(blog_id, owner_address, new_owner_address);
|
||||
|
||||
const { error } = await supabase.from('BlogOwners').insert([{ blog_id, owner_id: new_user.id }]);
|
||||
|
||||
if (error) {
|
||||
console.error('Error adding new owner:', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeOwner(
|
||||
blog_id: string,
|
||||
owner_address: string,
|
||||
address_to_remove: string,
|
||||
bypassCheck = false
|
||||
) {
|
||||
console.debug('removeOwner', blog_id, owner_address, address_to_remove);
|
||||
|
||||
if (owner_address === address_to_remove) {
|
||||
throw new Error("You can't remove yourself as an owner.");
|
||||
}
|
||||
const owner = await getUserByAddress(owner_address);
|
||||
if (!bypassCheck) {
|
||||
const isCurrentOwner = await isFirstOwner(blog_id, owner_address);
|
||||
if (!isCurrentOwner) {
|
||||
throw new Error('Only the original blog owner can remove other owners.');
|
||||
}
|
||||
}
|
||||
|
||||
const { data: currentOwnerData, error: currentOwnerError } = await supabase
|
||||
.from('BlogOwners')
|
||||
.select('created_at')
|
||||
.eq('blog_id', blog_id)
|
||||
.eq('owner_id', owner.id)
|
||||
.single();
|
||||
|
||||
if (currentOwnerError) {
|
||||
throw new Error('Error fetching current owner information.');
|
||||
}
|
||||
|
||||
const { data: ownerToRemoveData, error: ownerToRemoveError } = await supabase
|
||||
.from('BlogOwners')
|
||||
.select('created_at')
|
||||
.eq('blog_id', blog_id)
|
||||
.eq('owner_id', owner.id)
|
||||
.single();
|
||||
|
||||
if (ownerToRemoveError) {
|
||||
throw new Error('Error fetching owner to remove information.');
|
||||
}
|
||||
|
||||
if (new Date(ownerToRemoveData.created_at) < new Date(currentOwnerData.created_at)) {
|
||||
throw new Error('You cannot remove an owner who was added before you.');
|
||||
}
|
||||
const user_to_remove = await getUserByAddress(address_to_remove);
|
||||
const { error: removeError } = await supabase
|
||||
.from('BlogOwners')
|
||||
.delete()
|
||||
.eq('blog_id', blog_id)
|
||||
.eq('owner_id', user_to_remove.id);
|
||||
|
||||
if (removeError) {
|
||||
console.error('Error removing owner:', removeError);
|
||||
throw new Error('Error removing owner.');
|
||||
}
|
||||
|
||||
console.debug('Owner removed successfully');
|
||||
}
|
||||
|
||||
export async function addAuthor(
|
||||
blog_id: string,
|
||||
owner_address: string,
|
||||
new_author_address: string
|
||||
) {
|
||||
console.debug('addAuthor', blog_id, owner_address, new_author_address);
|
||||
|
||||
const isCurrentOwner = await isOwner(blog_id, owner_address);
|
||||
const new_user = await getUserByAddress(new_author_address);
|
||||
if (!isCurrentOwner) {
|
||||
throw new Error('Only blog owners can add authors.');
|
||||
}
|
||||
|
||||
await removeOwner(blog_id, owner_address, new_author_address, true);
|
||||
await removeReviewer(blog_id, owner_address, new_author_address);
|
||||
|
||||
const { error } = await supabase
|
||||
.from('BlogAuthors')
|
||||
.insert([{ blog_id, author_id: new_user.id }]);
|
||||
|
||||
if (error) {
|
||||
console.error('Error adding new author:', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeAuthor(blog_id: string, owner_address: string, address: string) {
|
||||
console.debug('removeAuthor', blog_id, owner_address, address);
|
||||
|
||||
const isCurrentOwner = await isOwner(blog_id, owner_address);
|
||||
const user_to_remove = await getUserByAddress(address);
|
||||
if (!isCurrentOwner) {
|
||||
throw new Error('Only blog owners can remove authors.');
|
||||
}
|
||||
|
||||
const { error } = await supabase
|
||||
.from('BlogAuthors')
|
||||
.delete()
|
||||
.eq('blog_id', blog_id)
|
||||
.eq('author_id', user_to_remove.id);
|
||||
|
||||
if (error) {
|
||||
console.error('Error removing author:', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function addReviewer(
|
||||
blog_id: string,
|
||||
owner_address: string,
|
||||
new_reviewer_address: string
|
||||
) {
|
||||
console.debug('addReviewer', blog_id, owner_address, new_reviewer_address);
|
||||
|
||||
const isCurrentOwner = await isOwner(blog_id, owner_address);
|
||||
const new_reviewer = await getUserByAddress(new_reviewer_address);
|
||||
if (!isCurrentOwner) {
|
||||
throw new Error('Only blog owners can add reviewers.');
|
||||
}
|
||||
|
||||
await removeOwner(blog_id, owner_address, new_reviewer_address, true);
|
||||
await removeAuthor(blog_id, owner_address, new_reviewer_address);
|
||||
|
||||
const { error } = await supabase
|
||||
.from('BlogReviewers')
|
||||
.insert([{ blog_id, reviewer_id: new_reviewer.id }]);
|
||||
|
||||
if (error) {
|
||||
console.error('Error adding new reviewer:', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeReviewer(blog_id: string, owner_address: string, address: string) {
|
||||
console.debug('removeReviewer', blog_id, owner_address, address);
|
||||
|
||||
const isCurrentOwner = await isOwner(blog_id, owner_address);
|
||||
const user_to_remove = await getUserByAddress(address);
|
||||
if (!isCurrentOwner) {
|
||||
throw new Error('Only blog owners can remove reviewers.');
|
||||
}
|
||||
|
||||
const { error } = await supabase
|
||||
.from('BlogReviewers')
|
||||
.delete()
|
||||
.eq('blog_id', blog_id)
|
||||
.eq('reviewer_id', user_to_remove.id);
|
||||
|
||||
if (error) {
|
||||
console.error('Error removing reviewer:', error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getBlogOwners(blog_id: string, ownerAddress: string, bypassCheck = false) {
|
||||
console.debug('getBlogOwners', blog_id, ownerAddress);
|
||||
if (!bypassCheck) {
|
||||
const owner = await isOwner(blog_id, ownerAddress);
|
||||
|
||||
if (!owner) {
|
||||
return { success: false, message: 'Only owners can view the list of blog owners.' };
|
||||
}
|
||||
}
|
||||
const { data, error } = await supabase
|
||||
.from('BlogOwners')
|
||||
.select('Users(address, username)')
|
||||
.eq('blog_id', blog_id);
|
||||
|
||||
if (error) {
|
||||
return { success: false, message: `Error fetching blog owners: ${error.message}` };
|
||||
}
|
||||
|
||||
const owners = data.map((owner) => ({
|
||||
address: owner.Users.address,
|
||||
username: owner.Users.username
|
||||
}));
|
||||
console.debug('getBlogOwners owners', owners);
|
||||
return { success: true, owners };
|
||||
}
|
||||
|
||||
export async function getBlogAuthors(blog_id: string) {
|
||||
console.debug('getBlogAuthors', blog_id);
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('BlogAuthors')
|
||||
.select('Users(address, username)')
|
||||
.eq('blog_id', blog_id);
|
||||
|
||||
if (error) {
|
||||
return { success: false, message: `Error fetching blog authors: ${error.message}` };
|
||||
}
|
||||
|
||||
const authors = data.map((author) => ({
|
||||
address: author.Users.address,
|
||||
username: author.Users.username
|
||||
}));
|
||||
console.debug('getBlogAuthors authors', authors);
|
||||
return { success: true, authors };
|
||||
}
|
||||
|
||||
export async function getBlogReviewers(blog_id: string, address: string, bypassCheck = false) {
|
||||
console.debug('getBlogReviewers', blog_id, address);
|
||||
if (!bypassCheck) {
|
||||
const hasOwnerPermission = await isOwner(blog_id, address);
|
||||
const hasAuthorPermission = await isAuthor(blog_id, address);
|
||||
|
||||
if (!hasOwnerPermission && !hasAuthorPermission) {
|
||||
return {
|
||||
success: false,
|
||||
message: 'Only owners or authors can view the list of blog reviewers.'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('BlogReviewers')
|
||||
.select('Users(address, username)')
|
||||
.eq('blog_id', blog_id);
|
||||
|
||||
if (error) {
|
||||
return { success: false, message: `Error fetching blog reviewers: ${error.message}` };
|
||||
}
|
||||
|
||||
const reviewers = data.map((reviewer) => ({
|
||||
address: reviewer.Users.address,
|
||||
username: reviewer.Users.username
|
||||
}));
|
||||
console.debug('getBlogReviewers reviewers', reviewers);
|
||||
|
||||
return { success: true, reviewers };
|
||||
}
|
||||
@@ -1,42 +1,76 @@
|
||||
import { supabase } from '$lib/supabaseClient';
|
||||
import type { UserT } from './types';
|
||||
|
||||
export const getUserByAddress = async (address: string) => {
|
||||
const { data: userData, error } = await supabase
|
||||
.from('Users')
|
||||
.select()
|
||||
.eq('address', address)
|
||||
.single();
|
||||
/**
|
||||
* Get a user by Ethereum address.
|
||||
* @param address The user's Ethereum address.
|
||||
* @returns The user data or null if not found.
|
||||
*/
|
||||
export async function getUserByAddress(address: string) {
|
||||
console.debug('getUserByAddress', address);
|
||||
try {
|
||||
let { data: userData, error } = await supabase
|
||||
.from('Users')
|
||||
.select('*')
|
||||
.eq('address', address)
|
||||
.single();
|
||||
|
||||
console.log(userData);
|
||||
if (error) {
|
||||
console.error('Error fetching user:', error);
|
||||
if (error) {
|
||||
userData = await createUser(address);
|
||||
return userData ? userData : null;
|
||||
}
|
||||
|
||||
return userData;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return null;
|
||||
}
|
||||
return userData;
|
||||
};
|
||||
}
|
||||
|
||||
export const createUser = async (address: string) => {
|
||||
const { data, error } = await supabase.from('Users').insert([{ address }]);
|
||||
/**
|
||||
* Create a new user with the given Ethereum address.
|
||||
* @param address The user's Ethereum address.
|
||||
* @returns true if the user was created, otherwise null.
|
||||
*/
|
||||
export async function createUser(address: string) {
|
||||
console.debug('createUser', address);
|
||||
try {
|
||||
const { error } = await supabase.from('Users').insert([{ address }]);
|
||||
|
||||
if (error) {
|
||||
console.error('Error creating user:', error);
|
||||
if (error) throw new Error(`Error creating user: ${error.message}`);
|
||||
|
||||
console.info('User created', address);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return null;
|
||||
}
|
||||
console.info('User created:', address);
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
export const updateUser = async (address: string, update: UserT) => {
|
||||
const { data, error } = await supabase
|
||||
.from('Users')
|
||||
.update({ username: update.username, idc: update.idc, public_key: update.public_key })
|
||||
.eq('address', address);
|
||||
/**
|
||||
* Update a user's information based on their Ethereum address.
|
||||
* @param address The user's Ethereum address.
|
||||
* @param update An object containing the fields to update (username, idc, public_key).
|
||||
* @returns The updated address or null in case of an error.
|
||||
*/
|
||||
export async function updateUser(address: string, update: UserT) {
|
||||
console.debug('updateUser', address, update);
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('Users')
|
||||
.update({
|
||||
username: update.username,
|
||||
idc: update.idc,
|
||||
public_key: update.public_key
|
||||
})
|
||||
.eq('address', address);
|
||||
|
||||
if (error) {
|
||||
console.error('Error updating user:', error);
|
||||
if (error) throw new Error(`Error updating user: ${error.message}`);
|
||||
|
||||
console.debug('User updated', address);
|
||||
return address;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return null;
|
||||
}
|
||||
console.info('User updated:', address);
|
||||
return address;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
export function slug(name: string) {
|
||||
return name.toLowerCase().replace(/ /g, '-');
|
||||
export function sluggify(name: string) {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.replace(/ /g, '-') // Replace spaces with hyphens
|
||||
.replace(/[^a-z0-9-_]/g, ''); // Remove any non-alphanumeric character except hyphen and underscore
|
||||
}
|
||||
|
||||
export function unslug(slug: string) {
|
||||
return slug.replace(/-/g, ' ');
|
||||
}
|
||||
|
||||
export async function hashMessage(message: string) {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(message);
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
const hashHex = hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
|
||||
// first 32 bytes of the hash
|
||||
const hash = hashHex.slice(0, 31);
|
||||
console.log(hash);
|
||||
return hash;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ export const load = async ({ locals }) => {
|
||||
const address = siwe_state ? siwe_state.address : null;
|
||||
if (address) {
|
||||
const userExists: boolean = await getUserByAddress(address);
|
||||
console.log('User exists:', userExists);
|
||||
console.log('User Found', userExists.address);
|
||||
}
|
||||
return { address };
|
||||
};
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
<script lang="ts">
|
||||
import SIWE from '$lib/components/siwe.svelte';
|
||||
import { onMount } from 'svelte';
|
||||
export let data: string | null;
|
||||
</script>
|
||||
|
||||
<header>
|
||||
<nav>
|
||||
<a href="/" class="nav-title">Freed Ink</a>
|
||||
<SIWE address={data.address} />
|
||||
<div>
|
||||
<a href="/" class="nav-title">Freed.Ink</a>
|
||||
<a href="/b">Blogs</a>
|
||||
</div>
|
||||
<div>
|
||||
<a href="/admin">Admin Dashboard</a>
|
||||
<SIWE address={data.address} />
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
@@ -29,32 +34,43 @@
|
||||
:root {
|
||||
--heading-font: 'Source Code Pro', sans-serif;
|
||||
--text-font: 'Lato', sans-serif;
|
||||
--color-primary: #16423c;
|
||||
--color-secondary: #6a9c89;
|
||||
--color-light: #c4dad2;
|
||||
--color-lightest: #e9efec;
|
||||
--color-darkest: #0b1f1c;
|
||||
--text-color-primary: #0a0a0a;
|
||||
--color-green: hsl(172, 60%, 25%);
|
||||
--color-green-light: hsl(172, 42%, 50%);
|
||||
--color-green-lightest: hsl(172, 40%, 75%);
|
||||
--color-green-white: hsl(172, 60%, 95%);
|
||||
--color-green-dark: hsl(172, 84%, 15%);
|
||||
--color-red: hsl(354, 60%, 37%);
|
||||
--color-red-dark: hsl(354, 80%, 23%);
|
||||
--color-blue: hsl(215, 60%, 30%);
|
||||
--color-blue-light: hsl(215, 40%, 50%);
|
||||
--color-blue-lightest: hsl(215, 40%, 70%);
|
||||
--color-blue-dark: hsl(215, 70%, 20%);
|
||||
--color-purple: hsl(270, 60%, 30%);
|
||||
--color-purple-light: hsl(270, 40%, 50%);
|
||||
--color-purple-lightest: hsl(270, 40%, 75%);
|
||||
--color-purple-dark: hsl(270, 70%, 20%);
|
||||
|
||||
--text-color-green-darkest: #000a03;
|
||||
}
|
||||
|
||||
:global(body) {
|
||||
background-color: var(--color-lightest);
|
||||
background-color: var(--color-green-white);
|
||||
font-family: var(--text-font);
|
||||
color: var(--text-color-primary);
|
||||
color: var(--text-color-green-darkest);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
:global(a, a:visited) {
|
||||
color: var(--color-primary);
|
||||
color: var(--text-color-green-darkest);
|
||||
text-decoration: none;
|
||||
|
||||
text-shadow: 1px 1px 1px var(--color-light);
|
||||
text-shadow: 1px 1px 2px var(--color-green-lightest);
|
||||
}
|
||||
|
||||
:global(a:hover) {
|
||||
text-decoration: underline;
|
||||
text-shadow: 1px 1px 1px var(--color-secondary);
|
||||
text-shadow: 1px 1px 1px var(--color-green-lightest);
|
||||
}
|
||||
|
||||
:global(h1, h2, h3, h4, h5, h6, nav > a, button) {
|
||||
@@ -79,8 +95,8 @@
|
||||
}
|
||||
|
||||
nav {
|
||||
background-color: var(--color-primary);
|
||||
color: var(--color-light);
|
||||
background-color: var(--color-green-dark);
|
||||
color: var(--color-green-lightest);
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -94,22 +110,31 @@
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
text-shadow: 0px 0px 2px var(--color-secondary);
|
||||
text-shadow: 0px 0px 2px var(--color-green-lightest);
|
||||
}
|
||||
|
||||
nav div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
:global(button, .btn) {
|
||||
background-color: var(--color-secondary) !important;
|
||||
color: var(--color-lightest) !important;
|
||||
border: 1px solid var(--color-secondary);
|
||||
background-color: var(--color-green) !important;
|
||||
color: var(--color-green-white) !important;
|
||||
border: 1px solid var(--color-green-light);
|
||||
border-radius: 0.25rem;
|
||||
padding: 0.35rem 0.75rem;
|
||||
margin: 0 0.5rem;
|
||||
cursor: pointer;
|
||||
text-shadow: none;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:global(button:hover, .btn:hover) {
|
||||
background-color: var(--color-light) !important;
|
||||
color: var(--color-primary) !important;
|
||||
background-color: var(--color-green-light) !important;
|
||||
color: var(--color-green) !important;
|
||||
text-decoration: none;
|
||||
text-shadow: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
This is a blogging platform that is a bit different than others. We aim to construct blogs
|
||||
shaped around like minds who want to speak more freely, but with some weight behind it.
|
||||
</p>
|
||||
<p></p>
|
||||
<a href="/signup" class="btn btn-primary">Free Your Ink</a>
|
||||
</div>
|
||||
<div class="featured">
|
||||
@@ -26,10 +25,10 @@
|
||||
|
||||
<style>
|
||||
.jumbotron {
|
||||
padding: 4rem 2rem;
|
||||
padding: 4rem;
|
||||
margin-inline: auto;
|
||||
margin-bottom: 3rem;
|
||||
background-color: var(--color-light);
|
||||
background-color: var(--color-green-lightest);
|
||||
border-radius: 0.5rem;
|
||||
text-align: center;
|
||||
max-width: 120ch;
|
||||
@@ -57,6 +56,12 @@
|
||||
width: 100%;
|
||||
max-width: 120ch;
|
||||
}
|
||||
|
||||
p {
|
||||
padding-inline: 2rem;
|
||||
padding-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
31
src/routes/admin/+layout.server.ts
Normal file
31
src/routes/admin/+layout.server.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { error, redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import {
|
||||
getAuthoredBlogsByAddress,
|
||||
getOwnedBlogsByAddress,
|
||||
getReviewedBlogsByAddress
|
||||
} from '$lib/db/blogs';
|
||||
import { getUserByAddress } from '$lib/db/users';
|
||||
|
||||
export const load: PageServerLoad = async ({ locals }) => {
|
||||
if (!locals.siwe) {
|
||||
console.error('SIWE data not found in locals:', locals);
|
||||
throw redirect(303, '/'); // Redirect to login if not authenticated
|
||||
}
|
||||
|
||||
const address = locals.siwe.address;
|
||||
const user = await getUserByAddress(address);
|
||||
const ownedBlogs = await getOwnedBlogsByAddress(address);
|
||||
const authoredBlogs = await getAuthoredBlogsByAddress(address);
|
||||
const reviewedBlogs = await getReviewedBlogsByAddress(address);
|
||||
|
||||
const data = {
|
||||
address,
|
||||
user,
|
||||
ownedBlogs,
|
||||
authoredBlogs,
|
||||
reviewedBlogs
|
||||
};
|
||||
|
||||
return data;
|
||||
};
|
||||
34
src/routes/admin/+layout.svelte
Normal file
34
src/routes/admin/+layout.svelte
Normal file
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import { setContext } from 'svelte';
|
||||
export let data;
|
||||
setContext('address', data.address);
|
||||
setContext('user', data.user);
|
||||
setContext('owned', data.ownedBlogs);
|
||||
setContext('authored', data.authoredBlogs);
|
||||
setContext('reviewer', data.reviewedBlogs);
|
||||
|
||||
const username = data.user.username;
|
||||
</script>
|
||||
|
||||
<div id="header">
|
||||
<h3>Welcome {username}</h3>
|
||||
<nav>
|
||||
<a href="/admin/" class="btn">Dashboard</a>
|
||||
<a href="/admin/new" class="btn">+ Create a new blog</a>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<slot></slot>
|
||||
|
||||
<style>
|
||||
#header,
|
||||
nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#header {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
0
src/routes/admin/+page.server.ts
Normal file
0
src/routes/admin/+page.server.ts
Normal file
14
src/routes/admin/+page.svelte
Normal file
14
src/routes/admin/+page.svelte
Normal file
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import Owned from './owner.svelte';
|
||||
import Authored from './author.svelte';
|
||||
import Reviewer from './reviewer.svelte';
|
||||
import { getContext } from 'svelte';
|
||||
|
||||
const ownedBlogs = getContext('owned');
|
||||
const authoredBlogs = getContext('authored');
|
||||
const reviewedBlogs = getContext('reviewer');
|
||||
</script>
|
||||
|
||||
<Owned {ownedBlogs} />
|
||||
<Reviewer {reviewedBlogs} />
|
||||
<Authored {authoredBlogs} />
|
||||
14
src/routes/admin/author.svelte
Normal file
14
src/routes/admin/author.svelte
Normal file
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
export let authoredBlogs;
|
||||
</script>
|
||||
|
||||
<h3>Author</h3>
|
||||
<ul>
|
||||
{#if authoredBlogs.length === 0}
|
||||
<li>You aren't the author on any blogs yet.</li>
|
||||
{:else}
|
||||
{#each authoredBlogs as blog}
|
||||
<li><a href="/admin/b/{blog.slug}/author">{blog.title}</a></li>
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
||||
38
src/routes/admin/b/[blog]/+layout.server.ts
Normal file
38
src/routes/admin/b/[blog]/+layout.server.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { error, redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { getBlogBySlug } from '$lib/db/blogs';
|
||||
import { getUserByAddress } from '$lib/db/users';
|
||||
import { isAuthor, isOwner, isReviewer } from '$lib/db/roles';
|
||||
|
||||
export const load: PageServerLoad = async ({ locals, params }) => {
|
||||
console.log(params);
|
||||
if (!locals.siwe) {
|
||||
console.error('SIWE data not found in locals:', locals);
|
||||
throw redirect(303, '/'); // Redirect to login if not authenticated
|
||||
}
|
||||
|
||||
const address = locals.siwe.address;
|
||||
const user = await getUserByAddress(address);
|
||||
const blog_slug = params.blog;
|
||||
const blog = await getBlogBySlug(blog_slug);
|
||||
const blog_title = blog.title;
|
||||
const owner = await isOwner(blog.id, address);
|
||||
const reviewer = await isReviewer(blog.id, address);
|
||||
const author = await isAuthor(blog.id, address);
|
||||
console.log('owner', owner);
|
||||
console.log('reviewer', reviewer);
|
||||
console.log('author', author);
|
||||
if (!owner && !reviewer && !author) {
|
||||
console.error(
|
||||
'admin/b/blog/layout: User does not have any administrative access to',
|
||||
blog_title
|
||||
);
|
||||
throw redirect(303, '/admin'); // Redirect to login if not authenticated
|
||||
}
|
||||
const data = {
|
||||
address,
|
||||
user
|
||||
};
|
||||
|
||||
return data;
|
||||
};
|
||||
1
src/routes/admin/b/[blog]/+layout.svelte
Normal file
1
src/routes/admin/b/[blog]/+layout.svelte
Normal file
@@ -0,0 +1 @@
|
||||
<slot></slot>
|
||||
0
src/routes/admin/b/[blog]/+page.server.ts
Normal file
0
src/routes/admin/b/[blog]/+page.server.ts
Normal file
0
src/routes/admin/b/[blog]/+page.svelte
Normal file
0
src/routes/admin/b/[blog]/+page.svelte
Normal file
39
src/routes/admin/b/[blog]/author/+page.server.ts
Normal file
39
src/routes/admin/b/[blog]/author/+page.server.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { error, redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { isOwner, isReviewer, isAuthor } from '$lib/db/roles';
|
||||
import { getBlogBySlug } from '$lib/db/blogs';
|
||||
|
||||
export const load: PageServerLoad = async ({ locals, params }) => {
|
||||
if (!locals.siwe) {
|
||||
console.error('SIWE data not found in locals:', locals);
|
||||
throw redirect(303, '/'); // Redirect to login if not authenticated
|
||||
}
|
||||
|
||||
const address = locals.siwe.address;
|
||||
const blog_slug = params.blog;
|
||||
const blog = await getBlogBySlug(blog_slug);
|
||||
const blog_title = blog.title;
|
||||
const blog_id = blog.id;
|
||||
|
||||
const owner = await isOwner(blog.id, address);
|
||||
const reviewer = await isReviewer(blog.id, address);
|
||||
const author = await isAuthor(blog.id, address);
|
||||
console.log('owner', owner);
|
||||
console.log('reviewer', reviewer);
|
||||
console.log('author', author);
|
||||
if (!owner && !reviewer && !author) {
|
||||
console.error(
|
||||
'admin/b/blog/author User does not have any administrative access to',
|
||||
blog_title
|
||||
);
|
||||
throw redirect(303, '/admin'); // Redirect to login if not authenticated
|
||||
}
|
||||
|
||||
const data = {
|
||||
address,
|
||||
blog_title,
|
||||
blog_id
|
||||
};
|
||||
|
||||
return data;
|
||||
};
|
||||
100
src/routes/admin/b/[blog]/author/+page.svelte
Normal file
100
src/routes/admin/b/[blog]/author/+page.svelte
Normal file
@@ -0,0 +1,100 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { hashMessage, sluggify } from '$lib/utils';
|
||||
import { Identity } from '@semaphore-protocol/core';
|
||||
import { Group } from '@semaphore-protocol/group';
|
||||
import { generateProof } from '@semaphore-protocol/proof';
|
||||
|
||||
export let data;
|
||||
const blog_slug = sluggify(data.blog_title);
|
||||
$: title = '';
|
||||
$: titleSlug = sluggify(title);
|
||||
$: content = '';
|
||||
|
||||
async function createPost() {
|
||||
const group_res = await fetch(`/api/blog/group`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ blog_slug })
|
||||
});
|
||||
|
||||
if (group_res.ok) {
|
||||
console.log('Group fetched');
|
||||
const members = await group_res.json();
|
||||
const group = new Group(members);
|
||||
const identity_string = localStorage.getItem('semaphoreIdentity');
|
||||
const identity = new Identity(identity_string as string);
|
||||
const scope = group.root;
|
||||
const messageHash = await hashMessage(title + content);
|
||||
const proof = await generateProof(identity, group, messageHash, scope);
|
||||
const post_res = await fetch(`/api/blog/post`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ blog_slug, title, content, proof })
|
||||
});
|
||||
if (post_res.ok) {
|
||||
console.log('Post created');
|
||||
goto(`/admin/`);
|
||||
} else {
|
||||
console.error('Failed to create post');
|
||||
}
|
||||
} else {
|
||||
console.error('Failed to fetch group');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<h3>New post for: {data.blog_title}</h3>
|
||||
<form>
|
||||
<div id="title_wrapper">
|
||||
<div id="title">
|
||||
<label for="title">Post Title</label>
|
||||
<input type="text" id="title" name="title" bind:value={title} />
|
||||
</div>
|
||||
<div id="title_slug">
|
||||
<label for="title">Mock URL</label>
|
||||
<input type="text" bind:value={titleSlug} disabled />
|
||||
</div>
|
||||
</div>
|
||||
<div id="content">
|
||||
<label for="content">Enter the content of the new post:</label>
|
||||
<textarea id="content" name="content" bind:value={content}></textarea>
|
||||
</div>
|
||||
<button on:click={createPost} style="max-width: 20ch !important">Create Post</button>
|
||||
</form>
|
||||
|
||||
<style>
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
#title_wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
}
|
||||
#title,
|
||||
#title_slug,
|
||||
#title input,
|
||||
#title_slug input {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#content {
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
height: 10rem;
|
||||
}
|
||||
</style>
|
||||
37
src/routes/admin/b/[blog]/manage/+page.server.ts
Normal file
37
src/routes/admin/b/[blog]/manage/+page.server.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { error, redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { getBlogAuthors, getBlogOwners, getBlogReviewers, isOwner } from '$lib/db/roles';
|
||||
import { getBlogBySlug } from '$lib/db/blogs';
|
||||
|
||||
export const load: PageServerLoad = async ({ locals, params }) => {
|
||||
if (!locals.siwe) {
|
||||
console.error('SIWE data not found in locals:', locals);
|
||||
throw redirect(303, '/'); // Redirect to login if not authenticated
|
||||
}
|
||||
|
||||
const address = locals.siwe.address;
|
||||
const blog_slug = params.blog;
|
||||
const blog = await getBlogBySlug(blog_slug);
|
||||
const blog_title = blog.title;
|
||||
const blog_id = blog.id;
|
||||
|
||||
const owner = await isOwner(blog.id, address);
|
||||
if (!owner) {
|
||||
console.error('User does not have any administrative access to', blog_title);
|
||||
throw redirect(303, '/admin'); // Redirect to login if not authenticated
|
||||
}
|
||||
const authors = await getBlogAuthors(blog_id);
|
||||
const owners = await getBlogOwners(blog_id, address);
|
||||
const reviewers = await getBlogReviewers(blog_id, address);
|
||||
|
||||
const data = {
|
||||
address,
|
||||
blog_title,
|
||||
blog_id,
|
||||
owners: owners.owners,
|
||||
authors: authors.authors,
|
||||
reviewers: reviewers.reviewers
|
||||
};
|
||||
|
||||
return data;
|
||||
};
|
||||
299
src/routes/admin/b/[blog]/manage/+page.svelte
Normal file
299
src/routes/admin/b/[blog]/manage/+page.svelte
Normal file
@@ -0,0 +1,299 @@
|
||||
<script lang="ts">
|
||||
export let data;
|
||||
let { blog_title = 'Loading...', authors = [], owners = [], reviewers = [], blog_id } = data;
|
||||
|
||||
async function addUser(event: Event) {
|
||||
event.preventDefault();
|
||||
const form = event.target as HTMLFormElement;
|
||||
const address = form.address.value;
|
||||
const role = form.role.value;
|
||||
|
||||
if (role === 'owner') {
|
||||
addOwner(address);
|
||||
} else if (role === 'reviewer') {
|
||||
addReviewer(address);
|
||||
} else if (role === 'author') {
|
||||
addAuthor(address);
|
||||
}
|
||||
}
|
||||
|
||||
async function addOwner(new_owner_address: string) {
|
||||
const res = await fetch(`/api/blog/owner/add`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ blog_id, new_owner_address })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const newOwner = await res.json();
|
||||
owners = [...owners, newOwner];
|
||||
authors = authors.filter((author) => author.address !== new_owner_address);
|
||||
reviewers = reviewers.filter((reviewer) => reviewer.address !== new_owner_address);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeOwner(owner_to_remove_address: string) {
|
||||
const res = await fetch(`/api/blog/owner/remove`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ blog_id, owner_to_remove_address })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
owners = owners.filter((owner) => owner.address !== owner_to_remove_address);
|
||||
}
|
||||
}
|
||||
|
||||
async function addReviewer(new_reviewer_address: string) {
|
||||
const res = await fetch(`/api/blog/reviewer/add`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ blog_id, new_reviewer_address })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const newReviewer = await res.json();
|
||||
reviewers = [...reviewers, newReviewer];
|
||||
authors = authors.filter((author) => author.address !== new_reviewer_address);
|
||||
owners = owners.filter((reviewer) => reviewer.address !== new_reviewer_address);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeReviewer(reviewer_to_remove_address: string) {
|
||||
const res = await fetch(`/api/blog/reviewer/remove`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ blog_id, reviewer_to_remove_address })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
reviewers = reviewers.filter((reviewer) => reviewer.address !== reviewer_to_remove_address);
|
||||
}
|
||||
}
|
||||
|
||||
async function addAuthor(new_author_address: string) {
|
||||
const res = await fetch(`/api/blog/author/add`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ blog_id, new_author_address })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
const newAuthor = await res.json();
|
||||
authors = [...authors, newAuthor];
|
||||
reviewers = reviewers.filter((reviewer) => reviewer.address !== new_author_address);
|
||||
owners = owners.filter((reviewer) => reviewer.address !== new_author_address);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeAuthor(author_to_remove_address: string) {
|
||||
const res = await fetch(`/api/blog/author/remove`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ blog_id, author_to_remove_address })
|
||||
});
|
||||
|
||||
if (res.ok) {
|
||||
authors = authors.filter((author) => author.address !== author_to_remove_address);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<h3>Manage {blog_title}</h3>
|
||||
|
||||
<h4>Owners</h4>
|
||||
<p>Owners can add new authors and reviewers, vote to publish drafts, and author draft posts.</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Address</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each owners as owner}
|
||||
<tr>
|
||||
<td>{owner.username}</td>
|
||||
<td>{owner.address}</td>
|
||||
<td>
|
||||
<button class="reviewer" on:click={() => addReviewer(owner.address)}>Make Reviewer</button
|
||||
>
|
||||
<button class="author" on:click={() => addAuthor(owner.address)}>Make Author</button>
|
||||
<button class="remove" on:click={() => removeOwner(owner.address)}>Remove</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h4>Reviewers</h4>
|
||||
<p>Reviewers can vote on draft posts to be published, and author draft posts.</p>
|
||||
{#if reviewers.length > 0}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Address</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each reviewers as reviewer}
|
||||
<tr>
|
||||
<td>{reviewer.username}</td>
|
||||
<td>{reviewer.address}</td>
|
||||
<td>
|
||||
<button class="owner" on:click={() => addOwner(reviewer.address)}>Make Owner</button>
|
||||
<button class="author" on:click={() => addAuthor(reviewer.address)}>Make Author</button>
|
||||
<button class="remove" on:click={() => removeReviewer(reviewer.address)}>Remove</button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{:else}
|
||||
<p>No reviewers yet.</p>
|
||||
{/if}
|
||||
|
||||
<h4>Authors Only</h4>
|
||||
<p>Authors can draft posts that are voted on to be published by reviewers.</p>
|
||||
{#if authors.length > 0}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Address</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each authors as author}
|
||||
<tr>
|
||||
<td>{author.username}</td>
|
||||
<td>{author.address}</td>
|
||||
<td
|
||||
><button class="owner" on:click={() => addOwner(author.address)}>Make Owner</button>
|
||||
<button class="reviewer" on:click={() => addReviewer(author.address)}
|
||||
>Make Reviewer</button
|
||||
>
|
||||
<button class="remove" on:click={() => removeAuthor(author.address)}>Remove</button></td
|
||||
>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{:else}
|
||||
<p>No authors yet.</p>
|
||||
{/if}
|
||||
|
||||
<h4>Add User</h4>
|
||||
<form on:submit={addUser}>
|
||||
<label for="address">User Address</label>
|
||||
<input type="text" id="address" name="address" required />
|
||||
|
||||
<label for="role">Description</label>
|
||||
<select id="role" name="role" required>
|
||||
<option value="author" selected>Author</option>
|
||||
<option value="reviewer">Reviewer & Author</option>
|
||||
<option value="owner">Owner</option>
|
||||
</select>
|
||||
|
||||
<button type="submit">Add</button>
|
||||
</form>
|
||||
|
||||
<style>
|
||||
form {
|
||||
display: flex;
|
||||
padding: 0.5rem 0.125rem;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
max-width: 80ch;
|
||||
}
|
||||
p {
|
||||
margin: 0 0 1rem 0.5rem;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 0.75em;
|
||||
text-align: left;
|
||||
border: 1px solid var(--color-green-light);
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: var(--color-green);
|
||||
color: var(--color-green-lightest);
|
||||
}
|
||||
|
||||
.remove {
|
||||
background-color: var(--color-red) !important;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5em;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.remove:hover {
|
||||
background-color: var(--color-red-dark) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.owner {
|
||||
background-color: var(--color-green) !important;
|
||||
border-color: var(--color-green-dark);
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.author {
|
||||
background-color: var(--color-blue) !important;
|
||||
border-color: var(--color-blue-dark);
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.reviewer {
|
||||
background-color: var(--color-purple) !important;
|
||||
border-color: var(--color-purple-dark);
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.owner:hover {
|
||||
background-color: var(--color-green-light) !important;
|
||||
border-color: var(--color-green) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.author:hover {
|
||||
background-color: var(--color-blue-light) !important;
|
||||
border-color: var(--color-blue) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.reviewer:hover {
|
||||
background-color: var(--color-purple-light) !important;
|
||||
border-color: var(--color-purple) !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
h4:not(:first-of-type) {
|
||||
margin-top: 1.25em;
|
||||
}
|
||||
</style>
|
||||
44
src/routes/admin/b/[blog]/review/+page.server.ts
Normal file
44
src/routes/admin/b/[blog]/review/+page.server.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { error, redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
import {
|
||||
getBlogAuthors,
|
||||
getBlogOwners,
|
||||
getBlogReviewers,
|
||||
isOwner,
|
||||
isReviewer
|
||||
} from '$lib/db/roles';
|
||||
import { getBlogBySlug } from '$lib/db/blogs';
|
||||
|
||||
export const load: PageServerLoad = async ({ locals, params }) => {
|
||||
if (!locals.siwe) {
|
||||
console.error('SIWE data not found in locals:', locals);
|
||||
throw redirect(303, '/'); // Redirect to login if not authenticated
|
||||
}
|
||||
|
||||
const address = locals.siwe.address;
|
||||
const blog_slug = params.blog;
|
||||
const blog = await getBlogBySlug(blog_slug);
|
||||
const blog_title = blog.title;
|
||||
const blog_id = blog.id;
|
||||
|
||||
const owner = await isOwner(blog.id, address);
|
||||
const reviewer = await isReviewer(blog.id, address);
|
||||
if (!owner || !reviewer) {
|
||||
console.error('User does not have any administrative access to', blog_title);
|
||||
throw redirect(303, '/admin'); // Redirect to login if not authenticated
|
||||
}
|
||||
const authors = await getBlogAuthors(blog_id);
|
||||
const owners = await getBlogOwners(blog_id, address);
|
||||
const reviewers = await getBlogReviewers(blog_id, address);
|
||||
|
||||
const data = {
|
||||
address,
|
||||
blog_title,
|
||||
blog_id,
|
||||
owners: owners.owners,
|
||||
authors: authors.authors,
|
||||
reviewers: reviewers.reviewers
|
||||
};
|
||||
|
||||
return data;
|
||||
};
|
||||
1
src/routes/admin/b/[blog]/review/+page.svelte
Normal file
1
src/routes/admin/b/[blog]/review/+page.svelte
Normal file
@@ -0,0 +1 @@
|
||||
Review Page
|
||||
46
src/routes/admin/new/+page.svelte
Normal file
46
src/routes/admin/new/+page.svelte
Normal file
@@ -0,0 +1,46 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
const createBlog = async (event) => {
|
||||
event.preventDefault();
|
||||
const title = event.target.title.value;
|
||||
const description = event.target.description.value;
|
||||
const res = await fetch('/api/blog/create', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ title, description })
|
||||
});
|
||||
const status = (await res.status) === 200 ? 'success' : 'failure';
|
||||
|
||||
if (status === 'success') {
|
||||
const json = await res.json();
|
||||
const slug = json.slug;
|
||||
console.log(json);
|
||||
event.target.reset();
|
||||
goto(`/admin/b/${slug}/manage`);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<h3>Create a Blog</h3>
|
||||
|
||||
<form on:submit={createBlog}>
|
||||
<label for="title">Title</label>
|
||||
<input type="text" id="title" name="title" required />
|
||||
|
||||
<label for="description">Description</label>
|
||||
<textarea id="description" name="description" required rows="5"></textarea>
|
||||
|
||||
<button type="submit">Create</button>
|
||||
</form>
|
||||
|
||||
<style>
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
max-width: 80ch;
|
||||
}
|
||||
</style>
|
||||
14
src/routes/admin/owner.svelte
Normal file
14
src/routes/admin/owner.svelte
Normal file
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
export let ownedBlogs;
|
||||
</script>
|
||||
|
||||
<h3>Manage Blogs</h3>
|
||||
<ul>
|
||||
{#if ownedBlogs.length === 0}
|
||||
<li>You don't own any blogs yet.</li>
|
||||
{:else}
|
||||
{#each ownedBlogs as blog}
|
||||
<li><a href="/admin/b/{blog.slug}/manage">{blog.title}</a></li>
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
||||
14
src/routes/admin/reviewer.svelte
Normal file
14
src/routes/admin/reviewer.svelte
Normal file
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
export let reviewedBlogs;
|
||||
</script>
|
||||
|
||||
<h3>Review Posts</h3>
|
||||
<ul>
|
||||
{#if reviewedBlogs.length === 0}
|
||||
<li>You aren't the reviewer on any blogs yet.</li>
|
||||
{:else}
|
||||
{#each reviewedBlogs as blog}
|
||||
<li><a href="/admin/b/{blog.slug}/reviewer">{blog.title}</a></li>
|
||||
{/each}
|
||||
{/if}
|
||||
</ul>
|
||||
14
src/routes/api/blog/author/add/+server.ts
Normal file
14
src/routes/api/blog/author/add/+server.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { addAuthor } from '$lib/db/roles';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||
const { blog_id, new_author_address } = await request.json();
|
||||
const siwe_state = locals['siwe'];
|
||||
const result = await addAuthor(blog_id, siwe_state.address, new_author_address);
|
||||
|
||||
if (!result) {
|
||||
return new Response(JSON.stringify({ message: 'Failed to add author.' }), { status: 422 });
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(result), { status: 200 });
|
||||
};
|
||||
14
src/routes/api/blog/author/remove/+server.ts
Normal file
14
src/routes/api/blog/author/remove/+server.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { removeAuthor } from '$lib/db/roles';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||
const { blog_id, author_to_remove_address } = await request.json();
|
||||
const siwe_state = locals['siwe']; // Get the current owner address from the session
|
||||
const result = await removeAuthor(blog_id, siwe_state.address, author_to_remove_address);
|
||||
|
||||
if (!result) {
|
||||
return new Response(JSON.stringify({ message: 'Failed to remove author.' }), { status: 422 });
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(result), { status: 200 });
|
||||
};
|
||||
12
src/routes/api/blog/create/+server.ts
Normal file
12
src/routes/api/blog/create/+server.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { createBlog } from '$lib/db/blogs';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||
const { title, description } = await request.json();
|
||||
const siwe_state = locals['siwe'];
|
||||
const result = await createBlog(siwe_state.address, title, description);
|
||||
if (!result) {
|
||||
return new Response(JSON.stringify({ message: 'Blog creation failed.' }), { status: 422 });
|
||||
}
|
||||
return new Response(JSON.stringify(result), { status: 200 });
|
||||
};
|
||||
14
src/routes/api/blog/group/+server.ts
Normal file
14
src/routes/api/blog/group/+server.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { getAuthorIDCs, getBlogBySlug } from '$lib/db/blogs';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const POST: RequestHandler = async ({ request }) => {
|
||||
const { blog_slug } = await request.json();
|
||||
const blog = await getBlogBySlug(blog_slug);
|
||||
const result = await getAuthorIDCs(blog.id);
|
||||
|
||||
if (!result) {
|
||||
return new Response(JSON.stringify({ message: 'Failed to fetch group.' }), { status: 422 });
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(result), { status: 200 });
|
||||
};
|
||||
14
src/routes/api/blog/owner/add/+server.ts
Normal file
14
src/routes/api/blog/owner/add/+server.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { addOwner } from '$lib/db/roles';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||
const { blog_id, new_owner_address } = await request.json();
|
||||
const siwe_state = locals['siwe']; // Get the current owner address from the session
|
||||
const result = await addOwner(blog_id, siwe_state.address, new_owner_address);
|
||||
|
||||
if (!result) {
|
||||
return new Response(JSON.stringify({ message: 'Failed to add owner.' }), { status: 422 });
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(result), { status: 200 });
|
||||
};
|
||||
14
src/routes/api/blog/owner/remove/+server.ts
Normal file
14
src/routes/api/blog/owner/remove/+server.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { removeOwner } from '$lib/db/roles';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||
const { blog_id, owner_to_remove_address } = await request.json();
|
||||
const siwe_state = locals['siwe']; // Get the current owner address from the session
|
||||
const result = await removeOwner(blog_id, siwe_state.address, owner_to_remove_address);
|
||||
|
||||
if (!result) {
|
||||
return new Response(JSON.stringify({ message: 'Failed to remove owner.' }), { status: 422 });
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(result), { status: 200 });
|
||||
};
|
||||
15
src/routes/api/blog/post/+server.ts
Normal file
15
src/routes/api/blog/post/+server.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { getBlogBySlug } from '$lib/db/blogs';
|
||||
import { createBlogPost } from '$lib/db/posts';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const POST: RequestHandler = async ({ request }) => {
|
||||
const { blog_slug, title, content, proof } = await request.json();
|
||||
const blog = await getBlogBySlug(blog_slug);
|
||||
const result = await createBlogPost(blog.id, title, content, proof);
|
||||
|
||||
if (!result) {
|
||||
return new Response(JSON.stringify({ message: 'Failed to create post.' }), { status: 422 });
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(result), { status: 200 });
|
||||
};
|
||||
14
src/routes/api/blog/reviewer/add/+server.ts
Normal file
14
src/routes/api/blog/reviewer/add/+server.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { addReviewer } from '$lib/db/roles';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||
const { blog_id, new_reviewer_address } = await request.json();
|
||||
const siwe_state = locals['siwe']; // Get the current owner address from the session
|
||||
const result = await addReviewer(blog_id, siwe_state.address, new_reviewer_address);
|
||||
|
||||
if (!result) {
|
||||
return new Response(JSON.stringify({ message: 'Failed to add reviewer.' }), { status: 422 });
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(result), { status: 200 });
|
||||
};
|
||||
14
src/routes/api/blog/reviewer/remove/+server.ts
Normal file
14
src/routes/api/blog/reviewer/remove/+server.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { removeReviewer } from '$lib/db/roles';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const POST: RequestHandler = async ({ request, locals }) => {
|
||||
const { blog_id, reviewer_to_remove_address } = await request.json();
|
||||
const siwe_state = locals['siwe']; // Get the current owner address from the session
|
||||
const result = await removeReviewer(blog_id, siwe_state.address, reviewer_to_remove_address);
|
||||
|
||||
if (!result) {
|
||||
return new Response(JSON.stringify({ message: 'Failed to remove reviewer.' }), { status: 422 });
|
||||
}
|
||||
|
||||
return new Response(JSON.stringify(result), { status: 200 });
|
||||
};
|
||||
0
src/routes/api/blog/update/+server.ts
Normal file
0
src/routes/api/blog/update/+server.ts
Normal file
@@ -7,7 +7,7 @@ export const POST: RequestHandler = async ({ request, locals, cookies }) => {
|
||||
const { message, signature } = await request.json();
|
||||
const nonce = cookies.get('nonce'); // Retrieve the nonce from the cookie
|
||||
|
||||
console.log('Received Nonce:', nonce);
|
||||
console.log('Received Nonce', nonce);
|
||||
|
||||
if (!message || !nonce) {
|
||||
return new Response(JSON.stringify({ message: 'signed message or nonce missing.' }), {
|
||||
@@ -17,7 +17,7 @@ export const POST: RequestHandler = async ({ request, locals, cookies }) => {
|
||||
|
||||
try {
|
||||
const siweMessage = new SiweMessage(message);
|
||||
console.log('SiweMessage:', siweMessage);
|
||||
console.log('Sign In With Ethereum Message', siweMessage);
|
||||
const verification = await siweMessage.verify({ signature, nonce });
|
||||
|
||||
// Store SIWE data in a secure, HTTP-only cookie
|
||||
|
||||
@@ -6,6 +6,14 @@
|
||||
|
||||
<h1>{data.Blog.title}</h1>
|
||||
<code>{data.Blog.description}</code>
|
||||
|
||||
<div id="authors">
|
||||
<h4>Authors:</h4>
|
||||
{#each data.Blog.authors as author}
|
||||
<div>{author}</div>
|
||||
{/each}
|
||||
</div>
|
||||
<h3>Posts</h3>
|
||||
<ul>
|
||||
{#each data.Posts as Post}
|
||||
<li>
|
||||
@@ -21,6 +29,18 @@
|
||||
ul {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
div#authors {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
div#authors div::after {
|
||||
content: ',';
|
||||
}
|
||||
|
||||
.post_short {
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import { error, redirect } from '@sveltejs/kit';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = ({ locals }) => {
|
||||
if (!locals.siwe) {
|
||||
console.error('SIWE data not found in locals:', locals);
|
||||
throw redirect(303, '/'); // Redirect to login if not authenticated
|
||||
}
|
||||
|
||||
console.log('Address returned from load:', locals.siwe.address);
|
||||
const data = {
|
||||
address: locals.siwe.address
|
||||
};
|
||||
return data;
|
||||
};
|
||||
@@ -1,7 +0,0 @@
|
||||
<script lang="ts">
|
||||
export let data;
|
||||
</script>
|
||||
|
||||
<h2>DASHBOARD</h2>
|
||||
|
||||
<h3>Welcome, {data.address}</h3>
|
||||
Reference in New Issue
Block a user