Compare commits
3 Commits
14c34066f3
...
46fd498c32
| Author | SHA1 | Date | |
|---|---|---|---|
|
46fd498c32
|
|||
|
4022136970
|
|||
|
d0140f4cfb
|
111
cutt/forgotten_password.html
Normal file
111
cutt/forgotten_password.html
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>cutt - set password</title>
|
||||||
|
</head>
|
||||||
|
<body
|
||||||
|
style="
|
||||||
|
margin: auto;
|
||||||
|
max-width: 800px;
|
||||||
|
padding: 2rem 1.5rem;
|
||||||
|
background-color: white;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
style="
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="200" viewBox="0 0 80 50">
|
||||||
|
<ellipse
|
||||||
|
cx="40"
|
||||||
|
cy="25"
|
||||||
|
rx="20.263"
|
||||||
|
ry="20.633"
|
||||||
|
style="
|
||||||
|
fill: #c7d6f1;
|
||||||
|
fill-opacity: 1;
|
||||||
|
stroke: #36c;
|
||||||
|
stroke-width: 8.73336;
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M0 17.67h80v14.66H0Z"
|
||||||
|
style="
|
||||||
|
fill: #000;
|
||||||
|
stroke-width: 3.56018;
|
||||||
|
paint-order: stroke fill markers;
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
x="39.788"
|
||||||
|
y="29.819"
|
||||||
|
style="
|
||||||
|
font-style: normal;
|
||||||
|
font-variant: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-stretch: normal;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1;
|
||||||
|
font-family: Sans;
|
||||||
|
-inkscape-font-specification: "Sans Bold";
|
||||||
|
text-align: center;
|
||||||
|
letter-spacing: 2.83px;
|
||||||
|
writing-mode: lr-tb;
|
||||||
|
direction: ltr;
|
||||||
|
text-anchor: middle;
|
||||||
|
fill: #fff;
|
||||||
|
stroke: #fff;
|
||||||
|
stroke-width: 0.655;
|
||||||
|
stroke-dasharray: none;
|
||||||
|
stroke-opacity: 1;
|
||||||
|
paint-order: stroke fill markers;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<tspan
|
||||||
|
x="39.788"
|
||||||
|
y="29.819"
|
||||||
|
style="
|
||||||
|
font-style: normal;
|
||||||
|
font-variant: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
font-stretch: normal;
|
||||||
|
font-size: 14px;
|
||||||
|
font-family: Sans;
|
||||||
|
-inkscape-font-specification: "Sans Bold";
|
||||||
|
letter-spacing: 2.83px;
|
||||||
|
fill: #fff;
|
||||||
|
stroke: #fff;
|
||||||
|
stroke-width: 0.655;
|
||||||
|
stroke-dasharray: none;
|
||||||
|
stroke-opacity: 1;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
CUTT
|
||||||
|
</tspan>
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
</p>
|
||||||
|
<p>Hello USER,</p>
|
||||||
|
<p>click on the following link to set yourself a new password.</p>
|
||||||
|
<p style="text-align: center; padding: 2rem">
|
||||||
|
<a
|
||||||
|
href="LINK"
|
||||||
|
style="
|
||||||
|
color: ghostwhite;
|
||||||
|
font-weight: bold;
|
||||||
|
background-color: #36c;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
padding: 1rem;
|
||||||
|
text-decoration: none;
|
||||||
|
"
|
||||||
|
>reset password</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
<p>Cheers,<br />Julius</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
66
cutt/mail.py
Normal file
66
cutt/mail.py
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
from email.message import EmailMessage
|
||||||
|
from email.utils import formataddr
|
||||||
|
from random import random
|
||||||
|
import smtplib
|
||||||
|
import os
|
||||||
|
import ssl
|
||||||
|
from time import sleep
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
from fastapi.responses import PlainTextResponse
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from sqlmodel import Session, select
|
||||||
|
from cutt.db import Player, TokenDB, engine
|
||||||
|
from cutt.security import set_password_token
|
||||||
|
|
||||||
|
P = Player
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
|
||||||
|
def generate_password_link(user: Player):
|
||||||
|
with Session(engine) as session:
|
||||||
|
token = set_password_token(user)
|
||||||
|
if token:
|
||||||
|
session.add(TokenDB(token=token))
|
||||||
|
session.commit()
|
||||||
|
return f"https://cutt.0124816.xyz/setpassword?token={token}"
|
||||||
|
|
||||||
|
|
||||||
|
class EmailRequest(BaseModel):
|
||||||
|
email: str
|
||||||
|
|
||||||
|
|
||||||
|
def send_forgotten_password_link(email: EmailRequest):
|
||||||
|
with Session(engine) as session:
|
||||||
|
user = session.exec(
|
||||||
|
select(P).where(P.email == email.email, P.disabled != True)
|
||||||
|
).one_or_none()
|
||||||
|
if user and user.email:
|
||||||
|
link = generate_password_link(user)
|
||||||
|
msg = EmailMessage()
|
||||||
|
msg["Subject"] = "CUTT - reset password"
|
||||||
|
msg["From"] = "CUTT - cool ultimate team tool <cutt@0124816.xyz>"
|
||||||
|
msg["To"] = formataddr((user.display_name, user.email))
|
||||||
|
msg.set_content(
|
||||||
|
f"Hello {user.display_name},\nclick on the following link to set yourself a new password.\n\n{link}\n\nCheers,\nJulius"
|
||||||
|
)
|
||||||
|
with open("cutt/forgotten_password.html") as f:
|
||||||
|
html_body = (
|
||||||
|
f.read().replace("USER", user.display_name).replace("LINK", link)
|
||||||
|
)
|
||||||
|
msg.add_alternative(html_body, subtype="html")
|
||||||
|
context = ssl.create_default_context()
|
||||||
|
with smtplib.SMTP(
|
||||||
|
host=os.environ["SMTP_HOST"],
|
||||||
|
port=int(os.environ["SMTP_PORT"]),
|
||||||
|
timeout=20,
|
||||||
|
) as server:
|
||||||
|
server.starttls(context=context)
|
||||||
|
server.login(os.environ["SMTP_USER"], os.environ["SMTP_PASS"])
|
||||||
|
server.send_message(msg)
|
||||||
|
else:
|
||||||
|
sleep(random())
|
||||||
|
|
||||||
|
return PlainTextResponse(
|
||||||
|
"a link will be sent to this email, if it belongs to an existing user."
|
||||||
|
)
|
||||||
@@ -10,6 +10,7 @@ from sqlmodel import (
|
|||||||
)
|
)
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from cutt.analysis import analysis_router
|
from cutt.analysis import analysis_router
|
||||||
|
from cutt.mail import send_forgotten_password_link
|
||||||
from cutt.security import (
|
from cutt.security import (
|
||||||
get_current_active_user,
|
get_current_active_user,
|
||||||
login_for_access_token,
|
login_for_access_token,
|
||||||
@@ -229,6 +230,9 @@ api_router.include_router(
|
|||||||
api_router.include_router(team_router, dependencies=[Depends(get_current_active_user)])
|
api_router.include_router(team_router, dependencies=[Depends(get_current_active_user)])
|
||||||
api_router.include_router(analysis_router)
|
api_router.include_router(analysis_router)
|
||||||
api_router.add_api_route("/token", endpoint=login_for_access_token, methods=["POST"])
|
api_router.add_api_route("/token", endpoint=login_for_access_token, methods=["POST"])
|
||||||
|
api_router.add_api_route(
|
||||||
|
"/forgotten_password", endpoint=send_forgotten_password_link, methods=["POST"]
|
||||||
|
)
|
||||||
api_router.add_api_route("/set_password", endpoint=set_first_password, methods=["POST"])
|
api_router.add_api_route("/set_password", endpoint=set_first_password, methods=["POST"])
|
||||||
api_router.add_api_route("/register", endpoint=register, methods=["POST"])
|
api_router.add_api_route("/register", endpoint=register, methods=["POST"])
|
||||||
api_router.add_api_route("/logout", endpoint=logout, methods=["POST"])
|
api_router.add_api_route("/logout", endpoint=logout, methods=["POST"])
|
||||||
|
|||||||
@@ -161,9 +161,9 @@ def disable_player_team(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def disable_player(r: DisablePlayerRequest):
|
def disable_player(player_id: int):
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
player = session.exec(select(P).where(P.id == r.player_id)).one_or_none()
|
player = session.exec(select(P).where(P.id == player_id)).one_or_none()
|
||||||
if player:
|
if player:
|
||||||
player.disabled = True
|
player.disabled = True
|
||||||
session.add(player)
|
session.add(player)
|
||||||
@@ -308,7 +308,7 @@ player_router.add_api_route(
|
|||||||
dependencies=[Security(get_current_active_user, scopes=["admin"])],
|
dependencies=[Security(get_current_active_user, scopes=["admin"])],
|
||||||
)
|
)
|
||||||
player_router.add_api_route(
|
player_router.add_api_route(
|
||||||
"/disable",
|
"/disable/{player_id}",
|
||||||
endpoint=disable_player,
|
endpoint=disable_player,
|
||||||
methods=["DELETE"],
|
methods=["DELETE"],
|
||||||
dependencies=[Security(get_current_active_user, scopes=["admin"])],
|
dependencies=[Security(get_current_active_user, scopes=["admin"])],
|
||||||
|
|||||||
@@ -212,14 +212,12 @@ async def logout(response: Response):
|
|||||||
return {"message": "Successfully logged out"}
|
return {"message": "Successfully logged out"}
|
||||||
|
|
||||||
|
|
||||||
def set_password_token(username: str):
|
def set_password_token(user: Player):
|
||||||
user = get_user(username)
|
|
||||||
if user:
|
|
||||||
expire = timedelta(days=30)
|
expire = timedelta(days=30)
|
||||||
token = create_access_token(
|
token = create_access_token(
|
||||||
data={
|
data={
|
||||||
"sub": "set password",
|
"sub": "set password",
|
||||||
"username": username,
|
"username": user.username,
|
||||||
"name": user.display_name,
|
"name": user.display_name,
|
||||||
},
|
},
|
||||||
expires_delta=expire,
|
expires_delta=expire,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from cutt.db import Player, TokenDB, engine
|
|||||||
if len(sys.argv) > 1:
|
if len(sys.argv) > 1:
|
||||||
with Session(engine) as session:
|
with Session(engine) as session:
|
||||||
for p in session.exec(
|
for p in session.exec(
|
||||||
select(Player.username).where(Player.username == sys.argv[1].strip())
|
select(Player).where(Player.username == sys.argv[1].strip())
|
||||||
):
|
):
|
||||||
print(p)
|
print(p)
|
||||||
token = set_password_token(p)
|
token = set_password_token(p)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { GraphComponent } from "./Network";
|
|||||||
import MVPChart from "./MVPChart";
|
import MVPChart from "./MVPChart";
|
||||||
import { SetPassword } from "./SetPassword";
|
import { SetPassword } from "./SetPassword";
|
||||||
import { Register } from "./Register";
|
import { Register } from "./Register";
|
||||||
|
import { ForgotPassword } from "./Login";
|
||||||
|
|
||||||
const Maintenance = () => {
|
const Maintenance = () => {
|
||||||
return (
|
return (
|
||||||
@@ -28,6 +29,7 @@ function App() {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/register" element={<Register />} />
|
<Route path="/register" element={<Register />} />
|
||||||
<Route path="/setpassword" element={<SetPassword />} />
|
<Route path="/setpassword" element={<SetPassword />} />
|
||||||
|
<Route path="/forgotten_password" element={<ForgotPassword />} />
|
||||||
<Route
|
<Route
|
||||||
path="/*"
|
path="/*"
|
||||||
element={
|
element={
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { FormEvent, useEffect, useState } from "react";
|
||||||
import { currentUser, login, User } from "./api";
|
import { baseUrl, currentUser, login, User } from "./api";
|
||||||
import Header from "./Header";
|
import Header from "./Header";
|
||||||
import { useLocation, useNavigate } from "react-router";
|
import { useLocation, useNavigate } from "react-router";
|
||||||
import { Eye, EyeSlash } from "./Icons";
|
import { Eye, EyeSlash } from "./Icons";
|
||||||
@@ -35,7 +35,7 @@ export const Login = ({ onLogin }: LoginProps) => {
|
|||||||
onLogin(user);
|
onLogin(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSubmit(e: React.FormEvent) {
|
function handleSubmit(e: FormEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
doLogin();
|
doLogin();
|
||||||
}
|
}
|
||||||
@@ -86,6 +86,9 @@ export const Login = ({ onLogin }: LoginProps) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
|
<p className="help is-link has-text-right">
|
||||||
|
<a href="forgotten_password">forgot password?</a>
|
||||||
|
</p>
|
||||||
{error && <p className="help is-danger">{error}</p>}
|
{error && <p className="help is-danger">{error}</p>}
|
||||||
</div>
|
</div>
|
||||||
<div className="control">
|
<div className="control">
|
||||||
@@ -102,3 +105,81 @@ export const Login = ({ onLogin }: LoginProps) => {
|
|||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const ForgotPassword = () => {
|
||||||
|
const [email, setEmail] = useState("");
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
async function handleSubmit(e: FormEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
const req = new Request(`${baseUrl}api/forgotten_password`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email: decodeURI(email) }),
|
||||||
|
});
|
||||||
|
let resp: Response;
|
||||||
|
try {
|
||||||
|
resp = await fetch(req);
|
||||||
|
} catch (e) {
|
||||||
|
throw new Error(`request failed: ${e}`);
|
||||||
|
}
|
||||||
|
if (resp.ok) {
|
||||||
|
const content = await resp.text();
|
||||||
|
if (content) setError(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!resp.ok) {
|
||||||
|
const content = await resp.text();
|
||||||
|
if (content) setError(content);
|
||||||
|
else setError("unauthorized");
|
||||||
|
throw new Error("Unauthorized");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="section is-medium">
|
||||||
|
<div className="container is-max-tablet">
|
||||||
|
<div className="block">
|
||||||
|
<p className="level-item has-text-centered">
|
||||||
|
<img
|
||||||
|
className="image"
|
||||||
|
alt="cool ultimate team tool"
|
||||||
|
src="cutt.svg"
|
||||||
|
style={{ width: 200 }}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<h1 className="title">Forgot password</h1>
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="field">
|
||||||
|
<div className="field">
|
||||||
|
<label className="label">email</label>
|
||||||
|
<div className="control">
|
||||||
|
<input
|
||||||
|
className="input"
|
||||||
|
required
|
||||||
|
type="email"
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => {
|
||||||
|
setEmail(e.target.value);
|
||||||
|
setError("");
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="field">
|
||||||
|
<p className={"help" + (error ? " is-danger" : " is-success")}>
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="field is-grouped is-grouped-centered">
|
||||||
|
<button className="button is-light is-primary">send email</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -21,4 +21,5 @@ dependencies = [
|
|||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
"pyqt6>=6.10.1",
|
"pyqt6>=6.10.1",
|
||||||
|
"python-dotenv>=1.2.1",
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user