Merge pull request #61 from simple-login/custom-alias-oauth-authorize
Custom alias oauth authorize
This commit is contained in:
commit
d1baca67ff
|
@ -1,4 +1,4 @@
|
||||||
from flask import render_template, redirect, url_for, flash, request, session
|
from flask import render_template, redirect, url_for, flash, request
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
|
|
||||||
from app.config import (
|
from app.config import (
|
||||||
|
@ -9,7 +9,7 @@ from app.dashboard.base import dashboard_bp
|
||||||
from app.email_utils import email_belongs_to_alias_domains, get_email_domain_part
|
from app.email_utils import email_belongs_to_alias_domains, get_email_domain_part
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.log import LOG
|
from app.log import LOG
|
||||||
from app.models import GenEmail, CustomDomain
|
from app.models import GenEmail, CustomDomain, DeletedAlias
|
||||||
from app.utils import convert_to_id, random_word, word_exist
|
from app.utils import convert_to_id, random_word, word_exist
|
||||||
|
|
||||||
|
|
||||||
|
@ -49,7 +49,9 @@ def custom_alias():
|
||||||
):
|
):
|
||||||
full_alias = alias_prefix + alias_suffix
|
full_alias = alias_prefix + alias_suffix
|
||||||
|
|
||||||
if GenEmail.get_by(email=full_alias):
|
if GenEmail.get_by(email=full_alias) or DeletedAlias.get_by(
|
||||||
|
email=full_alias
|
||||||
|
):
|
||||||
LOG.d("full alias already used %s", full_alias)
|
LOG.d("full alias already used %s", full_alias)
|
||||||
flash(
|
flash(
|
||||||
f"Alias {full_alias} already exists, please choose another one",
|
f"Alias {full_alias} already exists, please choose another one",
|
||||||
|
|
|
@ -84,24 +84,42 @@
|
||||||
<div class="col-md-9">
|
<div class="col-md-9">
|
||||||
<select class="custom-select custom-select" name="suggested-email">
|
<select class="custom-select custom-select" name="suggested-email">
|
||||||
<option selected value="{{ suggested_email }}">{{ suggested_email }}</option>
|
<option selected value="{{ suggested_email }}">{{ suggested_email }}</option>
|
||||||
<option value="{{ personal_email }}">{{ personal_email }} (Personal Email)</option>
|
<option value="{{ current_user.email }}">{{ current_user.email }} (Personal Email)</option>
|
||||||
{% for email in other_emails %}
|
{% for email in other_emails %}
|
||||||
<option value="{{ email }}">{{ email }}</option>
|
<option value="{{ email }}">{{ email }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
{% if current_user.can_create_new_alias() %}
|
{% if current_user.can_create_new_alias() %}
|
||||||
<div class="mt-2">OR</div>
|
<div class="mt-2 mb-2">OR</div>
|
||||||
<div style="display: flex; align-items: center" class="mt-2">
|
|
||||||
<input class="form-control"
|
<div class="row mb-2">
|
||||||
pattern="[0-9a-z-_]{1,}"
|
<div class="col-sm-6 pr-1 mb-1" style="min-width: 5em">
|
||||||
title="Only lowercase letter, number, dash (-), underscore (_) can be used in alias prefix."
|
<input name="prefix" class="form-control"
|
||||||
style="flex-grow: 2" name="custom-email-prefix">
|
type="text"
|
||||||
<input type="hidden" name="email-suffix" value="{{ email_suffix }}">
|
pattern="[0-9a-z-_]{1,}"
|
||||||
<div class="ml-2">
|
title="Only lowercase letter, number, dash (-), underscore (_) can be used in alias prefix."
|
||||||
.{{ email_suffix }}@{{ EMAIL_DOMAIN }}
|
placeholder="email alias"
|
||||||
|
autofocus>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="col-sm-6"
|
||||||
|
style="padding-left: 5px">
|
||||||
|
<select class="form-control" name="suffix">
|
||||||
|
{% for suffix in suffixes %}
|
||||||
|
<option value="{{ suffix[1] }}">
|
||||||
|
{% if suffix[0] %}
|
||||||
|
{{ suffix[1] }} (your domain)
|
||||||
|
{% else %}
|
||||||
|
{{ suffix[1] }}
|
||||||
|
{% endif %}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
Alias can use letter, number, dash and cannot be empty
|
Alias can use letter, number, dash and cannot be empty
|
||||||
</small>
|
</small>
|
||||||
|
|
|
@ -1,47 +1,64 @@
|
||||||
{% extends "single.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block single_content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="bg-white p-6" style="margin: auto; max-width: 600px">
|
||||||
<b>{{ client.name }}</b> would like to have access to your following data:
|
<div class="text-center mb-6">
|
||||||
|
<a href="https://simplelogin.io">
|
||||||
<ul class="mt-3">
|
<img src="/static/logo.png" style="background-color: transparent; height: 40px">
|
||||||
{% for scope in client.get_scopes() %}
|
|
||||||
<li>
|
|
||||||
{% if scope == Scope.AVATAR_URL %}
|
|
||||||
avatar
|
|
||||||
{% else %}
|
|
||||||
{{ scope.value }}
|
|
||||||
{% endif %}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<label>
|
|
||||||
In order to accept the request, you need to sign in.
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mt-4">
|
|
||||||
<div class="btn-group w-100">
|
|
||||||
|
|
||||||
<a href="{{ url_for('auth.login', next=next) }}" class="btn btn-success">
|
|
||||||
Login
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<a href="{{ url_for('auth.register', next=next) }}" class="btn btn-info">
|
|
||||||
Sign Up
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr>
|
<div>
|
||||||
|
<b>{{ client.name }}</b> would like to have access to your following data:
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<ul class="mt-3">
|
||||||
|
{% for scope in client.get_scopes() %}
|
||||||
|
<li>
|
||||||
|
{% if scope == Scope.AVATAR_URL %}
|
||||||
|
avatar
|
||||||
|
{% else %}
|
||||||
|
{{ scope.value }}
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
In order to accept the request, you need to sign in.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<div class="btn-group w-100">
|
||||||
|
|
||||||
|
<a href="{{ url_for('auth.login', next=next) }}" class="btn btn-success">
|
||||||
|
Login
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<a href="{{ url_for('auth.register', next=next) }}" class="btn btn-info">
|
||||||
|
Sign Up
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr>
|
||||||
|
|
||||||
|
<div class="">
|
||||||
|
<p class="text-center col">Cancel and go back to <b>{{ client.name }}</b></p>
|
||||||
|
<a class="btn btn-block btn-secondary back-or-close">
|
||||||
|
<i class="fe fe-arrow-left mr-2"></i>Cancel
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="small-text mt-4">
|
||||||
|
<a href="https://simplelogin.io">SimpleLogin</a> is an open source social login provider that protects your privacy.
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<p class="text-center col">Cancel and go back to <b>{{ client.name }}</b></p>
|
|
||||||
<a class="btn btn-block btn-secondary back-or-close">
|
|
||||||
<i class="fe fe-arrow-left mr-2"></i>Cancel
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,8 @@ from urllib.parse import urlparse
|
||||||
from flask import request, render_template, redirect, flash
|
from flask import request, render_template, redirect, flash
|
||||||
from flask_login import current_user
|
from flask_login import current_user
|
||||||
|
|
||||||
from app.config import EMAIL_DOMAIN
|
from app.config import EMAIL_DOMAIN, ALIAS_DOMAINS, DISABLE_ALIAS_SUFFIX
|
||||||
|
from app.email_utils import get_email_domain_part
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.jose_utils import make_id_token
|
from app.jose_utils import make_id_token
|
||||||
from app.log import LOG
|
from app.log import LOG
|
||||||
|
@ -16,6 +17,7 @@ from app.models import (
|
||||||
RedirectUri,
|
RedirectUri,
|
||||||
OauthToken,
|
OauthToken,
|
||||||
DeletedAlias,
|
DeletedAlias,
|
||||||
|
CustomDomain,
|
||||||
)
|
)
|
||||||
from app.oauth.base import oauth_bp
|
from app.oauth.base import oauth_bp
|
||||||
from app.oauth_models import (
|
from app.oauth_models import (
|
||||||
|
@ -103,21 +105,33 @@ def authorize():
|
||||||
client.name
|
client.name
|
||||||
)
|
)
|
||||||
suggested_name, other_names = current_user.suggested_names()
|
suggested_name, other_names = current_user.suggested_names()
|
||||||
email_suffix = random_word()
|
|
||||||
|
user_custom_domains = [
|
||||||
|
cd.domain for cd in current_user.verified_custom_domains()
|
||||||
|
]
|
||||||
|
# List of (is_custom_domain, alias-suffix)
|
||||||
|
suffixes = []
|
||||||
|
|
||||||
|
# put custom domain first
|
||||||
|
for alias_domain in user_custom_domains:
|
||||||
|
suffixes.append((True, "@" + alias_domain))
|
||||||
|
|
||||||
|
# then default domain
|
||||||
|
for domain in ALIAS_DOMAINS:
|
||||||
|
suffixes.append(
|
||||||
|
(
|
||||||
|
False,
|
||||||
|
("" if DISABLE_ALIAS_SUFFIX else "." + random_word())
|
||||||
|
+ "@"
|
||||||
|
+ domain,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"oauth/authorize.html",
|
"oauth/authorize.html",
|
||||||
client=client,
|
|
||||||
user_info=user_info,
|
|
||||||
client_user=client_user,
|
|
||||||
Scope=Scope,
|
Scope=Scope,
|
||||||
suggested_email=suggested_email,
|
|
||||||
personal_email=current_user.email,
|
|
||||||
suggested_name=suggested_name,
|
|
||||||
other_names=other_names,
|
|
||||||
other_emails=other_emails,
|
|
||||||
email_suffix=email_suffix,
|
|
||||||
EMAIL_DOMAIN=EMAIL_DOMAIN,
|
EMAIL_DOMAIN=EMAIL_DOMAIN,
|
||||||
|
**locals(),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# after user logs in, redirect user back to this page
|
# after user logs in, redirect user back to this page
|
||||||
|
@ -127,7 +141,7 @@ def authorize():
|
||||||
next=request.url,
|
next=request.url,
|
||||||
Scope=Scope,
|
Scope=Scope,
|
||||||
)
|
)
|
||||||
else: # user allows or denies
|
else: # POST - user allows or denies
|
||||||
if request.form.get("button") == "deny":
|
if request.form.get("button") == "deny":
|
||||||
LOG.debug("User %s denies Client %s", current_user, client)
|
LOG.debug("User %s denies Client %s", current_user, client)
|
||||||
final_redirect_uri = f"{redirect_uri}?error=deny&state={state}"
|
final_redirect_uri = f"{redirect_uri}?error=deny&state={state}"
|
||||||
|
@ -140,32 +154,55 @@ def authorize():
|
||||||
if client_user:
|
if client_user:
|
||||||
LOG.d("user %s has already allowed client %s", current_user, client)
|
LOG.d("user %s has already allowed client %s", current_user, client)
|
||||||
else:
|
else:
|
||||||
email_suffix = request.form.get("email-suffix")
|
alias_prefix = request.form.get("prefix")
|
||||||
custom_email_prefix = request.form.get("custom-email-prefix")
|
alias_suffix = request.form.get("suffix")
|
||||||
chosen_email = request.form.get("suggested-email")
|
|
||||||
|
|
||||||
suggested_name = request.form.get("suggested-name")
|
|
||||||
custom_name = request.form.get("custom-name")
|
|
||||||
|
|
||||||
use_default_avatar = request.form.get("avatar-choice") == "default"
|
|
||||||
|
|
||||||
gen_email = None
|
gen_email = None
|
||||||
if custom_email_prefix:
|
|
||||||
# check if user can generate custom email
|
# user creates a new alias, not using suggested alias
|
||||||
|
if alias_prefix:
|
||||||
|
# should never happen as this is checked on the front-end
|
||||||
if not current_user.can_create_new_alias():
|
if not current_user.can_create_new_alias():
|
||||||
raise Exception(f"User {current_user} cannot create custom email")
|
raise Exception(f"User {current_user} cannot create custom email")
|
||||||
|
|
||||||
email = f"{convert_to_id(custom_email_prefix)}.{email_suffix}@{EMAIL_DOMAIN}"
|
user_custom_domains = [
|
||||||
LOG.d("create custom email alias %s for user %s", email, current_user)
|
cd.domain for cd in current_user.verified_custom_domains()
|
||||||
|
]
|
||||||
|
|
||||||
if GenEmail.get_by(email=email) or DeletedAlias.get_by(email=email):
|
from app.dashboard.views.custom_alias import verify_prefix_suffix
|
||||||
LOG.error("email %s already used, very rare!", email)
|
|
||||||
flash(f"alias {email} already used", "error")
|
if verify_prefix_suffix(
|
||||||
|
current_user, alias_prefix, alias_suffix, user_custom_domains
|
||||||
|
):
|
||||||
|
full_alias = alias_prefix + alias_suffix
|
||||||
|
|
||||||
|
if GenEmail.get_by(email=full_alias) or DeletedAlias.get_by(
|
||||||
|
email=full_alias
|
||||||
|
):
|
||||||
|
LOG.error("alias %s already used, very rare!", full_alias)
|
||||||
|
flash(f"Alias {full_alias} already used", "error")
|
||||||
|
return redirect(request.url)
|
||||||
|
else:
|
||||||
|
gen_email = GenEmail.create(
|
||||||
|
user_id=current_user.id, email=full_alias
|
||||||
|
)
|
||||||
|
|
||||||
|
# get the custom_domain_id if alias is created with a custom domain
|
||||||
|
alias_domain = get_email_domain_part(full_alias)
|
||||||
|
custom_domain = CustomDomain.get_by(domain=alias_domain)
|
||||||
|
if custom_domain:
|
||||||
|
gen_email.custom_domain_id = custom_domain.id
|
||||||
|
|
||||||
|
db.session.flush()
|
||||||
|
flash(f"Alias {full_alias} has been created", "success")
|
||||||
|
# only happen if the request has been "hacked"
|
||||||
|
else:
|
||||||
|
flash("something went wrong", "warning")
|
||||||
return redirect(request.url)
|
return redirect(request.url)
|
||||||
|
# User chooses one of the suggestions
|
||||||
gen_email = GenEmail.create(email=email, user_id=current_user.id)
|
else:
|
||||||
db.session.flush()
|
chosen_email = request.form.get("suggested-email")
|
||||||
else: # user picks an email from suggestion
|
# todo: add some checks on chosen_email
|
||||||
if chosen_email != current_user.email:
|
if chosen_email != current_user.email:
|
||||||
gen_email = GenEmail.get_by(email=chosen_email)
|
gen_email = GenEmail.get_by(email=chosen_email)
|
||||||
if not gen_email:
|
if not gen_email:
|
||||||
|
@ -174,6 +211,11 @@ def authorize():
|
||||||
)
|
)
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
|
|
||||||
|
suggested_name = request.form.get("suggested-name")
|
||||||
|
custom_name = request.form.get("custom-name")
|
||||||
|
|
||||||
|
use_default_avatar = request.form.get("avatar-choice") == "default"
|
||||||
|
|
||||||
client_user = ClientUser.create(
|
client_user = ClientUser.create(
|
||||||
client_id=client.id, user_id=current_user.id
|
client_id=client.id, user_id=current_user.id
|
||||||
)
|
)
|
||||||
|
@ -181,20 +223,8 @@ def authorize():
|
||||||
client_user.gen_email_id = gen_email.id
|
client_user.gen_email_id = gen_email.id
|
||||||
|
|
||||||
if custom_name:
|
if custom_name:
|
||||||
LOG.d(
|
|
||||||
"use custom name %s for user %s client %s",
|
|
||||||
custom_name,
|
|
||||||
current_user,
|
|
||||||
client,
|
|
||||||
)
|
|
||||||
client_user.name = custom_name
|
client_user.name = custom_name
|
||||||
elif suggested_name != current_user.name:
|
elif suggested_name != current_user.name:
|
||||||
LOG.d(
|
|
||||||
"use another name %s for user %s client %s",
|
|
||||||
custom_name,
|
|
||||||
current_user,
|
|
||||||
client,
|
|
||||||
)
|
|
||||||
client_user.name = suggested_name
|
client_user.name = suggested_name
|
||||||
|
|
||||||
if use_default_avatar:
|
if use_default_avatar:
|
||||||
|
|
Loading…
Reference in a new issue