Compare commits

..

13 Commits

Author SHA1 Message Date
rubenwardy
99548ea65f Fix licenses being prefilled in package editor 2020-02-23 20:40:14 +00:00
rubenwardy
325ee02b49 Fix lack of download counter checks on non-release package download 2020-02-23 20:14:56 +00:00
rubenwardy
a60786d32c Fix non-admin users not being able to set profile URLs 2020-02-23 20:12:32 +00:00
rubenwardy
2976afd5d1 Update git-archive-all 2020-02-15 15:23:43 +00:00
rubenwardy
744c52ba18 Add links to GitHub oauth connection settings 2020-01-30 21:39:51 +00:00
rubenwardy
c31c1fd92a Change API Token warning to be friendlier 2020-01-30 21:01:50 +00:00
rubenwardy
36615ef656 Fix access token being exposed after APIToken edit 2020-01-25 18:26:55 +00:00
rubenwardy
53a5dffb26 Rename 'new tag' event to contain 'GitHub release' 2020-01-25 17:25:05 +00:00
rubenwardy
74f3a77a84 Fix 404 on GituHub log in 2020-01-25 17:23:14 +00:00
rubenwardy
a15f1ac223 Fix crash on existing GitHub App Integration 2020-01-25 03:09:59 +00:00
rubenwardy
19a626e237 Fix auto-webhook creation failure due to wrong scheme 2020-01-25 03:03:45 +00:00
rubenwardy
43c2ee6b7b Improve documentation 2020-01-25 02:36:10 +00:00
rubenwardy
b1555bfcd5 Fix git-created release regression 2020-01-25 02:36:10 +00:00
14 changed files with 82 additions and 39 deletions

View File

@@ -80,14 +80,13 @@ def create_edit_token(username, id=None):
token.owner = user token.owner = user
token.access_token = randomString(32) token.access_token = randomString(32)
# Store token so it can be shown in the edit page
session["token_" + str(token.id)] = token.access_token
form.populate_obj(token) form.populate_obj(token)
db.session.add(token) db.session.add(token)
db.session.commit() # save db.session.commit() # save
# Store token so it can be shown in the edit page
session["token_" + str(token.id)] = token.access_token
return redirect(url_for("api.create_edit_token", username=username, id=token.id)) return redirect(url_for("api.create_edit_token", username=username, id=token.id))
return render_template("api/create_edit_token.html", user=user, form=form, token=token, access_token=access_token) return render_template("api/create_edit_token.html", user=user, form=form, token=token, access_token=access_token)

View File

@@ -18,13 +18,13 @@ from flask import Blueprint
bp = Blueprint("github", __name__) bp = Blueprint("github", __name__)
from flask import redirect, url_for, request, flash, abort, render_template, jsonify from flask import redirect, url_for, request, flash, abort, render_template, jsonify, current_app
from flask_user import current_user, login_required from flask_user import current_user, login_required
from sqlalchemy import func from sqlalchemy import func
from flask_github import GitHub from flask_github import GitHub
from app import github, csrf from app import github, csrf
from app.models import db, User, APIToken, Package, Permission from app.models import db, User, APIToken, Package, Permission
from app.utils import loginUser, randomString from app.utils import loginUser, randomString, abs_url_for
from app.blueprints.api.support import error, handleCreateRelease from app.blueprints.api.support import error, handleCreateRelease
import hmac, requests, json import hmac, requests, json
@@ -33,7 +33,13 @@ from wtforms import SelectField, SubmitField
@bp.route("/github/start/") @bp.route("/github/start/")
def start(): def start():
return github.authorize("", redirect_uri=url_for("github.callback")) return github.authorize("", redirect_uri=abs_url_for("github.callback"))
@bp.route("/github/view/")
def view_permissions():
url = "https://github.com/settings/connections/applications/" + \
current_app.config["GITHUB_CLIENT_ID"]
return redirect(url)
@bp.route("/github/callback/") @bp.route("/github/callback/")
@github.authorized_handler @github.authorized_handler
@@ -141,7 +147,7 @@ def webhook():
class SetupWebhookForm(FlaskForm): class SetupWebhookForm(FlaskForm):
event = SelectField("Event Type", choices=[('create', 'New tag'), ('push', 'Push')]) event = SelectField("Event Type", choices=[('create', 'New tag or GitHub release'), ('push', 'Push')])
submit = SubmitField("Save") submit = SubmitField("Save")
@@ -180,12 +186,12 @@ def setup_webhook():
if current_user.github_access_token is None: if current_user.github_access_token is None:
return github.authorize("write:repo_hook", \ return github.authorize("write:repo_hook", \
redirect_uri=url_for("github.callback_webhook", pid=pid, _external=True)) redirect_uri=abs_url_for("github.callback_webhook", pid=pid))
form = SetupWebhookForm(formdata=request.form) form = SetupWebhookForm(formdata=request.form)
if request.method == "POST" and form.validate(): if request.method == "POST" and form.validate():
token = APIToken() token = APIToken()
token.name = "Github Webhook for " + package.title token.name = "GitHub Webhook for " + package.title
token.owner = current_user token.owner = current_user
token.access_token = randomString(32) token.access_token = randomString(32)
token.package = package token.package = package
@@ -196,6 +202,7 @@ def setup_webhook():
if handleMakeWebhook(gh_user, gh_repo, package, \ if handleMakeWebhook(gh_user, gh_repo, package, \
current_user.github_access_token, event, token): current_user.github_access_token, event, token):
flash("Successfully created webhook", "success")
return redirect(package.getDetailsURL()) return redirect(package.getDetailsURL())
else: else:
return redirect(url_for("github.setup_webhook", pid=package.id)) return redirect(url_for("github.setup_webhook", pid=package.id))
@@ -214,7 +221,7 @@ def handleMakeWebhook(gh_user, gh_repo, package, oauth, event, token):
"active": True, "active": True,
"events": [event], "events": [event],
"config": { "config": {
"url": url_for("github.webhook", _external=True), "url": abs_url_for("github.webhook"),
"content_type": "json", "content_type": "json",
"secret": token.access_token "secret": token.access_token
}, },
@@ -235,7 +242,8 @@ def handleMakeWebhook(gh_user, gh_repo, package, oauth, event, token):
return False return False
for hook in r.json(): for hook in r.json():
if hook["config"]["url"] == data["config"]["url"]: if hook.get("config") and hook["config"].get("url") and \
hook["config"]["url"] == data["config"]["url"]:
flash("Failed to create webhook, as it already exists", "danger") flash("Failed to create webhook, as it already exists", "danger")
return False return False

View File

@@ -164,12 +164,7 @@ def download(package):
flash("No download available.", "danger") flash("No download available.", "danger")
return redirect(package.getDetailsURL()) return redirect(package.getDetailsURL())
else: else:
PackageRelease.query.filter_by(id=release.id).update({ return redirect(release.getDownloadURL(), code=302)
"downloads": PackageRelease.downloads + 1
})
db.session.commit()
return redirect(release.url, code=302)
class PackageForm(FlaskForm): class PackageForm(FlaskForm):
@@ -178,8 +173,8 @@ class PackageForm(FlaskForm):
short_desc = StringField("Short Description (Plaintext)", [InputRequired(), Length(1,200)]) short_desc = StringField("Short Description (Plaintext)", [InputRequired(), Length(1,200)])
desc = TextAreaField("Long Description (Markdown)", [Optional(), Length(0,10000)]) desc = TextAreaField("Long Description (Markdown)", [Optional(), Length(0,10000)])
type = SelectField("Type", [InputRequired()], choices=PackageType.choices(), coerce=PackageType.coerce, default=PackageType.MOD) type = SelectField("Type", [InputRequired()], choices=PackageType.choices(), coerce=PackageType.coerce, default=PackageType.MOD)
license = QuerySelectField("License", [InputRequired()], query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name) license = QuerySelectField("License", [DataRequired()], allow_blank=True, query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name)
media_license = QuerySelectField("Media License", [InputRequired()], query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name) media_license = QuerySelectField("Media License", [DataRequired()], allow_blank=True, query_factory=lambda: License.query.order_by(db.asc(License.name)), get_pk=lambda a: a.id, get_label=lambda a: a.name)
provides_str = StringField("Provides (mods included in package)", [Optional()]) provides_str = StringField("Provides (mods included in package)", [Optional()])
tags = QuerySelectMultipleField('Tags', query_factory=lambda: Tag.query.order_by(db.asc(Tag.name)), get_pk=lambda a: a.id, get_label=lambda a: a.title) tags = QuerySelectMultipleField('Tags', query_factory=lambda: Tag.query.order_by(db.asc(Tag.name)), get_pk=lambda a: a.id, get_label=lambda a: a.title)
harddep_str = StringField("Hard Dependencies", [Optional()]) harddep_str = StringField("Hard Dependencies", [Optional()])
@@ -227,6 +222,8 @@ def create_edit(author=None, name=None):
form.title.data = request.args.get("title") form.title.data = request.args.get("title")
form.repo.data = request.args.get("repo") form.repo.data = request.args.get("repo")
form.forums.data = request.args.get("forums") form.forums.data = request.args.get("forums")
form.license.data = None
form.media_license.data = None
else: else:
form.harddep_str.data = ",".join([str(x) for x in package.getSortedHardDependencies() ]) form.harddep_str.data = ",".join([str(x) for x in package.getSortedHardDependencies() ])
form.softdep_str.data = ",".join([str(x) for x in package.getSortedOptionalDependencies() ]) form.softdep_str.data = ",".join([str(x) for x in package.getSortedOptionalDependencies() ])

View File

@@ -63,6 +63,8 @@ def profile(username):
# Copy form fields to user_profile fields # Copy form fields to user_profile fields
if user.checkPerm(current_user, Permission.CHANGE_DNAME): if user.checkPerm(current_user, Permission.CHANGE_DNAME):
user.display_name = form["display_name"].data user.display_name = form["display_name"].data
if user.checkPerm(current_user, Permission.CHANGE_PROFILE_URLS):
user.website_url = form["website_url"].data user.website_url = form["website_url"].data
user.donate_url = form["donate_url"].data user.donate_url = form["donate_url"].data

View File

@@ -2,6 +2,9 @@ title: Creating Releases using Webhooks
## What does this mean? ## What does this mean?
A webhook is a notification from one service to another. Put simply, a webhook
is used to notify ContentDB that the git repository has changed.
ContentDB offers the ability to automatically create releases using webhooks ContentDB offers the ability to automatically create releases using webhooks
from either Github or Gitlab. If you're not using either of those services, from either Github or Gitlab. If you're not using either of those services,
you can also use the [API](../api) to create releases. you can also use the [API](../api) to create releases.
@@ -12,7 +15,7 @@ The process is as follows:
for Github. for Github.
2. The user pushes a commit to the git host (Gitlab or Github). 2. The user pushes a commit to the git host (Gitlab or Github).
3. The git host posts a webhook notification to ContentDB, using the API token assigned to it. 3. The git host posts a webhook notification to ContentDB, using the API token assigned to it.
4. ContentDB checks the API token and issues a new releases. 4. ContentDB checks the API token and issues a new release.
<p class="alert alert-info"> <p class="alert alert-info">
This feature is in beta, and is only available for Trusted Members. This feature is in beta, and is only available for Trusted Members.
@@ -20,14 +23,20 @@ The process is as follows:
## Setting up ## Setting up
### Github (automatic) ### GitHub (automatic)
1. Go to your package page. 1. Go to your package's page.
2. Make sure that the repository URL is set to a Github repository. 2. Make sure that the repository URL is set to a Github repository.
Only github.com is supported. Only github.com is supported.
3. Click "Set up a webhook to create releases automatically" below the releases 3. Go to "Releases" > "+", and click "Setup webhook" at the top of the create release
panel on the side bar. page.
4. Grant ContentDB the ability to manage Webhooks If you do not see this, either the repository isn't using Github or you do
not have permission to use webhook releases (ie: you're not a Trusted Member).
4. Grant ContentDB the ability to manage Webhooks.
5. Set the event to either "New tag or Github Release" (highly recommended) or "Push".
N.B.: GitHub uses tags to power GitHub Releases, meaning that creating a webhook
on "New tag" will sync GitHub and ContentDB releases.
### GitHub (manual) ### GitHub (manual)
@@ -40,7 +49,7 @@ The process is as follows:
7. Set the events 7. Set the events
* If you want a rolling release, choose "just the push event". * If you want a rolling release, choose "just the push event".
* Or if you want a stable release cycle based on tags, * Or if you want a stable release cycle based on tags,
choose "Let me select" > Branch or tag creation. choose "Let me select" > Branch or tag creation.
### GitLab (manual) ### GitLab (manual)

View File

@@ -93,6 +93,7 @@ class Permission(enum.Enum):
UNAPPROVE_PACKAGE = "UNAPPROVE_PACKAGE" UNAPPROVE_PACKAGE = "UNAPPROVE_PACKAGE"
TOPIC_DISCARD = "TOPIC_DISCARD" TOPIC_DISCARD = "TOPIC_DISCARD"
CREATE_TOKEN = "CREATE_TOKEN" CREATE_TOKEN = "CREATE_TOKEN"
CHANGE_PROFILE_URLS = "CHANGE_PROFILE_URLS"
# Only return true if the permission is valid for *all* contexts # Only return true if the permission is valid for *all* contexts
# See Package.checkPerm for package-specific contexts # See Package.checkPerm for package-specific contexts
@@ -192,7 +193,7 @@ class User(db.Model, UserMixin):
return user.rank.atLeast(UserRank.EDITOR) return user.rank.atLeast(UserRank.EDITOR)
elif perm == Permission.CHANGE_RANK or perm == Permission.CHANGE_DNAME: elif perm == Permission.CHANGE_RANK or perm == Permission.CHANGE_DNAME:
return user.rank.atLeast(UserRank.MODERATOR) return user.rank.atLeast(UserRank.MODERATOR)
elif perm == Permission.CHANGE_EMAIL: elif perm == Permission.CHANGE_EMAIL or perm == Permission.CHANGE_PROFILE_URLS:
return user == self or (user.rank.atLeast(UserRank.MODERATOR) and user.rank.atLeast(self.rank)) return user == self or (user.rank.atLeast(UserRank.MODERATOR) and user.rank.atLeast(self.rank))
elif perm == Permission.CREATE_TOKEN: elif perm == Permission.CREATE_TOKEN:
if user == self: if user == self:

View File

@@ -19,6 +19,7 @@ from flask import render_template, url_for
from flask_mail import Message from flask_mail import Message
from app import mail from app import mail
from app.tasks import celery from app.tasks import celery
from app.utils import abs_url_for
@celery.task() @celery.task()
def sendVerifyEmail(newEmail, token): def sendVerifyEmail(newEmail, token):
@@ -34,7 +35,7 @@ def sendVerifyEmail(newEmail, token):
If this was you, then please click this link to verify the address: If this was you, then please click this link to verify the address:
{} {}
""".format(url_for('users.verify_email', token=token, _external=True)) """.format(abs_url_for('users.verify_email', token=token))
msg.html = render_template("emails/verify.html", token=token) msg.html = render_template("emails/verify.html", token=token)
mail.send(msg) mail.send(msg)

View File

@@ -160,10 +160,7 @@ def cloneRepo(urlstr, ref=None, recursive=False):
origin = repo.create_remote("origin", url=gitUrl) origin = repo.create_remote("origin", url=gitUrl)
assert origin.exists() assert origin.exists()
origin.fetch() origin.fetch()
origin.pull(ref)
new_head = repo.commit(ref) #repo.create_head("target", ref)
repo.head.reference = new_head
repo.head.reset(index=True, working_tree=True)
return gitDir, repo return gitDir, repo

View File

@@ -21,7 +21,8 @@
<h1 class="mt-0">{{ self.title() }}</h1> <h1 class="mt-0">{{ self.title() }}</h1>
<div class="alert alert-warning"> <div class="alert alert-warning">
{{ _("Use carefully, as you may be held responsible for any damage caused by rogue scripts") }} {{ _("API Tokens allow scripts to act on your behalf.") }}
{{ _("Be careful with what/whom you share tokens with, as you are responsible for your account's actions.") }}
</div> </div>
{% if token %} {% if token %}

View File

@@ -16,12 +16,12 @@
If this was you, then please click this link to verify the address: If this was you, then please click this link to verify the address:
</p> </p>
<a class="btn" href="{{ url_for('users.verify_email', token=token, _external=True) }}"> <a class="btn" href="{{ abs_url_for('users.verify_email', token=token) }}">
Confirm Email Address Confirm Email Address
</a> </a>
<p style="font-size: 80%;"> <p style="font-size: 80%;">
Or paste this into your browser: {{ url_for('users.verify_email', token=token, _external=True) }} Or paste this into your browser: {{ abs_url_for('users.verify_email', token=token) }}
<p> <p>
{% endblock %} {% endblock %}

View File

@@ -20,4 +20,10 @@
{{ render_submit_field(form.submit) }} {{ render_submit_field(form.submit) }}
</form> </form>
<p class="mt-4">
You will need admin access to the repository.
When setting up hooks on an organisation,
<a href="{{ url_for('github.view_permissions') }}">make sure that you have granted access</a>.
</p>
{% endblock %} {% endblock %}

View File

@@ -64,6 +64,7 @@
| <a href="{{ user.website_url }}" rel="nofollow">Website</a> | <a href="{{ user.website_url }}" rel="nofollow">Website</a>
{% endif %} {% endif %}
{% if user == current_user %} {% if user == current_user %}
<br> <br>
<small class="text-muted"> <small class="text-muted">
@@ -73,6 +74,16 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% if user == current_user and user.github_username %}
<tr>
<td>Privacy:</td>
<td>
<a href="{{ url_for('github.view_permissions') }}">View ContentDB's GitHub Permissions</a>
</td>
</tr>
{% endif %}
{% if current_user.is_authenticated and current_user.rank.atLeast(current_user.rank.MODERATOR) %} {% if current_user.is_authenticated and current_user.rank.atLeast(current_user.rank.MODERATOR) %}
<tr> <tr>
<td>Admin</td> <td>Admin</td>
@@ -115,7 +126,7 @@
</a> </a>
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
<tr> <tr>
<td>Password:</td> <td>Password:</td>
<td> <td>
@@ -153,6 +164,9 @@
{% if user.checkPerm(current_user, "CHANGE_DNAME") %} {% if user.checkPerm(current_user, "CHANGE_DNAME") %}
{{ render_field(form.display_name, tabindex=230) }} {{ render_field(form.display_name, tabindex=230) }}
{% endif %}
{% if user.checkPerm(current_user, "CHANGE_PROFILE_URLS") %}
{{ render_field(form.website_url, tabindex=232) }} {{ render_field(form.website_url, tabindex=232) }}
{{ render_field(form.donate_url, tabindex=233) }} {{ render_field(form.donate_url, tabindex=233) }}
{% endif %} {% endif %}
@@ -166,7 +180,9 @@
{{ render_field(form.rank, tabindex=250) }} {{ render_field(form.rank, tabindex=250) }}
{% endif %} {% endif %}
{{ render_submit_field(form.submit, tabindex=280) }} <p>
{{ render_submit_field(form.submit, tabindex=280) }}
</p>
</form> </form>
</div> </div>
</div> </div>

View File

@@ -22,6 +22,12 @@ from app.models import *
from app import app from app import app
import random, string, os, imghdr import random, string, os, imghdr
@app.template_filter()
def abs_url_for(path, **kwargs):
scheme = "https" if app.config["BASE_URL"][:5] == "https" else "http"
return url_for(path, _external=True, _scheme=scheme, **kwargs)
def get_int_or_abort(v, default=None): def get_int_or_abort(v, default=None):
try: try:
return int(v or default) return int(v or default)

View File

@@ -17,7 +17,7 @@ beautifulsoup4~=4.6
celery~=4.4 celery~=4.4
kombu~=4.6 kombu~=4.6
GitPython~=3.0 GitPython~=3.0
git-archive-all~=1.20 git-archive-all~=1.21
lxml~=4.2 lxml~=4.2
pillow~=7.0 pillow~=7.0
pyScss~=1.3 pyScss~=1.3