mirror of
https://github.com/GOSTSec/gostweb
synced 2025-02-11 14:14:15 +00:00
First public release
This commit is contained in:
commit
294982b746
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.db
|
||||
*.pot
|
||||
*.doctree
|
||||
*.swp
|
||||
.doctrees
|
||||
.DS_Store
|
||||
.coverage
|
||||
.idea/
|
||||
local_settings.py
|
||||
data/db.sqlite3
|
||||
data/*.log
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
static/*
|
20
LICENSE
Normal file
20
LICENSE
Normal file
@ -0,0 +1,20 @@
|
||||
Copyright (c) 2017 Purple Tech
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
11
README.md
Normal file
11
README.md
Normal file
@ -0,0 +1,11 @@
|
||||
GOSTCoin Web Wallet
|
||||
===================
|
||||
|
||||
Django-powered web wallet for [GOSTCoin](http://gostco.in)
|
||||
|
||||
|
||||
# Celery development workers
|
||||
|
||||
celery -A gst_web_wallet.celery worker --loglevel=debug
|
||||
|
||||
celery -A gst_web_wallet.celery beat --loglevel=debug
|
0
data/.keep
Normal file
0
data/.keep
Normal file
4
gst_web_wallet/__init__.py
Normal file
4
gst_web_wallet/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
# import celery stuff
|
||||
from .celery import app as celery_app
|
||||
|
||||
__all__ = ['celery_app']
|
21
gst_web_wallet/celery.py
Normal file
21
gst_web_wallet/celery.py
Normal file
@ -0,0 +1,21 @@
|
||||
from __future__ import absolute_import, unicode_literals
|
||||
import os
|
||||
from celery import Celery
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'gst_web_wallet.settings')
|
||||
app = Celery('gst_web_wallet')
|
||||
app.config_from_object('django.conf:settings', namespace='CELERY')
|
||||
# Load task modules from all registered Django app configs.
|
||||
app.autodiscover_tasks()
|
||||
|
||||
app.conf.beat_schedule = {
|
||||
'check-received-txs': {
|
||||
'task': 'wallet.tasks.check_transactions_task',
|
||||
'schedule': 120.0,
|
||||
# 'args': (123,),
|
||||
}
|
||||
}
|
||||
|
||||
@app.task(bind=True)
|
||||
def debug_task(self):
|
||||
print('Request: {0!r}'.format(self.request))
|
58
gst_web_wallet/local_settings.template
Normal file
58
gst_web_wallet/local_settings.template
Normal file
@ -0,0 +1,58 @@
|
||||
from gst_web_wallet.settings import *
|
||||
from decimal import Decimal
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'change this key'
|
||||
DEBUG = True
|
||||
ALLOWED_HOSTS = []
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(BASE_DIR, 'data', 'db.sqlite3'),
|
||||
}
|
||||
}
|
||||
CELERY_BROKER_URL = 'redis://localhost:6379/0'
|
||||
|
||||
# gostcoind settngs
|
||||
COIN_USER = "gostcoinrpc"
|
||||
COIN_PASSWORD = "change this key"
|
||||
COIN_ADDRESS = ("127.0.0.1", 9386)
|
||||
COIN_CONNECTION = "http://{}:{}@{}:{}".format(
|
||||
COIN_USER, COIN_PASSWORD, COIN_ADDRESS[0], COIN_ADDRESS[1])
|
||||
|
||||
# webapp config
|
||||
GST_NETWORK_FEE = Decimal("0.002")
|
||||
SERVICE_FEE = Decimal("0.0")
|
||||
GST_DUST = Decimal("0.0001")
|
||||
# address to receive change
|
||||
GST_CHANGE_ADDRESS = "change this to your GST address"
|
||||
|
||||
# LOGGING = {
|
||||
# 'version': 1,
|
||||
# 'disable_existing_loggers': True,
|
||||
# 'handlers': {
|
||||
# 'file': {
|
||||
# 'level': 'INFO',
|
||||
# 'class': 'logging.FileHandler',
|
||||
# 'filename': os.path.join(BASE_DIR, 'data', 'app.log'),
|
||||
# },
|
||||
# },
|
||||
# 'loggers': {
|
||||
# 'django': {
|
||||
# 'handlers': ['file'],
|
||||
# 'level': 'INFO',
|
||||
# 'propagate': True,
|
||||
# },
|
||||
# },
|
||||
# }
|
||||
# ALLOWED_HOSTS = ['127.0.0.1', 'localhost', 'gostwallet.i2p']
|
||||
# DATABASES = {
|
||||
# 'default': {
|
||||
# 'ENGINE': 'django.db.backends.postgresql',
|
||||
# 'NAME': 'gstwallet',
|
||||
# 'USER': 'gstwallet',
|
||||
# 'PASSWORD': 'gstwallet',
|
||||
# 'HOST': '127.0.0.1',
|
||||
# 'PORT': '',
|
||||
# }
|
||||
# }
|
113
gst_web_wallet/settings.py
Normal file
113
gst_web_wallet/settings.py
Normal file
@ -0,0 +1,113 @@
|
||||
"""
|
||||
Django settings for gst_web_wallet project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 1.11.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/1.11/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/1.11/ref/settings/
|
||||
"""
|
||||
|
||||
import os
|
||||
from django.contrib.messages import constants as message_constants
|
||||
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
|
||||
'captcha',
|
||||
'crispy_forms', # reusable Bootstrap forms
|
||||
|
||||
'wallet.apps.WalletConfig',
|
||||
|
||||
'integral_auth.apps.IntegralAuthConfig',
|
||||
|
||||
# 'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'gst_web_wallet.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'gst_web_wallet.wsgi.application'
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/1.11/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/1.11/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/1.11/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
CRISPY_TEMPLATE_PACK = 'bootstrap3'
|
||||
|
||||
LOGIN_URL = "integral_auth:signin"
|
||||
|
||||
# Override message tag for compatibility with bootstrap3
|
||||
MESSAGE_TAGS = {message_constants.ERROR: 'danger'}
|
||||
|
27
gst_web_wallet/urls.py
Normal file
27
gst_web_wallet/urls.py
Normal file
@ -0,0 +1,27 @@
|
||||
"""gst_web_wallet URL Configuration
|
||||
|
||||
The `urlpatterns` list routes URLs to views. For more information please see:
|
||||
https://docs.djangoproject.com/en/1.11/topics/http/urls/
|
||||
Examples:
|
||||
Function views
|
||||
1. Add an import: from my_app import views
|
||||
2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
|
||||
Class-based views
|
||||
1. Add an import: from other_app.views import Home
|
||||
2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
|
||||
Including another URLconf
|
||||
1. Import the include() function: from django.conf.urls import url, include
|
||||
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
|
||||
"""
|
||||
from django.conf.urls import url, include
|
||||
from django.contrib import admin
|
||||
|
||||
from wallet.views import index
|
||||
|
||||
urlpatterns = [
|
||||
# url(r'^admin/', admin.site.urls),
|
||||
url(r'^captcha/', include('captcha.urls')),
|
||||
|
||||
url(r'^$', index, name="site_index"),
|
||||
url(r'^auth/', include('integral_auth.urls')),
|
||||
]
|
16
gst_web_wallet/wsgi.py
Normal file
16
gst_web_wallet/wsgi.py
Normal file
@ -0,0 +1,16 @@
|
||||
"""
|
||||
WSGI config for gst_web_wallet project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gst_web_wallet.settings")
|
||||
|
||||
application = get_wsgi_application()
|
0
integral_auth/__init__.py
Normal file
0
integral_auth/__init__.py
Normal file
3
integral_auth/admin.py
Normal file
3
integral_auth/admin.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
5
integral_auth/apps.py
Normal file
5
integral_auth/apps.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class IntegralAuthConfig(AppConfig):
|
||||
name = 'integral_auth'
|
23
integral_auth/forms.py
Normal file
23
integral_auth/forms.py
Normal file
@ -0,0 +1,23 @@
|
||||
from django.contrib.auth.forms import UserCreationForm, AuthenticationForm
|
||||
from crispy_forms.helper import FormHelper
|
||||
from crispy_forms.layout import Submit
|
||||
from captcha.fields import CaptchaField
|
||||
|
||||
class PasswordSignUpForm(UserCreationForm):
|
||||
password1 = None
|
||||
password2 = None
|
||||
captcha = CaptchaField()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(PasswordSignUpForm, self).__init__(*args, **kwargs)
|
||||
self.helper = FormHelper()
|
||||
self.helper.form_method = 'post'
|
||||
self.helper.add_input(Submit('submit', 'Continue'))
|
||||
|
||||
class PasswordSignInForm(AuthenticationForm):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(PasswordSignInForm, self).__init__(*args, **kwargs)
|
||||
self.helper = FormHelper()
|
||||
self.helper.form_method = 'post'
|
||||
self.helper.add_input(Submit('submit', 'Sign in'))
|
||||
|
0
integral_auth/migrations/__init__.py
Normal file
0
integral_auth/migrations/__init__.py
Normal file
3
integral_auth/models.py
Normal file
3
integral_auth/models.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.db import models
|
||||
|
||||
# Create your models here.
|
25
integral_auth/templates/integral_auth/base.html
Normal file
25
integral_auth/templates/integral_auth/base.html
Normal file
@ -0,0 +1,25 @@
|
||||
{% extends "wallet/base.html" %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
{% block signup_form %}{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br>
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
|
||||
<div class="text-center">
|
||||
<a href="{% url 'integral_auth:signin' %}">
|
||||
Sign in</a> •
|
||||
<a href="{% url 'integral_auth:signup' %}">
|
||||
Sign up</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
14
integral_auth/templates/integral_auth/signin.html
Normal file
14
integral_auth/templates/integral_auth/signin.html
Normal file
@ -0,0 +1,14 @@
|
||||
{% extends "integral_auth/base.html" %}
|
||||
|
||||
{% block signup_form %}
|
||||
|
||||
<h1 class="text-center">
|
||||
Sign in
|
||||
</h1>
|
||||
|
||||
<div>
|
||||
{% load crispy_forms_tags %}
|
||||
{% crispy form form.helper %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
14
integral_auth/templates/integral_auth/signup.html
Normal file
14
integral_auth/templates/integral_auth/signup.html
Normal file
@ -0,0 +1,14 @@
|
||||
{% extends "integral_auth/base.html" %}
|
||||
|
||||
{% block signup_form %}
|
||||
|
||||
<h1 class="text-center">
|
||||
Sign up
|
||||
</h1>
|
||||
|
||||
<div>
|
||||
{% load crispy_forms_tags %}
|
||||
{% crispy form form.helper %}
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
143
integral_auth/tests_func.py
Normal file
143
integral_auth/tests_func.py
Normal file
@ -0,0 +1,143 @@
|
||||
from django.test import TestCase, RequestFactory, Client
|
||||
from django.urls import reverse_lazy, reverse
|
||||
from django.contrib.auth.models import User, AnonymousUser
|
||||
from django.contrib.sessions.backends.db import SessionStore
|
||||
from django.http import Http404
|
||||
from django.conf import settings
|
||||
|
||||
import os.path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import integral_auth.views as views
|
||||
from integral_auth.utils import rand_string
|
||||
|
||||
import captcha
|
||||
captcha.conf.settings.CAPTCHA_TEST_MODE = True
|
||||
|
||||
views.conn = MagicMock()
|
||||
views.conn.getnewaddress = lambda: "G" + rand_string(33)
|
||||
|
||||
class SignupTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user("sophia", password="qweqweqwe")
|
||||
self.page_url = reverse_lazy("integral_auth:signup")
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def test_page(self):
|
||||
request = self.factory.get(self.page_url)
|
||||
request.user = AnonymousUser()
|
||||
resp = views.PasswordSignUp.as_view()(request)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
request = self.factory.get(self.page_url)
|
||||
request.user = self.user
|
||||
resp = views.PasswordSignUp.as_view()(request)
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
|
||||
|
||||
def test_invalid_signup(self):
|
||||
|
||||
resp = Client().post(self.page_url, {})
|
||||
self.assertIs(resp.status_code, 200)
|
||||
self.assertFalse(resp.context["form"].is_valid())
|
||||
|
||||
resp = Client().post(self.page_url, {
|
||||
"username": "sophia",
|
||||
'captcha_0': 'abc', "captcha_1": "passed"})
|
||||
self.assertFalse(resp.context["form"].is_valid())
|
||||
|
||||
resp = Client().post(self.page_url, {
|
||||
"username": "test11",
|
||||
'captcha_0': 'abc', "captcha_1": "wrong"})
|
||||
self.assertFalse(resp.context["form"].is_valid())
|
||||
|
||||
|
||||
def test_success_signup(self):
|
||||
|
||||
resp = Client().post(self.page_url, {
|
||||
"username": "testuser",
|
||||
'captcha_0': 'abc', "captcha_1": "passed"})
|
||||
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
|
||||
class SigninTests(TestCase):
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user("sophia", password="qweqweqwe")
|
||||
self.page_url = reverse_lazy("integral_auth:signin")
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def post_request(self, data):
|
||||
request = self.factory.post(self.page_url)
|
||||
request.user, request.session = AnonymousUser(), SessionStore()
|
||||
request.POST = data
|
||||
return request
|
||||
|
||||
def test_page(self):
|
||||
request = self.factory.get(self.page_url)
|
||||
request.user, request.session = AnonymousUser(), SessionStore()
|
||||
resp = views.PasswordSignIn.as_view()(request)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
|
||||
request = self.factory.get(self.page_url)
|
||||
request.user = self.user
|
||||
resp = views.PasswordSignIn.as_view()(request)
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
|
||||
def test_signin_incorrect(self):
|
||||
request = self.post_request({"username": "", "password": ""})
|
||||
resp = views.PasswordSignIn.as_view()(request)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertTrue(request.user.is_anonymous)
|
||||
|
||||
request = self.post_request({"username": "hacker", "password": "qweqweqwe"})
|
||||
resp = views.PasswordSignIn.as_view()(request)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertTrue(request.user.is_anonymous)
|
||||
|
||||
request = self.post_request({"username": "sophia", "password": ""})
|
||||
resp = views.PasswordSignIn.as_view()(request)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertTrue(request.user.is_anonymous)
|
||||
|
||||
request = self.post_request({"username": "sophia", "password": "aiosoidsoaas"})
|
||||
resp = views.PasswordSignIn.as_view()(request)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertTrue(request.user.is_anonymous)
|
||||
|
||||
def test_signin_success(self):
|
||||
request = self.post_request({"username": "sophia", "password": "qweqweqwe"})
|
||||
resp = views.PasswordSignIn.as_view()(request)
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertEqual(request.user.username, "sophia")
|
||||
|
||||
class SigninUrlTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.user = User.objects.create_user("sophia", password="qweqweqwe")
|
||||
self.user1 = User.objects.create_user("hacker", password="qweqweqwe")
|
||||
self.factory = RequestFactory()
|
||||
|
||||
def test_signin_incorrect(self):
|
||||
creds = {"username": "sophia", "password": "qweqweqw"}
|
||||
page_url = reverse("integral_auth:signin_url", kwargs=creds)
|
||||
request = self.factory.get(page_url)
|
||||
request.session, request.user = SessionStore(), AnonymousUser()
|
||||
with self.assertRaises(Http404):
|
||||
views.signin_url(request, **creds)
|
||||
self.assertTrue(request.user.is_anonymous)
|
||||
# it redirects instead of 404 if user is logged in
|
||||
c = Client()
|
||||
c.login(username="hacker", password="qweqweqwe")
|
||||
resp = c.get(page_url)
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
|
||||
def test_signin_success(self):
|
||||
creds = {"username": "sophia", "password": "qweqweqwe"}
|
||||
page_url = reverse("integral_auth:signin_url", kwargs=creds)
|
||||
request = self.factory.get(page_url)
|
||||
request.session, request.user = SessionStore(), AnonymousUser()
|
||||
resp = views.signin_url(request, **creds)
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertEqual(request.user.username, "sophia")
|
||||
|
41
integral_auth/tests_unit.py
Normal file
41
integral_auth/tests_unit.py
Normal file
@ -0,0 +1,41 @@
|
||||
from django.test import TestCase
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from integral_auth import forms
|
||||
|
||||
import captcha
|
||||
captcha.conf.settings.CAPTCHA_TEST_MODE = True
|
||||
|
||||
|
||||
class PasswordSignUpFormTests(TestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.form = forms.PasswordSignUpForm
|
||||
self.user = User.objects.create_user("sophia", password="qweqweqwe")
|
||||
|
||||
def test_signup_valid(self):
|
||||
data = {'username': 'paul',
|
||||
'captcha_0':'abc', 'captcha_1': 'passed'}
|
||||
form = self.form(data)
|
||||
self.assertTrue(form.is_valid())
|
||||
|
||||
def test_signup_invalid_username(self):
|
||||
data = {'username': 'sophia',
|
||||
'captcha_0':'abc', 'captcha_1': 'passed'}
|
||||
form = self.form(data)
|
||||
self.assertFalse(form.is_valid())
|
||||
|
||||
def test_signup_invalid_input(self):
|
||||
data = {}
|
||||
form = self.form(data)
|
||||
self.assertFalse(form.is_valid())
|
||||
|
||||
data = {'username': 'sophia3'}
|
||||
form = self.form(data)
|
||||
self.assertFalse(form.is_valid())
|
||||
|
||||
data = {'username': 'sophia3',
|
||||
'captcha_0':'abc', 'captcha_1': 'wtf'}
|
||||
form = self.form(data)
|
||||
self.assertFalse(form.is_valid())
|
||||
|
21
integral_auth/urls.py
Normal file
21
integral_auth/urls.py
Normal file
@ -0,0 +1,21 @@
|
||||
from django.conf.urls import url, include
|
||||
from django.urls import reverse_lazy
|
||||
|
||||
from django.contrib.auth import views as auth_views
|
||||
from django.contrib.auth.decorators import user_passes_test
|
||||
|
||||
from . import views
|
||||
app_name = "integral_auth"
|
||||
|
||||
anon_only = user_passes_test(
|
||||
lambda u: u.is_anonymous(), reverse_lazy('site_index'))
|
||||
|
||||
urlpatterns = [
|
||||
url(r'^$', views.PasswordSignUp.as_view(), name='signup'),
|
||||
url(r'^signin$', views.PasswordSignIn.as_view(), name='signin'),
|
||||
url(r'^url/(?P<username>\w{1,150})/(?P<password>\w+)$',
|
||||
anon_only(views.signin_url), name='signin_url'),
|
||||
|
||||
url(r'^logout/$', auth_views.LogoutView.as_view(), name='logout'),
|
||||
]
|
||||
|
7
integral_auth/utils.py
Normal file
7
integral_auth/utils.py
Normal file
@ -0,0 +1,7 @@
|
||||
import random
|
||||
import string
|
||||
|
||||
def rand_string(length):
|
||||
"""Generate lame random hexdigest string"""
|
||||
return "".join([random.choice(string.hexdigits) for _ in range(length)])
|
||||
|
77
integral_auth/views.py
Normal file
77
integral_auth/views.py
Normal file
@ -0,0 +1,77 @@
|
||||
from django.shortcuts import redirect
|
||||
from django.http import Http404
|
||||
from django.urls import reverse_lazy, reverse
|
||||
from django.contrib.auth.models import User
|
||||
from django.contrib.auth import authenticate, login
|
||||
from django.views.generic.edit import FormView
|
||||
from django.contrib.auth.mixins import UserPassesTestMixin
|
||||
from django.contrib import messages
|
||||
from django.db import IntegrityError
|
||||
|
||||
from integral_auth.utils import rand_string
|
||||
|
||||
from .forms import PasswordSignUpForm, PasswordSignInForm
|
||||
|
||||
from wallet.models import Account, Address
|
||||
from wallet.gostcoin import GOSTCOIN_CONNECTION as conn
|
||||
|
||||
class PasswordAuth(UserPassesTestMixin, FormView):
|
||||
login_url = reverse_lazy("site_index")
|
||||
def test_func(self):
|
||||
return self.request.user.is_anonymous()
|
||||
|
||||
class PasswordSignUp(PasswordAuth):
|
||||
template_name = "integral_auth/signup.html"
|
||||
form_class = PasswordSignUpForm
|
||||
|
||||
def form_valid(self, form):
|
||||
addr = conn.getnewaddress()
|
||||
|
||||
while True:
|
||||
try:
|
||||
username, password = rand_string(16), rand_string(32)
|
||||
user = User.objects.create_user(username, password=password)
|
||||
break
|
||||
except IntegrityError:
|
||||
pass
|
||||
|
||||
name = rand_string(16)
|
||||
account = Account.objects.create(user=user, name=name)
|
||||
Address.objects.create(account=account, address=addr, used=False)
|
||||
|
||||
signin_url = self.request.build_absolute_uri(
|
||||
reverse('integral_auth:signin_url', kwargs={
|
||||
"username": username, "password": password}))
|
||||
|
||||
messages.success(self.request,
|
||||
"Success! Username: {}, password: {}".format(
|
||||
username, password))
|
||||
messages.success(self.request,
|
||||
"You can login by visiting this link: {}".format(signin_url))
|
||||
messages.warning(self.request,
|
||||
"""Save username/password RIGHT NOW in a secure place. There is
|
||||
NO WAY to recover them!""")
|
||||
|
||||
login(self.request, user)
|
||||
return redirect("site_index")
|
||||
|
||||
class PasswordSignIn(PasswordAuth):
|
||||
template_name = "integral_auth/signin.html"
|
||||
form_class = PasswordSignInForm
|
||||
|
||||
def form_valid(self, form):
|
||||
user = authenticate(username=form.cleaned_data["username"],
|
||||
password=form.cleaned_data["password"])
|
||||
if user is not None and user.is_active:
|
||||
login(self.request, user)
|
||||
return redirect("site_index")
|
||||
|
||||
def signin_url(request, username, password):
|
||||
"""Handle sign in by url"""
|
||||
user = authenticate(username=username, password=password)
|
||||
if user is not None and user.is_active:
|
||||
login(request, user)
|
||||
else:
|
||||
raise Http404
|
||||
|
||||
return redirect("site_index")
|
22
manage.py
Executable file
22
manage.py
Executable file
@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gst_web_wallet.settings")
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError:
|
||||
# The above import may fail for some other reason. Ensure that the
|
||||
# issue is really that Django is missing to avoid masking other
|
||||
# exceptions on Python 2.
|
||||
try:
|
||||
import django
|
||||
except ImportError:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
)
|
||||
raise
|
||||
execute_from_command_line(sys.argv)
|
7
requirements.txt
Normal file
7
requirements.txt
Normal file
@ -0,0 +1,7 @@
|
||||
celery
|
||||
Django==1.11
|
||||
django-crispy-forms
|
||||
django-simple-captcha
|
||||
Markdown
|
||||
redis
|
||||
python-bitcoinrpc
|
0
wallet/__init__.py
Normal file
0
wallet/__init__.py
Normal file
3
wallet/admin.py
Normal file
3
wallet/admin.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
5
wallet/apps.py
Normal file
5
wallet/apps.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class WalletConfig(AppConfig):
|
||||
name = 'wallet'
|
58
wallet/forms.py
Normal file
58
wallet/forms.py
Normal file
@ -0,0 +1,58 @@
|
||||
from django import forms
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.conf import settings
|
||||
|
||||
from crispy_forms.helper import FormHelper
|
||||
from crispy_forms.layout import Submit, Layout
|
||||
|
||||
from decimal import Decimal
|
||||
|
||||
from .models import Account
|
||||
|
||||
class SendCoins(forms.Form):
|
||||
recipient = forms.RegexField(
|
||||
regex="^[a-zA-Z0-9]{16,35}$",
|
||||
label="Address or local account")
|
||||
amount = forms.DecimalField(label="amount",
|
||||
min_value=Decimal("0.01"),
|
||||
max_value=Decimal("2000000"))
|
||||
|
||||
|
||||
def clean_recipient(self):
|
||||
"""Validate recipient"""
|
||||
data = self.cleaned_data["recipient"]
|
||||
|
||||
if (len(data) == 34 or len(data) == 35) and data.startswith("G"):
|
||||
"""Valid GST address"""
|
||||
return data
|
||||
elif len(data) == 16:
|
||||
"""Account name"""
|
||||
try:
|
||||
a = Account.objects.get(name=data)
|
||||
except Account.DoesNotExist:
|
||||
raise ValidationError("Invalid account name", code="invalid")
|
||||
else:
|
||||
raise ValidationError("Incorrect recipient address/account name",
|
||||
code="invalid")
|
||||
|
||||
return data
|
||||
|
||||
def clean(self):
|
||||
"""Check balance"""
|
||||
if self.user and self.is_valid():
|
||||
if len(self.cleaned_data["recipient"]) == 16:
|
||||
total_amount = self.cleaned_data["amount"]
|
||||
else:
|
||||
total_amount = self.cleaned_data["amount"] + \
|
||||
settings.GST_NETWORK_FEE + settings.SERVICE_FEE
|
||||
if self.user.account.balance < total_amount:
|
||||
raise ValidationError("Not enough coins on balance")
|
||||
|
||||
return self.cleaned_data
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.user = kwargs.pop('user', None)
|
||||
super(SendCoins, self).__init__(*args, **kwargs)
|
||||
self.helper = FormHelper()
|
||||
self.helper.form_method = 'post'
|
||||
self.helper.add_input(Submit('submit', 'Send'))
|
42
wallet/gostcoin.py
Normal file
42
wallet/gostcoin.py
Normal file
@ -0,0 +1,42 @@
|
||||
from django.conf import settings
|
||||
from bitcoinrpc.authproxy import AuthServiceProxy
|
||||
|
||||
def select_inputs(conn, amount):
|
||||
"""Select unspent inputs to craft tx"""
|
||||
unspent_inputs = conn.listunspent(0)
|
||||
unspent_inputs.sort(key=lambda u: u['amount'] * u['confirmations'],
|
||||
reverse=True)
|
||||
|
||||
inputs, total = [], 0
|
||||
for usin in unspent_inputs:
|
||||
inputs.append(usin)
|
||||
total += usin["amount"]
|
||||
if total >= amount: break
|
||||
|
||||
if total < amount:
|
||||
raise GostCoinException("Not enough coins on the server")
|
||||
|
||||
return inputs, total
|
||||
|
||||
def create_raw_tx(conn, address, amount):
|
||||
"""Prepare raw transaction and return with output amount"""
|
||||
# TODO calculate fee per kB
|
||||
output_amount = amount + settings.GST_NETWORK_FEE
|
||||
inputs, total = select_inputs(conn, output_amount)
|
||||
|
||||
change_amount = total - output_amount
|
||||
outputs = {address: amount}
|
||||
if change_amount > settings.GST_DUST:
|
||||
outputs[settings.GST_CHANGE_ADDRESS] = change_amount
|
||||
|
||||
return conn.createrawtransaction(inputs, outputs)
|
||||
|
||||
|
||||
class GostCoinException(Exception):
|
||||
"""
|
||||
Raised when something is wrong with account balance
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
GOSTCOIN_CONNECTION = AuthServiceProxy(settings.COIN_CONNECTION)
|
47
wallet/migrations/0001_initial.py
Normal file
47
wallet/migrations/0001_initial.py
Normal file
@ -0,0 +1,47 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2017-04-27 09:17
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Account',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=16, unique=True)),
|
||||
('balance', models.DecimalField(decimal_places=8, default=0, max_digits=15)),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Address',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('address', models.CharField(max_length=35, unique=True)),
|
||||
('used', models.BooleanField(default=False)),
|
||||
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='wallet.Account')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Transaction',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('txid', models.CharField(max_length=64, unique=True)),
|
||||
('confirmed', models.BooleanField(default=False)),
|
||||
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='wallet.Account')),
|
||||
('address', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='wallet.Address')),
|
||||
],
|
||||
),
|
||||
]
|
55
wallet/migrations/0002_auto_20171021_2029.py
Normal file
55
wallet/migrations/0002_auto_20171021_2029.py
Normal file
@ -0,0 +1,55 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2017-10-21 20:29
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wallet', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='DepositTransaction',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('txid', models.CharField(max_length=64, unique=True)),
|
||||
('confirmed', models.BooleanField(default=False)),
|
||||
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='wallet.Account')),
|
||||
('address', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='wallet.Address')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='WithdrawalTransaction',
|
||||
fields=[
|
||||
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('txid', models.CharField(max_length=64, unique=True)),
|
||||
('confirmed', models.BooleanField(default=False)),
|
||||
('address', models.CharField(max_length=35)),
|
||||
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='wallet.Account')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='transaction',
|
||||
name='account',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='transaction',
|
||||
name='address',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='Transaction',
|
||||
),
|
||||
]
|
25
wallet/migrations/0003_auto_20171021_2051.py
Normal file
25
wallet/migrations/0003_auto_20171021_2051.py
Normal file
@ -0,0 +1,25 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11 on 2017-10-21 20:51
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('wallet', '0002_auto_20171021_2029'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='deposittransaction',
|
||||
name='amount',
|
||||
field=models.DecimalField(decimal_places=8, default=0, max_digits=15),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='withdrawaltransaction',
|
||||
name='amount',
|
||||
field=models.DecimalField(decimal_places=8, default=0, max_digits=15),
|
||||
),
|
||||
]
|
0
wallet/migrations/__init__.py
Normal file
0
wallet/migrations/__init__.py
Normal file
38
wallet/models.py
Normal file
38
wallet/models.py
Normal file
@ -0,0 +1,38 @@
|
||||
from django.db import models, IntegrityError
|
||||
from django.contrib.auth.models import User
|
||||
from django.dispatch import receiver
|
||||
from django.db.models.signals import post_save
|
||||
|
||||
from integral_auth.utils import rand_string
|
||||
|
||||
import logging
|
||||
logger = logging.getLogger("django")
|
||||
|
||||
class Account(models.Model):
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||
name = models.CharField(max_length=16, unique=True)
|
||||
balance = models.DecimalField(default=0, decimal_places=8, max_digits=15)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Address(models.Model):
|
||||
account = models.ForeignKey(Account, on_delete=models.CASCADE)
|
||||
address = models.CharField(max_length=35, unique=True)
|
||||
used = models.BooleanField(default=False)
|
||||
|
||||
class Transaction(models.Model):
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
account = models.ForeignKey(Account, on_delete=models.CASCADE)
|
||||
txid = models.CharField(max_length=64, unique=True)
|
||||
confirmed = models.BooleanField(default=False)
|
||||
amount = models.DecimalField(default=0, decimal_places=8, max_digits=15)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
class DepositTransaction(Transaction):
|
||||
address = models.ForeignKey(Address, on_delete=models.CASCADE)
|
||||
|
||||
class WithdrawalTransaction(Transaction):
|
||||
address = models.CharField(max_length=35)
|
7
wallet/static/wallet/css/bootstrap.css
vendored
Normal file
7
wallet/static/wallet/css/bootstrap.css
vendored
Normal file
File diff suppressed because one or more lines are too long
11
wallet/static/wallet/css/bootstrap.css.bak
Normal file
11
wallet/static/wallet/css/bootstrap.css.bak
Normal file
File diff suppressed because one or more lines are too long
19
wallet/static/wallet/css/main.css
Normal file
19
wallet/static/wallet/css/main.css
Normal file
@ -0,0 +1,19 @@
|
||||
p.flash-message {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.toggle-block {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#auth-password-signup:checked + div {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#auth-retrieve-options:checked + div {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#content {
|
||||
margin-bottom: 50px;
|
||||
}
|
424
wallet/static/wallet/css/normalize.css
vendored
Normal file
424
wallet/static/wallet/css/normalize.css
vendored
Normal file
@ -0,0 +1,424 @@
|
||||
/*! normalize.css v3.0.3 | MIT License | github.com/necolas/normalize.css */
|
||||
|
||||
/**
|
||||
* 1. Set default font family to sans-serif.
|
||||
* 2. Prevent iOS and IE text size adjust after device orientation change,
|
||||
* without disabling user zoom.
|
||||
*/
|
||||
|
||||
html {
|
||||
font-family: sans-serif; /* 1 */
|
||||
-ms-text-size-adjust: 100%; /* 2 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove default margin.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* HTML5 display definitions
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Correct `block` display not defined for any HTML5 element in IE 8/9.
|
||||
* Correct `block` display not defined for `details` or `summary` in IE 10/11
|
||||
* and Firefox.
|
||||
* Correct `block` display not defined for `main` in IE 11.
|
||||
*/
|
||||
|
||||
article,
|
||||
aside,
|
||||
details,
|
||||
figcaption,
|
||||
figure,
|
||||
footer,
|
||||
header,
|
||||
hgroup,
|
||||
main,
|
||||
menu,
|
||||
nav,
|
||||
section,
|
||||
summary {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct `inline-block` display not defined in IE 8/9.
|
||||
* 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.
|
||||
*/
|
||||
|
||||
audio,
|
||||
canvas,
|
||||
progress,
|
||||
video {
|
||||
display: inline-block; /* 1 */
|
||||
vertical-align: baseline; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent modern browsers from displaying `audio` without controls.
|
||||
* Remove excess height in iOS 5 devices.
|
||||
*/
|
||||
|
||||
audio:not([controls]) {
|
||||
display: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address `[hidden]` styling not present in IE 8/9/10.
|
||||
* Hide the `template` element in IE 8/9/10/11, Safari, and Firefox < 22.
|
||||
*/
|
||||
|
||||
[hidden],
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Links
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the gray background color from active links in IE 10.
|
||||
*/
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Improve readability of focused elements when they are also in an
|
||||
* active/hover state.
|
||||
*/
|
||||
|
||||
a:active,
|
||||
a:hover {
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
/* Text-level semantics
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Address styling not present in IE 8/9/10/11, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: 1px dotted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address style set to `bolder` in Firefox 4+, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address styling not present in Safari and Chrome.
|
||||
*/
|
||||
|
||||
dfn {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address variable `h1` font-size and margin within `section` and `article`
|
||||
* contexts in Firefox 4+, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address styling not present in IE 8/9.
|
||||
*/
|
||||
|
||||
mark {
|
||||
background: #ff0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address inconsistent and variable font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent `sub` and `sup` affecting `line-height` in all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
/* Embedded content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove border when inside `a` element in IE 8/9/10.
|
||||
*/
|
||||
|
||||
img {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct overflow not hidden in IE 9/10/11.
|
||||
*/
|
||||
|
||||
svg:not(:root) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Grouping content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Address margin not present in IE 8/9 and Safari.
|
||||
*/
|
||||
|
||||
figure {
|
||||
margin: 1em 40px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address differences between Firefox and other browsers.
|
||||
*/
|
||||
|
||||
hr {
|
||||
box-sizing: content-box;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Contain overflow in all browsers.
|
||||
*/
|
||||
|
||||
pre {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address odd `em`-unit font size rendering in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
pre,
|
||||
samp {
|
||||
font-family: monospace, monospace;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/* Forms
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Known limitation: by default, Chrome and Safari on OS X allow very limited
|
||||
* styling of `select`, unless a `border` property is set.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 1. Correct color not being inherited.
|
||||
* Known issue: affects color of disabled elements.
|
||||
* 2. Correct font properties not being inherited.
|
||||
* 3. Address margins set differently in Firefox 4+, Safari, and Chrome.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
color: inherit; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
margin: 0; /* 3 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Address `overflow` set to `hidden` in IE 8/9/10/11.
|
||||
*/
|
||||
|
||||
button {
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Address inconsistent `text-transform` inheritance for `button` and `select`.
|
||||
* All other form control elements do not inherit `text-transform` values.
|
||||
* Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.
|
||||
* Correct `select` style inheritance in Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select {
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
|
||||
* and `video` controls.
|
||||
* 2. Correct inability to style clickable `input` types in iOS.
|
||||
* 3. Improve usability and consistency of cursor style between image-type
|
||||
* `input` and others.
|
||||
*/
|
||||
|
||||
button,
|
||||
html input[type="button"], /* 1 */
|
||||