mirror of
https://github.com/simple-login/app.git
synced 2024-09-29 21:21:29 +02:00
Merge pull request #792 from acasajus/new/zendesk-support
Create support tickets via zendesk
This commit is contained in:
commit
69c8980c18
@ -414,3 +414,6 @@ PHONE_PROVIDER_1_SECRET = os.environ.get("PHONE_PROVIDER_1_SECRET")
|
|||||||
|
|
||||||
PHONE_PROVIDER_2_HEADER = os.environ.get("PHONE_PROVIDER_2_HEADER")
|
PHONE_PROVIDER_2_HEADER = os.environ.get("PHONE_PROVIDER_2_HEADER")
|
||||||
PHONE_PROVIDER_2_SECRET = os.environ.get("PHONE_PROVIDER_2_SECRET")
|
PHONE_PROVIDER_2_SECRET = os.environ.get("PHONE_PROVIDER_2_SECRET")
|
||||||
|
|
||||||
|
ZENDESK_HOST = os.environ.get("ZENDESK_HOST")
|
||||||
|
ZENDESK_API_TOKEN = os.environ.get("ZENDESK_API_TOKEN")
|
||||||
|
@ -31,4 +31,5 @@ from .views import (
|
|||||||
app,
|
app,
|
||||||
delete_account,
|
delete_account,
|
||||||
notification,
|
notification,
|
||||||
|
support,
|
||||||
)
|
)
|
||||||
|
106
app/dashboard/views/support.py
Normal file
106
app/dashboard/views/support.py
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
import json
|
||||||
|
import urllib.parse
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
|
import requests
|
||||||
|
from flask import render_template, request, flash, url_for, redirect, g
|
||||||
|
from flask_login import login_required, current_user
|
||||||
|
from werkzeug.datastructures import FileStorage
|
||||||
|
|
||||||
|
from app.dashboard.base import dashboard_bp
|
||||||
|
from app.extensions import limiter
|
||||||
|
from app.log import LOG
|
||||||
|
from app.config import ZENDESK_HOST, ZENDESK_API_TOKEN
|
||||||
|
|
||||||
|
VALID_MIME_TYPES = ["text/plain", "message/rfc822"]
|
||||||
|
|
||||||
|
|
||||||
|
def check_zendesk_response_status(response_code: int) -> bool:
|
||||||
|
if response_code != 201:
|
||||||
|
if response_code in (401, 422):
|
||||||
|
LOG.e("Could not authenticate")
|
||||||
|
else:
|
||||||
|
LOG.e("Problem with the request. Status {}".format(response_code))
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def upload_file_to_zendesk_and_get_upload_token(
|
||||||
|
email: str, file: FileStorage
|
||||||
|
) -> Union[None, str]:
|
||||||
|
if file.mimetype not in VALID_MIME_TYPES and not file.mimetype.startswith("image/"):
|
||||||
|
flash(
|
||||||
|
"File {} is not an image, text or an email".format(file.filename), "warning"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
escaped_filename = urllib.parse.urlencode({"filename": file.filename})
|
||||||
|
url = "https://{}/api/v2/uploads?{}".format(ZENDESK_HOST, escaped_filename)
|
||||||
|
headers = {"content-type": file.mimetype}
|
||||||
|
auth = ("{}/token".format(email), ZENDESK_API_TOKEN)
|
||||||
|
response = requests.post(url, headers=headers, data=file.stream, auth=auth)
|
||||||
|
if not check_zendesk_response_status(response.status_code):
|
||||||
|
return
|
||||||
|
data = response.json()
|
||||||
|
return data["upload"]["token"]
|
||||||
|
|
||||||
|
|
||||||
|
def create_zendesk_request(email: str, content: str, files: [FileStorage]) -> bool:
|
||||||
|
tokens = []
|
||||||
|
for file in files:
|
||||||
|
if not file.filename:
|
||||||
|
continue
|
||||||
|
token = upload_file_to_zendesk_and_get_upload_token(email, file)
|
||||||
|
if token is None:
|
||||||
|
return False
|
||||||
|
tokens.append(token)
|
||||||
|
data = {
|
||||||
|
"request": {
|
||||||
|
"subject": "Ticket created for user {}".format(current_user.id),
|
||||||
|
"comment": {"type": "Comment", "body": content, "uploads": tokens},
|
||||||
|
"requester": {
|
||||||
|
"name": "SimpleLogin user {}".format(current_user.id),
|
||||||
|
"email": email,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
url = "https://{}/api/v2/requests.json".format(ZENDESK_HOST)
|
||||||
|
headers = {"content-type": "application/json"}
|
||||||
|
auth = ("{}/token".format(email), ZENDESK_API_TOKEN)
|
||||||
|
response = requests.post(url, data=json.dumps(data), headers=headers, auth=auth)
|
||||||
|
if not check_zendesk_response_status(response.status_code):
|
||||||
|
return False
|
||||||
|
LOG.d("Ticket created")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@dashboard_bp.route("/support", methods=["GET", "POST"])
|
||||||
|
@login_required
|
||||||
|
@limiter.limit(
|
||||||
|
"2/hour",
|
||||||
|
methods=["POST"],
|
||||||
|
deduct_when=lambda r: hasattr(g, "deduct_limit") and g.deduct_limit,
|
||||||
|
)
|
||||||
|
def process_support_dialog():
|
||||||
|
if not ZENDESK_HOST:
|
||||||
|
return render_template("dashboard/support_disabled.html")
|
||||||
|
if request.method == "GET":
|
||||||
|
return render_template(
|
||||||
|
"dashboard/support.html", ticket_email=current_user.email
|
||||||
|
)
|
||||||
|
content = request.form.get("ticket_content")
|
||||||
|
email = request.form.get("ticket_email")
|
||||||
|
if not content:
|
||||||
|
flash("Please add a description", "error")
|
||||||
|
return render_template("dashboard/support.html", ticket_email=email)
|
||||||
|
if not email:
|
||||||
|
flash("Please add an email", "error")
|
||||||
|
return render_template("dashboard/support.html", ticket_content=content)
|
||||||
|
if not create_zendesk_request(
|
||||||
|
email, content, request.files.getlist("ticket_files")
|
||||||
|
):
|
||||||
|
return render_template(
|
||||||
|
"dashboard/support.html", ticket_email=email, ticket_content=content
|
||||||
|
)
|
||||||
|
g.deduct_limit = True
|
||||||
|
flash("Ticket created. You should have received an email notification.", "success")
|
||||||
|
return redirect(url_for("dashboard.index"))
|
97
templates/dashboard/support.html
Normal file
97
templates/dashboard/support.html
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
{% extends 'default.html' %}
|
||||||
|
|
||||||
|
{% set active_page = 'dashboard' %}
|
||||||
|
|
||||||
|
{% block title %}
|
||||||
|
Support
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<style>
|
||||||
|
.card-title {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block script %}
|
||||||
|
<script>
|
||||||
|
new Vue({
|
||||||
|
el: '#alias-group',
|
||||||
|
data: {
|
||||||
|
ticket_email: '{{ ticket_email }}'
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
generateRandomAlias: async function(event){
|
||||||
|
let result = await fetch('/api/alias/random/new', { method: 'POST'})
|
||||||
|
if(result.ok){
|
||||||
|
let data = await result.json();
|
||||||
|
this.ticket_email = data.alias;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('.custom-file input').change(function (e) {
|
||||||
|
let files = [];
|
||||||
|
for (let i = 0; i < $(this)[0].files.length; i++) {
|
||||||
|
files.push($(this)[0].files[i].name);
|
||||||
|
}
|
||||||
|
$(this).next('.custom-file-label').html(files.join(', '));
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block default_content %}
|
||||||
|
|
||||||
|
<div class="col pb-3">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="card-title mb-3">Report a problem</div>
|
||||||
|
<div class="alert alert-info">
|
||||||
|
If an email cannot be delivered to your mailbox, please check <a href="/dashboard/notifications">your notifications</a> for error messages
|
||||||
|
</div>
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
A support ticket will be created in Zendesk. Please do not include any sensitive information in the ticket.
|
||||||
|
</div>
|
||||||
|
<form id="supportZendeskForm" method="post" enctype="multipart/form-data">
|
||||||
|
<div class="mt-4 mb-5">
|
||||||
|
<label for="issueDescription" class="form-label">What happened?</label>
|
||||||
|
<textarea class="form-control" required name="ticket_content" id="issueDescription" rows="3" placeholder="Please provide as much information as possible. For example which alias(es), mailbox(es) ar affected, if this is a persistent issue...">{{- ticket_content or '' -}}</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 font-weight-bold">
|
||||||
|
Attach files to support request
|
||||||
|
</div>
|
||||||
|
<div class="mt-1 text-muted">Only images, text and emails are accepted</div>
|
||||||
|
<div class="custom-file mt-2">
|
||||||
|
<input type="file" class="custom-file-input" name="ticket_files" id="ticketFileGroup" multiple="multiple">
|
||||||
|
<label class="custom-file-label" for="ticketFileGroup">Choose file</label>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5 font-weight-bold">
|
||||||
|
Where can we reach you?
|
||||||
|
</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
Conversations related to this ticket will be sent to this address. Feel free to use an alias here.
|
||||||
|
</div>
|
||||||
|
<div class="input-group mb-3" id="alias-group">
|
||||||
|
<input type="text" required class="form-control" placeholder="Email" name="ticket_email" v-model='ticket_email' aria-label="Email to send responses to" aria-describedby="button-addon2">
|
||||||
|
<div class="input-group-append">
|
||||||
|
<button class="btn btn-outline-primary" type="button" @click="generateRandomAlias" id="button-addon2">Generate a random alias</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5">
|
||||||
|
<button class="btn btn-primary">Create support ticket</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -71,12 +71,26 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="nav-item">
|
<div class="dropdown nav-item d-flex align-items-center">
|
||||||
|
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Help</a>
|
||||||
|
<div class="dropdown-menu dropdown-menu-left dropdown-menu-arrow">
|
||||||
|
<div class="dropdown-item">
|
||||||
<a href="https://simplelogin.io/docs/" target="_blank">
|
<a href="https://simplelogin.io/docs/" target="_blank">
|
||||||
Docs
|
Docs
|
||||||
<i class="fa fa-external-link" aria-hidden="true"></i>
|
<i class="fa fa-external-link" aria-hidden="true"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="dropdown-item">
|
||||||
|
<a href="https://github.com/simple-login/app/discussions" target="_blank">
|
||||||
|
Forum
|
||||||
|
<i class="fa fa-external-link" aria-hidden="true"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown-item">
|
||||||
|
<a href="/dashboard/support">Support</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if current_user.should_show_upgrade_button() %}
|
{% if current_user.should_show_upgrade_button() %}
|
||||||
<div class="nav-item">
|
<div class="nav-item">
|
||||||
|
Loading…
Reference in New Issue
Block a user