fediflax 1.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fediflax/__init__.py +7 -0
- fediflax/admin.py +127 -0
- fediflax/auth.py +292 -0
- fediflax/core/__init__.py +1 -0
- fediflax/core/instance.py +104 -0
- fediflax/core/web/__init__.py +4 -0
- fediflax/core/web/templates/web/create_page.html +51 -0
- fediflax/core/web/templates/web/delete_page.html +28 -0
- fediflax/core/web/utils.py +16 -0
- fediflax/core/web/web_manager.py +251 -0
- fediflax/database.py +8 -0
- fediflax/extensions/__init__.py +1 -0
- fediflax/extensions/custom_pages/__init__.py +1 -0
- fediflax/extensions/custom_pages/custom_page.py +37 -0
- fediflax/extensions/custom_pages/extension.py +58 -0
- fediflax/extensions/custom_pages/templates/custom/dummy.html +10 -0
- fediflax/extensions/custom_pages/templates/custom_pages/edit.html +26 -0
- fediflax/extensions/custom_pages/templates/samples/privacy_policy.html +111 -0
- fediflax/extensions/custom_pages/templates/samples/privacy_policy.params.html +13 -0
- fediflax/extensions/documents/__init__.py +1 -0
- fediflax/extensions/documents/document_page.py +77 -0
- fediflax/extensions/documents/extension.py +109 -0
- fediflax/extensions/documents/templates/documents/show.html +6 -0
- fediflax/extensions/documents/templates/documents/update.html +31 -0
- fediflax/federation.py +520 -0
- fediflax/legacy/templates/admin/setup.html +77 -0
- fediflax/legacy/templates/auth/authorization.html +14 -0
- fediflax/legacy/templates/auth/login.html +15 -0
- fediflax/legacy/templates/auth/register.html +26 -0
- fediflax/legacy/templates/media/media_details.html +62 -0
- fediflax/legacy/templates/media/media_gallery.html +23 -0
- fediflax/legacy/templates/media/upload_media.html +28 -0
- fediflax/legacy/templates/settings/settings.html +37 -0
- fediflax/legacy/templates.py +6 -0
- fediflax/media.py +114 -0
- fediflax/migrations/1768149513_initial_database_schema.py +224 -0
- fediflax/migrations/script.py.mako +28 -0
- fediflax/models.py +487 -0
- fediflax/settings.py +74 -0
- fediflax/startup.py +53 -0
- fediflax/static/smiley.png +0 -0
- fediflax/templates/home.html +10 -0
- fediflax/themes/.gitignore +1 -0
- fediflax/themes/__init__.py +1 -0
- fediflax/themes/fiber/__init__.py +3 -0
- fediflax/themes/fiber/static/css/app.css +24 -0
- fediflax/themes/fiber/static/css/foundation.css +7753 -0
- fediflax/themes/fiber/static/css/foundation.min.css +1 -0
- fediflax/themes/fiber/static/favicon.ico +0 -0
- fediflax/themes/fiber/static/fontawesome/LICENSE.txt +165 -0
- fediflax/themes/fiber/static/fontawesome/css/all.css +10879 -0
- fediflax/themes/fiber/static/fontawesome/css/all.min.css +9 -0
- fediflax/themes/fiber/static/fontawesome/css/brands.css +2371 -0
- fediflax/themes/fiber/static/fontawesome/css/brands.min.css +6 -0
- fediflax/themes/fiber/static/fontawesome/css/fontawesome.css +8425 -0
- fediflax/themes/fiber/static/fontawesome/css/fontawesome.min.css +8 -0
- fediflax/themes/fiber/static/fontawesome/css/regular.css +31 -0
- fediflax/themes/fiber/static/fontawesome/css/regular.min.css +6 -0
- fediflax/themes/fiber/static/fontawesome/css/solid.css +31 -0
- fediflax/themes/fiber/static/fontawesome/css/solid.min.css +6 -0
- fediflax/themes/fiber/static/fontawesome/css/svg-with-js.css +557 -0
- fediflax/themes/fiber/static/fontawesome/css/svg-with-js.min.css +6 -0
- fediflax/themes/fiber/static/fontawesome/css/svg.css +183 -0
- fediflax/themes/fiber/static/fontawesome/css/svg.min.css +6 -0
- fediflax/themes/fiber/static/fontawesome/css/v4-font-face.css +27 -0
- fediflax/themes/fiber/static/fontawesome/css/v4-font-face.min.css +6 -0
- fediflax/themes/fiber/static/fontawesome/css/v4-shims.css +2818 -0
- fediflax/themes/fiber/static/fontawesome/css/v4-shims.min.css +6 -0
- fediflax/themes/fiber/static/fontawesome/css/v5-font-face.css +23 -0
- fediflax/themes/fiber/static/fontawesome/css/v5-font-face.min.css +6 -0
- fediflax/themes/fiber/static/fontawesome/js/all.js +8128 -0
- fediflax/themes/fiber/static/fontawesome/js/all.min.js +6 -0
- fediflax/themes/fiber/static/fontawesome/js/brands.js +1308 -0
- fediflax/themes/fiber/static/fontawesome/js/brands.min.js +6 -0
- fediflax/themes/fiber/static/fontawesome/js/conflict-detection.js +1519 -0
- fediflax/themes/fiber/static/fontawesome/js/conflict-detection.min.js +6 -0
- fediflax/themes/fiber/static/fontawesome/js/fontawesome.js +3728 -0
- fediflax/themes/fiber/static/fontawesome/js/fontawesome.min.js +6 -0
- fediflax/themes/fiber/static/fontawesome/js/regular.js +927 -0
- fediflax/themes/fiber/static/fontawesome/js/regular.min.js +6 -0
- fediflax/themes/fiber/static/fontawesome/js/solid.js +2180 -0
- fediflax/themes/fiber/static/fontawesome/js/solid.min.js +6 -0
- fediflax/themes/fiber/static/fontawesome/js/v4-shims.js +731 -0
- fediflax/themes/fiber/static/fontawesome/js/v4-shims.min.js +6 -0
- fediflax/themes/fiber/static/fontawesome/webfonts/fa-brands-400.woff2 +0 -0
- fediflax/themes/fiber/static/fontawesome/webfonts/fa-regular-400.woff2 +0 -0
- fediflax/themes/fiber/static/fontawesome/webfonts/fa-solid-900.woff2 +0 -0
- fediflax/themes/fiber/static/fontawesome/webfonts/fa-v4compatibility.woff2 +0 -0
- fediflax/themes/fiber/static/js/app.js +1 -0
- fediflax/themes/fiber/static/js/vendor/foundation.js +460 -0
- fediflax/themes/fiber/static/js/vendor/foundation.min.js +1 -0
- fediflax/themes/fiber/static/js/vendor/jquery.js +10716 -0
- fediflax/themes/fiber/static/js/vendor/what-input.js +517 -0
- fediflax/themes/fiber/static/logo.png +0 -0
- fediflax/themes/fiber/templates/base.html +136 -0
- fediflax/themes/fiber/theme.py +30 -0
- fediflax/types.py +44 -0
- fediflax/well_known.py +57 -0
- fediflax-1.4.0.dist-info/METADATA +63 -0
- fediflax-1.4.0.dist-info/RECORD +102 -0
- fediflax-1.4.0.dist-info/WHEEL +4 -0
- fediflax-1.4.0.dist-info/licenses/LICENSE +235 -0
fediflax/__init__.py
ADDED
fediflax/admin.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
|
|
3
|
+
import click
|
|
4
|
+
from flask import (
|
|
5
|
+
Blueprint,
|
|
6
|
+
current_app,
|
|
7
|
+
flash,
|
|
8
|
+
g,
|
|
9
|
+
redirect,
|
|
10
|
+
render_template,
|
|
11
|
+
request,
|
|
12
|
+
session,
|
|
13
|
+
url_for,
|
|
14
|
+
)
|
|
15
|
+
from sqlalchemy import select
|
|
16
|
+
|
|
17
|
+
from fediflax import alembic, auth
|
|
18
|
+
from fediflax.database import db
|
|
19
|
+
from fediflax.federation import create_actor
|
|
20
|
+
from fediflax.models import Domain, FediActor, User
|
|
21
|
+
|
|
22
|
+
bp = Blueprint("admin", __name__, url_prefix="/admin")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def add_domain(domainname, owned):
|
|
26
|
+
# create actor and add it to the database
|
|
27
|
+
domain = Domain(name=domainname, owned_by_server=owned)
|
|
28
|
+
db.session.add(domain)
|
|
29
|
+
db.session.commit()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@bp.cli.command("add_domain")
|
|
33
|
+
@click.argument("domainname")
|
|
34
|
+
@click.option("--owned/--no-owned", default=False, prompt=True)
|
|
35
|
+
def add_domain_cli(domainname, owned):
|
|
36
|
+
add_domain(domainname, owned)
|
|
37
|
+
print("ok")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@bp.get("/setup")
|
|
41
|
+
def setup():
|
|
42
|
+
current_database_revision = alembic.migration_context.get_current_revision()
|
|
43
|
+
print(current_app.config.get("ADMIN_USER"))
|
|
44
|
+
# is_database_current = alembic.needs_upgrade()
|
|
45
|
+
if current_app.config.get("ADMIN_USER"):
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
else:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
return render_template(
|
|
52
|
+
"admin/setup.html", current_database_revision=current_database_revision
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@bp.post("/upgrade_database")
|
|
57
|
+
def upgrade_database():
|
|
58
|
+
admin_user = current_app.config.get("ADMIN_USER")
|
|
59
|
+
if admin_user is None or admin_user == session.get("username"):
|
|
60
|
+
alembic.upgrade()
|
|
61
|
+
flash(f"Database updated", "success")
|
|
62
|
+
else:
|
|
63
|
+
flash(f"Please login to update database", "warning")
|
|
64
|
+
|
|
65
|
+
return redirect(url_for("admin.setup"))
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@bp.post("/enable_admin")
|
|
69
|
+
def enable_admin():
|
|
70
|
+
admin_user = current_app.config.get("ADMIN_USER")
|
|
71
|
+
if admin_user is not None:
|
|
72
|
+
auth.enable_user(admin_user)
|
|
73
|
+
flash(f"Admin enabled.", "success")
|
|
74
|
+
else:
|
|
75
|
+
flash(f"No admin in config.", "warning")
|
|
76
|
+
|
|
77
|
+
return redirect(url_for("admin.setup"))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@bp.post("/add_domain")
|
|
81
|
+
def add_domain_post():
|
|
82
|
+
admin_user = current_app.config.get("ADMIN_USER")
|
|
83
|
+
if admin_user is not None and admin_user == session.get("username"):
|
|
84
|
+
add_domain(request.form.get("domain_name"), request.form.get("owned") == "on")
|
|
85
|
+
flash(f"Domain added.", "success")
|
|
86
|
+
else:
|
|
87
|
+
flash(f"Login as admin to add a domain", "warning")
|
|
88
|
+
|
|
89
|
+
return redirect(url_for("admin.setup"))
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@bp.post("/add_actor")
|
|
93
|
+
def add_actor():
|
|
94
|
+
admin_user = current_app.config.get("ADMIN_USER")
|
|
95
|
+
if admin_user is not None and admin_user == session.get("username"):
|
|
96
|
+
subject = (
|
|
97
|
+
request.form.get("actor_prefix") + "@" + request.form.get("domain_name")
|
|
98
|
+
)
|
|
99
|
+
actor: FediActor = db.session.execute(
|
|
100
|
+
select(FediActor).filter_by(subject=subject)
|
|
101
|
+
).scalar()
|
|
102
|
+
if actor is None:
|
|
103
|
+
actor = create_actor(
|
|
104
|
+
request.form.get("actor_prefix"), request.form.get("domain_name")
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
user: User = db.session.execute(
|
|
108
|
+
select(User).filter_by(username=request.form.get("user"))
|
|
109
|
+
).scalar()
|
|
110
|
+
if user is not None:
|
|
111
|
+
if request.form.get("user_default") == "on":
|
|
112
|
+
user.default_actor_uri = actor.uri
|
|
113
|
+
user.enabled = request.form.get("enable_user") == "on"
|
|
114
|
+
|
|
115
|
+
domain: Domain = db.session.execute(
|
|
116
|
+
select(Domain).filter_by(name=request.form.get("domain_name"))
|
|
117
|
+
).scalar()
|
|
118
|
+
if domain is not None and request.form.get("domain_default") == "on":
|
|
119
|
+
domain.main_actor_uri = actor.uri
|
|
120
|
+
|
|
121
|
+
db.session.commit()
|
|
122
|
+
|
|
123
|
+
flash(f"Actor added.", "success")
|
|
124
|
+
else:
|
|
125
|
+
flash(f"Login as admin to add a actor", "warning")
|
|
126
|
+
|
|
127
|
+
return redirect(url_for("admin.setup"))
|
fediflax/auth.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import logging
|
|
3
|
+
import secrets
|
|
4
|
+
from datetime import datetime, timedelta
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
from authlib.integrations.flask_oauth2 import (
|
|
8
|
+
AuthorizationServer,
|
|
9
|
+
ResourceProtector,
|
|
10
|
+
current_token,
|
|
11
|
+
)
|
|
12
|
+
from authlib.integrations.sqla_oauth2 import OAuth2ClientMixin
|
|
13
|
+
from authlib.oauth2 import OAuth2Request
|
|
14
|
+
from authlib.oauth2.rfc6749 import grants
|
|
15
|
+
from authlib.oauth2.rfc6750 import BearerTokenValidator
|
|
16
|
+
from flask import (
|
|
17
|
+
Blueprint,
|
|
18
|
+
current_app,
|
|
19
|
+
flash,
|
|
20
|
+
g,
|
|
21
|
+
redirect,
|
|
22
|
+
render_template,
|
|
23
|
+
request,
|
|
24
|
+
session,
|
|
25
|
+
url_for,
|
|
26
|
+
)
|
|
27
|
+
from sqlalchemy import func, select
|
|
28
|
+
from sqlalchemy.exc import IntegrityError
|
|
29
|
+
from werkzeug.security import check_password_hash, generate_password_hash
|
|
30
|
+
|
|
31
|
+
from fediflax.database import db
|
|
32
|
+
from fediflax.models import Client, FediActor, Grant, Token, User
|
|
33
|
+
|
|
34
|
+
bp = Blueprint("auth", __name__, url_prefix="/auth")
|
|
35
|
+
oauth_server = AuthorizationServer()
|
|
36
|
+
require_oauth = ResourceProtector()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AuthorizationCodeGrant(grants.AuthorizationCodeGrant):
|
|
40
|
+
def save_authorization_code(self, code, request):
|
|
41
|
+
print("save authorization code")
|
|
42
|
+
expires = datetime.utcnow() + timedelta(seconds=100)
|
|
43
|
+
grant = Grant(
|
|
44
|
+
client_id=request.client.client_id,
|
|
45
|
+
code=code,
|
|
46
|
+
user_name=request.user.get_user_id(),
|
|
47
|
+
redirect_uri=request.redirect_uri,
|
|
48
|
+
_scopes=request.client._default_scopes,
|
|
49
|
+
)
|
|
50
|
+
db.session.add(grant)
|
|
51
|
+
db.session.commit()
|
|
52
|
+
return grant
|
|
53
|
+
|
|
54
|
+
def query_authorization_code(self, code, client):
|
|
55
|
+
print("query authorization code")
|
|
56
|
+
grant = db.session.execute(
|
|
57
|
+
select(Grant).filter_by(client_id=client.client_id, code=code)
|
|
58
|
+
).scalar_one()
|
|
59
|
+
return grant
|
|
60
|
+
|
|
61
|
+
def delete_authorization_code(self, authorization_code):
|
|
62
|
+
db.session.delete(authorization_code)
|
|
63
|
+
db.session.commit()
|
|
64
|
+
|
|
65
|
+
def authenticate_user(self, authorization_code):
|
|
66
|
+
return authorization_code.user
|
|
67
|
+
|
|
68
|
+
def create_token_response(self):
|
|
69
|
+
(status_code, body, headers) = super().create_token_response()
|
|
70
|
+
if self.client.default_actor_uri:
|
|
71
|
+
body["me"] = self.client.default_actor_uri
|
|
72
|
+
return status_code, body, headers
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class MyBearerTokenValidator(BearerTokenValidator):
|
|
76
|
+
def authenticate_token(self, token_string):
|
|
77
|
+
token = db.session.execute(
|
|
78
|
+
select(Token).filter_by(access_token=token_string)
|
|
79
|
+
).scalar_one()
|
|
80
|
+
return token
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def init_oauth(app):
|
|
84
|
+
|
|
85
|
+
def get_client(client_id):
|
|
86
|
+
print("get client")
|
|
87
|
+
client = db.session.execute(
|
|
88
|
+
select(Client).filter_by(client_id=client_id)
|
|
89
|
+
).scalar_one()
|
|
90
|
+
return client
|
|
91
|
+
|
|
92
|
+
def save_token(token_data, request):
|
|
93
|
+
|
|
94
|
+
expires = datetime.utcnow() + timedelta(seconds=token_data.get("expires_in"))
|
|
95
|
+
|
|
96
|
+
tok = Token(
|
|
97
|
+
access_token=token_data["access_token"],
|
|
98
|
+
refresh_token=token_data.get("refresh_token"),
|
|
99
|
+
token_type=token_data["token_type"],
|
|
100
|
+
_scopes=token_data["scope"],
|
|
101
|
+
client_id=request.client.client_id,
|
|
102
|
+
user_name=request.user.username,
|
|
103
|
+
expires=expires,
|
|
104
|
+
)
|
|
105
|
+
db.session.add(tok)
|
|
106
|
+
db.session.commit()
|
|
107
|
+
return tok
|
|
108
|
+
|
|
109
|
+
oauth_server.init_app(app, query_client=get_client, save_token=save_token)
|
|
110
|
+
oauth_server.register_grant(AuthorizationCodeGrant)
|
|
111
|
+
require_oauth.register_token_validator(MyBearerTokenValidator())
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@bp.before_app_request
|
|
115
|
+
def load_logged_in_user():
|
|
116
|
+
username = session.get("username")
|
|
117
|
+
|
|
118
|
+
if username is None:
|
|
119
|
+
g.user = None
|
|
120
|
+
else:
|
|
121
|
+
try:
|
|
122
|
+
g.user = db.session.execute(
|
|
123
|
+
select(User).filter_by(username=username)
|
|
124
|
+
).scalar_one()
|
|
125
|
+
except Exception as e:
|
|
126
|
+
g.user = None
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def enable_user(username):
|
|
130
|
+
user = db.session.execute(select(User).filter_by(username=username)).scalar_one()
|
|
131
|
+
user.enabled = True
|
|
132
|
+
db.session.commit()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@bp.cli.command("enable_user")
|
|
136
|
+
@click.argument("username")
|
|
137
|
+
def enable_user_cli(username):
|
|
138
|
+
enable_user(username)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@bp.route("/login", methods=("GET", "POST"))
|
|
142
|
+
def login():
|
|
143
|
+
if request.method == "POST":
|
|
144
|
+
user = db.session.execute(
|
|
145
|
+
select(User).filter_by(username=request.form["username"])
|
|
146
|
+
).scalar()
|
|
147
|
+
if user is None:
|
|
148
|
+
flash(f"Login failed.", "warning")
|
|
149
|
+
return render_template("auth/login.html")
|
|
150
|
+
password_okay = check_password_hash(user.password, request.form["password"])
|
|
151
|
+
if user.enabled and password_okay:
|
|
152
|
+
session["username"] = user.username
|
|
153
|
+
actor = db.session.execute(
|
|
154
|
+
select(FediActor).filter_by(object_uri=user.default_actor_uri)
|
|
155
|
+
).scalar()
|
|
156
|
+
if actor is not None:
|
|
157
|
+
session["actor"] = actor.subject
|
|
158
|
+
flash(f"User {user.username} successful logged in.", "success")
|
|
159
|
+
current_app.logger.log(logging.INFO, user.username + "logged in.")
|
|
160
|
+
if session.get("login_redirect"):
|
|
161
|
+
return redirect(session["login_redirect"])
|
|
162
|
+
else:
|
|
163
|
+
return redirect(url_for("web.home"))
|
|
164
|
+
else:
|
|
165
|
+
flash(f"Login failed.", "warning")
|
|
166
|
+
return render_template("auth/login.html")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@bp.route("/register", methods=("GET", "POST"))
|
|
170
|
+
def register():
|
|
171
|
+
if request.method == "POST":
|
|
172
|
+
error = None
|
|
173
|
+
try:
|
|
174
|
+
username = request.form["username"]
|
|
175
|
+
user = User(
|
|
176
|
+
id=username,
|
|
177
|
+
username=username,
|
|
178
|
+
remarks=request.form["remarks"],
|
|
179
|
+
email=request.form["email"],
|
|
180
|
+
password=generate_password_hash(request.form["password"]),
|
|
181
|
+
)
|
|
182
|
+
db.session.add(user)
|
|
183
|
+
db.session.commit()
|
|
184
|
+
except IntegrityError:
|
|
185
|
+
flash(f"User {username} already exists", "warning")
|
|
186
|
+
else:
|
|
187
|
+
flash(f"User {username} successful created.", "success")
|
|
188
|
+
return redirect(url_for("auth.login"))
|
|
189
|
+
return render_template("auth/register.html")
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@bp.route("/logout", methods=("GET", "POST"))
|
|
193
|
+
def logout():
|
|
194
|
+
session.clear()
|
|
195
|
+
return redirect(url_for("auth.login"))
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@bp.post("client_registration")
|
|
199
|
+
def client_registration():
|
|
200
|
+
client_id = secrets.token_hex(16)
|
|
201
|
+
client_secret = secrets.token_urlsafe(32)
|
|
202
|
+
|
|
203
|
+
data = request.json
|
|
204
|
+
|
|
205
|
+
description = (
|
|
206
|
+
"Created on "
|
|
207
|
+
+ datetime.today().strftime("%Y-%m-%d %H:%M:%S")
|
|
208
|
+
+ "\n"
|
|
209
|
+
+ request.user_agent.string
|
|
210
|
+
)
|
|
211
|
+
if data.get("client_uri"):
|
|
212
|
+
description = description + "\n" + data["client_uri"]
|
|
213
|
+
|
|
214
|
+
client = Client(
|
|
215
|
+
name="",
|
|
216
|
+
description=description,
|
|
217
|
+
id=client_id,
|
|
218
|
+
client_id=client_id,
|
|
219
|
+
client_secret=generate_password_hash(client_secret),
|
|
220
|
+
is_confidential=True,
|
|
221
|
+
_redirect_uris=" ".join(data["redirect_uris"]),
|
|
222
|
+
)
|
|
223
|
+
if data.get("client_name"):
|
|
224
|
+
client.name = data["client_name"]
|
|
225
|
+
if data.get("token_endpoint_auth_method") == "none":
|
|
226
|
+
client.is_confidential = False
|
|
227
|
+
if data.get("scope"):
|
|
228
|
+
client._default_scopes = data["scope"]
|
|
229
|
+
|
|
230
|
+
db.session.add(client)
|
|
231
|
+
db.session.commit()
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
"client_id": client_id,
|
|
235
|
+
"client_secret": client_secret,
|
|
236
|
+
"client_secret_expires_at": 0,
|
|
237
|
+
"scope": data["scope"],
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
@bp.route("/authorization", methods=("GET", "POST"))
|
|
242
|
+
def authorize(*args, **kwargs):
|
|
243
|
+
if not g.user:
|
|
244
|
+
session["login_redirect"] = request.url
|
|
245
|
+
return redirect(url_for("auth.login"))
|
|
246
|
+
|
|
247
|
+
client = db.session.execute(
|
|
248
|
+
select(Client).filter_by(client_id=request.args.get("client_id"))
|
|
249
|
+
).scalar_one()
|
|
250
|
+
if client.owner and client.default_actor_uri:
|
|
251
|
+
return oauth_server.create_authorization_response(grant_user=client.owner)
|
|
252
|
+
|
|
253
|
+
if request.method == "GET":
|
|
254
|
+
return render_template("auth/authorization.html")
|
|
255
|
+
|
|
256
|
+
if request.method == "POST":
|
|
257
|
+
confirm = request.form.get("confirm", "no")
|
|
258
|
+
if confirm == "yes":
|
|
259
|
+
client.enabled = True
|
|
260
|
+
client.owner_id = g.user.username
|
|
261
|
+
actor = db.session.execute(
|
|
262
|
+
select(FediActor).filter_by(subject=request.form.get("actor"))
|
|
263
|
+
).scalar_one()
|
|
264
|
+
client.default_actor_uri = actor.object_uri
|
|
265
|
+
db.session.commit()
|
|
266
|
+
return oauth_server.create_authorization_response(grant_user=g.user)
|
|
267
|
+
else:
|
|
268
|
+
return oauth_server.create_authorization_response(grant_user=None)
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
@bp.route("/token", methods=["POST"])
|
|
272
|
+
def issue_token():
|
|
273
|
+
return oauth_server.create_token_response()
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@bp.route("/hello")
|
|
277
|
+
@require_oauth("read")
|
|
278
|
+
def hello():
|
|
279
|
+
return {"text": "Hello World!"}
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def require_permission():
|
|
283
|
+
def permission_decorator(view):
|
|
284
|
+
@functools.wraps(view)
|
|
285
|
+
def wrapper(*args, **kwargs):
|
|
286
|
+
if g.user is None:
|
|
287
|
+
return redirect(url_for("auth.login"))
|
|
288
|
+
return view(*args, **kwargs)
|
|
289
|
+
|
|
290
|
+
return wrapper
|
|
291
|
+
|
|
292
|
+
return permission_decorator
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""This folder contains the Fediflax modules required for core functionality."""
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
from os import PathLike
|
|
2
|
+
from typing import override
|
|
3
|
+
|
|
4
|
+
import yaml
|
|
5
|
+
from flask import Flask
|
|
6
|
+
from flask_sqlalchemy_lite import SQLAlchemy
|
|
7
|
+
|
|
8
|
+
from fediflax import alembic, talisman
|
|
9
|
+
from fediflax.database import db
|
|
10
|
+
|
|
11
|
+
from ..types import Core, Extension, PageType
|
|
12
|
+
from .web import WebManager
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class FediflaxInstance(Core):
|
|
16
|
+
""" """
|
|
17
|
+
|
|
18
|
+
def __init__(self, config_file: str | PathLike[str]):
|
|
19
|
+
self.extensions: list[Extension] = []
|
|
20
|
+
self.config_file: str | PathLike[str] = config_file
|
|
21
|
+
self.web: WebManager = WebManager()
|
|
22
|
+
self._db: SQLAlchemy = db
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
@override
|
|
26
|
+
def database(self):
|
|
27
|
+
return self._db
|
|
28
|
+
|
|
29
|
+
@override
|
|
30
|
+
def register_page_type(self, page_type: PageType):
|
|
31
|
+
self.web.register_page_type(page_type)
|
|
32
|
+
|
|
33
|
+
def add_extension(self, extension: Extension):
|
|
34
|
+
self.extensions.append(extension)
|
|
35
|
+
extension.init_core(self)
|
|
36
|
+
|
|
37
|
+
def add_default_extensions(self, custom_pages: bool = True, documents: bool = True):
|
|
38
|
+
if custom_pages:
|
|
39
|
+
from ..extensions.custom_pages import CustomPageExtension
|
|
40
|
+
|
|
41
|
+
self.add_extension(CustomPageExtension())
|
|
42
|
+
if documents:
|
|
43
|
+
from ..extensions.documents import DocumentPageExtension
|
|
44
|
+
|
|
45
|
+
self.add_extension(DocumentPageExtension())
|
|
46
|
+
|
|
47
|
+
def init_app(self, app: Flask):
|
|
48
|
+
self._init_config(app)
|
|
49
|
+
db.init_app(app)
|
|
50
|
+
alembic.init_app(app)
|
|
51
|
+
self._init_talisman(app)
|
|
52
|
+
self._register_core(app)
|
|
53
|
+
for extension in self.extensions:
|
|
54
|
+
extension.init_app(app, self)
|
|
55
|
+
|
|
56
|
+
def _init_config(self, app: Flask):
|
|
57
|
+
app.config.from_mapping(
|
|
58
|
+
SECRET_KEY="dev",
|
|
59
|
+
SQLALCHEMY_ENGINES={"default": "sqlite:///default.sqlite"},
|
|
60
|
+
INSTANCE_NAME="Instanz-Infos",
|
|
61
|
+
)
|
|
62
|
+
app.config.from_file(self.config_file, load=yaml.safe_load, text=False)
|
|
63
|
+
|
|
64
|
+
def _init_talisman(self, app: Flask):
|
|
65
|
+
csp = {
|
|
66
|
+
"default-src": "'self'",
|
|
67
|
+
"script-src": ["'strict-dynamic'", "'unsafe-eval'"],
|
|
68
|
+
"img-src": ["'self'", "data:"],
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
talisman.init_app(
|
|
72
|
+
app,
|
|
73
|
+
content_security_policy=csp,
|
|
74
|
+
content_security_policy_nonce_in=["script-src"],
|
|
75
|
+
)
|
|
76
|
+
pass
|
|
77
|
+
|
|
78
|
+
def _register_core(self, app: Flask):
|
|
79
|
+
|
|
80
|
+
self.web.init_app(app, self)
|
|
81
|
+
|
|
82
|
+
from ..legacy import templates as legacy_templates
|
|
83
|
+
|
|
84
|
+
legacy_templates.init_app(app)
|
|
85
|
+
|
|
86
|
+
from .. import (
|
|
87
|
+
admin,
|
|
88
|
+
auth,
|
|
89
|
+
federation,
|
|
90
|
+
media,
|
|
91
|
+
settings,
|
|
92
|
+
well_known,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
app.register_blueprint(admin.bp)
|
|
96
|
+
app.register_blueprint(auth.bp)
|
|
97
|
+
auth.init_oauth(app)
|
|
98
|
+
app.register_blueprint(federation.bp)
|
|
99
|
+
federation.register_hooks()
|
|
100
|
+
app.register_blueprint(media.bp)
|
|
101
|
+
app.register_blueprint(settings.bp)
|
|
102
|
+
# app.register_blueprint(web.bp)
|
|
103
|
+
app.register_blueprint(well_known.bp)
|
|
104
|
+
app.add_url_rule("/", endpoint="web.home")
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{% extends 'base.html' %}
|
|
2
|
+
|
|
3
|
+
{% block header %}
|
|
4
|
+
<h1>{% block title %}Create Page{% endblock %}</h1>
|
|
5
|
+
{% endblock %}
|
|
6
|
+
|
|
7
|
+
{% block content %}
|
|
8
|
+
<form method="post">
|
|
9
|
+
|
|
10
|
+
<label>Actor
|
|
11
|
+
<input type="text" name="actor" id="nav_actor" value="{{actor}}" required readonly>
|
|
12
|
+
</label>
|
|
13
|
+
|
|
14
|
+
<label>Navigation Menu
|
|
15
|
+
|
|
16
|
+
<select name="nav_section" required>
|
|
17
|
+
<option value="home">Home</option>
|
|
18
|
+
<option value="main">Main</option>
|
|
19
|
+
<option value="profile_page">Profile Page</option>
|
|
20
|
+
<option value="domain_footer">Domain Footer</option>
|
|
21
|
+
<option value="instance_footer">Instance Footer</option>
|
|
22
|
+
|
|
23
|
+
</select>
|
|
24
|
+
</label>
|
|
25
|
+
|
|
26
|
+
<label>Name
|
|
27
|
+
<input type="text" name="nav_name" id="nav_name" required />
|
|
28
|
+
</label>
|
|
29
|
+
|
|
30
|
+
<label>Order hint
|
|
31
|
+
<input type="text" name="nav_order" id="nav_order" />
|
|
32
|
+
</label>
|
|
33
|
+
|
|
34
|
+
<label>Path
|
|
35
|
+
<input type="text" name="nav_path" id="nav_path" required />
|
|
36
|
+
</label>
|
|
37
|
+
|
|
38
|
+
<label>Page Type
|
|
39
|
+
<select name="page_type" required>
|
|
40
|
+
<option value="document">Markdown Document</option>
|
|
41
|
+
<option value="custom_page">Angepasste Seite</option>
|
|
42
|
+
</select>
|
|
43
|
+
</label>
|
|
44
|
+
|
|
45
|
+
<!-- <label> Draft
|
|
46
|
+
<input type="checkbox" name="draft" checked />
|
|
47
|
+
</label> -->
|
|
48
|
+
|
|
49
|
+
<button type="submit" name="action" class="button" value="create">Create page</button>
|
|
50
|
+
</form>
|
|
51
|
+
{% endblock %}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{% extends 'base.html' %}
|
|
2
|
+
|
|
3
|
+
{% block header %}
|
|
4
|
+
<h1>{% block title %}Delete Page{% endblock %}</h1>
|
|
5
|
+
{% endblock %}
|
|
6
|
+
|
|
7
|
+
{% block content %}
|
|
8
|
+
Do you want to delete the following page?
|
|
9
|
+
<form method="post">
|
|
10
|
+
|
|
11
|
+
<input type="hidden" name="csrf-check" value="{{csrf_check}}" required/>
|
|
12
|
+
|
|
13
|
+
<label>ID
|
|
14
|
+
<input type="text" name="id" id="page_id" value="{{page.id}}" required readonly />
|
|
15
|
+
</label>
|
|
16
|
+
|
|
17
|
+
<label>Actor
|
|
18
|
+
<input type="text" name="actor" id="nav_actor" value="{{page.actor}}" readonly />
|
|
19
|
+
</label>
|
|
20
|
+
|
|
21
|
+
<label>Path
|
|
22
|
+
<input type="text" name="nav_path" id="nav_path" value="{{page.subpath}}"" readonly />
|
|
23
|
+
</label>
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
<button type="submit" name="action" value="delete" class="button">Delete Page</button>
|
|
27
|
+
</form>
|
|
28
|
+
{% endblock %}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from flask import (
|
|
2
|
+
request,
|
|
3
|
+
url_for,
|
|
4
|
+
)
|
|
5
|
+
|
|
6
|
+
from fediflax.models import WebPage
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def url_for_page(page: WebPage) -> str:
|
|
10
|
+
actor, domain = page.actor.split("@")
|
|
11
|
+
return (
|
|
12
|
+
request.scheme
|
|
13
|
+
+ "://"
|
|
14
|
+
+ domain
|
|
15
|
+
+ url_for("web.show_page", actor=actor, subpath=page.subpath)
|
|
16
|
+
)
|