triflix commited on
Commit
131b4ff
·
verified ·
1 Parent(s): 08e6fc6

Upload 6 files

Browse files
Files changed (6) hide show
  1. Dockerfile +19 -0
  2. main.py +179 -0
  3. requirements.txt +7 -0
  4. templates/index.html +91 -0
  5. templates/password.html +66 -0
  6. templates/view.html +45 -0
Dockerfile ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9-slim
2
+
3
+ # Set working directory
4
+ WORKDIR /app
5
+
6
+ # Copy requirements first for better caching
7
+ COPY requirements.txt .
8
+
9
+ # Install dependencies
10
+ RUN pip install --no-cache-dir -r requirements.txt
11
+
12
+ # Copy application code
13
+ COPY . .
14
+
15
+ # Expose the port the app runs on
16
+ EXPOSE 7860
17
+
18
+ # Command to run the application
19
+ CMD ["python", "main.py"]
main.py ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, Request, Form, Depends, HTTPException
2
+ from fastapi.templating import Jinja2Templates
3
+ from fastapi.staticfiles import StaticFiles
4
+ from fastapi.responses import HTMLResponse, RedirectResponse
5
+ from sqlalchemy import create_engine, Column, Integer, String, Boolean, DateTime, Text
6
+ from sqlalchemy.ext.declarative import declarative_base
7
+ from sqlalchemy.orm import sessionmaker, Session
8
+ from datetime import datetime
9
+ import random
10
+ import string
11
+ import re
12
+ import os
13
+ import bcrypt
14
+ from typing import Optional
15
+
16
+ # Create the FastAPI app
17
+ app = FastAPI(title="Notepad App")
18
+
19
+ # Set up templates and static files directories
20
+ templates_dir = os.path.join(os.path.dirname(__file__), "templates")
21
+ templates = Jinja2Templates(directory=templates_dir)
22
+
23
+ # Create static directory if it doesn't exist
24
+ static_dir = os.path.join(os.path.dirname(__file__), "static")
25
+ os.makedirs(static_dir, exist_ok=True)
26
+ app.mount("/static", StaticFiles(directory=static_dir), name="static")
27
+
28
+ # Database setup
29
+ DATABASE_URL = "sqlite:///./notepad.db"
30
+ engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
31
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
32
+ Base = declarative_base()
33
+
34
+ # Define the Note model
35
+ class Note(Base):
36
+ __tablename__ = "notes"
37
+
38
+ id = Column(Integer, primary_key=True, index=True)
39
+ code = Column(String, unique=True, index=True)
40
+ content = Column(Text)
41
+ is_private = Column(Boolean, default=False)
42
+ password = Column(String, nullable=True)
43
+ created_at = Column(DateTime, default=datetime.utcnow)
44
+
45
+ # Create the database tables
46
+ Base.metadata.create_all(bind=engine)
47
+
48
+ # Dependency to get the database session
49
+ def get_db():
50
+ db = SessionLocal()
51
+ try:
52
+ yield db
53
+ finally:
54
+ db.close()
55
+
56
+ # Function to generate a random code for the note URL
57
+ def generate_random_code(length=6):
58
+ characters = string.ascii_letters + string.digits
59
+ return ''.join(random.choice(characters) for _ in range(length))
60
+
61
+ # Function to detect image URLs in text and convert them to HTML img tags
62
+ def process_content(content):
63
+ # Regular expression to find image URLs
64
+ image_pattern = r'(https?://\S+\.(?:jpg|jpeg|png|gif|webp|svg))'
65
+
66
+ # Replace image URLs with img tags
67
+ processed_content = re.sub(
68
+ image_pattern,
69
+ r'<img src="\1" class="my-2 max-w-full h-auto rounded" alt="Image">',
70
+ content
71
+ )
72
+
73
+ # Convert newlines to <br> tags for proper HTML rendering
74
+ processed_content = processed_content.replace('\n', '<br>')
75
+
76
+ return processed_content
77
+
78
+ # Home page route
79
+ @app.get("/", response_class=HTMLResponse)
80
+ async def home(request: Request):
81
+ return templates.TemplateResponse("index.html", {"request": request})
82
+
83
+ # Create note route
84
+ @app.post("/create", response_class=HTMLResponse)
85
+ async def create_note(
86
+ request: Request,
87
+ content: str = Form(...),
88
+ is_private: bool = Form(False),
89
+ password: Optional[str] = Form(None),
90
+ db: Session = Depends(get_db)
91
+ ):
92
+ # Generate a unique random code
93
+ code = generate_random_code()
94
+ while db.query(Note).filter(Note.code == code).first():
95
+ code = generate_random_code()
96
+
97
+ # Hash password if provided
98
+ hashed_password = None
99
+ if is_private and password:
100
+ hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
101
+
102
+ # Create new note
103
+ new_note = Note(
104
+ code=code,
105
+ content=content,
106
+ is_private=is_private,
107
+ password=hashed_password
108
+ )
109
+
110
+ db.add(new_note)
111
+ db.commit()
112
+
113
+ # Redirect to the note view page
114
+ return RedirectResponse(url=f"/{code}", status_code=303)
115
+
116
+ # View note route
117
+ @app.get("/{code}", response_class=HTMLResponse)
118
+ async def view_note(request: Request, code: str, db: Session = Depends(get_db)):
119
+ # Find the note by code
120
+ note = db.query(Note).filter(Note.code == code).first()
121
+
122
+ if not note:
123
+ raise HTTPException(status_code=404, detail="Note not found")
124
+
125
+ # If the note is private, show password prompt
126
+ if note.is_private:
127
+ return templates.TemplateResponse(
128
+ "password.html",
129
+ {"request": request, "code": code}
130
+ )
131
+
132
+ # Process content to render images
133
+ processed_content = process_content(note.content)
134
+
135
+ return templates.TemplateResponse(
136
+ "view.html",
137
+ {"request": request, "note": note, "content": processed_content}
138
+ )
139
+
140
+ # Verify password route
141
+ @app.post("/{code}/verify", response_class=HTMLResponse)
142
+ async def verify_password(
143
+ request: Request,
144
+ code: str,
145
+ password: str = Form(...),
146
+ db: Session = Depends(get_db)
147
+ ):
148
+ # Find the note by code
149
+ note = db.query(Note).filter(Note.code == code).first()
150
+
151
+ if not note:
152
+ raise HTTPException(status_code=404, detail="Note not found")
153
+
154
+ # Verify the password
155
+ if not note.is_private or not note.password:
156
+ # Note is not private or doesn't have a password
157
+ return RedirectResponse(url=f"/{code}", status_code=303)
158
+
159
+ is_valid = bcrypt.checkpw(password.encode('utf-8'), note.password.encode('utf-8'))
160
+
161
+ if not is_valid:
162
+ # Password is incorrect
163
+ return templates.TemplateResponse(
164
+ "password.html",
165
+ {"request": request, "code": code, "error": "Invalid password"}
166
+ )
167
+
168
+ # Process content to render images
169
+ processed_content = process_content(note.content)
170
+
171
+ return templates.TemplateResponse(
172
+ "view.html",
173
+ {"request": request, "note": note, "content": processed_content}
174
+ )
175
+
176
+ # Run the application
177
+ if __name__ == "__main__":
178
+ import uvicorn
179
+ uvicorn.run("main:app", host="0.0.0.0", port=7860, reload=True)
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fastapi==0.104.1
2
+ uvicorn==0.23.2
3
+ jinja2==3.1.2
4
+ python-multipart==0.0.6
5
+ sqlalchemy==2.0.23
6
+ bcrypt==4.0.1
7
+ pydantic==2.4.2
templates/index.html ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Notepad App</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script>
9
+ tailwind.config = {
10
+ theme: {
11
+ extend: {
12
+ spacing: {
13
+ '18': '4.5rem',
14
+ },
15
+ }
16
+ }
17
+ }
18
+ </script>
19
+ </head>
20
+ <body class="bg-white min-h-screen">
21
+ <div class="container mx-auto px-4 py-8 sm:py-12">
22
+ <div class="max-w-xl mx-auto bg-white rounded-lg shadow-sm border border-gray-200 p-5 sm:p-8">
23
+ <h1 class="text-3xl font-bold text-center text-black mb-4 sm:mb-6">Notepad</h1>
24
+ <p class="text-gray-700 mb-6 text-center text-sm sm:text-base">Create a note with text, emojis, and image URLs</p>
25
+
26
+ <form action="/create" method="post" class="space-y-6">
27
+ <div>
28
+ <label for="content" class="block text-sm font-medium text-black mb-2">Your Note</label>
29
+ <textarea
30
+ id="content"
31
+ name="content"
32
+ rows="10"
33
+ class="w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-black focus:border-black text-base"
34
+ placeholder="Write your note here. Image URLs (ending in .jpg, .png, etc.) will be displayed as images."
35
+ required
36
+ ></textarea>
37
+ </div>
38
+
39
+ <div class="flex items-center py-2">
40
+ <input
41
+ type="checkbox"
42
+ id="is_private"
43
+ name="is_private"
44
+ class="h-5 w-5 text-black focus:ring-black border-gray-400 rounded"
45
+ onchange="togglePasswordField()"
46
+ >
47
+ <label for="is_private" class="ml-3 block text-base text-black">
48
+ Make this note private
49
+ </label>
50
+ </div>
51
+
52
+ <div id="password_field" class="hidden">
53
+ <label for="password" class="block text-sm font-medium text-black mb-2">Password</label>
54
+ <input
55
+ type="password"
56
+ id="password"
57
+ name="password"
58
+ class="w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-black focus:border-black text-base"
59
+ placeholder="Enter a password to protect your note"
60
+ >
61
+ <p class="mt-2 text-sm text-gray-600">This password will be required to view the note</p>
62
+ </div>
63
+
64
+ <div class="mt-8">
65
+ <button
66
+ type="submit"
67
+ class="w-full flex justify-center py-3 px-4 border border-gray-800 rounded-lg shadow-sm text-base font-medium text-white bg-black hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-colors duration-200"
68
+ >
69
+ Create Note
70
+ </button>
71
+ </div>
72
+ </form>
73
+ </div>
74
+ </div>
75
+
76
+ <script>
77
+ function togglePasswordField() {
78
+ const isPrivate = document.getElementById('is_private').checked;
79
+ const passwordField = document.getElementById('password_field');
80
+
81
+ if (isPrivate) {
82
+ passwordField.classList.remove('hidden');
83
+ document.getElementById('password').setAttribute('required', 'required');
84
+ } else {
85
+ passwordField.classList.add('hidden');
86
+ document.getElementById('password').removeAttribute('required');
87
+ }
88
+ }
89
+ </script>
90
+ </body>
91
+ </html>
templates/password.html ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Password Protected Note</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script>
9
+ tailwind.config = {
10
+ theme: {
11
+ extend: {
12
+ spacing: {
13
+ '18': '4.5rem',
14
+ },
15
+ }
16
+ }
17
+ }
18
+ </script>
19
+ </head>
20
+ <body class="bg-white min-h-screen">
21
+ <div class="container mx-auto px-4 py-8 sm:py-12">
22
+ <div class="max-w-md mx-auto bg-white rounded-lg shadow-sm border border-gray-200 p-5 sm:p-8">
23
+ <h1 class="text-2xl font-bold text-center text-black mb-4">Password Protected Note</h1>
24
+ <p class="text-gray-700 mb-6 text-center text-sm sm:text-base">This note is private and requires a password to view</p>
25
+
26
+ {% if error %}
27
+ <div class="bg-gray-100 border-l-4 border-black text-gray-800 px-4 py-3 rounded mb-6" role="alert">
28
+ <p class="font-medium">{{ error }}</p>
29
+ </div>
30
+ {% endif %}
31
+
32
+ <form action="/{{ code }}/verify" method="post" class="space-y-6">
33
+ <div>
34
+ <label for="password" class="block text-sm font-medium text-black mb-2">Password</label>
35
+ <input
36
+ type="password"
37
+ id="password"
38
+ name="password"
39
+ class="w-full px-4 py-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-black focus:border-black text-base"
40
+ placeholder="Enter the note password"
41
+ required
42
+ >
43
+ </div>
44
+
45
+ <div>
46
+ <button
47
+ type="submit"
48
+ class="w-full flex justify-center py-3 px-4 border border-gray-800 rounded-lg shadow-sm text-base font-medium text-white bg-black hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 transition-colors duration-200"
49
+ >
50
+ Access Note
51
+ </button>
52
+ </div>
53
+ </form>
54
+
55
+ <div class="mt-8 text-center">
56
+ <a href="/" class="inline-flex items-center justify-center text-black hover:text-gray-700 font-medium transition-colors duration-200">
57
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
58
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
59
+ </svg>
60
+ Back to Notepad
61
+ </a>
62
+ </div>
63
+ </div>
64
+ </div>
65
+ </body>
66
+ </html>
templates/view.html ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>View Note</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script>
9
+ tailwind.config = {
10
+ theme: {
11
+ extend: {
12
+ spacing: {
13
+ '18': '4.5rem',
14
+ },
15
+ }
16
+ }
17
+ }
18
+ </script>
19
+ </head>
20
+ <body class="bg-white min-h-screen">
21
+ <div class="container mx-auto px-4 py-8 sm:py-12">
22
+ <div class="max-w-xl mx-auto bg-white rounded-lg shadow-sm border border-gray-200 p-5 sm:p-8">
23
+ <div class="flex flex-col sm:flex-row sm:justify-between sm:items-center mb-6">
24
+ <h1 class="text-2xl font-bold text-black mb-2 sm:mb-0">Note #{{ note.code }}</h1>
25
+ <span class="text-sm text-gray-600">Created: {{ note.created_at.strftime('%Y-%m-%d %H:%M') }}</span>
26
+ </div>
27
+
28
+ <div class="border-t border-gray-200 pt-5">
29
+ <div class="prose max-w-none text-black">
30
+ {{ content | safe }}
31
+ </div>
32
+ </div>
33
+
34
+ <div class="mt-8 pt-5 border-t border-gray-200">
35
+ <a href="/" class="inline-flex items-center text-black hover:text-gray-700 font-medium transition-colors duration-200">
36
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
37
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
38
+ </svg>
39
+ Back to Notepad
40
+ </a>
41
+ </div>
42
+ </div>
43
+ </div>
44
+ </body>
45
+ </html>