-
Notifications
You must be signed in to change notification settings - Fork 0
4 blog website setup #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a6ae8c0
07d2683
c00373b
04b0159
12e16d2
0c0310d
022f58a
b317ec5
4333953
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -1,24 +1,95 @@ | ||||
| from fastapi import APIRouter, File, UploadFile, Request | ||||
| from fastapi import APIRouter, File, UploadFile, Request, HTTPException | ||||
|
|
||||
| import datetime | ||||
| import shutil | ||||
| import time | ||||
| import re | ||||
|
|
||||
| import json | ||||
|
|
||||
| from sqlmodel import Session, select, text | ||||
|
|
||||
| from app.models.yesno import YesNo | ||||
| from app.models.blog_post import BlogPost, BlogPostCreate, BlogPostUpdate | ||||
| from app.core.database import SessionDep | ||||
| from typing import List, Optional | ||||
|
|
||||
| router = APIRouter() | ||||
|
|
||||
| @router.post("/upload") | ||||
| async def upload_answer(request: Request, db: SessionDep): | ||||
| answer = await request.body() | ||||
| answer = answer.decode("utf-8") | ||||
| db.add(YesNo(timestamp=datetime.datetime.now(), answer=answer)) | ||||
|
|
||||
| def sanitise_html(html: str) -> str: | ||||
| return html | ||||
|
|
||||
|
|
||||
| def generate_unique_slug(title: str, db: Session) -> str: | ||||
| # Slugify title (basic version; you can improve with unidecode or slugify lib) | ||||
| base_slug = re.sub(r"[^a-z0-9]+", "-", title.lower()).strip("-") | ||||
| slug = base_slug | ||||
| counter = 1 | ||||
| while db.exec(select(BlogPost).where(BlogPost.slug == slug)).first(): | ||||
| slug = f"{base_slug}-{counter}" | ||||
| counter += 1 | ||||
| return slug | ||||
|
|
||||
|
|
||||
| @router.post("/posts/create_post", response_model=BlogPost) | ||||
| def create_post(post: BlogPostCreate, db: SessionDep): | ||||
| created_post = BlogPost( | ||||
| title=post.title, | ||||
| content=sanitise_html(post.content), | ||||
| slug=generate_unique_slug(post.title, db), | ||||
| cover_image=post.cover_image, | ||||
| ) | ||||
| db.add(created_post) | ||||
| db.commit() | ||||
| db.refresh(created_post) | ||||
| return created_post | ||||
|
|
||||
|
|
||||
| @router.get("/posts/", response_model=List[BlogPost]) | ||||
| def read_posts(db: SessionDep): | ||||
| return db.exec(select(BlogPost)).all() | ||||
|
|
||||
|
|
||||
| @router.get("/posts/{post_id}", response_model=BlogPost) | ||||
| def read_post(post_id: int, db: SessionDep): | ||||
| post = db.get(BlogPost, post_id) | ||||
| if not post: | ||||
| raise HTTPException(status_code=404, detail="Post not found") | ||||
| return post | ||||
|
|
||||
| @router.get(("/get")) | ||||
| def get_answer(db: SessionDep): | ||||
| all_answers = db.exec(select(YesNo)).all() | ||||
| return all_answers | ||||
|
|
||||
| @router.put("/posts/{post_id}", response_model=BlogPost) | ||||
| def update_post( | ||||
| post_id: int, | ||||
| updated_post: BlogPostUpdate, | ||||
| db: SessionDep, | ||||
| ): | ||||
| db_post = db.get(BlogPost, post_id) | ||||
| if not db_post: | ||||
| raise HTTPException(status_code=404, detail="Post not found") | ||||
|
|
||||
| if updated_post.title is not None: | ||||
| db_post.title = updated_post.title | ||||
| db_post.slug = generate_unique_slug(updated_post.title, db) | ||||
|
|
||||
| if updated_post.content is not None: | ||||
| db_post.content = sanitise_html(updated_post.content) | ||||
|
|
||||
| if updated_post.cover_image is not None: | ||||
| db_post.cover_image = updated_post.cover_image | ||||
|
|
||||
| # db_post.timestamp = int(time.time()) | ||||
|
||||
| # db_post.timestamp = int(time.time()) |
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
| @@ -0,0 +1,26 @@ | ||||
| from typing import Optional | ||||
|
||||
| from typing import Optional |
Copilot
AI
Aug 9, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Duplicate import statement. The pydantic.BaseModel import on line 10 is redundant as it's already imported on line 5.
| from pydantic import BaseModel |
Copilot
AI
Aug 9, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Duplicate import statement. The time import on line 11 is redundant as it's already imported on line 3.
| import time |
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| <script lang="ts"> | ||
| import { browser } from '$app/environment'; | ||
| import { onMount, onDestroy } from 'svelte'; | ||
| import TinyEditor from './TinyEditor.svelte'; | ||
|
|
||
| let title: string = ''; | ||
| let cover_image_path: string = ''; | ||
| let content: string = ''; | ||
|
|
||
| function handleSubmit() { | ||
| console.log('Submitted Title:', title); | ||
| console.log('Submitted Cover Image:', cover_image_path); | ||
| console.log('Submitted Content:', content); | ||
| fetch('http://127.0.0.1:8000/posts/create_post', { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-type': 'application/json; charset=UTF-8' | ||
| }, | ||
| body: JSON.stringify({ | ||
| title: title, | ||
| content: content, | ||
| cover_image: cover_image_path | ||
| }) | ||
| }) | ||
| .then((response) => response.json()) | ||
| .then((data) => { | ||
| console.log('Response from server:', data); | ||
| }) | ||
| .catch((error) => { | ||
| console.error('Error submitting content:', error); | ||
| }); | ||
| } | ||
| </script> | ||
|
|
||
| {#if browser} | ||
| <div class="editor-container"> | ||
| <input type="text" bind:value={title} placeholder="Enter title" class="title-input" /> | ||
| <input | ||
| type="text" | ||
| bind:value={cover_image_path} | ||
| placeholder="Enter cover image path" | ||
| class="title-input" | ||
| /> | ||
|
|
||
| <TinyEditor bind:content /> | ||
|
|
||
| <button on:click={handleSubmit} class="submit-button">Submit</button> | ||
| </div> | ||
| {:else} | ||
| <p>Editor loading...</p> | ||
| {/if} | ||
|
|
||
| <style> | ||
| .editor-container { | ||
| max-width: 60%; | ||
| margin: 2rem auto; | ||
| padding: 1rem; | ||
| display: flex; | ||
| flex-direction: column; | ||
| gap: 1rem; | ||
| } | ||
|
|
||
| .title-input { | ||
| padding: 0.5rem; | ||
| font-size: 1.5rem; | ||
| border: 1px solid #ccc; | ||
| border-radius: 4px; | ||
| width: 100%; | ||
| } | ||
|
|
||
| .submit-button { | ||
| align-self: flex-start; | ||
| padding: 0.6rem 1.2rem; | ||
| font-size: 1rem; | ||
| background-color: #007bff; | ||
| color: white; | ||
| border: none; | ||
| border-radius: 4px; | ||
| cursor: pointer; | ||
| } | ||
|
|
||
| .submit-button:hover { | ||
| background-color: #0056b3; | ||
| } | ||
|
|
||
| :global(.tox) { | ||
| max-width: 100%; | ||
| } | ||
| </style> |
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,45 @@ | ||||||
| <script lang="ts"> | ||||||
| import { onMount } from 'svelte'; | ||||||
|
|
||||||
| type Post = { | ||||||
| id: number; | ||||||
| timestamp: number; | ||||||
| title: string; | ||||||
| slug: string; | ||||||
| content: string; | ||||||
| cover_image: string; | ||||||
| }; | ||||||
|
|
||||||
| let posts: Post[] = []; | ||||||
| let loading = true; | ||||||
| let error: string | null = null; | ||||||
|
|
||||||
| onMount(async () => { | ||||||
| try { | ||||||
| const res = await fetch('http://127.0.0.1:8000/posts/'); | ||||||
| if (!res.ok) throw new Error(`Failed to fetch: ${res.status}`); | ||||||
|
|
||||||
| posts = await res.json(); | ||||||
| } catch (e: any) { | ||||||
| error = e.message; | ||||||
| } finally { | ||||||
| loading = false; | ||||||
| } | ||||||
| }); | ||||||
| </script> | ||||||
|
|
||||||
| {#if loading} | ||||||
| <p>Loading posts...</p> | ||||||
| {:else if error} | ||||||
| <p style="color: red;">Error: {error}</p> | ||||||
| {:else} | ||||||
| {#each posts as post} | ||||||
| <article> | ||||||
| <h1>{post.id}</h1> | ||||||
|
||||||
| <h1>{post.id}</h1> | |
| <span>{post.id}</span> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,13 @@ | ||
| <script lang="ts"> | ||
| import PostList from '$lib/components/PostList.svelte'; | ||
| import TinyEditor from '$lib/components/TinyEditor.svelte'; | ||
| import PostEditor from '$lib/components/PostEditor.svelte'; | ||
| let content: string = '<p>Hello, TinyMCE!</p>'; | ||
| </script> | ||
|
|
||
| <h1>My Editor</h1> | ||
| <TinyEditor bind:content height={400} /> | ||
| <PostEditor /> | ||
| <PostList /> | ||
|
|
||
| <!-- <h2>Preview:</h2> | ||
| <div>{@html content}</div> --> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
sanitise_htmlfunction returns the input unchanged, providing no HTML sanitization. This creates a security risk as unsanitized HTML content could lead to XSS vulnerabilities when rendered in the frontend.