Merge branch 'master' of https://bitbucket.org/yackob03/quay
This commit is contained in:
commit
b924fa5336
12 changed files with 276 additions and 12 deletions
2
app.py
2
app.py
|
@ -28,8 +28,6 @@ else:
|
||||||
|
|
||||||
app.config.from_object(config)
|
app.config.from_object(config)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
Principal(app, use_sessions=True)
|
Principal(app, use_sessions=True)
|
||||||
|
|
||||||
login_manager = LoginManager()
|
login_manager = LoginManager()
|
||||||
|
|
22
config.py
22
config.py
|
@ -72,18 +72,33 @@ class MixpanelProdConfig(object):
|
||||||
MIXPANEL_KEY = '50ff2b2569faa3a51c8f5724922ffb7e'
|
MIXPANEL_KEY = '50ff2b2569faa3a51c8f5724922ffb7e'
|
||||||
|
|
||||||
|
|
||||||
|
class GitHubTestConfig(object):
|
||||||
|
GITHUB_CLIENT_ID = 'cfbc4aca88e5c1b40679'
|
||||||
|
GITHUB_CLIENT_SECRET = '7d1cc21e17e10cd8168410e2cd1e4561cb854ff9'
|
||||||
|
GITHUB_TOKEN_URL = 'https://github.com/login/oauth/access_token'
|
||||||
|
GITHUB_USER_URL = 'https://api.github.com/user'
|
||||||
|
GITHUB_USER_EMAILS = GITHUB_USER_URL + '/emails'
|
||||||
|
|
||||||
|
|
||||||
|
class GitHubProdConfig(GitHubTestConfig):
|
||||||
|
GITHUB_CLIENT_ID = '5a8c08b06c48d89d4d1e'
|
||||||
|
GITHUB_CLIENT_SECRET = 'f89d8bb28ea3bd4e1c68808500d185a816be53b1'
|
||||||
|
|
||||||
|
|
||||||
class DebugConfig(FlaskConfig, MailConfig, LocalStorage, SQLiteDB,
|
class DebugConfig(FlaskConfig, MailConfig, LocalStorage, SQLiteDB,
|
||||||
StripeTestConfig, MixpanelTestConfig):
|
StripeTestConfig, MixpanelTestConfig, GitHubTestConfig):
|
||||||
REGISTRY_SERVER = 'localhost:5000'
|
REGISTRY_SERVER = 'localhost:5000'
|
||||||
LOGGING_CONFIG = {
|
LOGGING_CONFIG = {
|
||||||
'level': logging.DEBUG,
|
'level': logging.DEBUG,
|
||||||
'format': LOG_FORMAT
|
'format': LOG_FORMAT
|
||||||
}
|
}
|
||||||
SEND_FILE_MAX_AGE_DEFAULT = 0
|
SEND_FILE_MAX_AGE_DEFAULT = 0
|
||||||
|
POPULATE_DB_TEST_DATA = True
|
||||||
|
|
||||||
|
|
||||||
class LocalHostedConfig(FlaskConfig, MailConfig, S3Storage, RDSMySQL,
|
class LocalHostedConfig(FlaskConfig, MailConfig, S3Storage, RDSMySQL,
|
||||||
StripeLiveConfig, MixpanelTestConfig):
|
StripeLiveConfig, MixpanelTestConfig,
|
||||||
|
GitHubProdConfig):
|
||||||
REGISTRY_SERVER = 'localhost:5000'
|
REGISTRY_SERVER = 'localhost:5000'
|
||||||
LOGGING_CONFIG = {
|
LOGGING_CONFIG = {
|
||||||
'level': logging.DEBUG,
|
'level': logging.DEBUG,
|
||||||
|
@ -93,7 +108,8 @@ class LocalHostedConfig(FlaskConfig, MailConfig, S3Storage, RDSMySQL,
|
||||||
|
|
||||||
|
|
||||||
class ProductionConfig(FlaskConfig, MailConfig, S3Storage, RDSMySQL,
|
class ProductionConfig(FlaskConfig, MailConfig, S3Storage, RDSMySQL,
|
||||||
StripeLiveConfig, MixpanelProdConfig):
|
StripeLiveConfig, MixpanelProdConfig,
|
||||||
|
GitHubProdConfig):
|
||||||
REGISTRY_SERVER = 'quay.io'
|
REGISTRY_SERVER = 'quay.io'
|
||||||
LOGGING_CONFIG = {
|
LOGGING_CONFIG = {
|
||||||
'stream': sys.stderr,
|
'stream': sys.stderr,
|
||||||
|
|
|
@ -36,12 +36,32 @@ class BaseModel(Model):
|
||||||
|
|
||||||
class User(BaseModel):
|
class User(BaseModel):
|
||||||
username = CharField(unique=True, index=True)
|
username = CharField(unique=True, index=True)
|
||||||
password_hash = CharField()
|
password_hash = CharField(null=True)
|
||||||
email = CharField(unique=True, index=True)
|
email = CharField(unique=True, index=True)
|
||||||
verified = BooleanField(default=False)
|
verified = BooleanField(default=False)
|
||||||
stripe_id = CharField(index=True, null=True)
|
stripe_id = CharField(index=True, null=True)
|
||||||
|
|
||||||
|
|
||||||
|
class LoginService(BaseModel):
|
||||||
|
name = CharField(unique=True, index=True)
|
||||||
|
|
||||||
|
|
||||||
|
class FederatedLogin(BaseModel):
|
||||||
|
user = ForeignKeyField(User, index=True)
|
||||||
|
service = ForeignKeyField(LoginService, index=True)
|
||||||
|
service_ident = CharField()
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
database = db
|
||||||
|
indexes = (
|
||||||
|
# create a unique index on service and the local service id
|
||||||
|
(('service', 'service_ident'), True),
|
||||||
|
|
||||||
|
# a user may only have one federated login per service
|
||||||
|
(('service', 'user'), True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Visibility(BaseModel):
|
class Visibility(BaseModel):
|
||||||
name = CharField(index=True)
|
name = CharField(index=True)
|
||||||
|
|
||||||
|
@ -136,9 +156,10 @@ class RepositoryTag(BaseModel):
|
||||||
def initialize_db():
|
def initialize_db():
|
||||||
create_model_tables([User, Repository, Image, AccessToken, Role,
|
create_model_tables([User, Repository, Image, AccessToken, Role,
|
||||||
RepositoryPermission, Visibility, RepositoryTag,
|
RepositoryPermission, Visibility, RepositoryTag,
|
||||||
EmailConfirmation])
|
EmailConfirmation, FederatedLogin, LoginService])
|
||||||
Role.create(name='admin')
|
Role.create(name='admin')
|
||||||
Role.create(name='write')
|
Role.create(name='write')
|
||||||
Role.create(name='read')
|
Role.create(name='read')
|
||||||
Visibility.create(name='public')
|
Visibility.create(name='public')
|
||||||
Visibility.create(name='private')
|
Visibility.create(name='private')
|
||||||
|
LoginService.create(name='github')
|
||||||
|
|
|
@ -34,6 +34,34 @@ def create_user(username, password, email):
|
||||||
raise DataModelException(ex.message)
|
raise DataModelException(ex.message)
|
||||||
|
|
||||||
|
|
||||||
|
def create_federated_user(username, email, service_name, service_id):
|
||||||
|
try:
|
||||||
|
new_user = User.create(username=username, email=email, verified=True)
|
||||||
|
service = LoginService.get(LoginService.name == service_name)
|
||||||
|
federated_user = FederatedLogin.create(user=new_user, service=service,
|
||||||
|
service_ident=service_id)
|
||||||
|
|
||||||
|
return new_user
|
||||||
|
|
||||||
|
except Exception as ex:
|
||||||
|
raise DataModelException(ex.message)
|
||||||
|
|
||||||
|
|
||||||
|
def verify_federated_login(service_name, service_id):
|
||||||
|
selected = FederatedLogin.select(FederatedLogin, User)
|
||||||
|
with_service = selected.join(LoginService)
|
||||||
|
with_user = with_service.switch(FederatedLogin).join(User)
|
||||||
|
found = with_user.where(FederatedLogin.service_ident == service_id,
|
||||||
|
LoginService.name == service_name)
|
||||||
|
|
||||||
|
found_list = list(found)
|
||||||
|
|
||||||
|
if found_list:
|
||||||
|
return found_list[0].user
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
def create_confirm_email_code(user):
|
def create_confirm_email_code(user):
|
||||||
code = EmailConfirmation.create(user=user, email_confirm=True)
|
code = EmailConfirmation.create(user=user, email_confirm=True)
|
||||||
return code
|
return code
|
||||||
|
|
|
@ -4,6 +4,7 @@ import stripe
|
||||||
from flask import request, make_response, jsonify, abort
|
from flask import request, make_response, jsonify, abort
|
||||||
from flask.ext.login import login_required, current_user
|
from flask.ext.login import login_required, current_user
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
from data import model
|
from data import model
|
||||||
from app import app
|
from app import app
|
||||||
|
@ -263,6 +264,31 @@ def role_view(repo_perm_obj):
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/repository/<path:repository>/image/', methods=['GET'])
|
||||||
|
@parse_repository_name
|
||||||
|
def list_repository_images(namespace, repository):
|
||||||
|
permission = ReadRepositoryPermission(namespace, repository)
|
||||||
|
if permission.can() or model.repository_is_public(namespace, repository):
|
||||||
|
all_images = model.get_repository_images(namespace, repository)
|
||||||
|
all_tags = model.list_repository_tags(namespace, repository)
|
||||||
|
|
||||||
|
tags_by_image_id = defaultdict(list)
|
||||||
|
for tag in all_tags:
|
||||||
|
tags_by_image_id[tag.image.docker_image_id].append(tag.name)
|
||||||
|
|
||||||
|
|
||||||
|
def add_tags(image_json):
|
||||||
|
image_json['tags'] = tags_by_image_id[image_json['id']]
|
||||||
|
return image_json
|
||||||
|
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'images': [add_tags(image_view(image)) for image in all_images]
|
||||||
|
})
|
||||||
|
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/repository/<path:repository>/tag/<tag>/images',
|
@app.route('/api/repository/<path:repository>/tag/<tag>/images',
|
||||||
methods=['GET'])
|
methods=['GET'])
|
||||||
@parse_repository_name
|
@parse_repository_name
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
|
import requests
|
||||||
|
|
||||||
from flask import (abort, send_file, redirect, request, url_for,
|
from flask import (abort, send_file, redirect, request, url_for,
|
||||||
render_template, make_response)
|
render_template, make_response)
|
||||||
|
@ -66,7 +67,8 @@ def common_login(db_user):
|
||||||
|
|
||||||
@app.route('/signin', methods=['GET'])
|
@app.route('/signin', methods=['GET'])
|
||||||
def render_signin_page():
|
def render_signin_page():
|
||||||
return render_template('signin.html')
|
return render_template('signin.html',
|
||||||
|
github_client_id=app.config['GITHUB_CLIENT_ID'])
|
||||||
|
|
||||||
|
|
||||||
@app.route('/signin', methods=['POST'])
|
@app.route('/signin', methods=['POST'])
|
||||||
|
@ -81,12 +83,66 @@ def signin():
|
||||||
return redirect(request.args.get('next') or url_for('index'))
|
return redirect(request.args.get('next') or url_for('index'))
|
||||||
else:
|
else:
|
||||||
return render_template('signin.html',
|
return render_template('signin.html',
|
||||||
needs_email_verification=True)
|
needs_email_verification=True,
|
||||||
|
github_client_id=app.config['GITHUB_CLIENT_ID'])
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return render_template('signin.html',
|
return render_template('signin.html',
|
||||||
username=username,
|
username=username,
|
||||||
invalid_credentials=True)
|
invalid_credentials=True,
|
||||||
|
github_client_id=app.config['GITHUB_CLIENT_ID'])
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/oauth2/github/callback', methods=['GET'])
|
||||||
|
def github_oauth_callback():
|
||||||
|
code = request.args.get('code')
|
||||||
|
payload = {
|
||||||
|
'client_id': app.config['GITHUB_CLIENT_ID'],
|
||||||
|
'client_secret': app.config['GITHUB_CLIENT_SECRET'],
|
||||||
|
'code': code,
|
||||||
|
}
|
||||||
|
headers = {
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
|
||||||
|
get_access_token = requests.post(app.config['GITHUB_TOKEN_URL'],
|
||||||
|
params=payload, headers=headers)
|
||||||
|
|
||||||
|
token = get_access_token.json()['access_token']
|
||||||
|
|
||||||
|
token_param = {
|
||||||
|
'access_token': token,
|
||||||
|
}
|
||||||
|
get_user = requests.get(app.config['GITHUB_USER_URL'], params=token_param)
|
||||||
|
|
||||||
|
user_data = get_user.json()
|
||||||
|
username = user_data['login']
|
||||||
|
github_id = user_data['id']
|
||||||
|
|
||||||
|
v3_media_type = {
|
||||||
|
'Accept': 'application/vnd.github.v3'
|
||||||
|
}
|
||||||
|
get_email = requests.get(app.config['GITHUB_USER_EMAILS'],
|
||||||
|
params=token_param, headers=v3_media_type)
|
||||||
|
|
||||||
|
# We will accept any email, but we prefer the primary
|
||||||
|
found_email = None
|
||||||
|
for user_email in get_email.json():
|
||||||
|
found_email = user_email['email']
|
||||||
|
if user_email['primary']:
|
||||||
|
break
|
||||||
|
|
||||||
|
to_login = model.verify_federated_login('github', github_id)
|
||||||
|
if not to_login:
|
||||||
|
# try to create the user
|
||||||
|
to_login = model.create_federated_user(username, found_email, 'github',
|
||||||
|
github_id)
|
||||||
|
|
||||||
|
if common_login(to_login):
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
# TODO something bad happened, we need to tell the user somehow
|
||||||
|
return redirect(url_for('signin'))
|
||||||
|
|
||||||
|
|
||||||
@app.route('/confirm', methods=['GET'])
|
@app.route('/confirm', methods=['GET'])
|
||||||
|
|
93
initdb.py
93
initdb.py
|
@ -1,4 +1,97 @@
|
||||||
|
import logging
|
||||||
|
import string
|
||||||
|
|
||||||
|
from random import SystemRandom
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
from data.database import initialize_db
|
from data.database import initialize_db
|
||||||
|
from data import model
|
||||||
|
from app import app
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
logging.basicConfig(**app.config['LOGGING_CONFIG'])
|
||||||
|
|
||||||
|
|
||||||
|
def __gen_hex_id(length=64):
|
||||||
|
random = SystemRandom()
|
||||||
|
return ''.join([random.choice('abcdef' + string.digits)
|
||||||
|
for x in range(length)])
|
||||||
|
|
||||||
|
|
||||||
|
def __gen_checksum():
|
||||||
|
return 'tarsum+sha256:' + __gen_hex_id(64)
|
||||||
|
|
||||||
|
|
||||||
|
def create_subtree(repo, structure, parent):
|
||||||
|
num_nodes, subtrees, last_node_tags = structure
|
||||||
|
|
||||||
|
# create the nodes
|
||||||
|
for i in range(num_nodes):
|
||||||
|
docker_image_id = __gen_hex_id()
|
||||||
|
checksum = __gen_checksum()
|
||||||
|
|
||||||
|
new_image = model.create_image(docker_image_id, repo)
|
||||||
|
model.set_image_checksum(docker_image_id, repo, checksum)
|
||||||
|
|
||||||
|
new_image = model.set_image_metadata(docker_image_id, repo.namespace,
|
||||||
|
repo.name, str(datetime.now()),
|
||||||
|
'no comment', parent)
|
||||||
|
|
||||||
|
parent = new_image
|
||||||
|
|
||||||
|
if last_node_tags:
|
||||||
|
if not isinstance(last_node_tags, list):
|
||||||
|
last_node_tags = [last_node_tags]
|
||||||
|
|
||||||
|
for tag_name in last_node_tags:
|
||||||
|
model.create_or_update_tag(repo.namespace, repo.name, tag_name,
|
||||||
|
new_image.docker_image_id)
|
||||||
|
|
||||||
|
for subtree in subtrees:
|
||||||
|
create_subtree(repo, subtree, new_image)
|
||||||
|
|
||||||
|
|
||||||
|
def __generate_repository(user, name, is_public, permissions, structure):
|
||||||
|
repo = model.create_repository(user.username, name, user)
|
||||||
|
|
||||||
|
if is_public:
|
||||||
|
model.set_repository_visibility(repo, 'public')
|
||||||
|
|
||||||
|
for delegate, role in permissions:
|
||||||
|
model.set_user_repo_permission(delegate.username, user.username, name,
|
||||||
|
role)
|
||||||
|
|
||||||
|
create_subtree(repo, structure, None)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
initialize_db()
|
initialize_db()
|
||||||
|
|
||||||
|
if app.config.get('POPULATE_DB_TEST_DATA', False):
|
||||||
|
logger.debug('Populating the DB with test data.')
|
||||||
|
|
||||||
|
new_user_1 = model.create_user('devtable', 'password',
|
||||||
|
'jake@devtable.com')
|
||||||
|
new_user_1.verified = True
|
||||||
|
new_user_1.save()
|
||||||
|
|
||||||
|
new_user_2 = model.create_user('public', 'password',
|
||||||
|
'jacob.moshenko@gmail.com')
|
||||||
|
new_user_2.verified = True
|
||||||
|
new_user_2.save()
|
||||||
|
|
||||||
|
__generate_repository(new_user_1, 'simple', False, [], (4, [],
|
||||||
|
['latest', 'prod']))
|
||||||
|
|
||||||
|
__generate_repository(new_user_1, 'complex', False, [],
|
||||||
|
(2, [(3, [], 'v2.0'),
|
||||||
|
(1, [(1, [(1, [], ['latest', 'prod'])],
|
||||||
|
'staging'),
|
||||||
|
(1, [], None)], None)], None))
|
||||||
|
|
||||||
|
__generate_repository(new_user_2, 'publicrepo', True, [],
|
||||||
|
(10, [], 'latest'))
|
||||||
|
|
||||||
|
__generate_repository(new_user_1, 'shared', False,
|
||||||
|
[(new_user_2, 'write')], (5, [], 'latest'))
|
||||||
|
|
|
@ -8,6 +8,7 @@ body {
|
||||||
max-width: 330px;
|
max-width: 330px;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
.form-signin .form-signin-heading,
|
.form-signin .form-signin-heading,
|
||||||
.form-signin .checkbox {
|
.form-signin .checkbox {
|
||||||
|
@ -42,4 +43,20 @@ body {
|
||||||
.alert {
|
.alert {
|
||||||
max-width: 300px;
|
max-width: 300px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-alternate {
|
||||||
|
color: #777;
|
||||||
|
font-size: 3em;
|
||||||
|
margin-left: 43px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-alternate .inner-text {
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
color: white;
|
||||||
|
left: -43px;
|
||||||
|
top: -9px;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: .4em;
|
||||||
}
|
}
|
|
@ -123,8 +123,8 @@ quayApp = angular.module('quay', ['restangular', 'angularMoment', 'angulartics',
|
||||||
when('/repository/', {title: 'Repositories', templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl}).
|
when('/repository/', {title: 'Repositories', templateUrl: '/static/partials/repo-list.html', controller: RepoListCtrl}).
|
||||||
when('/user', {title: 'User Admin', templateUrl: '/static/partials/user-admin.html', controller: UserAdminCtrl}).
|
when('/user', {title: 'User Admin', templateUrl: '/static/partials/user-admin.html', controller: UserAdminCtrl}).
|
||||||
when('/guide/', {title: 'Getting Started Guide', templateUrl: '/static/partials/guide.html', controller: GuideCtrl}).
|
when('/guide/', {title: 'Getting Started Guide', templateUrl: '/static/partials/guide.html', controller: GuideCtrl}).
|
||||||
when('/plans/', {title: 'Quay Plans', templateUrl: '/static/partials/plans.html', controller: PlansCtrl}).
|
when('/plans/', {title: 'Plans and Pricing', templateUrl: '/static/partials/plans.html', controller: PlansCtrl}).
|
||||||
when('/', {title: 'Quay: Private docker repository hosting', templateUrl: '/static/partials/landing.html', controller: LandingCtrl}).
|
when('/', {title: 'Hosted Private Docker Registry', templateUrl: '/static/partials/landing.html', controller: LandingCtrl}).
|
||||||
otherwise({redirectTo: '/'});
|
otherwise({redirectTo: '/'});
|
||||||
}]).
|
}]).
|
||||||
config(function(RestangularProvider) {
|
config(function(RestangularProvider) {
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
<title ng-bind="title + ' · Quay'">Quay - Private Docker Repository</title>
|
<title ng-bind="title + ' · Quay'">Quay - Private Docker Repository</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta name="description" content="Hosted private docker repositories. Includes full user management and history. Free for public repositories.">
|
<meta name="description" content="Hosted private docker repositories. Includes full user management and history. Free for public repositories.">
|
||||||
|
<meta name="google-site-verification" content="GalDznToijTsHYmLjJvE4QaB9uk_IP16aaGDz5D75T4" />
|
||||||
|
|
||||||
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.min.css">
|
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.min.css">
|
||||||
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.no-icons.min.css">
|
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.no-icons.min.css">
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
<title>Sign In - Quay</title>
|
<title>Sign In - Quay</title>
|
||||||
|
|
||||||
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.no-icons.min.css">
|
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.no-icons.min.css">
|
||||||
|
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.min.css">
|
||||||
|
|
||||||
<link rel="stylesheet" href="static/css/signin.css">
|
<link rel="stylesheet" href="static/css/signin.css">
|
||||||
</head>
|
</head>
|
||||||
|
@ -13,6 +14,13 @@
|
||||||
<input type="text" class="form-control" placeholder="Username" name="username" value="{{ username }}"autofocus>
|
<input type="text" class="form-control" placeholder="Username" name="username" value="{{ username }}"autofocus>
|
||||||
<input type="password" class="form-control" placeholder="Password" name="password">
|
<input type="password" class="form-control" placeholder="Password" name="password">
|
||||||
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign In</button>
|
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign In</button>
|
||||||
|
|
||||||
|
<span class="social-alternate">
|
||||||
|
<i class="icon-circle"></i>
|
||||||
|
<span class="inner-text">OR</i>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<a href="https://github.com/login/oauth/authorize?client_id={{ github_client_id }}&scope=user:email" class="btn btn-primary btn-lg btn-block"><i class="icon-github icon-large"></i> Sign In with GitHub</a>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{% if invalid_credentials %}
|
{% if invalid_credentials %}
|
||||||
|
|
BIN
test.db
BIN
test.db
Binary file not shown.
Reference in a new issue