mirror of https://github.com/GOSTSec/gostweb

commit
294982b746
51 changed files with 2113 additions and 0 deletions
@ -0,0 +1,16 @@
@@ -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/* |
@ -0,0 +1,20 @@
@@ -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. |
@ -0,0 +1,11 @@
@@ -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,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
# import celery stuff |
||||
from .celery import app as celery_app |
||||
|
||||
__all__ = ['celery_app'] |
@ -0,0 +1,21 @@
@@ -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)) |
@ -0,0 +1,58 @@
@@ -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': '', |
||||
# } |
||||
# } |
@ -0,0 +1,113 @@
@@ -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'} |
||||
|
@ -0,0 +1,27 @@
@@ -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')), |
||||
] |
@ -0,0 +1,16 @@
@@ -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,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin |
||||
|
||||
# Register your models here. |
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig |
||||
|
||||
|
||||
class IntegralAuthConfig(AppConfig): |
||||
name = 'integral_auth' |
@ -0,0 +1,23 @@
@@ -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,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
from django.db import models |
||||
|
||||
# Create your models here. |
@ -0,0 +1,25 @@
@@ -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 %} |
@ -0,0 +1,14 @@
@@ -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 %} |
@ -0,0 +1,14 @@
@@ -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 %} |
@ -0,0 +1,143 @@
@@ -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") |
||||
|
@ -0,0 +1,41 @@
@@ -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()) |
||||
|
@ -0,0 +1,21 @@
@@ -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'), |
||||
] |
||||
|
@ -0,0 +1,7 @@
@@ -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)]) |
||||
|
@ -0,0 +1,77 @@
@@ -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") |
@ -0,0 +1,22 @@
@@ -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) |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
celery |
||||
Django==1.11 |
||||
django-crispy-forms |
||||
django-simple-captcha |
||||
Markdown |
||||
redis |
||||
python-bitcoinrpc |
@ -0,0 +1,3 @@
@@ -0,0 +1,3 @@
|
||||
from django.contrib import admin |
||||
|
||||
# Register your models here. |
@ -0,0 +1,5 @@
@@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig |
||||
|
||||
|
||||
class WalletConfig(AppConfig): |
||||
name = 'wallet' |
@ -0,0 +1,58 @@
@@ -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')) |
@ -0,0 +1,42 @@
@@ -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) |
@ -0,0 +1,47 @@
@@ -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')), |
||||
], |
||||
), |
||||
] |
@ -0,0 +1,55 @@
@@ -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', |
||||
), |
||||
] |
@ -0,0 +1,25 @@
@@ -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,0 +1,38 @@
@@ -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) |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -0,0 +1,19 @@
@@ -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; |
||||
} |
@ -0,0 +1,424 @@
@@ -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 */ |
||||
input[type="reset"], |
||||
input[type="submit"] { |
||||
-webkit-appearance: button; /* 2 */ |
||||
cursor: pointer; /* 3 */ |
||||
} |
||||
|
||||
/** |
||||
* Re-set default cursor for disabled elements. |
||||
*/ |
||||
|
||||
button[disabled], |
||||
html input[disabled] { |
||||
cursor: default; |
||||
} |
||||
|
||||
/** |
||||
* Remove inner padding and border in Firefox 4+. |
||||
*/ |
||||
|
||||
button::-moz-focus-inner, |
||||
input::-moz-focus-inner { |
||||
border: 0; |
||||
padding: 0; |
||||
} |
||||
|
||||
/** |
||||
* Address Firefox 4+ setting `line-height` on `input` using `!important` in |
||||
* the UA stylesheet. |
||||
*/ |
||||
|
||||
input { |
||||
line-height: normal; |
||||
} |
||||
|
||||
/** |
||||
* It's recommended that you don't attempt to style these elements. |
||||
* Firefox's implementation doesn't respect box-sizing, padding, or width. |
||||
* |
||||
* 1. Address box sizing set to `content-box` in IE 8/9/10. |
||||
* 2. Remove excess padding in IE 8/9/10. |
||||
*/ |
||||
|
||||
input[type="checkbox"], |
||||
input[type="radio"] { |
||||
box-sizing: border-box; /* 1 */ |
||||
padding: 0; /* 2 */ |
||||
} |
||||
|
||||
/** |
||||
* Fix the cursor style for Chrome's increment/decrement buttons. For certain |
||||
* `font-size` values of the `input`, it causes the cursor style of the |
||||
* decrement button to change from `default` to `text`. |
||||
*/ |
||||
|
||||
input[type="number"]::-webkit-inner-spin-button, |
||||
input[type="number"]::-webkit-outer-spin-button { |
||||
height: auto; |
||||
} |
||||
|
||||
/** |
||||
* 1. Address `appearance` set to `searchfield` in Safari and Chrome. |
||||
* 2. Address `box-sizing` set to `border-box` in Safari and Chrome. |
||||
*/ |
||||
|
||||
input[type="search"] { |
||||
-webkit-appearance: textfield; /* 1 */ |
||||
box-sizing: content-box; /* 2 */ |
||||
} |
||||
|
||||
/** |
||||
* Remove inner padding and search cancel button in Safari and Chrome on OS X. |
||||
* Safari (but not Chrome) clips the cancel button when the search input has |
||||
* padding (and `textfield` appearance). |
||||
*/ |
||||
|
||||
input[type="search"]::-webkit-search-cancel-button, |
||||
input[type="search"]::-webkit-search-decoration { |
||||
-webkit-appearance: none; |
||||
} |
||||
|
||||
/** |
||||
* Define consistent border, margin, and padding. |
||||