crude working version without a review process complete

This commit is contained in:
AtHeartEngineer
2024-09-06 19:42:07 -04:00
parent 4e969fa70b
commit 5807a379ff
46 changed files with 1817 additions and 169 deletions

100
db_definition.sql Normal file
View 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
View File

@@ -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": {

View File

@@ -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"
}

View File

@@ -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

View File

@@ -81,7 +81,7 @@
if (data.new_user) {
goto('/signup/profile');
} else {
window.location.href = '/dashboard';
window.location.href = '/admin';
}
});
}

View File

@@ -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;
}
}

View File

@@ -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
View 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 };
}

View File

@@ -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;
};
}

View File

@@ -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;
}

View File

@@ -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 };
};

View File

@@ -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>

View File

@@ -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;
}

View 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;
};

View 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>

View File

View 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} />

View 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>

View 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;
};

View File

@@ -0,0 +1 @@
<slot></slot>

View File

View 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;
};

View 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>

View 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;
};

View 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>

View 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;
};

View File

@@ -0,0 +1 @@
Review Page

View 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>

View 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>

View 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>

View 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 });
};

View 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 });
};

View 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 });
};

View 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 });
};

View 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 });
};

View 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 });
};

View 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 });
};

View 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 });
};

View 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 });
};

View File

View 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

View File

@@ -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 {

View File

@@ -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;
};

View File

@@ -1,7 +0,0 @@
<script lang="ts">
export let data;
</script>
<h2>DASHBOARD</h2>
<h3>Welcome, {data.address}</h3>