feat: Initialize application with Flask framework
Implemented the foundation of a Flask application designed to manage email requests with rate limiting and SQLite database integration. This includes setting up basic app infrastructure, such as Flask app initialization, database creation with email request tracking, SMTP configuration for email sending, and rate limiting based on IP addresses to prevent abuse. Additionally, the commit introduces the core frontend structure along with styling, utilizing templates for basic request handling and display. The `.gitignore` file was also set up to ignore common Python and development artifacts. Relevant dependencies required for the application are outlined in `requirements.txt`. This setup lays the groundwork for future expansions, including more detailed request handling, user authentication, and enhanced security features. No specific issues are addressed in this commit; it represents the initial application setup and starting point for further development.
This commit is contained in:
commit
9e5f237b6f
6 changed files with 372 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
venv/
|
||||
*.pyc
|
||||
__pycache__/
|
||||
settings.ini
|
||||
db.sqlite3
|
154
app.py
Normal file
154
app.py
Normal file
|
@ -0,0 +1,154 @@
|
|||
from flask import Flask, request, redirect, url_for, render_template
|
||||
from plankapy import Planka
|
||||
|
||||
from configparser import ConfigParser
|
||||
|
||||
import sqlite3
|
||||
import smtplib
|
||||
import uuid
|
||||
|
||||
app = Flask(__name__, static_folder="static", template_folder="templates")
|
||||
|
||||
config = ConfigParser()
|
||||
config.read("settings.ini")
|
||||
|
||||
|
||||
def initialize_database():
|
||||
conn = sqlite3.connect("db.sqlite3")
|
||||
|
||||
conn.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS requests (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT NOT NULL,
|
||||
token TEXT NOT NULL,
|
||||
ip TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
"""
|
||||
)
|
||||
|
||||
conn.close()
|
||||
|
||||
|
||||
def rate_limit(request):
|
||||
conn = sqlite3.connect("db.sqlite3")
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT COUNT(*)
|
||||
FROM requests
|
||||
WHERE ip = ? AND created_at > datetime('now', '-1 hour')
|
||||
""",
|
||||
(request.remote_addr,),
|
||||
)
|
||||
|
||||
count = cursor.fetchone()[0]
|
||||
|
||||
conn.close()
|
||||
|
||||
return count >= config.getint("App", "rate_limit", fallback=5)
|
||||
|
||||
|
||||
def get_mailserver():
|
||||
if config["SMTP"]["ssl"] == "True":
|
||||
port = config.getint("SMTP", "port", fallback=465)
|
||||
mailserver = smtplib.SMTP_SSL(config["SMTP"]["host"], port)
|
||||
else:
|
||||
port = config.getint("SMTP", "port", fallback=587)
|
||||
mailserver = smtplib.SMTP(config["SMTP"]["host"], port)
|
||||
|
||||
if config.getboolean("SMTP", "starttls", fallback=True):
|
||||
mailserver.starttls()
|
||||
|
||||
mailserver.login(config["SMTP"]["username"], config["SMTP"]["password"])
|
||||
return mailserver
|
||||
|
||||
|
||||
def send_email(email, token):
|
||||
mailserver = get_mailserver()
|
||||
|
||||
message = f"""
|
||||
From: {config['SMTP']['from']}
|
||||
To: {email}
|
||||
Subject: Confirm your email address
|
||||
|
||||
Hi,
|
||||
|
||||
Thank you for registering with {config['App']['name']}! Please click the link below to confirm your email address:
|
||||
|
||||
https://{config['App']['domain']}/confirm/{token}
|
||||
|
||||
If you did not register with {config['App']['name']}, please ignore this email.
|
||||
|
||||
Thanks,
|
||||
The {config['App']['name']} Team
|
||||
"""
|
||||
|
||||
mailserver.sendmail(config["SMTP"]["from"], email, message)
|
||||
|
||||
mailserver.quit()
|
||||
|
||||
|
||||
def process_request(request):
|
||||
email = request.form["email"]
|
||||
|
||||
conn = sqlite3.connect("db.sqlite3")
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if the email address is already in the database
|
||||
cursor.execute(
|
||||
"""
|
||||
SELECT COUNT(*)
|
||||
FROM requests
|
||||
WHERE email = ?
|
||||
""",
|
||||
(email,),
|
||||
)
|
||||
|
||||
count = cursor.fetchone()[0]
|
||||
|
||||
if count > 0:
|
||||
return render_template("already_requested.html")
|
||||
|
||||
token = str(uuid.uuid4())
|
||||
|
||||
cursor.execute(
|
||||
"""
|
||||
INSERT INTO requests (email, token, ip)
|
||||
VALUES (?, ?, ?)
|
||||
""",
|
||||
(email, token, request.remote_addr),
|
||||
)
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
send_email(email, token)
|
||||
|
||||
return redirect(url_for("post_request"))
|
||||
|
||||
|
||||
@app.route("/", methods=["GET", "POST"])
|
||||
def start_request():
|
||||
if rate_limit(request):
|
||||
return render_template("rate_limit.html")
|
||||
|
||||
if request.method == "POST":
|
||||
return process_request(request)
|
||||
|
||||
return render_template(
|
||||
"request.html",
|
||||
app=config["App"]["name"],
|
||||
title="Request Access",
|
||||
subtitle="Please enter your email address to request access.",
|
||||
)
|
||||
|
||||
|
||||
@app.route("/check", methods=["GET"])
|
||||
def check():
|
||||
return render_template("check.html")
|
||||
|
||||
|
||||
initialize_database()
|
2
requirements.txt
Normal file
2
requirements.txt
Normal file
|
@ -0,0 +1,2 @@
|
|||
Flask
|
||||
git+https://git.private.coffee/PrivateCoffee/plankapy.git
|
165
static/style.css
Normal file
165
static/style.css
Normal file
|
@ -0,0 +1,165 @@
|
|||
/* Reset and base styles */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
background-color: #f7f7f7;
|
||||
color: #333;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #5d5d5d;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 90%;
|
||||
max-width: 1200px;
|
||||
margin: auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
header {
|
||||
background-color: #fff;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
color: #5d5d5d;
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
header nav ul {
|
||||
list-style: none;
|
||||
text-align: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
header nav ul li {
|
||||
display: inline;
|
||||
margin: 0 15px;
|
||||
}
|
||||
|
||||
header nav ul li a {
|
||||
color: #5d5d5d;
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* Hero Section */
|
||||
.hero {
|
||||
background-color: #e8e8e8;
|
||||
padding: 50px 0;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.hero h2 {
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
color: #333;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.hero p {
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* IP Display Cards */
|
||||
.ip-display {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.ip-card {
|
||||
background-color: #fff;
|
||||
padding: 30px 10px;
|
||||
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
||||
border-radius: 8px;
|
||||
flex-basis: calc(50% - 10px);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ip-card h3 {
|
||||
margin-bottom: 15px;
|
||||
color: #5d5d5d;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.ip-card p {
|
||||
font-size: 20px;
|
||||
color: #333;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* About Section */
|
||||
#about {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
#about h2 {
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
#about p {
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* API Section */
|
||||
#api {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
#api h2 {
|
||||
margin-bottom: 15px;
|
||||
color: #333;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
#api p {
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
footer {
|
||||
background-color: #fff;
|
||||
box-shadow: 0 -2px 4px rgba(0,0,0,0.1);
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
footer p {
|
||||
margin: 0;
|
||||
color: #5d5d5d;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: #5d5d5d;
|
||||
font-weight: 700;
|
||||
}
|
32
templates/base.html
Normal file
32
templates/base.html
Normal file
|
@ -0,0 +1,32 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{ app }} - {{ title }}</title>
|
||||
<link rel="stylesheet" href="static/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="container">
|
||||
<h1>{{ app }}</h1>
|
||||
</div>
|
||||
</header>
|
||||
<section class="hero">
|
||||
<div class="container">
|
||||
<h2>{{ title }}</h2>
|
||||
<p>{{ subtitle }}</p>
|
||||
</div>
|
||||
</section>
|
||||
<main>
|
||||
{% block content %}
|
||||
{% endblock %}
|
||||
</main>
|
||||
<footer>
|
||||
<div class="container">
|
||||
<p>© 2024 Private.coffee</p>
|
||||
</div>
|
||||
</footer>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
14
templates/request.html
Normal file
14
templates/request.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Request</h1>
|
||||
<p>Request method: {{ request.method }}</p>
|
||||
<p>Request path: {{ request.path }}</p>
|
||||
<p>Request query string: {{ request.query_string }}</p>
|
||||
<p>Request headers:</p>
|
||||
<ul>
|
||||
{% for key, value in request.headers.items() %}
|
||||
<li>{{ key }}: {{ value }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endblock %}
|
Loading…
Reference in a new issue