支持google身份验证器做双因子登录了。

pull/32/head
Apex Liu 2017-06-04 03:26:42 +08:00
parent 7881d0f1ee
commit 6ef17c79b1
43 changed files with 3494 additions and 981 deletions

View File

@ -0,0 +1,25 @@
from qrcode.main import QRCode
from qrcode.main import make # noqa
from qrcode.constants import ( # noqa
ERROR_CORRECT_L, ERROR_CORRECT_M, ERROR_CORRECT_Q, ERROR_CORRECT_H)
from qrcode import image # noqa
def run_example(data="http://www.lincolnloop.com", *args, **kwargs):
"""
Build an example QR Code and display it.
There's an even easier way than the code here though: just use the ``make``
shortcut.
"""
qr = QRCode(*args, **kwargs)
qr.add_data(data)
im = qr.make_image()
im.show()
if __name__ == '__main__': # pragma: no cover
import sys
run_example(*sys.argv[1:])

View File

@ -0,0 +1,361 @@
from qrcode import constants
EXP_TABLE = list(range(256))
LOG_TABLE = list(range(256))
for i in range(8):
EXP_TABLE[i] = 1 << i
for i in range(8, 256):
EXP_TABLE[i] = (
EXP_TABLE[i - 4] ^ EXP_TABLE[i - 5] ^ EXP_TABLE[i - 6] ^
EXP_TABLE[i - 8])
for i in range(255):
LOG_TABLE[EXP_TABLE[i]] = i
RS_BLOCK_OFFSET = {
constants.ERROR_CORRECT_L: 0,
constants.ERROR_CORRECT_M: 1,
constants.ERROR_CORRECT_Q: 2,
constants.ERROR_CORRECT_H: 3,
}
RS_BLOCK_TABLE = [
# L
# M
# Q
# H
# 1
[1, 26, 19],
[1, 26, 16],
[1, 26, 13],
[1, 26, 9],
# 2
[1, 44, 34],
[1, 44, 28],
[1, 44, 22],
[1, 44, 16],
# 3
[1, 70, 55],
[1, 70, 44],
[2, 35, 17],
[2, 35, 13],
# 4
[1, 100, 80],
[2, 50, 32],
[2, 50, 24],
[4, 25, 9],
# 5
[1, 134, 108],
[2, 67, 43],
[2, 33, 15, 2, 34, 16],
[2, 33, 11, 2, 34, 12],
# 6
[2, 86, 68],
[4, 43, 27],
[4, 43, 19],
[4, 43, 15],
# 7
[2, 98, 78],
[4, 49, 31],
[2, 32, 14, 4, 33, 15],
[4, 39, 13, 1, 40, 14],
# 8
[2, 121, 97],
[2, 60, 38, 2, 61, 39],
[4, 40, 18, 2, 41, 19],
[4, 40, 14, 2, 41, 15],
# 9
[2, 146, 116],
[3, 58, 36, 2, 59, 37],
[4, 36, 16, 4, 37, 17],
[4, 36, 12, 4, 37, 13],
# 10
[2, 86, 68, 2, 87, 69],
[4, 69, 43, 1, 70, 44],
[6, 43, 19, 2, 44, 20],
[6, 43, 15, 2, 44, 16],
# 11
[4, 101, 81],
[1, 80, 50, 4, 81, 51],
[4, 50, 22, 4, 51, 23],
[3, 36, 12, 8, 37, 13],
# 12
[2, 116, 92, 2, 117, 93],
[6, 58, 36, 2, 59, 37],
[4, 46, 20, 6, 47, 21],
[7, 42, 14, 4, 43, 15],
# 13
[4, 133, 107],
[8, 59, 37, 1, 60, 38],
[8, 44, 20, 4, 45, 21],
[12, 33, 11, 4, 34, 12],
# 14
[3, 145, 115, 1, 146, 116],
[4, 64, 40, 5, 65, 41],
[11, 36, 16, 5, 37, 17],
[11, 36, 12, 5, 37, 13],
# 15
[5, 109, 87, 1, 110, 88],
[5, 65, 41, 5, 66, 42],
[5, 54, 24, 7, 55, 25],
[11, 36, 12, 7, 37, 13],
# 16
[5, 122, 98, 1, 123, 99],
[7, 73, 45, 3, 74, 46],
[15, 43, 19, 2, 44, 20],
[3, 45, 15, 13, 46, 16],
# 17
[1, 135, 107, 5, 136, 108],
[10, 74, 46, 1, 75, 47],
[1, 50, 22, 15, 51, 23],
[2, 42, 14, 17, 43, 15],
# 18
[5, 150, 120, 1, 151, 121],
[9, 69, 43, 4, 70, 44],
[17, 50, 22, 1, 51, 23],
[2, 42, 14, 19, 43, 15],
# 19
[3, 141, 113, 4, 142, 114],
[3, 70, 44, 11, 71, 45],
[17, 47, 21, 4, 48, 22],
[9, 39, 13, 16, 40, 14],
# 20
[3, 135, 107, 5, 136, 108],
[3, 67, 41, 13, 68, 42],
[15, 54, 24, 5, 55, 25],
[15, 43, 15, 10, 44, 16],
# 21
[4, 144, 116, 4, 145, 117],
[17, 68, 42],
[17, 50, 22, 6, 51, 23],
[19, 46, 16, 6, 47, 17],
# 22
[2, 139, 111, 7, 140, 112],
[17, 74, 46],
[7, 54, 24, 16, 55, 25],
[34, 37, 13],
# 23
[4, 151, 121, 5, 152, 122],
[4, 75, 47, 14, 76, 48],
[11, 54, 24, 14, 55, 25],
[16, 45, 15, 14, 46, 16],
# 24
[6, 147, 117, 4, 148, 118],
[6, 73, 45, 14, 74, 46],
[11, 54, 24, 16, 55, 25],
[30, 46, 16, 2, 47, 17],
# 25
[8, 132, 106, 4, 133, 107],
[8, 75, 47, 13, 76, 48],
[7, 54, 24, 22, 55, 25],
[22, 45, 15, 13, 46, 16],
# 26
[10, 142, 114, 2, 143, 115],
[19, 74, 46, 4, 75, 47],
[28, 50, 22, 6, 51, 23],
[33, 46, 16, 4, 47, 17],
# 27
[8, 152, 122, 4, 153, 123],
[22, 73, 45, 3, 74, 46],
[8, 53, 23, 26, 54, 24],
[12, 45, 15, 28, 46, 16],
# 28
[3, 147, 117, 10, 148, 118],
[3, 73, 45, 23, 74, 46],
[4, 54, 24, 31, 55, 25],
[11, 45, 15, 31, 46, 16],
# 29
[7, 146, 116, 7, 147, 117],
[21, 73, 45, 7, 74, 46],
[1, 53, 23, 37, 54, 24],
[19, 45, 15, 26, 46, 16],
# 30
[5, 145, 115, 10, 146, 116],
[19, 75, 47, 10, 76, 48],
[15, 54, 24, 25, 55, 25],
[23, 45, 15, 25, 46, 16],
# 31
[13, 145, 115, 3, 146, 116],
[2, 74, 46, 29, 75, 47],
[42, 54, 24, 1, 55, 25],
[23, 45, 15, 28, 46, 16],
# 32
[17, 145, 115],
[10, 74, 46, 23, 75, 47],
[10, 54, 24, 35, 55, 25],
[19, 45, 15, 35, 46, 16],
# 33
[17, 145, 115, 1, 146, 116],
[14, 74, 46, 21, 75, 47],
[29, 54, 24, 19, 55, 25],
[11, 45, 15, 46, 46, 16],
# 34
[13, 145, 115, 6, 146, 116],
[14, 74, 46, 23, 75, 47],
[44, 54, 24, 7, 55, 25],
[59, 46, 16, 1, 47, 17],
# 35
[12, 151, 121, 7, 152, 122],
[12, 75, 47, 26, 76, 48],
[39, 54, 24, 14, 55, 25],
[22, 45, 15, 41, 46, 16],
# 36
[6, 151, 121, 14, 152, 122],
[6, 75, 47, 34, 76, 48],
[46, 54, 24, 10, 55, 25],
[2, 45, 15, 64, 46, 16],
# 37
[17, 152, 122, 4, 153, 123],
[29, 74, 46, 14, 75, 47],
[49, 54, 24, 10, 55, 25],
[24, 45, 15, 46, 46, 16],
# 38
[4, 152, 122, 18, 153, 123],
[13, 74, 46, 32, 75, 47],
[48, 54, 24, 14, 55, 25],
[42, 45, 15, 32, 46, 16],
# 39
[20, 147, 117, 4, 148, 118],
[40, 75, 47, 7, 76, 48],
[43, 54, 24, 22, 55, 25],
[10, 45, 15, 67, 46, 16],
# 40
[19, 148, 118, 6, 149, 119],
[18, 75, 47, 31, 76, 48],
[34, 54, 24, 34, 55, 25],
[20, 45, 15, 61, 46, 16]
]
def glog(n):
if n < 1: # pragma: no cover
raise ValueError("glog(%s)" % n)
return LOG_TABLE[n]
def gexp(n):
return EXP_TABLE[n % 255]
class Polynomial:
def __init__(self, num, shift):
if not num: # pragma: no cover
raise Exception("%s/%s" % (len(num), shift))
offset = 0
for item in num:
if item != 0:
break
offset += 1
self.num = [0] * (len(num) - offset + shift)
for i in range(len(num) - offset):
self.num[i] = num[i + offset]
def __getitem__(self, index):
return self.num[index]
def __iter__(self):
return iter(self.num)
def __len__(self):
return len(self.num)
def __mul__(self, other):
num = [0] * (len(self) + len(other) - 1)
for i, item in enumerate(self):
for j, other_item in enumerate(other):
num[i + j] ^= gexp(glog(item) + glog(other_item))
return Polynomial(num, 0)
def __mod__(self, other):
difference = len(self) - len(other)
if difference < 0:
return self
ratio = glog(self[0]) - glog(other[0])
num = self[:]
num = [
item ^ gexp(glog(other_item) + ratio)
for item, other_item in zip(self, other)]
if difference:
num.extend(self[-difference:])
# recursive call
return Polynomial(num, 0) % other
class RSBlock:
def __init__(self, total_count, data_count):
self.total_count = total_count
self.data_count = data_count
def rs_blocks(version, error_correction):
if error_correction not in RS_BLOCK_OFFSET: # pragma: no cover
raise Exception(
"bad rs block @ version: %s / error_correction: %s" %
(version, error_correction))
offset = RS_BLOCK_OFFSET[error_correction]
rs_block = RS_BLOCK_TABLE[(version - 1) * 4 + offset]
blocks = []
for i in range(0, len(rs_block), 3):
count, total_count, data_count = rs_block[i:i + 3]
for j in range(count):
blocks.append(RSBlock(total_count, data_count))
return blocks

View File

@ -0,0 +1,96 @@
#!/usr/bin/env python
"""
qr - Convert stdin (or the first argument) to a QR Code.
When stdout is a tty the QR Code is printed to the terminal and when stdout is
a pipe to a file an image is written. The default image format is PNG.
"""
import sys
import optparse
import os
import qrcode
# The next block is added to get the terminal to display properly on MS platforms
if sys.platform.startswith(('win', 'cygwin')):
import colorama
colorama.init()
default_factories = {
'pil': 'qrcode.image.pil.PilImage',
'pymaging': 'qrcode.image.pure.PymagingImage',
'svg': 'qrcode.image.svg.SvgImage',
'svg-fragment': 'qrcode.image.svg.SvgFragmentImage',
'svg-path': 'qrcode.image.svg.SvgPathImage',
}
error_correction = {
'L': qrcode.ERROR_CORRECT_L,
'M': qrcode.ERROR_CORRECT_M,
'Q': qrcode.ERROR_CORRECT_Q,
'H': qrcode.ERROR_CORRECT_H,
}
def main(args=sys.argv[1:]):
parser = optparse.OptionParser(usage=__doc__.strip())
parser.add_option(
"--factory", help="Full python path to the image factory class to "
"create the image with. You can use the following shortcuts to the "
"built-in image factory classes: {0}.".format(
", ".join(sorted(default_factories.keys()))))
parser.add_option(
"--optimize", type=int, help="Optimize the data by looking for chunks "
"of at least this many characters that could use a more efficient "
"encoding method. Use 0 to turn off chunk optimization.")
parser.add_option(
"--error-correction", type='choice',
choices=sorted(error_correction.keys()), default='M',
help="The error correction level to use. Choices are L (7%), "
"M (15%, default), Q (25%), and H (30%).")
opts, args = parser.parse_args(args)
qr = qrcode.QRCode(
error_correction=error_correction[opts.error_correction])
if opts.factory:
module = default_factories.get(opts.factory, opts.factory)
if '.' not in module:
parser.error("The image factory is not a full python path")
module, name = module.rsplit('.', 1)
imp = __import__(module, {}, [], [name])
image_factory = getattr(imp, name)
else:
image_factory = None
if args:
data = args[0]
else:
# Use sys.stdin.buffer if available (Python 3) avoiding
# UnicodeDecodeErrors.
stdin_buffer = getattr(sys.stdin, 'buffer', sys.stdin)
data = stdin_buffer.read()
if opts.optimize is None:
qr.add_data(data)
else:
qr.add_data(data, optimize=opts.optimize)
if image_factory is None and os.isatty(sys.stdout.fileno()):
qr.print_ascii(tty=True)
return
img = qr.make_image(image_factory=image_factory)
sys.stdout.flush()
# Use sys.stdout.buffer if available (Python 3), avoiding
# UnicodeDecodeErrors.
stdout_buffer = getattr(sys.stdout, 'buffer', None)
if not stdout_buffer:
if sys.platform == 'win32': # pragma: no cover
import msvcrt
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY)
stdout_buffer = sys.stdout
img.save(stdout_buffer)
if __name__ == "__main__":
main()

View File

@ -0,0 +1,5 @@
# QR error correct levels
ERROR_CORRECT_L = 1
ERROR_CORRECT_M = 0
ERROR_CORRECT_Q = 3
ERROR_CORRECT_H = 2

View File

@ -0,0 +1,2 @@
class DataOverflowError(Exception):
pass

View File

@ -0,0 +1,62 @@
class BaseImage(object):
"""
Base QRCode image output class.
"""
kind = None
allowed_kinds = None
def __init__(self, border, width, box_size, *args, **kwargs):
self.border = border
self.width = width
self.box_size = box_size
self.pixel_size = (self.width + self.border*2) * self.box_size
self._img = self.new_image(**kwargs)
def drawrect(self, row, col):
"""
Draw a single rectangle of the QR code.
"""
raise NotImplementedError("BaseImage.drawrect")
def save(self, stream, kind=None):
"""
Save the image file.
"""
raise NotImplementedError("BaseImage.save")
def pixel_box(self, row, col):
"""
A helper method for pixel-based image generators that specifies the
four pixel coordinates for a single rect.
"""
x = (col + self.border) * self.box_size
y = (row + self.border) * self.box_size
return [(x, y), (x + self.box_size - 1, y + self.box_size - 1)]
def new_image(self, **kwargs): # pragma: no cover
"""
Build the image class. Subclasses should return the class created.
"""
return None
def get_image(self, **kwargs):
"""
Return the image class for further processing.
"""
return self._img
def check_kind(self, kind, transform=None):
"""
Get the image type.
"""
if kind is None:
kind = self.kind
allowed = not self.allowed_kinds or kind in self.allowed_kinds
if transform:
kind = transform(kind)
if not allowed:
allowed = kind in self.allowed_kinds
if not allowed:
raise ValueError(
"Cannot set %s type to %s" % (type(self).__name__, kind))
return kind

View File

@ -0,0 +1,50 @@
# Needed on case-insensitive filesystems
from __future__ import absolute_import
# Try to import PIL in either of the two ways it can be installed.
try:
from PIL import Image, ImageDraw
except ImportError: # pragma: no cover
import Image
import ImageDraw
import qrcode.image.base
class PilImage(qrcode.image.base.BaseImage):
"""
PIL image builder, default format is PNG.
"""
kind = "PNG"
def new_image(self, **kwargs):
back_color = kwargs.get("fill_color", "white")
fill_color = kwargs.get("back_color", "black")
if fill_color.lower() != "black" or back_color.lower() != "white":
if back_color.lower() == "transparent":
mode = "RGBA"
back_color = None
else:
mode = "RGB"
else:
mode = "1"
img = Image.new(mode, (self.pixel_size, self.pixel_size), back_color)
self.fill_color = fill_color
self._idr = ImageDraw.Draw(img)
return img
def drawrect(self, row, col):
box = self.pixel_box(row, col)
self._idr.rectangle(box, fill=self.fill_color)
def save(self, stream, format=None, **kwargs):
if format is None:
format = kwargs.get("kind", self.kind)
if "kind" in kwargs:
del kwargs["kind"]
self._img.save(stream, format=format, **kwargs)
def __getattr__(self, name):
return getattr(self._img, name)

View File

@ -0,0 +1,49 @@
from pymaging import Image
from pymaging.colors import RGB
from pymaging.formats import registry
from pymaging.shapes import Line
from pymaging.webcolors import Black, White
from pymaging_png.png import PNG
import qrcode.image.base
class PymagingImage(qrcode.image.base.BaseImage):
"""
pymaging image builder, default format is PNG.
"""
kind = "PNG"
allowed_kinds = ("PNG",)
def __init__(self, *args, **kwargs):
"""
Register PNG with pymaging.
"""
registry.formats = []
registry.names = {}
registry._populate()
registry.register(PNG)
super(PymagingImage, self).__init__(*args, **kwargs)
def new_image(self, **kwargs):
return Image.new(RGB, self.pixel_size, self.pixel_size, White)
def drawrect(self, row, col):
(x, y), (x2, y2) = self.pixel_box(row, col)
for r in range(self.box_size):
line_y = y + r
line = Line(x, line_y, x2, line_y)
self._img.draw(line, Black)
def save(self, stream, kind=None):
self._img.save(stream, self.check_kind(kind))
def check_kind(self, kind, transform=None, **kwargs):
"""
pymaging (pymaging_png at least) uses lower case for the type.
"""
if transform is None:
transform = lambda x: x.lower()
return super(PymagingImage, self).check_kind(
kind, transform=transform, **kwargs)

View File

@ -0,0 +1,159 @@
from decimal import Decimal
# On Python 2.6 must install lxml since the older xml.etree.ElementTree
# version can not be used to create SVG images.
try:
import lxml.etree as ET
except ImportError:
import xml.etree.ElementTree as ET
import qrcode.image.base
class SvgFragmentImage(qrcode.image.base.BaseImage):
"""
SVG image builder
Creates a QR-code image as a SVG document fragment.
"""
_SVG_namespace = "http://www.w3.org/2000/svg"
kind = "SVG"
allowed_kinds = ("SVG",)
def __init__(self, *args, **kwargs):
ET.register_namespace("svg", self._SVG_namespace)
super(SvgFragmentImage, self).__init__(*args, **kwargs)
# Save the unit size, for example the default box_size of 10 is '1mm'.
self.unit_size = self.units(self.box_size)
def drawrect(self, row, col):
self._img.append(self._rect(row, col))
def units(self, pixels, text=True):
"""
A box_size of 10 (default) equals 1mm.
"""
units = Decimal(pixels) / 10
if not text:
return units
return '%smm' % units
def save(self, stream, kind=None):
self.check_kind(kind=kind)
self._write(stream)
def new_image(self, **kwargs):
return self._svg()
def _svg(self, tag=None, version='1.1', **kwargs):
if tag is None:
tag = ET.QName(self._SVG_namespace, "svg")
dimension = self.units(self.pixel_size)
return ET.Element(
tag, width=dimension, height=dimension, version=version,
**kwargs)
def _rect(self, row, col, tag=None):
if tag is None:
tag = ET.QName(self._SVG_namespace, "rect")
x, y = self.pixel_box(row, col)[0]
return ET.Element(
tag, x=self.units(x), y=self.units(y),
width=self.unit_size, height=self.unit_size)
def _write(self, stream):
ET.ElementTree(self._img).write(stream, xml_declaration=False)
class SvgImage(SvgFragmentImage):
"""
Standalone SVG image builder
Creates a QR-code image as a standalone SVG document.
"""
background = None
def _svg(self, tag='svg', **kwargs):
svg = super(SvgImage, self)._svg(tag=tag, **kwargs)
svg.set("xmlns", self._SVG_namespace)
if self.background:
svg.append(
ET.Element(
'rect', fill=self.background, x='0', y='0', width='100%',
height='100%'))
return svg
def _rect(self, row, col):
return super(SvgImage, self)._rect(row, col, tag="rect")
def _write(self, stream):
ET.ElementTree(self._img).write(stream, encoding="UTF-8",
xml_declaration=True)
class SvgPathImage(SvgImage):
"""
SVG image builder with one single <path> element (removes white spaces
between individual QR points).
"""
QR_PATH_STYLE = 'fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none'
def __init__(self, *args, **kwargs):
self._points = set()
super(SvgPathImage, self).__init__(*args, **kwargs)
def _svg(self, viewBox=None, **kwargs):
if viewBox is None:
dimension = self.units(self.pixel_size, text=False)
viewBox = '0 0 %(d)s %(d)s' % {'d': dimension}
return super(SvgPathImage, self)._svg(viewBox=viewBox, **kwargs)
def drawrect(self, row, col):
# (x, y)
self._points.add((col, row))
def _generate_subpaths(self):
"""Generates individual QR points as subpaths"""
rect_size = self.units(self.box_size, text=False)
for point in self._points:
x_base = self.units(
(point[0]+self.border)*self.box_size, text=False)
y_base = self.units(
(point[1]+self.border)*self.box_size, text=False)
yield (
'M %(x0)s %(y0)s L %(x0)s %(y1)s L %(x1)s %(y1)s L %(x1)s '
'%(y0)s z' % dict(
x0=x_base, y0=y_base,
x1=x_base+rect_size, y1=y_base+rect_size,
))
def make_path(self):
subpaths = self._generate_subpaths()
return ET.Element(
ET.QName("path"),
style=self.QR_PATH_STYLE,
d=' '.join(subpaths),
id="qr-path"
)
def _write(self, stream):
self._img.append(self.make_path())
super(SvgPathImage, self)._write(stream)
class SvgFillImage(SvgImage):
"""
An SvgImage that fills the background to white.
"""
background = 'white'
class SvgPathFillImage(SvgPathImage):
"""
An SvgPathImage that fills the background to white.
"""
background = 'white'

View File

@ -0,0 +1,422 @@
from qrcode import constants, exceptions, util
from qrcode.image.base import BaseImage
import six
from bisect import bisect_left
def make(data=None, **kwargs):
qr = QRCode(**kwargs)
qr.add_data(data)
return qr.make_image()
def _check_version(version):
if version < 1 or version > 40:
raise ValueError(
"Invalid version (was %s, expected 1 to 40)" % version)
def _check_box_size(size):
if int(size) <= 0:
raise ValueError(
"Invalid box size (was %s, expected larger than 0)" % size)
class QRCode:
def __init__(self, version=None,
error_correction=constants.ERROR_CORRECT_M,
box_size=10, border=4,
image_factory=None):
_check_box_size(box_size)
self.version = version and int(version)
self.error_correction = int(error_correction)
self.box_size = int(box_size)
# Spec says border should be at least four boxes wide, but allow for
# any (e.g. for producing printable QR codes).
self.border = int(border)
self.image_factory = image_factory
if image_factory is not None:
assert issubclass(image_factory, BaseImage)
self.clear()
def clear(self):
"""
Reset the internal data.
"""
self.modules = None
self.modules_count = 0
self.data_cache = None
self.data_list = []
def add_data(self, data, optimize=20):
"""
Add data to this QR Code.
:param optimize: Data will be split into multiple chunks to optimize
the QR size by finding to more compressed modes of at least this
length. Set to ``0`` to avoid optimizing at all.
"""
if isinstance(data, util.QRData):
self.data_list.append(data)
else:
if optimize:
self.data_list.extend(util.optimal_data_chunks(data))
else:
self.data_list.append(util.QRData(data))
self.data_cache = None
def make(self, fit=True):
"""
Compile the data into a QR Code array.
:param fit: If ``True`` (or if a size has not been provided), find the
best fit for the data to avoid data overflow errors.
"""
if fit or (self.version is None):
self.best_fit(start=self.version)
self.makeImpl(False, self.best_mask_pattern())
def makeImpl(self, test, mask_pattern):
_check_version(self.version)
self.modules_count = self.version * 4 + 17
self.modules = [None] * self.modules_count
for row in range(self.modules_count):
self.modules[row] = [None] * self.modules_count
for col in range(self.modules_count):
self.modules[row][col] = None # (col + row) % 3
self.setup_position_probe_pattern(0, 0)
self.setup_position_probe_pattern(self.modules_count - 7, 0)
self.setup_position_probe_pattern(0, self.modules_count - 7)
self.setup_position_adjust_pattern()
self.setup_timing_pattern()
self.setup_type_info(test, mask_pattern)
if self.version >= 7:
self.setup_type_number(test)
if self.data_cache is None:
self.data_cache = util.create_data(
self.version, self.error_correction, self.data_list)
self.map_data(self.data_cache, mask_pattern)
def setup_position_probe_pattern(self, row, col):
for r in range(-1, 8):
if row + r <= -1 or self.modules_count <= row + r:
continue
for c in range(-1, 8):
if col + c <= -1 or self.modules_count <= col + c:
continue
if (0 <= r and r <= 6 and (c == 0 or c == 6)
or (0 <= c and c <= 6 and (r == 0 or r == 6))
or (2 <= r and r <= 4 and 2 <= c and c <= 4)):
self.modules[row + r][col + c] = True
else:
self.modules[row + r][col + c] = False
def best_fit(self, start=None):
"""
Find the minimum size required to fit in the data.
"""
if start is None:
start = 1
_check_version(start)
# Corresponds to the code in util.create_data, except we don't yet know
# version, so optimistically assume start and check later
mode_sizes = util.mode_sizes_for_version(start)
buffer = util.BitBuffer()
for data in self.data_list:
buffer.put(data.mode, 4)
buffer.put(len(data), mode_sizes[data.mode])
data.write(buffer)
needed_bits = len(buffer)
self.version = bisect_left(util.BIT_LIMIT_TABLE[self.error_correction],
needed_bits, start)
if self.version == 41:
raise exceptions.DataOverflowError()
# Now check whether we need more bits for the mode sizes, recursing if
# our guess was too low
if mode_sizes is not util.mode_sizes_for_version(self.version):
self.best_fit(start=self.version)
return self.version
def best_mask_pattern(self):
"""
Find the most efficient mask pattern.
"""
min_lost_point = 0
pattern = 0
for i in range(8):
self.makeImpl(True, i)
lost_point = util.lost_point(self.modules)
if i == 0 or min_lost_point > lost_point:
min_lost_point = lost_point
pattern = i
return pattern
def print_tty(self, out=None):
"""
Output the QR Code only using TTY colors.
If the data has not been compiled yet, make it first.
"""
if out is None:
import sys
out = sys.stdout
if not out.isatty():
raise OSError("Not a tty")
if self.data_cache is None:
self.make()
modcount = self.modules_count
out.write("\x1b[1;47m" + (" " * (modcount * 2 + 4)) + "\x1b[0m\n")
for r in range(modcount):
out.write("\x1b[1;47m \x1b[40m")
for c in range(modcount):
if self.modules[r][c]:
out.write(" ")
else:
out.write("\x1b[1;47m \x1b[40m")
out.write("\x1b[1;47m \x1b[0m\n")
out.write("\x1b[1;47m" + (" " * (modcount * 2 + 4)) + "\x1b[0m\n")
out.flush()
def print_ascii(self, out=None, tty=False, invert=False):
"""
Output the QR Code using ASCII characters.
:param tty: use fixed TTY color codes (forces invert=True)
:param invert: invert the ASCII characters (solid <-> transparent)
"""
if out is None:
import sys
if sys.version_info < (2, 7):
# On Python versions 2.6 and earlier, stdout tries to encode
# strings using ASCII rather than stdout.encoding, so use this
# workaround.
import codecs
out = codecs.getwriter(sys.stdout.encoding)(sys.stdout)
else:
out = sys.stdout
if tty and not out.isatty():
raise OSError("Not a tty")
if self.data_cache is None:
self.make()
modcount = self.modules_count
codes = [six.int2byte(code).decode('cp437')
for code in (255, 223, 220, 219)]
if tty:
invert = True
if invert:
codes.reverse()
def get_module(x, y):
if (invert and self.border and
max(x, y) >= modcount+self.border):
return 1
if min(x, y) < 0 or max(x, y) >= modcount:
return 0
return self.modules[x][y]
for r in range(-self.border, modcount+self.border, 2):
if tty:
if not invert or r < modcount+self.border-1:
out.write('\x1b[48;5;232m') # Background black
out.write('\x1b[38;5;255m') # Foreground white
for c in range(-self.border, modcount+self.border):
pos = get_module(r, c) + (get_module(r+1, c) << 1)
out.write(codes[pos])
if tty:
out.write('\x1b[0m')
out.write('\n')
out.flush()
def make_image(self, image_factory=None, **kwargs):
"""
Make an image from the QR Code data.
If the data has not been compiled yet, make it first.
"""
_check_box_size(self.box_size)
if self.data_cache is None:
self.make()
if image_factory is not None:
assert issubclass(image_factory, BaseImage)
else:
image_factory = self.image_factory
if image_factory is None:
# Use PIL by default
from qrcode.image.pil import PilImage
image_factory = PilImage
im = image_factory(
self.border, self.modules_count, self.box_size, **kwargs)
for r in range(self.modules_count):
for c in range(self.modules_count):
if self.modules[r][c]:
im.drawrect(r, c)
return im
def setup_timing_pattern(self):
for r in range(8, self.modules_count - 8):
if self.modules[r][6] is not None:
continue
self.modules[r][6] = (r % 2 == 0)
for c in range(8, self.modules_count - 8):
if self.modules[6][c] is not None:
continue
self.modules[6][c] = (c % 2 == 0)
def setup_position_adjust_pattern(self):
pos = util.pattern_position(self.version)
for i in range(len(pos)):
for j in range(len(pos)):
row = pos[i]
col = pos[j]
if self.modules[row][col] is not None:
continue
for r in range(-2, 3):
for c in range(-2, 3):
if (r == -2 or r == 2 or c == -2 or c == 2 or
(r == 0 and c == 0)):
self.modules[row + r][col + c] = True
else:
self.modules[row + r][col + c] = False
def setup_type_number(self, test):
bits = util.BCH_type_number(self.version)
for i in range(18):
mod = (not test and ((bits >> i) & 1) == 1)
self.modules[i // 3][i % 3 + self.modules_count - 8 - 3] = mod
for i in range(18):
mod = (not test and ((bits >> i) & 1) == 1)
self.modules[i % 3 + self.modules_count - 8 - 3][i // 3] = mod
def setup_type_info(self, test, mask_pattern):
data = (self.error_correction << 3) | mask_pattern
bits = util.BCH_type_info(data)
# vertical
for i in range(15):
mod = (not test and ((bits >> i) & 1) == 1)
if i < 6:
self.modules[i][8] = mod
elif i < 8:
self.modules[i + 1][8] = mod
else:
self.modules[self.modules_count - 15 + i][8] = mod
# horizontal
for i in range(15):
mod = (not test and ((bits >> i) & 1) == 1)
if i < 8:
self.modules[8][self.modules_count - i - 1] = mod
elif i < 9:
self.modules[8][15 - i - 1 + 1] = mod
else:
self.modules[8][15 - i - 1] = mod
# fixed module
self.modules[self.modules_count - 8][8] = (not test)
def map_data(self, data, mask_pattern):
inc = -1
row = self.modules_count - 1
bitIndex = 7
byteIndex = 0
mask_func = util.mask_func(mask_pattern)
data_len = len(data)
for col in six.moves.xrange(self.modules_count - 1, 0, -2):
if col <= 6:
col -= 1
col_range = (col, col-1)
while True:
for c in col_range:
if self.modules[row][c] is None:
dark = False
if byteIndex < data_len:
dark = (((data[byteIndex] >> bitIndex) & 1) == 1)
if mask_func(row, c):
dark = not dark
self.modules[row][c] = dark
bitIndex -= 1
if bitIndex == -1:
byteIndex += 1
bitIndex = 7
row += inc
if row < 0 or self.modules_count <= row:
row -= inc
inc = -inc
break
def get_matrix(self):
"""
Return the QR Code as a multidimensonal array, including the border.
To return the array without a border, set ``self.border`` to 0 first.
"""
if self.data_cache is None:
self.make()
if not self.border:
return self.modules
width = len(self.modules) + self.border*2
code = [[False]*width] * self.border
x_border = [False]*self.border
for module in self.modules:
code.append(x_border + module + x_border)
code += [[False]*width] * self.border
return code

View File

@ -0,0 +1,33 @@
import six
# {'code': 'N', 'label': 'Name', 'required': True, 'multipart': [
# 'Last Name', 'First Name']},
PROPERTIES = {
'NICKNAME': {'label': 'Nickname'},
'BDAY': {'label': 'Birthday', 'date': True},
'TEL': {'label': 'Phone'},
'EMAIL': {'label': 'E-mail'},
'ADR': {'label': 'Address', 'multipart': [
'PO Box', 'Room Number', 'House Number', 'City', 'Prefecture',
'Zip Code', 'Country']},
'URL': {'label': 'URL'},
'MEMO': {'label': 'Note'},
}
def build_code(data):
notation = []
name = data['N']
if not isinstance(name, six.text_type):
name = ','.join(name)
notation.append('N', name)
for prop in PROPERTIES:
value = data.get(prop['code'])
if not value:
continue
if prop['date']:
value = value.strftime('%Y%m%d')
elif prop['multipart']:
value = ','.join(value)

View File

@ -0,0 +1,8 @@
import string
import qrcode
qr = qrcode.QRCode()
qr.add_data(string.letters*13)
qr.make()
print(qr.version)

View File

@ -0,0 +1,556 @@
import re
import math
import six
from six.moves import xrange
from qrcode import base, exceptions
# QR encoding modes.
MODE_NUMBER = 1 << 0
MODE_ALPHA_NUM = 1 << 1
MODE_8BIT_BYTE = 1 << 2
MODE_KANJI = 1 << 3
# Encoding mode sizes.
MODE_SIZE_SMALL = {
MODE_NUMBER: 10,
MODE_ALPHA_NUM: 9,
MODE_8BIT_BYTE: 8,
MODE_KANJI: 8,
}
MODE_SIZE_MEDIUM = {
MODE_NUMBER: 12,
MODE_ALPHA_NUM: 11,
MODE_8BIT_BYTE: 16,
MODE_KANJI: 10,
}
MODE_SIZE_LARGE = {
MODE_NUMBER: 14,
MODE_ALPHA_NUM: 13,
MODE_8BIT_BYTE: 16,
MODE_KANJI: 12,
}
ALPHA_NUM = six.b('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:')
RE_ALPHA_NUM = re.compile(six.b('^[') + re.escape(ALPHA_NUM) + six.b(']*\Z'))
# The number of bits for numeric delimited data lengths.
NUMBER_LENGTH = {3: 10, 2: 7, 1: 4}
PATTERN_POSITION_TABLE = [
[],
[6, 18],
[6, 22],
[6, 26],
[6, 30],
[6, 34],
[6, 22, 38],
[6, 24, 42],
[6, 26, 46],
[6, 28, 50],
[6, 30, 54],
[6, 32, 58],
[6, 34, 62],
[6, 26, 46, 66],
[6, 26, 48, 70],
[6, 26, 50, 74],
[6, 30, 54, 78],
[6, 30, 56, 82],
[6, 30, 58, 86],
[6, 34, 62, 90],
[6, 28, 50, 72, 94],
[6, 26, 50, 74, 98],
[6, 30, 54, 78, 102],
[6, 28, 54, 80, 106],
[6, 32, 58, 84, 110],
[6, 30, 58, 86, 114],
[6, 34, 62, 90, 118],
[6, 26, 50, 74, 98, 122],
[6, 30, 54, 78, 102, 126],
[6, 26, 52, 78, 104, 130],
[6, 30, 56, 82, 108, 134],
[6, 34, 60, 86, 112, 138],
[6, 30, 58, 86, 114, 142],
[6, 34, 62, 90, 118, 146],
[6, 30, 54, 78, 102, 126, 150],
[6, 24, 50, 76, 102, 128, 154],
[6, 28, 54, 80, 106, 132, 158],
[6, 32, 58, 84, 110, 136, 162],
[6, 26, 54, 82, 110, 138, 166],
[6, 30, 58, 86, 114, 142, 170]
]
G15 = (
(1 << 10) | (1 << 8) | (1 << 5) | (1 << 4) | (1 << 2) | (1 << 1) |
(1 << 0))
G18 = (
(1 << 12) | (1 << 11) | (1 << 10) | (1 << 9) | (1 << 8) | (1 << 5) |
(1 << 2) | (1 << 0))
G15_MASK = (1 << 14) | (1 << 12) | (1 << 10) | (1 << 4) | (1 << 1)
PAD0 = 0xEC
PAD1 = 0x11
# Precompute bit count limits, indexed by error correction level and code size
_data_count = lambda block: block.data_count
BIT_LIMIT_TABLE = [
[0] + [8*sum(map(_data_count, base.rs_blocks(version, error_correction)))
for version in xrange(1, 41)]
for error_correction in xrange(4)
]
def BCH_type_info(data):
d = data << 10
while BCH_digit(d) - BCH_digit(G15) >= 0:
d ^= (G15 << (BCH_digit(d) - BCH_digit(G15)))
return ((data << 10) | d) ^ G15_MASK
def BCH_type_number(data):
d = data << 12
while BCH_digit(d) - BCH_digit(G18) >= 0:
d ^= (G18 << (BCH_digit(d) - BCH_digit(G18)))
return (data << 12) | d
def BCH_digit(data):
digit = 0
while data != 0:
digit += 1
data >>= 1
return digit
def pattern_position(version):
return PATTERN_POSITION_TABLE[version - 1]
def mask_func(pattern):
"""
Return the mask function for the given mask pattern.
"""
if pattern == 0: # 000
return lambda i, j: (i + j) % 2 == 0
if pattern == 1: # 001
return lambda i, j: i % 2 == 0
if pattern == 2: # 010
return lambda i, j: j % 3 == 0
if pattern == 3: # 011
return lambda i, j: (i + j) % 3 == 0
if pattern == 4: # 100
return lambda i, j: (math.floor(i / 2) + math.floor(j / 3)) % 2 == 0
if pattern == 5: # 101
return lambda i, j: (i * j) % 2 + (i * j) % 3 == 0
if pattern == 6: # 110
return lambda i, j: ((i * j) % 2 + (i * j) % 3) % 2 == 0
if pattern == 7: # 111
return lambda i, j: ((i * j) % 3 + (i + j) % 2) % 2 == 0
raise TypeError("Bad mask pattern: " + pattern) # pragma: no cover
def mode_sizes_for_version(version):
if version < 10:
return MODE_SIZE_SMALL
elif version < 27:
return MODE_SIZE_MEDIUM
else:
return MODE_SIZE_LARGE
def length_in_bits(mode, version):
if mode not in (
MODE_NUMBER, MODE_ALPHA_NUM, MODE_8BIT_BYTE, MODE_KANJI):
raise TypeError("Invalid mode (%s)" % mode) # pragma: no cover
if version < 1 or version > 40: # pragma: no cover
raise ValueError(
"Invalid version (was %s, expected 1 to 40)" % version)
return mode_sizes_for_version(version)[mode]
def lost_point(modules):
modules_count = len(modules)
lost_point = 0
lost_point = _lost_point_level1(modules, modules_count)
lost_point += _lost_point_level2(modules, modules_count)
lost_point += _lost_point_level3(modules, modules_count)
lost_point += _lost_point_level4(modules, modules_count)
return lost_point
def _lost_point_level1(modules, modules_count):
lost_point = 0
modules_range = xrange(modules_count)
row_range_first = (0, 1)
row_range_last = (-1, 0)
row_range_standard = (-1, 0, 1)
col_range_first = ((0, 1), (1,))
col_range_last = ((-1, 0), (-1,))
col_range_standard = ((-1, 0, 1), (-1, 1))
for row in modules_range:
if row == 0:
row_range = row_range_first
elif row == modules_count-1:
row_range = row_range_last
else:
row_range = row_range_standard
for col in modules_range:
sameCount = 0
dark = modules[row][col]
if col == 0:
col_range = col_range_first
elif col == modules_count-1:
col_range = col_range_last
else:
col_range = col_range_standard
for r in row_range:
row_offset = row + r
if r != 0:
col_idx = 0
else:
col_idx = 1
for c in col_range[col_idx]:
if dark == modules[row_offset][col + c]:
sameCount += 1
if sameCount > 5:
lost_point += (3 + sameCount - 5)
return lost_point
def _lost_point_level2(modules, modules_count):
lost_point = 0
modules_range = xrange(modules_count - 1)
for row in modules_range:
this_row = modules[row]
next_row = modules[row+1]
for col in modules_range:
count = 0
if this_row[col]:
count += 1
if next_row[col]:
count += 1
if this_row[col + 1]:
count += 1
if next_row[col + 1]:
count += 1
if count == 0 or count == 4:
lost_point += 3
return lost_point
def _lost_point_level3(modules, modules_count):
modules_range_short = xrange(modules_count-6)
lost_point = 0
for row in xrange(modules_count):
this_row = modules[row]
for col in modules_range_short:
if (this_row[col]
and not this_row[col + 1]
and this_row[col + 2]
and this_row[col + 3]
and this_row[col + 4]
and not this_row[col + 5]
and this_row[col + 6]):
lost_point += 40
for col in xrange(modules_count):
for row in modules_range_short:
if (modules[row][col]
and not modules[row + 1][col]
and modules[row + 2][col]
and modules[row + 3][col]
and modules[row + 4][col]
and not modules[row + 5][col]
and modules[row + 6][col]):
lost_point += 40
return lost_point
def _lost_point_level4(modules, modules_count):
modules_range = xrange(modules_count)
dark_count = 0
for row in modules_range:
this_row = modules[row]
for col in modules_range:
if this_row[col]:
dark_count += 1
ratio = abs(100 * dark_count / modules_count / modules_count - 50) / 5
return ratio * 10
def optimal_data_chunks(data, minimum=4):
"""
An iterator returning QRData chunks optimized to the data content.
:param minimum: The minimum number of bytes in a row to split as a chunk.
"""
data = to_bytestring(data)
re_repeat = (
six.b('{') + six.text_type(minimum).encode('ascii') + six.b(',}'))
num_pattern = re.compile(six.b('\d') + re_repeat)
num_bits = _optimal_split(data, num_pattern)
alpha_pattern = re.compile(
six.b('[') + re.escape(ALPHA_NUM) + six.b(']') + re_repeat)
for is_num, chunk in num_bits:
if is_num:
yield QRData(chunk, mode=MODE_NUMBER, check_data=False)
else:
for is_alpha, sub_chunk in _optimal_split(chunk, alpha_pattern):
if is_alpha:
mode = MODE_ALPHA_NUM
else:
mode = MODE_8BIT_BYTE
yield QRData(sub_chunk, mode=mode, check_data=False)
def _optimal_split(data, pattern):
while data:
match = re.search(pattern, data)
if not match:
break
start, end = match.start(), match.end()
if start:
yield False, data[:start]
yield True, data[start:end]
data = data[end:]
if data:
yield False, data
def to_bytestring(data):
"""
Convert data to a (utf-8 encoded) byte-string if it isn't a byte-string
already.
"""
if not isinstance(data, six.binary_type):
data = six.text_type(data).encode('utf-8')
return data
def optimal_mode(data):
"""
Calculate the optimal mode for this chunk of data.
"""
if data.isdigit():
return MODE_NUMBER
if RE_ALPHA_NUM.match(data):
return MODE_ALPHA_NUM
return MODE_8BIT_BYTE
class QRData:
"""
Data held in a QR compatible format.
Doesn't currently handle KANJI.
"""
def __init__(self, data, mode=None, check_data=True):
"""
If ``mode`` isn't provided, the most compact QR data type possible is
chosen.
"""
if check_data:
data = to_bytestring(data)
if mode is None:
self.mode = optimal_mode(data)
else:
self.mode = mode
if mode not in (MODE_NUMBER, MODE_ALPHA_NUM, MODE_8BIT_BYTE):
raise TypeError("Invalid mode (%s)" % mode) # pragma: no cover
if check_data and mode < optimal_mode(data): # pragma: no cover
raise ValueError(
"Provided data can not be represented in mode "
"{0}".format(mode))
self.data = data
def __len__(self):
return len(self.data)
def write(self, buffer):
if self.mode == MODE_NUMBER:
for i in xrange(0, len(self.data), 3):
chars = self.data[i:i + 3]
bit_length = NUMBER_LENGTH[len(chars)]
buffer.put(int(chars), bit_length)
elif self.mode == MODE_ALPHA_NUM:
for i in xrange(0, len(self.data), 2):
chars = self.data[i:i + 2]
if len(chars) > 1:
buffer.put(
ALPHA_NUM.find(chars[0]) * 45 +
ALPHA_NUM.find(chars[1]), 11)
else:
buffer.put(ALPHA_NUM.find(chars), 6)
else:
if six.PY3:
# Iterating a bytestring in Python 3 returns an integer,
# no need to ord().
data = self.data
else:
data = [ord(c) for c in self.data]
for c in data:
buffer.put(c, 8)
def __repr__(self):
return repr(self.data)
class BitBuffer:
def __init__(self):
self.buffer = []
self.length = 0
def __repr__(self):
return ".".join([str(n) for n in self.buffer])
def get(self, index):
buf_index = math.floor(index / 8)
return ((self.buffer[buf_index] >> (7 - index % 8)) & 1) == 1
def put(self, num, length):
for i in range(length):
self.put_bit(((num >> (length - i - 1)) & 1) == 1)
def __len__(self):
return self.length
def put_bit(self, bit):
buf_index = self.length // 8
if len(self.buffer) <= buf_index:
self.buffer.append(0)
if bit:
self.buffer[buf_index] |= (0x80 >> (self.length % 8))
self.length += 1
def create_bytes(buffer, rs_blocks):
offset = 0
maxDcCount = 0
maxEcCount = 0
dcdata = [0] * len(rs_blocks)
ecdata = [0] * len(rs_blocks)
for r in range(len(rs_blocks)):
dcCount = rs_blocks[r].data_count
ecCount = rs_blocks[r].total_count - dcCount
maxDcCount = max(maxDcCount, dcCount)
maxEcCount = max(maxEcCount, ecCount)
dcdata[r] = [0] * dcCount
for i in range(len(dcdata[r])):
dcdata[r][i] = 0xff & buffer.buffer[i + offset]
offset += dcCount
# Get error correction polynomial.
rsPoly = base.Polynomial([1], 0)
for i in range(ecCount):
rsPoly = rsPoly * base.Polynomial([1, base.gexp(i)], 0)
rawPoly = base.Polynomial(dcdata[r], len(rsPoly) - 1)
modPoly = rawPoly % rsPoly
ecdata[r] = [0] * (len(rsPoly) - 1)
for i in range(len(ecdata[r])):
modIndex = i + len(modPoly) - len(ecdata[r])
if (modIndex >= 0):
ecdata[r][i] = modPoly[modIndex]
else:
ecdata[r][i] = 0
totalCodeCount = 0
for rs_block in rs_blocks:
totalCodeCount += rs_block.total_count
data = [None] * totalCodeCount
index = 0
for i in range(maxDcCount):
for r in range(len(rs_blocks)):
if i < len(dcdata[r]):
data[index] = dcdata[r][i]
index += 1
for i in range(maxEcCount):
for r in range(len(rs_blocks)):
if i < len(ecdata[r]):
data[index] = ecdata[r][i]
index += 1
return data
def create_data(version, error_correction, data_list):
buffer = BitBuffer()
for data in data_list:
buffer.put(data.mode, 4)
buffer.put(len(data), length_in_bits(data.mode, version))
data.write(buffer)
# Calculate the maximum number of bits for the given version.
rs_blocks = base.rs_blocks(version, error_correction)
bit_limit = 0
for block in rs_blocks:
bit_limit += block.data_count * 8
if len(buffer) > bit_limit:
raise exceptions.DataOverflowError(
"Code length overflow. Data size (%s) > size available (%s)" %
(len(buffer), bit_limit))
# Terminate the bits (add up to four 0s).
for i in range(min(bit_limit - len(buffer), 4)):
buffer.put_bit(False)
# Delimit the string into 8-bit words, padding with 0s if necessary.
delimit = len(buffer) % 8
if delimit:
for i in range(8 - delimit):
buffer.put_bit(False)
# Add special alternating padding bitstrings until buffer is full.
bytes_to_fill = (bit_limit - len(buffer)) // 8
for i in range(bytes_to_fill):
if i % 2 == 0:
buffer.put(PAD0, 8)
else:
buffer.put(PAD1, 8)
return create_bytes(buffer, rs_blocks)

View File

@ -24,7 +24,8 @@ def create_and_init(db, step_begin, step_end):
`account_pwd` varchar(128) DEFAULT NULL, `account_pwd` varchar(128) DEFAULT NULL,
`account_status` int(11) DEFAULT 0, `account_status` int(11) DEFAULT 0,
`account_lock` int(11) DEFAULT 0, `account_lock` int(11) DEFAULT 0,
`account_desc` varchar(255) `account_desc` varchar(255),
`oath_secret` varchar(64),
);""".format(db.table_prefix, db.auto_increment)) );""".format(db.table_prefix, db.auto_increment))
_db_exec(db, step_begin, step_end, '创建表 auth', """CREATE TABLE `{}auth`( _db_exec(db, step_begin, step_end, '创建表 auth', """CREATE TABLE `{}auth`(
@ -53,7 +54,7 @@ PRIMARY KEY (`name` ASC)
_db_exec(db, step_begin, step_end, '创建表 group', """CREATE TABLE `{}group` ( _db_exec(db, step_begin, step_end, '创建表 group', """CREATE TABLE `{}group` (
`group_id` integer PRIMARY KEY {}, `group_id` integer PRIMARY KEY {},
`group_name` varchar(255) DEFAULT'' `group_name` varchar(255) DEFAULT ''
);""".format(db.table_prefix, db.auto_increment)) );""".format(db.table_prefix, db.auto_increment))
_db_exec(db, step_begin, step_end, '创建表 host_info', """CREATE TABLE `{}host_info`( _db_exec(db, step_begin, step_end, '创建表 host_info', """CREATE TABLE `{}host_info`(
@ -99,7 +100,7 @@ PRIMARY KEY (`name` ASC)
_db_exec(db, step_begin, step_end, _db_exec(db, step_begin, step_end,
'建立管理员账号', '建立管理员账号',
'INSERT INTO `{}account` VALUES (1, 100, "admin", "{}", 0, 0, "超级管理员");'.format(db.table_prefix, _admin_sec_password) 'INSERT INTO `{}account` VALUES (1, 100, "admin", "{}", 0, 0, "超级管理员", "");'.format(db.table_prefix, _admin_sec_password)
) )
_db_exec(db, step_begin, step_end, _db_exec(db, step_begin, step_end,

View File

@ -48,7 +48,7 @@ def export_database(db):
else: else:
return 'Unknown Database Type' return 'Unknown Database Type'
_fields = ['account_id', 'account_type', 'account_name', 'account_pwd', 'account_status', 'account_lock', 'account_desc'] _fields = ['account_id', 'account_type', 'account_name', 'account_pwd', 'account_status', 'account_lock', 'account_desc', 'oath_secret']
ret.append(_export_table(db, 'account', _fields)) ret.append(_export_table(db, 'account', _fields))
_fields = ['auth_id', 'account_name', 'host_id', 'host_auth_id'] _fields = ['auth_id', 'account_name', 'host_id', 'host_auth_id']
ret.append(_export_table(db, 'auth', _fields)) ret.append(_export_table(db, 'auth', _fields))
@ -66,101 +66,3 @@ def export_database(db):
ret.append(_export_table(db, 'log', _fields)) ret.append(_export_table(db, 'log', _fields))
return '\r\n'.join(ret) return '\r\n'.join(ret)
def create_and_init(db, step_begin, step_end):
try:
_db_exec(db, step_begin, step_end, '创建表 account', """CREATE TABLE `{}account` (
`account_id` integer PRIMARY KEY {},
`account_type` int(11) DEFAULT 0,
`account_name` varchar(32) DEFAULT NULL,
`account_pwd` varchar(128) DEFAULT NULL,
`account_status` int(11) DEFAULT 0,
`account_lock` int(11) DEFAULT 0,
`account_desc` varchar(255)
);""".format(db.table_prefix, db.auto_increment))
_db_exec(db, step_begin, step_end, '创建表 auth', """CREATE TABLE `{}auth`(
`auth_id` INTEGER PRIMARY KEY {},
`account_name` varchar(255),
`host_id` INTEGER,
`host_auth_id` int(11) NOT NULL
);""".format(db.table_prefix, db.auto_increment))
# 注意这个key表原名为cert考虑到其中存放的是ssh密钥对与证书无关因此改名为key
# 这也是升级到数据库版本5的标志
_db_exec(db, step_begin, step_end, '创建表 key', """CREATE TABLE `{}key` (
`cert_id` integer PRIMARY KEY {},
`cert_name` varchar(255),
`cert_pub` varchar(2048) DEFAULT '',
`cert_pri` varchar(4096) DEFAULT '',
`cert_desc` varchar(255)
);
""".format(db.table_prefix, db.auto_increment))
_db_exec(db, step_begin, step_end, '创建表 config', """CREATE TABLE `{}config` (
`name` varchar(128) NOT NULL,
`value` varchar(255),
PRIMARY KEY (`name` ASC)
);""".format(db.table_prefix))
_db_exec(db, step_begin, step_end, '创建表 group', """CREATE TABLE `{}group` (
`group_id` integer PRIMARY KEY {},
`group_name` varchar(255) DEFAULT''
);""".format(db.table_prefix, db.auto_increment))
_db_exec(db, step_begin, step_end, '创建表 host_info', """CREATE TABLE `{}host_info`(
`host_id` integer PRIMARY KEY {},
`group_id` int(11) DEFAULT 0,
`host_sys_type` int(11) DEFAULT 1,
`host_ip` varchar(32) DEFAULT '',
`host_port` int(11) DEFAULT 0,
`protocol` int(11) DEFAULT 0,
`host_lock` int(11) DEFAULT 0,
`host_desc` varchar(255) DEFAULT ''
);""".format(db.table_prefix, db.auto_increment))
_db_exec(db, step_begin, step_end, '创建表 auth_info', """CREATE TABLE `{}auth_info`(
`id` INTEGER PRIMARY KEY {},
`host_id` INTEGER,
`auth_mode` INTEGER,
`user_name` varchar(255),
`user_pswd` varchar(255),
`user_param` varchar(255),
`cert_id` INTEGER,
`encrypt` INTEGER,
`log_time` varchar(60)
);""".format(db.table_prefix, db.auto_increment))
_db_exec(db, step_begin, step_end, '创建表 key', """CREATE TABLE `{}log` (
`id` INTEGER PRIMARY KEY {},
`session_id` varchar(32),
`account_name` varchar(64),
`host_ip` varchar(32),
`host_port` INTEGER,
`sys_type` INTEGER DEFAULT 0,
`auth_type` INTEGER,
`protocol` INTEGER,
`user_name` varchar(64),
`ret_code` INTEGER,
`begin_time` INTEGER,
`end_time` INTEGER,
`log_time` varchar(64)
);""".format(db.table_prefix, db.auto_increment))
_admin_sec_password = sec_generate_password('admin')
_db_exec(db, step_begin, step_end,
'建立管理员账号',
'INSERT INTO `{}account` VALUES (1, 100, "admin", "{}", 0, 0, "超级管理员");'.format(db.table_prefix, _admin_sec_password)
)
_db_exec(db, step_begin, step_end,
'设定数据库版本',
'INSERT INTO `{}config` VALUES ("db_ver", "{}");'.format(db.table_prefix, db.DB_VERSION)
)
return True
except:
log.e('ERROR\n')
return False

View File

@ -526,3 +526,38 @@ class DatabaseUpgrade:
log.e('failed.\n') log.e('failed.\n')
self.step_end(_step, -1) self.step_end(_step, -1)
return False return False
def _upgrade_to_v6(self):
_step = self.step_begin('检查数据库版本v6...')
# 服务端升级到版本2.2.9.1时为增加双因子认证为account表增加oath_secret字段
# db_ret = self.db.is_field_exists('{}account'.format(self.db.table_prefix), 'oath_secret')
# if db_ret is None:
# self.step_end(_step, -1, '无法连接到数据库')
# return False
# if db_ret:
# self.step_end(_step, 0, '跳过 v5 到 v6 的升级操作')
# return True
self.step_end(_step, 0, '需要升级到v6')
try:
_step = self.step_begin(' - 在account表中加入oath_secret字段')
if not self.db.exec('ALTER TABLE {}account ADD oath_secret VARCHAR(64) DEFAULT ""'.format(self.db.table_prefix)):
self.step_end(_step, -1, '失败')
return False
_step = self.step_begin(' - 更新数据库版本号')
if not self.db.exec('UPDATE `{}config` SET `value`="6" WHERE `name`="db_ver";'.format(self.db.table_prefix)):
self.step_end(_step, -1, '无法更新数据库版本号')
return False
else:
self.step_end(_step, 0)
return True
except:
log.e('failed.\n')
self.step_end(_step, -1)
return False

View File

@ -19,7 +19,7 @@ __all__ = ['get_db', 'DbItem']
class TPDatabase: class TPDatabase:
# 注意,每次调整数据库结构,必须增加版本号,并且在升级接口中编写对应的升级操作 # 注意,每次调整数据库结构,必须增加版本号,并且在升级接口中编写对应的升级操作
DB_VERSION = 5 DB_VERSION = 6
DB_TYPE_UNKNOWN = 0 DB_TYPE_UNKNOWN = 0
DB_TYPE_SQLITE = 1 DB_TYPE_SQLITE = 1
@ -185,7 +185,30 @@ class TPDatabase:
if ret is None: if ret is None:
return None return None
if len(ret) == 0: if len(ret) == 0:
return False
else:
return True
else:
log.e('Unknown database type.\n')
return None
def is_field_exists(self, table_name, field_name):
if self.db_type == self.DB_TYPE_SQLITE:
ret = self.query('PRAGMA table_info(`{}`);'.format(table_name))
print(ret)
if ret is None:
return None return None
if len(ret) == 0:
return False
else:
return True
elif self.db_type == self.DB_TYPE_MYSQL:
ret = self.query('DESC `{}` `{}`;'.format(table_name, field_name))
print(ret)
if ret is None:
return None
if len(ret) == 0:
return False
else: else:
return True return True
else: else:
@ -312,8 +335,6 @@ class TPDatabase:
return False return False
def export_to_sql(self): def export_to_sql(self):
# TODO: not implement.
return export_database(self) return export_database(self)

View File

@ -0,0 +1,77 @@
# -*- coding: utf-8 -*-
import os
import io
import qrcode
import base64
import binascii
import hmac
import time
import hashlib
import struct
__all__ = ['gen_oath_secret', 'verify_oath_code', 'gen_oath_qrcode']
def gen_oath_secret():
return _convert_secret_to_base32(binascii.b2a_hex(os.urandom(16))).replace('=', '')
def _convert_secret_to_base32(secret):
return base64.b32encode(base64.b16decode(secret.upper())).decode()
def get_totp_token(secret, factor=None):
# 通过 secret 得到 原始密钥 key
# 需要对padding符进行处理
_len = len(secret)
_pad = 8 - (_len % 8)
if _pad > 0:
secret += '=' * _pad
key = base64.b32decode(secret)
if factor is None:
factor = int(time.time()) // 30 # input 为次数, 30为默认密码刷新间隔值
msg = struct.pack(">Q", factor)
# 然后使用 HMAC-SHA1算法计算hash
hsh = hmac.new(key, msg, hashlib.sha1).digest()
# 将hsh转换成数字(默认为6位)
i = hsh[-1] & 0xf # 以最后一个字节的后4个bits为数字作为接下来的索引
f = hsh[i:i + 4] # 以i为索引, 取hsh中的4个字节
n = struct.unpack('>I', f)[0] & 0x7fffffff # 将4个字节按big-endian转换为无符号整数, 转换时去掉最高位的符号位
# 等价于 n = ((f[0] & 0x7f) << 24) | ((f[1] & 0xff) << 16) | ((f[2] & 0xff) << 8) | (f[3] & 0xff)
# 将 n % 1000000 得到6位数字, 不足补零
r = '%06d' % (n % 1000000) # r 即为 生成的动态密码
return r
def verify_oath_code(secret, code):
# secret = '6OHEKKJPLMUBJ4EHCT5ZT5YLUQ'
cur_input = int(time.time()) // 30
window = 3
for i in range(cur_input - (window - 1) // 2, cur_input + window // 2 + 1): # [cur_input-(window-1)//2, cur_input + window//2]
if get_totp_token(secret, i) == code:
return True
return False
def gen_oath_qrcode(username, secret):
msg = 'otpauth://totp/{}?secret={}&issuer=teleport'.format(username, secret)
qr = qrcode.QRCode(
version=1,
error_correction=qrcode.constants.ERROR_CORRECT_L,
box_size=4,
border=4,
)
qr.add_data(msg)
qr.make(fit=True)
img = qr.make_image()
out = io.BytesIO()
img.save(out, "jpeg", quality=100)
return out.getvalue()

View File

@ -40,6 +40,10 @@ controllers = [
(r'/auth/get-captcha', auth.GetCaptchaHandler), (r'/auth/get-captcha', auth.GetCaptchaHandler),
(r'/auth/verify-captcha', auth.VerifyCaptchaHandler), (r'/auth/verify-captcha', auth.VerifyCaptchaHandler),
(r'/auth/modify-pwd', auth.ModifyPwd), (r'/auth/modify-pwd', auth.ModifyPwd),
(r'/auth/oath-verify', auth.OathVerifyHandler),
(r'/auth/oath-secret-qrcode', auth.OathSecretQrCodeHandler),
(r'/auth/oath-secret-reset', auth.OathSecretResetHandler),
(r'/auth/oath-update-secret', auth.OathUpdateSecretHandler),
(r'/group/list', group.GetListHandler), (r'/group/list', group.GetListHandler),
(r'/group/', group.IndexHandler), (r'/group/', group.IndexHandler),
@ -49,9 +53,10 @@ controllers = [
(r'/cert/', cert.IndexHandler), (r'/cert/', cert.IndexHandler),
(r'/cert', cert.IndexHandler), (r'/cert', cert.IndexHandler),
(r'/pwd', pwd.IndexHandler), # (r'/pwd', pwd.IndexHandler),
(r'/user', user.IndexHandler), (r'/user', user.IndexHandler),
(r'/user/list', user.GetListHandler), (r'/user/list', user.GetListHandler),
(r'/user/personal', user.PersonalHandler),
#(r"/log/replay/(.*)", tornado.web.StaticFileHandler, {"path": os.path.join(cfg.data_path, 'replay')}), #(r"/log/replay/(.*)", tornado.web.StaticFileHandler, {"path": os.path.join(cfg.data_path, 'replay')}),
(r"/log/replay/(.*)", record.ReplayStaticFileHandler, {"path": os.path.join(cfg.data_path, 'replay')}), (r"/log/replay/(.*)", record.ReplayStaticFileHandler, {"path": os.path.join(cfg.data_path, 'replay')}),
@ -120,6 +125,8 @@ controllers = [
(r'/uidesign', index.UIDesignHandler), (r'/uidesign', index.UIDesignHandler),
(r'/uidesign/without-sidebar', index.UIDesignWithoutSidebarHandler), (r'/uidesign/without-sidebar', index.UIDesignWithoutSidebarHandler),
(r'/uidesign/table', index.UIDesignTableHandler) (r'/uidesign/table', index.UIDesignTableHandler),
# (r'/test/oath-code', index.OathCodeHandler)
] ]

View File

@ -8,6 +8,7 @@ from eom_app.module import user
from eom_common.eomcore.logger import * from eom_common.eomcore.logger import *
from .base import TPBaseHandler, TPBaseUserAuthHandler, TPBaseJsonHandler, TPBaseUserAuthJsonHandler from .base import TPBaseHandler, TPBaseUserAuthHandler, TPBaseJsonHandler, TPBaseUserAuthJsonHandler
from eom_app.app.util import gen_captcha from eom_app.app.util import gen_captcha
from eom_app.app.oath import gen_oath_secret, gen_oath_qrcode, verify_oath_code
class LoginHandler(TPBaseHandler): class LoginHandler(TPBaseHandler):
@ -33,27 +34,40 @@ class LoginHandler(TPBaseHandler):
class VerifyUser(TPBaseJsonHandler): class VerifyUser(TPBaseJsonHandler):
def post(self): def post(self):
code = self.get_session('captcha') # code = self.get_session('captcha')
if code is None: # if code is None:
return self.write_json(-1, '验证码已失效') # return self.write_json(-1, '验证码已失效')
#
self.del_session('captcha') # self.del_session('captcha')
args = self.get_argument('args', None) args = self.get_argument('args', None)
if args is not None: if args is not None:
args = json.loads(args) args = json.loads(args)
captcha = args['captcha'] login_type = args['type'].strip()
username = args['username'] captcha = args['captcha'].strip()
userpwd = args['userpwd'] username = args['username'].strip()
password = args['password'].strip()
oath = args['oath'].strip()
remember = args['remember'] remember = args['remember']
else: else:
return self.write_json(-1, '参数错误') return self.write_json(-1, '参数错误')
if code.lower() != captcha.lower(): if login_type == 'password':
return self.write_json(-1, '验证码错误') oath = None
code = self.get_session('captcha')
if code is None:
return self.write_json(-1, '验证码已失效')
self.del_session('captcha')
if code.lower() != captcha.lower():
return self.write_json(-1, '验证码错误')
elif login_type == 'oath':
if len(oath) == 0:
return self.write_json(-1, '身份验证器动态验证码错误')
self.del_session('captcha')
try: try:
user_id, account_type, nickname, locked = user.verify_user(username, userpwd) user_id, account_type, nickname, locked = user.verify_user(username, password, oath)
if locked == 1: if locked == 1:
return self.write_json(-1, '账号被锁定,请联系管理员!') return self.write_json(-1, '账号被锁定,请联系管理员!')
if user_id == 0: if user_id == 0:
@ -81,7 +95,7 @@ class VerifyUser(TPBaseJsonHandler):
_user['type'] = account_type _user['type'] = account_type
if remember: if remember:
self.set_session('user', _user, 12*60*60) self.set_session('user', _user, 12 * 60 * 60)
else: else:
self.set_session('user', _user) self.set_session('user', _user)
return self.write_json(0) return self.write_json(0)
@ -150,3 +164,104 @@ class ModifyPwd(TPBaseUserAuthJsonHandler):
except: except:
log.e('modify password failed.') log.e('modify password failed.')
return self.write_json(-4, '发生异常') return self.write_json(-4, '发生异常')
class OathVerifyHandler(TPBaseUserAuthJsonHandler):
def post(self):
args = self.get_argument('args', None)
if args is not None:
try:
args = json.loads(args)
code = args['code']
except:
return self.write_json(-2, '参数错误')
else:
return self.write_json(-1, '参数错误')
# secret = self.get_session('tmp_oath_secret', None)
# if secret is None:
# return self.write_json(-1, '内部错误!')
# self.del_session('tmp_oath_secret')
user_info = self.get_current_user()
if not user.verify_oath(user_info['id'], code):
return self.write_json(-3, '验证失败!')
else:
return self.write_json(0)
class OathSecretQrCodeHandler(TPBaseUserAuthJsonHandler):
def get(self):
secret = self.get_session('tmp_oath_secret', None)
print('tmp-oath-secret:', secret)
user_info = self.get_current_user()
img_data = gen_oath_qrcode(user_info['name'], secret)
# secret = '6OHEKKJPLMUBJ4EHCT5ZT5YLUQ'
#
# print('TOPT should be:', get_totp_token(secret))
# # cur_input = int(time.time()) // 30
# # print('cur-input', cur_input, int(time.time()))
# # window = 10
# # for i in range(cur_input - (window - 1) // 2, cur_input + window // 2 + 1): # [cur_input-(window-1)//2, cur_input + window//2]
# # print(get_totp_token(secret, i))
#
# msg = 'otpauth://totp/Admin?secret={}&issuer=teleport'.format(secret)
# qr = qrcode.QRCode(
# version=1,
# error_correction=qrcode.constants.ERROR_CORRECT_L,
# box_size=4,
# border=4,
# )
# qr.add_data(msg)
# qr.make(fit=True)
# img = qr.make_image()
#
# # img = qrcode.make(msg)
# out = io.BytesIO()
# img.save(out, "jpeg", quality=100)
# # web.header('Content-Type','image/jpeg')
# # img.save('test.png')
self.set_header('Content-Type', 'image/jpeg')
self.write(img_data)
class OathSecretResetHandler(TPBaseUserAuthJsonHandler):
def post(self):
oath_secret = gen_oath_secret()
self.set_session('tmp_oath_secret', oath_secret)
return self.write_json(0, data={"tmp_oath_secret": oath_secret})
class OathUpdateSecretHandler(TPBaseUserAuthJsonHandler):
def post(self):
args = self.get_argument('args', None)
if args is not None:
try:
args = json.loads(args)
code = args['code']
except:
return self.write_json(-2, '参数错误')
else:
return self.write_json(-1, '参数错误')
secret = self.get_session('tmp_oath_secret', None)
if secret is None:
return self.write_json(-1, '内部错误!')
self.del_session('tmp_oath_secret')
if verify_oath_code(secret, code):
user_info = self.get_current_user()
try:
ret = user.update_oath_secret(user_info['id'], secret)
if 0 != ret:
return self.write_json(ret)
except:
log.e('update user oath-secret failed.')
return self.write_json(-2, '发生异常')
# self.set_session('oath_secret', secret)
return self.write_json(0)
else:
return self.write_json(-3, '验证失败!')

View File

@ -27,4 +27,11 @@ class UIDesignWithoutSidebarHandler(TPBaseHandler):
class UIDesignTableHandler(TPBaseHandler): class UIDesignTableHandler(TPBaseHandler):
def get(self): def get(self):
# from hashlib import sha1
# import hmac
# my_sign = hmac.new('key', 'msg', sha1).digest()
# # my_sign = base64.b64encode(my_sign)
# # print my_sign
self.render('uidesign/table.mako') self.render('uidesign/table.mako')

View File

@ -16,6 +16,12 @@ class IndexHandler(TPBaseAdminAuthHandler):
self.render('user/index.mako') self.render('user/index.mako')
class PersonalHandler(TPBaseAdminAuthHandler):
def get(self):
user_info = self.get_current_user()
self.render('user/personal.mako', user=user_info)
class AuthHandler(TPBaseAdminAuthHandler): class AuthHandler(TPBaseAdminAuthHandler):
def get(self, user_name): def get(self, user_name):
group_list = host.get_group_list() group_list = host.get_group_list()

View File

@ -6,20 +6,26 @@ from eom_app.app.configs import app_cfg
from eom_app.app.const import * from eom_app.app.const import *
from eom_app.app.db import get_db, DbItem from eom_app.app.db import get_db, DbItem
from eom_app.app.util import sec_generate_password, sec_verify_password from eom_app.app.util import sec_generate_password, sec_verify_password
from eom_app.app.oath import verify_oath_code
def verify_user(name, password): def verify_user(name, password, oath_code):
cfg = app_cfg() cfg = app_cfg()
if cfg.app_mode == APP_MODE_MAINTENANCE:
if name == 'admin' and password == 'admin':
return 1, 100, '系统管理员', 0
db = get_db() db = get_db()
sql = 'SELECT `account_id`, `account_type`, `account_name`, `account_pwd`, `account_lock` FROM `{}account` WHERE `account_name`="{}";'.format(db.table_prefix, name) sql = 'SELECT `account_id`, `account_type`, `account_desc`, `account_pwd`, `account_lock`, `oath_secret` FROM `{}account` WHERE `account_name`="{}";'.format(db.table_prefix, name)
db_ret = db.query(sql) db_ret = db.query(sql)
if db_ret is None: if db_ret is None:
# 特别地,如果无法取得数据库连接,有可能是新安装的系统,尚未建立数据库,此时应该处于维护模式 # 特别地,如果无法取得数据库连接,有可能是新安装的系统,尚未建立数据库,此时应该处于维护模式
# 因此可以特别地处理用户验证用户名admin密码admin可以登录为管理员 # 因此可以特别地处理用户验证用户名admin密码admin可以登录为管理员
if cfg.app_mode == APP_MODE_MAINTENANCE: if cfg.app_mode == APP_MODE_MAINTENANCE:
if name == 'admin' and password == 'admin': if name == 'admin' and password == 'admin':
return 1, 100, 'admin', 0 return 1, 100, '系统管理员', 0
return 0, 0, '', 0 return 0, 0, '', 0
if len(db_ret) != 1: if len(db_ret) != 1:
@ -27,8 +33,9 @@ def verify_user(name, password):
user_id = db_ret[0][0] user_id = db_ret[0][0]
account_type = db_ret[0][1] account_type = db_ret[0][1]
name = db_ret[0][2] desc = db_ret[0][2]
locked = db_ret[0][4] locked = db_ret[0][4]
oath_secret = db_ret[0][5]
if locked == 1: if locked == 1:
return 0, 0, '', locked return 0, 0, '', locked
@ -42,7 +49,27 @@ def verify_user(name, password):
sql = 'UPDATE `{}account` SET `account_pwd`="{}" WHERE `account_id`={}'.format(db.table_prefix, _new_sec_password, int(user_id)) sql = 'UPDATE `{}account` SET `account_pwd`="{}" WHERE `account_id`={}'.format(db.table_prefix, _new_sec_password, int(user_id))
db.exec(sql) db.exec(sql)
return user_id, account_type, name, locked if oath_code is not None:
if not verify_oath_code(oath_secret, oath_code):
return 0, 0, '', 0
return user_id, account_type, desc, locked
def verify_oath(user_id, oath_code):
db = get_db()
sql = 'SELECT `oath_secret` FROM `{}account` WHERE `account_id`={};'.format(db.table_prefix, user_id)
db_ret = db.query(sql)
if db_ret is None:
return False
if len(db_ret) != 1:
return False
oath_secret = db_ret[0][0]
return verify_oath_code(oath_secret, oath_code)
def modify_pwd(old_pwd, new_pwd, user_id): def modify_pwd(old_pwd, new_pwd, user_id):
@ -66,6 +93,16 @@ def modify_pwd(old_pwd, new_pwd, user_id):
return -102 return -102
def update_oath_secret(user_id, oath_secret):
db = get_db()
sql = 'UPDATE `{}account` SET `oath_secret`="{}" WHERE `account_id`={}'.format(db.table_prefix, oath_secret, int(user_id))
db_ret = db.exec(sql)
if db_ret:
return 0
else:
return -102
def get_user_list(with_admin=False): def get_user_list(with_admin=False):
db = get_db() db = get_db()
ret = list() ret = list()

View File

@ -1 +1 @@
@charset "utf-8";body{padding-top:70px;padding-bottom:24px;background-color:#ececed}#head nav.navbar{height:70px;line-height:70px;background-color:#333;color:#fff}#head .logo .desc{display:block;float:right;color:#ccc;margin-top:10px;font-size:18px}#foot nav.navbar{min-height:24px;height:24px;line-height:24px;background-color:#ddd;color:#fff;font-size:12px;border-top:1px solid #ccc}#foot nav.navbar .container{height:24px}#foot nav.navbar p{margin:0 auto;text-align:center;color:#333}#content{margin:10px 0 50px 0}.auth-box{margin-top:30px;min-height:120px;border:1px solid #ccc;border-radius:8px;background-color:rgba(255,255,255,0.6)}.auth-box .header{min-height:50px;height:50px;border:none;box-shadow:none;border-bottom:1px solid #ccc}.auth-box .header .title{display:inline-block;float:left;margin-left:60px;height:24px;margin-top:25px;line-height:16px;font-size:20px;color:#999}.auth-box .header .selected{border-bottom:1px solid #69c;color:#555}.auth-box .header .title:hover{border-bottom:1px solid #999}.auth-box .inputarea{margin:30px}.auth-box .inputarea .input-group-addon{padding:0 5px 0 5px}.auth-box .inputarea p.input-addon-desc{text-align:right;padding:0 5px 0 5px;color:#999}#leftside{width:560px;height:560px;padding-top:60px;background:url(../img/login/side-001.jpg) 0 0 no-repeat}@media screen and (max-width:990px){#leftside{display:none}}#leftside h1{font-size:24px;color:#888}#leftside p{font-size:18px;color:#888;padding-left:24px}.auth-box .inputbox{margin-bottom:10px}.auth-box-lg .inputbox{margin-bottom:20px}.auth-box .op_box{display:block;padding:5px;border-radius:3px;text-align:center;margin:5px 20px 10px 20px}.auth-box .op_error{background:#fbb}.auth-box .op_wait{background:#ccc}.auth-box .quick-area{padding:80px 0 80px 0}.auth-box .quick-area .quick-disc{text-align:center;margin-bottom:20px}.auth-box .quick-area .quick-no{padding-top:80px;padding-bottom:100px}.auth-box .quick-area .quick-yes{text-align:center}.auth-box .quick-area .quick-yes .quick-account{display:inline-block;margin:auto;margin-bottom:20px}.auth-box .quick-area .quick-yes .quick-account:hover .quick-image{box-shadow:0 0 8px #00c2f6}.auth-box .quick-area .quick-yes .quick-image{display:block;width:82px;height:82px;line-height:80px;font-size:64px;margin:auto;border:1px solid #a4cdf6;box-shadow:0 0 6px #a7d1fb}.auth-box .quick-area .quick-yes .quick-name{display:block;margin-top:5px} @charset "utf-8";body{padding-top:70px;padding-bottom:24px;background-color:#ececed}#head nav.navbar{height:70px;line-height:70px;background-color:#333;color:#fff}#head .logo .desc{display:block;float:right;color:#ccc;margin-top:10px;font-size:18px}#foot nav.navbar{min-height:24px;height:24px;line-height:24px;background-color:#ddd;color:#fff;font-size:12px;border-top:1px solid #ccc}#foot nav.navbar .container{height:24px}#foot nav.navbar p{margin:0 auto;text-align:center;color:#333}#content{margin:10px 0 50px 0}.auth-box{margin-top:30px;min-height:120px;border:1px solid #ccc;border-radius:8px;background-color:rgba(255,255,255,0.6)}.auth-box .header{min-height:50px;height:50px;border:none;box-shadow:none;border-bottom:1px solid #ccc}.auth-box .header .title{display:inline-block;float:left;margin-left:60px;height:24px;margin-top:25px;line-height:16px;font-size:20px;color:#999}.auth-box .header .selected{border-bottom:2px solid #4882cc;color:#555}.auth-box .header .title:hover{border-bottom:2px solid #5396eb}.auth-box .inputarea{margin:30px}.auth-box .inputarea .input-group-addon{padding:0 5px 0 5px}.auth-box .inputarea p.input-addon-desc{text-align:right;padding:0 5px 0 5px;color:#999}#leftside{width:560px;height:560px;padding-top:60px;background:url(../img/login/side-001.jpg) 0 0 no-repeat}@media screen and (max-width:990px){#leftside{display:none}}#leftside h1{font-size:24px;color:#888}#leftside p{font-size:18px;color:#888;padding-left:24px}.auth-box .inputbox{margin-bottom:10px}.auth-box-lg .inputbox{margin-bottom:20px}.auth-box .op_box{display:block;padding:5px;border-radius:3px;text-align:center;margin:5px 20px 10px 20px}.auth-box .op_error{background:#fbb}.auth-box .op_wait{background:#ccc}.auth-box .quick-area{padding:80px 0 80px 0}.auth-box .quick-area .quick-disc{text-align:center;margin-bottom:20px}.auth-box .quick-area .quick-no{padding-top:80px;padding-bottom:100px}.auth-box .quick-area .quick-yes{text-align:center}.auth-box .quick-area .quick-yes .quick-account{display:inline-block;margin:auto;margin-bottom:20px}.auth-box .quick-area .quick-yes .quick-account:hover .quick-image{box-shadow:0 0 8px #00c2f6}.auth-box .quick-area .quick-yes .quick-image{display:block;width:82px;height:82px;line-height:80px;font-size:64px;margin:auto;border:1px solid #a4cdf6;box-shadow:0 0 6px #a7d1fb}.auth-box .quick-area .quick-yes .quick-name{display:block;margin-top:5px}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,151 +1,218 @@
"use strict"; "use strict";
ywl.on_init = function (cb_stack, cb_args) { var LOGIN_TYPE_PASSWORD = 1; // 使用用户名密码登录(额外需要验证码)
if (ywl.page_options.user_name.length > 0) { var LOGIN_TYPE_OATH = 2; // 使用用户名密码登录(额外需要身份验证器的动态验证码)
$('#username_account').val(ywl.page_options.user_name);
} ywl.on_init = function (cb_stack, cb_args) {
if (ywl.page_options.user_name.length > 0) {
$('#captcha_image').attr('src', '/auth/get-captcha?' + Math.random()); $('#username-account').val(ywl.page_options.user_name);
}
ywl.app = ywl.create_app();
cb_stack $('#captcha-image').attr('src', '/auth/get-captcha?' + Math.random());
.add(ywl.app.init)
.exec(); ywl.app = ywl.create_app();
}; cb_stack
.add(ywl.app.init)
ywl.create_app = function () { .exec();
var _app = {}; };
_app.dom_login_account = null; ywl.create_app = function () {
var _app = {};
_app.init = function (cb_stack, cb_args) {
_app.dom_login_account = $('#login-type-account'); _app.login_type = LOGIN_TYPE_PASSWORD;
$('#btn-login-account').click(_app.login_account); _app.dom = {
btn_login_type_password: $('#login-type-password'),
$('#captcha_image').click(function () { btn_login_type_oath: $('#login-type-oath'),
$(this).attr('src', '/auth/get-captcha?' + Math.random()); area_captcha: $('#login-area-captcha'),
$('#captcha').focus().val(''); area_oath: $('#login-area-oath'),
}); captcha_image: $('#captcha-image'),
$('#username_account').keydown(function (event) {
$('[data-toggle="popover"]').popover('hide'); input_username: $('#username-account'),
if (event.which === 13) { input_password: $('#password-account'),
$('#password_account').focus(); input_captcha: $('#captcha'),
} input_oath: $('#oath-code'),
});
$('#password_account').keydown(function (event) { remember: $('#remember-me'),
$('[data-toggle="popover"]').popover('hide'); btn_login: $('#btn-login'),
if (event.which === 13) {
$('#captcha').focus(); message: $('#message')
} };
});
$('#captcha').keydown(function (event) {
$('[data-toggle="popover"]').popover('hide'); // _app.dom_login_account = null;
if (event.which === 13) { // _app.dom_login_google = null;
_app.login_account();
} _app.init = function (cb_stack, cb_args) {
}); // _app.dom_login_account = $('#login-type-account');
// _app.dom_login_google = $('#login-type-google');
cb_stack.exec();
}; _app.dom.btn_login_type_password.click(function () {
_app.login_type = LOGIN_TYPE_PASSWORD;
_app.login_account = function () { _app.dom.btn_login_type_oath.removeClass('selected');
var str_username = ''; $(this).addClass('selected');
var str_password = ''; _app.dom.area_oath.slideUp(100);
var str_captcha = ''; _app.dom.area_captcha.slideDown(100);
var is_remember = false; });
_app.dom.btn_login_type_oath.click(function () {
var dom_username = $('#username_account'); _app.login_type = LOGIN_TYPE_OATH;
var dom_password = $('#password_account'); _app.dom.btn_login_type_password.removeClass('selected');
var dom_captcha = $('#captcha'); $(this).addClass('selected');
var dom_remember = $('#remember-me'); _app.dom.area_oath.slideDown(100);
_app.dom.area_captcha.slideUp(100);
str_username = dom_username.val(); });
str_password = dom_password.val();
str_captcha = dom_captcha.val();
is_remember = dom_remember.is(':checked'); _app.dom.btn_login.click(_app.login_account);
if (str_username.length === 0) { _app.dom.captcha_image.click(function () {
show_op_box('error', '缺少账号!'); $(this).attr('src', '/auth/get-captcha?' + Math.random());
dom_username.attr('data-content', "请填写您的账号!").popover('show'); _app.dom.input_captcha.focus().val('');
dom_username.focus(); });
return; _app.dom.input_username.keydown(function (event) {
} $('[data-toggle="popover"]').popover('hide');
if (event.which === 13) {
if (str_password.length === 0) { _app.dom.input_password.focus();
show_op_box('error', '缺少密码!'); }
dom_password.attr('data-content', "请填写密码!").popover('show'); });
dom_password.focus(); _app.dom.input_password.keydown(function (event) {
return; $('[data-toggle="popover"]').popover('hide');
} if (event.which === 13) {
if(_app.login_type === LOGIN_TYPE_PASSWORD)
if (str_captcha.length !== 4) { _app.dom.input_captcha.focus();
show_op_box('error', '验证码错误!'); else if(_app.login_type === LOGIN_TYPE_OATH)
dom_captcha.attr('data-content', "验证码为4位数字和字母的组合请重新填写").popover('show').focus(); _app.dom.input_oath.focus();
return; }
} });
_app.dom.input_captcha.keydown(function (event) {
$('#btn_login').attr('disabled', 'disabled'); $('[data-toggle="popover"]').popover('hide');
show_op_box('wait', '<i class="fa fa-circle-o-notch fa-spin"></i> 正在进行身份认证,请稍候...'); if (event.which === 13) {
_app.login_account();
// 先判断一下captcha是否正确如果不正确拒绝登录 }
ywl.ajax_post_json('/auth/verify-captcha', {captcha: str_captcha}, });
function (ret) {
if (ret.code === TPE_OK) { cb_stack.exec();
// 验证成功 };
hide_op_box();
show_op_box('wait', '<i class="fa fa-circle-o-notch fa-spin"></i> 正在登录TELEPORT请稍候...'); _app.login_account = function () {
_app.do_account_login(str_username, str_password, str_captcha, is_remember); // var str_username = '';
} // var str_password = '';
else { // var str_captcha = '';
hide_op_box(); // var str_gcode = '';
show_op_box('error', '验证码错误!'); // var is_remember = false;
$('#captcha_image').attr('src', '/auth/get-captcha?' + Math.random()); //
$('#captcha').focus().val(''); // var dom_username = $('#username-account');
} // var dom_password = $('#password-account');
// var dom_captcha = $('#captcha');
$('#btn_login').removeAttr('disabled'); // var dom_gcode = $('#oath-code');
}, // var dom_remember = $('#remember-me');
function () {
hide_op_box(); var str_username = _app.dom.input_username.val();
show_op_box('error', '很抱歉,无法连接服务器!请稍后再试一次!'); var str_password = _app.dom.input_password.val();
$('#btn_login').removeAttr('disabled'); var str_captcha = _app.dom.input_captcha.val();
} var str_oath = _app.dom.input_oath.val();
); var is_remember = _app.dom.remember.is(':checked');
};
if (str_username.length === 0) {
_app.do_account_login = function (username, userpwd, captcha, is_remember) { show_op_box('error', '缺少账号!');
ywl.ajax_post_json('/auth/verify-user', {username: username, userpwd: userpwd, captcha: captcha, remember: is_remember}, _app.dom.input_username.attr('data-content', "请填写您的账号!").popover('show');
function (ret) { _app.dom.input_username.focus();
if (ret.code === TPE_OK) { return;
window.location.href = ywl.page_options.ref; }
} else {
hide_op_box(); if (str_password.length === 0) {
show_op_box('error', '无法登录TELEPORT' + ret.message); show_op_box('error', '缺少密码!');
console.log(ret); _app.dom.input_password.attr('data-content', "请填写密码!").popover('show');
} _app.dom.input_password.focus();
return;
$('#btn_login').removeAttr('disabled'); }
},
function () { if (_app.login_type === 'account') {
hide_op_box(); if (str_captcha.length !== 4) {
show_op_box('error', '很抱歉,无法连接服务器!请稍后再试一次!'); show_op_box('error', '验证码错误!');
$('#btn_login').removeAttr('disabled'); _app.dom.input_captcha.attr('data-content', "验证码为4位数字和字母的组合请重新填写").popover('show').focus();
} return;
); }
}; } else if (_app.login_type === 'google') {
if (str_oath.length !== 6) {
return _app; show_op_box('error', '身份验证器动态验证码错误!');
}; _app.dom.input_oath.attr('data-content', "身份验证器动态验证码为6位数字请重新填写").popover('show').focus();
return;
function hide_op_box() { }
$('#login_message').hide(); }
}
_app.dom.btn_login.attr('disabled', 'disabled');
function show_op_box(op_type, op_msg) { _app.show_op_box('wait', '<i class="fa fa-circle-o-notch fa-spin"></i> 正在进行身份认证,请稍候...');
var obj_box = $('#login_message');
// 先判断一下captcha是否正确如果不正确拒绝登录
obj_box.html(op_msg); if (_app.login_type === LOGIN_TYPE_PASSWORD) {
obj_box.removeClass().addClass('op_box op_' + op_type); ywl.ajax_post_json('/auth/verify-captcha', {captcha: str_captcha},
obj_box.show(); function (ret) {
} if (ret.code === TPE_OK) {
// 验证成功
_app.hide_op_box();
_app.show_op_box('wait', '<i class="fa fa-circle-o-notch fa-spin"></i> 正在登录TELEPORT请稍候...');
_app.do_account_login(str_username, str_password, str_captcha, str_oath, is_remember);
}
else {
_app.hide_op_box();
_app.show_op_box('error', '验证码错误!');
_app.dom.captcha_image.attr('src', '/auth/get-captcha?' + Math.random());
_app.dom.input_captcha.focus().val('');
}
_app.dom.btn_login.removeAttr('disabled');
},
function () {
_app.hide_op_box();
_app.show_op_box('error', '很抱歉,无法连接服务器!请稍后再试一次!');
_app.dom.btn_login.removeAttr('disabled');
}
);
} else {
_app.do_account_login(str_username, str_password, str_captcha, str_oath, is_remember);
}
};
_app.do_account_login = function (username, password, captcha, oath, is_remember) {
var login_type = '';
if(_app.login_type === LOGIN_TYPE_PASSWORD) {
login_type = 'password';
} else if(_app.login_type === LOGIN_TYPE_OATH) {
login_type = 'oath';
}
var args = {type:login_type, username: username, password: password, captcha: captcha, oath: oath, remember: is_remember};
console.log(args);
ywl.ajax_post_json('/auth/verify-user', args,
function (ret) {
if (ret.code === TPE_OK) {
window.location.href = ywl.page_options.ref;
} else {
_app.hide_op_box();
_app.show_op_box('error', '无法登录TELEPORT' + ret.message);
console.log(ret);
}
_app.dom.btn_login.removeAttr('disabled');
},
function () {
_app.hide_op_box();
_app.show_op_box('error', '很抱歉,无法连接服务器!请稍后再试一次!');
_app.dom.btn_login.removeAttr('disabled');
}
);
};
_app.hide_op_box = function() {
_app.dom.message.hide();
};
_app.show_op_box = function(op_type, op_msg) {
_app.dom.message.html(op_msg);
_app.dom.message.removeClass().addClass('op_box op_' + op_type);
_app.dom.message.show();
};
return _app;
};

View File

@ -0,0 +1,145 @@
"use strict";
ywl.on_init = function (cb_stack, cb_args) {
ywl.dom = {
btn_reset_oath_code: $('#btn-reset-oath-code'),
btn_verify_oath_code: $('#btn-verify-oath-code'),
btn_verify_oath_code_and_save: $('#btn-verify-oath-and-save'),
btn_modify_password: $('#btn-modify-password'),
btn_toggle_oath_download: $('#toggle-oath-download'),
oath_app_download_box: $('#oath-app-download-box'),
input_current_password: $('#current-password'),
input_new_password: $('#new-password-1'),
input_new_password_confirm: $('#new-password-2'),
input_oath_code: $('#oath-code'),
input_oath_code_verify: $('#oath-code-verify'),
dlg_reset_oath_code: $('#dialog-reset-oath-code'),
oath_secret_image: $('#oath-secret-qrcode'),
tmp_oath_secret: $('#tmp-oath-secret'),
};
// ywl.dom.tmp_oath_secret.text(ywl.page_options.tmp_oath_secret);
ywl.clear_password_input = function () {
ywl.dom.input_current_password.val('');
ywl.dom.input_new_password.val('');
ywl.dom.input_new_password_confirm.val('');
};
ywl.dom.btn_modify_password.click(function () {
var old_pwd = ywl.dom.input_current_password.val();
var new_pwd_1 = ywl.dom.input_new_password.val();
var new_pwd_2 = ywl.dom.input_new_password_confirm.val();
if (old_pwd.length === 0) {
ywl.notify_error('请输入当前密码!');
ywl.dom.input_current_password.focus();
return;
}
if (new_pwd_1.length === 0) {
ywl.notify_error('请设置新密码!');
ywl.dom.input_new_password.focus();
return;
}
if (new_pwd_1 !== new_pwd_2) {
ywl.notify_error('两次密码输入不一致!');
ywl.dom.input_new_password_confirm.focus();
return;
}
ywl.ajax_post_json('/auth/modify-pwd', {o_pwd: old_pwd, n_pwd: new_pwd_1, callback: 1},
function (ret) {
if (ret.code === TPE_OK) {
ywl.notify_success('密码修改成功!');
ywl.clear_password_input();
} else if (ret.code === -101) {
ywl.notify_error('密码错误!');
} else {
ywl.notify_error('密码修改失败:' + ret.message);
}
},
function () {
ywl.notify_error('密码修改失败!');
}
);
});
ywl.dom.btn_toggle_oath_download.click(function () {
if (ywl.dom.oath_app_download_box.is(':hidden')) {
ywl.dom.oath_app_download_box.slideDown('fast', function () {
ywl.dom.btn_toggle_oath_download.html('收起 <i class="fa fa-angle-up"></i>');
});
} else {
ywl.dom.oath_app_download_box.slideUp('fast', function () {
ywl.dom.btn_toggle_oath_download.html('显示下载地址 <i class="fa fa-angle-down"></i>');
});
}
});
ywl.dom.btn_verify_oath_code.click(function () {
var code = ywl.dom.input_oath_code.val().trim();
if (code.length !== 6) {
ywl.notify_error('动态验证码错误应该是6位数字');
ywl.dom.input_oath_code_verify.focus();
return;
}
ywl.ajax_post_json('/auth/oath-verify', {code: code},
function (ret) {
if (ret.code === TPE_OK) {
ywl.notify_success('动态验证码验证成功!');
} else if (ret.code === -3) {
ywl.notify_error('动态验证码验证失败!');
} else {
ywl.notify_error('发生内部错误!' + ret.code + ret.message);
}
},
function () {
ywl.notify_error('网路故障,无法连接到服务器!');
}
);
});
ywl.dom.btn_reset_oath_code.click(function () {
ywl.ajax_post_json('/auth/oath-secret-reset', {},
function (ret) {
if (ret.code === TPE_OK) {
ywl.dom.oath_secret_image.attr('src', '/auth/oath-secret-qrcode?' + Math.random());
ywl.dom.tmp_oath_secret.text(ret.data.tmp_oath_secret);
ywl.dom.dlg_reset_oath_code.modal({backdrop: 'static'});
} else {
ywl.notify_error('发生内部错误!');
}
},
function () {
ywl.notify_error('网路故障,无法连接到服务器!');
}
);
});
ywl.dom.btn_verify_oath_code_and_save.click(function () {
var code = ywl.dom.input_oath_code_verify.val().trim();
if (code.length !== 6) {
ywl.notify_error('动态验证码错误应该是6位数字');
ywl.dom.input_oath_code_verify.focus();
return;
}
ywl.ajax_post_json('/auth/oath-update-secret', {code: code},
function (ret) {
if (ret.code === TPE_OK) {
ywl.notify_success('身份验证器绑定成功!您可以用此身份验证器登录系统了!');
ywl.dom.dlg_reset_oath_code.modal('hide');
} else {
ywl.notify_error('发生内部错误!');
}
},
function () {
ywl.notify_error('网路故障,无法连接到服务器!');
}
);
});
};

View File

@ -5,515 +5,535 @@
@font-family-mono: Consolas, Lucida Console, Monaco, Courier, 'Courier New', monospace; @font-family-mono: Consolas, Lucida Console, Monaco, Courier, 'Courier New', monospace;
body { body {
font-family: @font-family-normal; font-family: @font-family-normal;
//font-family: "微软雅黑", "Microsoft YaHei", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; //font-family: "微软雅黑", "Microsoft YaHei", "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
font-size: 13px; font-size: 13px;
background-color: @page-bg; background-color: @page-bg;
color: @page-color; color: @page-color;
} }
html, body { html, body {
height: 100%; height: 100%;
} }
#page-container { #page-container {
min-width: 1260px; min-width: 1260px;
} }
a { a {
text-decoration: none; text-decoration: none;
} }
a:link { a:link {
text-decoration: none; text-decoration: none;
} }
a:hover { a:hover {
text-decoration: none; text-decoration: none;
} }
a:active { a:active {
text-decoration: none; text-decoration: none;
} }
a:visited { a:visited {
text-decoration: none; text-decoration: none;
} }
select { select {
outline: none; outline: none;
} }
label { label {
font-weight: normal; font-weight: normal;
} }
.clear-float { .clear-float {
clear: both; clear: both;
} }
.bigger { .bigger {
font-size: 120%; font-size: 120%;
} }
.normal-text { .normal-text {
font-size: 13px; font-size: 13px;
color: @page-color; color: @page-color;
} }
.mono { .mono {
font-family:@font-family-mono; font-family: @font-family-mono;
} }
hr.hr-sm { hr.hr-sm {
margin-top: 5px; margin-top: 5px;
margin-bottom: 5px; margin-bottom: 5px;
} }
//============================================== //==============================================
// 重载bootstrap的样式 // 重载bootstrap的样式
//============================================== //==============================================
.btn-group-sm > .btn, .btn-sm { .btn-group-sm > .btn, .btn-sm {
padding: 2px 5px; padding: 2px 5px;
//margin-bottom: 2px; //margin-bottom: 2px;
} }
.btn.btn-sm { .btn.btn-sm {
padding: 3px 8px; padding: 3px 8px;
} }
.btn.btn-icon { .btn.btn-icon {
padding: 3px 6px; padding: 3px 6px;
&.btn-sm { &.btn-sm {
//padding:1px 3px; //padding:1px 3px;
padding: 0; padding: 0;
font-size: 14px; font-size: 14px;
height: 24px; height: 24px;
width: 24px; width: 24px;
line-height: 24px; line-height: 24px;
border-radius: 0; border-radius: 0;
} }
} }
.form-group-sm .input-group .input-group-btn > .btn { .form-group-sm .input-group .input-group-btn > .btn {
height: 30px; height: 30px;
padding: 0 8px; padding: 0 8px;
} }
.pop-menu-backdrop { .pop-menu-backdrop {
position: fixed; position: fixed;
top: 0; top: 0;
right: 0; right: 0;
bottom: 0; bottom: 0;
left: 0; left: 0;
z-index: 1040; z-index: 1040;
} }
//.modal { //.modal {
.form-group { .form-group {
margin-bottom: 5px; margin-bottom: 5px;
} }
//} //}
.badge { .badge {
display: inline-block; display: inline-block;
min-width: 8px; min-width: 8px;
padding: 5px 10px; padding: 5px 10px;
border-radius: 10px; border-radius: 10px;
text-align: center; text-align: center;
white-space: nowrap; white-space: nowrap;
vertical-align: middle; vertical-align: middle;
font-size: 13px; font-size: 13px;
font-weight: 400; font-weight: 400;
line-height: 1em; line-height: 1em;
background-color: @color-bg-default; background-color: @color-bg-default;
color: @color-text-on-dark-bg; color: @color-text-on-dark-bg;
text-shadow: 1px 1px 0 @color-text-shadow-on-dark-bg; text-shadow: 1px 1px 0 @color-text-shadow-on-dark-bg;
&.badge-plain { &.badge-plain {
text-shadow: none; text-shadow: none;
} }
&.badge-sm { &.badge-sm {
font-size: 11px; font-size: 11px;
padding: 3px 6px; padding: 3px 6px;
margin-top: 0; margin-top: 0;
border-radius: 8px; border-radius: 8px;
text-shadow: none; text-shadow: none;
} }
&.badge-sup { &.badge-sup {
margin-left: -6px; margin-left: -6px;
margin-top: -16px; margin-top: -16px;
} }
&.badge-ignore { &.badge-ignore {
background-color: @color-bg-ignore; background-color: @color-bg-ignore;
color: @color-text-ignore; color: @color-text-ignore;
text-shadow: none; text-shadow: none;
} }
&.badge-info { &.badge-info {
background-color: @color-bg-info; background-color: @color-bg-info;
} }
&.badge-primary { &.badge-primary {
background-color: @color-bg-primary; background-color: @color-bg-primary;
} }
&.badge-success { &.badge-success {
background-color: @color-bg-success; background-color: @color-bg-success;
} }
&.badge-warning { &.badge-warning {
background-color: @color-bg-warning; background-color: @color-bg-warning;
} }
&.badge-danger { &.badge-danger {
background-color: @color-bg-danger; background-color: @color-bg-danger;
} }
} }
.label { .label {
display: inline-block; display: inline-block;
min-width: 8px; min-width: 8px;
padding: 5px 10px; padding: 5px 10px;
border-radius: 5px; border-radius: 5px;
text-align: center; text-align: center;
white-space: nowrap; white-space: nowrap;
vertical-align: middle; vertical-align: middle;
font-size: 13px; font-size: 13px;
font-weight: 400; font-weight: 400;
line-height: 1em; line-height: 1em;
background-color: @color-bg-default; background-color: @color-bg-default;
color: @color-text-on-dark-bg; color: @color-text-on-dark-bg;
text-shadow: 1px 1px 0 @color-text-shadow-on-dark-bg; text-shadow: 1px 1px 0 @color-text-shadow-on-dark-bg;
&.label-plain { &.label-plain {
text-shadow: none; text-shadow: none;
} }
&.label-sm { &.label-sm {
font-size: 11px; font-size: 11px;
padding: 3px 8px; padding: 3px 8px;
margin-top: 0; margin-top: 0;
border-radius: 5px; border-radius: 5px;
text-shadow: none; text-shadow: none;
} }
&.label-ignore { &.label-ignore {
background-color: @color-bg-ignore; background-color: @color-bg-ignore;
color: @color-text-ignore; color: @color-text-ignore;
text-shadow: none; text-shadow: none;
} }
&.label-info { &.label-info {
background-color: @color-bg-info; background-color: @color-bg-info;
} }
&.label-primary { &.label-primary {
background-color: @color-bg-primary; background-color: @color-bg-primary;
} }
&.label-success { &.label-success {
background-color: @color-bg-success; background-color: @color-bg-success;
} }
&.label-warning { &.label-warning {
background-color: @color-bg-warning; background-color: @color-bg-warning;
} }
&.label-danger { &.label-danger {
background-color: @color-bg-danger; background-color: @color-bg-danger;
} }
} }
// 表格页面中的一些小部件 // 表格页面中的一些小部件
.progress.progress-sm { .progress.progress-sm {
height: 18px; height: 18px;
margin-bottom: 2px; margin-bottom: 2px;
background-color: #aaa; background-color: #aaa;
&.button { &.button {
cursor: pointer; cursor: pointer;
} }
.progress-bar { .progress-bar {
display: block; display: block;
font-size: 11px; font-size: 11px;
float: none; float: none;
} }
} }
.alert-sm { .alert-sm {
padding: 5px; padding: 5px;
margin-bottom: 10px; margin-bottom: 10px;
} }
.modal-dialog-sm { .modal-dialog-sm {
.modal-header { .modal-header {
padding: 10px; padding: 10px;
} }
.modal-body { .modal-body {
padding: 10px; padding: 10px;
} }
.modal-footer { .modal-footer {
padding: 10px; padding: 10px;
} }
.form-horizontal .form-group { .form-horizontal .form-group {
margin-right: -5px; margin-right: -5px;
margin-left: -5px; margin-left: -5px;
} }
.col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11 { .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11 {
padding-right: 5px; padding-right: 5px;
padding-left: 5px; padding-left: 5px;
} }
} }
.btn-single-line { .btn-single-line {
white-space: nowrap; white-space: nowrap;
.btn { .btn {
&:first-child { &:first-child {
border-top-left-radius: 3px; border-top-left-radius: 3px;
border-bottom-left-radius: 3px; border-bottom-left-radius: 3px;
} }
&:last-child { &:last-child {
border-top-right-radius: 3px; border-top-right-radius: 3px;
border-bottom-right-radius: 3px; border-bottom-right-radius: 3px;
} }
} }
} }
.remote-action-group { .remote-action-group {
margin-bottom: 3px; margin-bottom: 3px;
height: 28px; height: 28px;
min-width: 390px; min-width: 390px;
ul { ul {
display: block; display: block;
height: 28px; height: 28px;
margin: 0; margin: 0;
padding: 0; padding: 0;
li { li {
float: left; float: left;
position: relative; position: relative;
display: block; display: block;
height: 28px; height: 28px;
padding: 4px 5px; padding: 4px 5px;
background-color: #eee; background-color: #eee;
border-top: 1px solid #ccc; border-top: 1px solid #ccc;
border-right: 1px solid #ccc; border-right: 1px solid #ccc;
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
&.remote-action-btn { &.remote-action-btn {
background: none; background: none;
padding: 0; padding: 0;
border: none; border: none;
} }
&.remote-action-input { &.remote-action-input {
background: none; background: none;
padding: 4px 0; padding: 4px 0;
select { select {
border: none; border: none;
} }
} }
&.remote-action-chk-protocol { &.remote-action-chk-protocol {
width: 86px; width: 86px;
} }
&.remote-action-username, &.remote-action-name, &.remote-action-protocol { &.remote-action-username, &.remote-action-name, &.remote-action-protocol {
width: 96px; width: 96px;
text-align: center; text-align: center;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
&.remote-action-username { &.remote-action-username {
font-size: 90%; font-size: 90%;
color: #999; color: #999;
} }
&.remote-action-name, &.remote-action-protocol, &.remote-action-chk-protocol { &.remote-action-name, &.remote-action-protocol, &.remote-action-chk-protocol {
color: #000; color: #000;
} }
&.remote-action-name, &.remote-action-chk-protocol { &.remote-action-name, &.remote-action-chk-protocol {
font-weight:bold; font-weight: bold;
} }
&.remote-action-password, &.remote-action-sshkey, &.remote-action-noauth { &.remote-action-password, &.remote-action-sshkey, &.remote-action-noauth {
text-align: center; text-align: center;
padding: 4px 8px; padding: 4px 8px;
width:45px; width: 45px;
} }
&.remote-action-password { &.remote-action-password {
background-color: #e3ffe3; background-color: #e3ffe3;
color: #999; color: #999;
} }
&.remote-action-sshkey { &.remote-action-sshkey {
background-color: #fbe9c8; background-color: #fbe9c8;
color: #666; color: #666;
} }
&.remote-action-noauth { &.remote-action-noauth {
background-color: #e0e0e0; background-color: #e0e0e0;
color: #666; color: #666;
} }
.btn { .btn {
line-height: 1.5; line-height: 1.5;
margin: 0; margin: 0;
padding: 4px 8px; padding: 4px 8px;
font-size: 12px; font-size: 12px;
border-radius: 0; border-radius: 0;
} }
label { label {
padding: 0; padding: 0;
display: block; display: block;
float: left; float: left;
margin-top: 1px; margin-top: 1px;
cursor: pointer; cursor: pointer;
} }
input[type=checkbox] { input[type=checkbox] {
display: block; display: block;
float: left; float: left;
margin: 3px 5px 0 0; margin: 3px 5px 0 0;
} }
select { select {
margin-top: -3px; margin-top: -3px;
} }
&:first-child { &:first-child {
border-left: 1px solid #ccc; border-left: 1px solid #ccc;
border-top-left-radius: 4px; border-top-left-radius: 4px;
border-bottom-left-radius: 4px; border-bottom-left-radius: 4px;
.btn { .btn {
border-top-left-radius: 4px; border-top-left-radius: 4px;
border-bottom-left-radius: 4px; border-bottom-left-radius: 4px;
} }
} }
&:last-child { &:last-child {
border-top-right-radius: 4px; border-top-right-radius: 4px;
border-bottom-right-radius: 4px; border-bottom-right-radius: 4px;
.btn { .btn {
border-top-right-radius: 4px; border-top-right-radius: 4px;
border-bottom-right-radius: 4px; border-bottom-right-radius: 4px;
} }
} }
} }
} }
} }
hr.small { hr.small {
margin: 5px 0; margin: 5px 0;
} }
.dlg-protocol-group { .dlg-protocol-group {
margin-bottom: 3px; margin-bottom: 3px;
ul { ul {
display: block; display: block;
height: 28px; height: 28px;
margin: 0; margin: 0;
padding: 0; padding: 0;
li { li {
float: left; float: left;
position: relative; position: relative;
display: block; display: block;
height: 28px; height: 28px;
padding: 4px 5px; padding: 4px 5px;
background-color: #eee; background-color: #eee;
border-top: 1px solid #ccc; border-top: 1px solid #ccc;
border-right: 1px solid #ccc; border-right: 1px solid #ccc;
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
&.item-name { &.item-name {
width: 120px; width: 120px;
//font-size: 90%; //font-size: 90%;
//color: #999; //color: #999;
//text-align: center; //text-align: center;
//white-space: nowrap; //white-space: nowrap;
//overflow: hidden; //overflow: hidden;
//text-overflow: ellipsis; //text-overflow: ellipsis;
} }
&.item-btn { &.item-btn {
background: none; background: none;
padding: 0; padding: 0;
border: none; border: none;
} }
&.item-input { &.item-input {
background: none; background: none;
border: none; border: none;
padding: 0; padding: 0;
} }
.form-control { .form-control {
line-height: 1.5; line-height: 1.5;
margin: 0; margin: 0;
padding: 4px 5px; padding: 4px 5px;
font-size: 12px; font-size: 12px;
height: 28px; height: 28px;
border-radius: 0; border-radius: 0;
border-left: none; border-left: none;
width: 100px; width: 100px;
} }
label { label {
padding: 0; padding: 0;
display: block; display: block;
float: left; float: left;
margin-top: 1px; margin-top: 1px;
cursor: pointer; cursor: pointer;
} }
input[type=checkbox] { input[type=checkbox] {
display: block; display: block;
float: left; float: left;
margin: 3px 5px 0 0; margin: 3px 5px 0 0;
} }
//select { //select {
// margin-top: -3px; // margin-top: -3px;
//} //}
&:first-child { &:first-child {
border-left: 1px solid #ccc; border-left: 1px solid #ccc;
border-top-left-radius: 4px; border-top-left-radius: 4px;
border-bottom-left-radius: 4px; border-bottom-left-radius: 4px;
.btn, .form-control { .btn, .form-control {
border-top-left-radius: 4px; border-top-left-radius: 4px;
border-bottom-left-radius: 4px; border-bottom-left-radius: 4px;
} }
} }
&:last-child { &:last-child {
border-top-right-radius: 4px; border-top-right-radius: 4px;
border-bottom-right-radius: 4px; border-bottom-right-radius: 4px;
.btn, .form-control { .btn, .form-control {
border-top-right-radius: 4px; border-top-right-radius: 4px;
border-bottom-right-radius: 4px; border-bottom-right-radius: 4px;
} }
} }
} }
} }
} }
.col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 { .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 {
padding-right: 5px; padding-right: 5px;
padding-left: 5px; padding-left: 5px;
} }
.form-group-sm .form-control-static { .form-group-sm .form-control-static {
padding: 6px 0; padding: 6px 0;
} }
.input-group {
:-moz-placeholder { /* Mozilla Firefox 4 to 18 */
color: @color-placeholder;
}
::-moz-placeholder { /* Mozilla Firefox 19+ */
color: @color-placeholder;
}
input:-ms-input-placeholder,
textarea:-ms-input-placeholder {
color: @color-placeholder;
}
input::-webkit-input-placeholder, textarea::-webkit-input-placeholder {
color: @color-placeholder;
}
}

View File

@ -27,6 +27,7 @@
@color-text-on-dark-bg: #fff; @color-text-on-dark-bg: #fff;
@color-text-shadow-on-dark-bg: #525252; @color-text-shadow-on-dark-bg: #525252;
@color-placeholder: #d2d2d2;
//.text-warning { //.text-warning {
// color: @color-text-warning !important; // color: @color-text-warning !important;

View File

@ -1,246 +1,246 @@
@charset "utf-8"; @charset "utf-8";
body { body {
padding-top: 70px; padding-top: 70px;
padding-bottom: 24px; padding-bottom: 24px;
background-color: #ececed; background-color: #ececed;
} }
#head nav.navbar { #head nav.navbar {
height: 70px; height: 70px;
line-height: 70px; line-height: 70px;
background-color: #333; background-color: #333;
color: #fff; color: #fff;
} }
#head .logo .desc { #head .logo .desc {
display: block; display: block;
float: right; float: right;
color: #ccc; color: #ccc;
margin-top: 10px; margin-top: 10px;
font-size: 18px; font-size: 18px;
} }
#foot nav.navbar { #foot nav.navbar {
min-height: 24px; min-height: 24px;
height: 24px; height: 24px;
line-height: 24px; line-height: 24px;
background-color: #ddd; background-color: #ddd;
color: #fff; color: #fff;
font-size: 12px; font-size: 12px;
border-top: 1px solid #ccc; border-top: 1px solid #ccc;
} }
#foot nav.navbar .container { #foot nav.navbar .container {
height: 24px; height: 24px;
} }
#foot nav.navbar p { #foot nav.navbar p {
margin: 0 auto; margin: 0 auto;
text-align: center; text-align: center;
color: #333; color: #333;
} }
#content { #content {
margin: 10px 0 50px 0; margin: 10px 0 50px 0;
} }
.auth-box { .auth-box {
margin-top: 30px; margin-top: 30px;
min-height: 120px; min-height: 120px;
border: 1px solid #ccc; border: 1px solid #ccc;
border-radius: 8px; border-radius: 8px;
background-color: rgba(255, 255, 255, 0.6); background-color: rgba(255, 255, 255, 0.6);
} }
.auth-box .header { .auth-box .header {
min-height: 50px; min-height: 50px;
height: 50px; height: 50px;
border: none; border: none;
box-shadow: none; box-shadow: none;
border-bottom: 1px solid #ccc; border-bottom: 1px solid #ccc;
} }
.auth-box .header .title { .auth-box .header .title {
display: inline-block; display: inline-block;
float: left; float: left;
margin-left: 60px; margin-left: 60px;
height: 24px; height: 24px;
margin-top: 25px; margin-top: 25px;
line-height: 16px; line-height: 16px;
font-size: 20px; font-size: 20px;
color: #999; color: #999;
} }
.auth-box .header .selected { .auth-box .header .selected {
border-bottom: 1px solid #6699cc; border-bottom: 2px solid #4882cc;
color: #555; color: #555;
} }
.auth-box .header .title:hover { .auth-box .header .title:hover {
border-bottom: 1px solid #999; border-bottom: 2px solid #5396eb;
} }
.auth-box .inputarea { .auth-box .inputarea {
margin: 30px; margin: 30px;
} }
.auth-box .inputarea .input-group-addon { .auth-box .inputarea .input-group-addon {
padding: 0 5px 0 5px; padding: 0 5px 0 5px;
} }
.auth-box .inputarea p.input-addon-desc { .auth-box .inputarea p.input-addon-desc {
text-align: right; text-align: right;
padding: 0 5px 0 5px; padding: 0 5px 0 5px;
color: #999; color: #999;
} }
/*.auth-box .inputbox { /*.auth-box .inputbox {
border:1px solid #6699cc; border:1px solid #6699cc;
border-radius:2px; border-radius:2px;
height:38px; height:38px;
margin-bottom:20px; margin-bottom:20px;
} }
.auth-box .inputbox .input { .auth-box .inputbox .input {
display: inline-block; display: inline-block;
outline:none; outline:none;
border-style:none; border-style:none;
position:relative; position:relative;
width:360px; width:360px;
padding:10px 10px 0 10px; padding:10px 10px 0 10px;
line-height: 18px; line-height: 18px;
font-family: Verdana, Tahoma, Ariall; font-family: Verdana, Tahoma, Ariall;
font-size:16px; font-size:16px;
} }
.auth-box .inputbox .clean { .auth-box .inputbox .clean {
display:inline-block; display:inline-block;
position:relative; position:relative;
top:5px; top:5px;
right:0px;height:22px;width:22px;background:url(../img/login/input_right_clean.png) 0 0 no-repeat; right:0px;height:22px;width:22px;background:url(../img/login/input_right_clean.png) 0 0 no-repeat;
} }
*/ */
#leftside { #leftside {
width: 560px; width: 560px;
height: 560px; height: 560px;
padding-top: 60px; padding-top: 60px;
background: url(../img/login/side-001.jpg) 0 0 no-repeat; background: url(../img/login/side-001.jpg) 0 0 no-repeat;
} }
@media screen and (max-width: 990px) { @media screen and (max-width: 990px) {
#leftside { #leftside {
display: none; display: none;
} }
} }
#leftside h1 { #leftside h1 {
font-size: 24px; font-size: 24px;
color: #888; color: #888;
} }
#leftside p { #leftside p {
font-size: 18px; font-size: 18px;
color: #888; color: #888;
padding-left: 24px; padding-left: 24px;
} }
.auth-box .inputbox { .auth-box .inputbox {
margin-bottom: 10px; margin-bottom: 10px;
} }
.auth-box-lg .inputbox { .auth-box-lg .inputbox {
margin-bottom: 20px; margin-bottom: 20px;
} }
/*.auth-box .inputbox .clean { /*.auth-box .inputbox .clean {
display: block; display: block;
height:22px;width:22px;background:url(../img/login/input_right_clean.png) 0 0 no-repeat; height:22px;width:22px;background:url(../img/login/input_right_clean.png) 0 0 no-repeat;
}*/ }*/
/*.auth-box .inputarea label { /*.auth-box .inputarea label {
font-size:12px; font-size:12px;
font-weight:400; font-weight:400;
color:#999; color:#999;
cursor:pointer; cursor:pointer;
}*/ }*/
.auth-box .op_box { .auth-box .op_box {
display: block; display: block;
padding: 5px; padding: 5px;
border-radius: 3px; border-radius: 3px;
text-align: center; text-align: center;
//margin-top: 5px; //margin-top: 5px;
margin: 5px 20px 10px 20px; margin: 5px 20px 10px 20px;
} }
.auth-box .op_error { .auth-box .op_error {
background: #fbb; background: #fbb;
} }
.auth-box .op_wait { .auth-box .op_wait {
background: #ccc; background: #ccc;
} }
.auth-box { .auth-box {
.quick-area { .quick-area {
padding: 80px 0 80px 0; padding: 80px 0 80px 0;
.quick-disc { .quick-disc {
//font-size:120%; //font-size:120%;
text-align: center; text-align: center;
//margin:auto; //margin:auto;
margin-bottom: 20px; margin-bottom: 20px;
} }
.quick-no { .quick-no {
padding-top: 80px; padding-top: 80px;
padding-bottom: 100px; padding-bottom: 100px;
} }
.quick-yes { .quick-yes {
text-align: center; text-align: center;
//margin: auto; //margin: auto;
.quick-account { .quick-account {
display: inline-block; display: inline-block;
margin: auto; margin: auto;
margin-bottom: 20px; margin-bottom: 20px;
//border:1px solid #888; //border:1px solid #888;
&:hover { &:hover {
.quick-image { .quick-image {
//background-color: #00bcee; //background-color: #00bcee;
box-shadow: 0 0 8px rgb(0, 194, 246); box-shadow: 0 0 8px rgb(0, 194, 246);
} }
} }
} }
.quick-image { .quick-image {
display: block; display: block;
width: 82px; width: 82px;
height: 82px; height: 82px;
line-height: 80px; line-height: 80px;
font-size: 64px; font-size: 64px;
margin: auto; margin: auto;
//border-radius: 5px; //border-radius: 5px;
//color:#fff; //color:#fff;
//background-color: #00acda; //background-color: #00acda;
//padding:3px; //padding:3px;
border: 1px solid #a4cdf6; border: 1px solid #a4cdf6;
box-shadow: 0 0 6px rgba(167, 209, 251, 1); box-shadow: 0 0 6px rgba(167, 209, 251, 1);
} }
.quick-name { .quick-name {
display: block; display: block;
margin-top: 5px; margin-top: 5px;
} }
} }
} }
} }

View File

@ -1,78 +1,89 @@
<%! <%!
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
page_title_ = '登录' page_title_ = '登录'
## page_menu_ = ['host'] ## page_menu_ = ['host']
## page_id_ = 'host' ## page_id_ = 'host'
%> %>
<%inherit file="page_base.mako"/> <%inherit file="page_base.mako"/>
<%block name="extend_js"> <%block name="extend_js">
<script type="text/javascript" src="${ static_url('js/ui/auth/login.js') }"></script> <script type="text/javascript" src="${ static_url('js/ui/auth/login.js') }"></script>
</%block> </%block>
<%block name="embed_js" > <%block name="embed_js" >
<script type="text/javascript"> <script type="text/javascript">
ywl.add_page_options(${ page_param }); ywl.add_page_options(${ page_param });
</script> </script>
</%block> </%block>
<div class="row"> <div class="row">
<div class="col-md-6"> <div class="col-md-6">
<div id="leftside"> <div id="leftside">
</div> </div>
</div> </div>
<div class="col-md-5"> <div class="col-md-5">
<div class="auth-box auth-box-lg"> <div class="auth-box auth-box-lg">
<div class="header"> <div class="header">
<div id="login-type-account" class="title selected">账号/密码 登录</div> <a id="login-type-password" class="title selected" href="javascript:;">账号/密码 登录</a>
</div> <a id="login-type-oath" class="title" href="javascript:;">身份验证器</a>
</div>
<div id="input-area-account" class="inputarea">
<div id="login_account" class="login-account"> <div class="inputarea">
<div class="inputbox"> <div id="login-area-account" class="login-account">
<div class="input-group input-group-lg"> <div class="inputbox">
<span class="input-group-addon"><i class="fa fa-user fa-fw"></i></span> <div class="input-group input-group-lg">
<input id="username_account" type="text" class="form-control" placeholder="账号:邮箱地址或手机号" data-toggle="popover" data-trigger="manual" data-placement="top"> <span class="input-group-addon"><i class="fa fa-user fa-fw"></i></span>
</div> <input id="username-account" type="text" class="form-control" placeholder="账号:邮箱地址或手机号" data-toggle="popover" data-trigger="manual" data-placement="top">
</div> </div>
</div>
<div class="inputbox">
<div class="input-group input-group-lg"> <div class="inputbox">
<span class="input-group-addon"><i class="fa fa-key fa-fw"></i></span> <div class="input-group input-group-lg">
<input id="password_account" type="password" class="form-control" placeholder="密码" data-toggle="popover" data-trigger="manual" data-placement="top"> <span class="input-group-addon"><i class="fa fa-key fa-fw"></i></span>
</div> <input id="password-account" type="password" class="form-control" placeholder="密码" data-toggle="popover" data-trigger="manual" data-placement="top">
</div> </div>
</div>
</div>
</div>
<div class="inputbox">
<div class="input-group input-group-lg"> <div id="login-area-captcha" class="inputbox">
<span class="input-group-addon"><i class="fa fa-check-square-o fa-fw"></i></span> <div class="input-group input-group-lg">
<input id="captcha" type="text" class="form-control" placeholder="验证码" <span class="input-group-addon"><i class="fa fa-check-square-o fa-fw"></i></span>
data-toggle="popover" data-trigger="manual" data-placement="top"> <input id="captcha" type="text" class="form-control" placeholder="验证码"
<span class="input-group-addon"><a href="javascript:;"><img id="captcha_image" src=""></a></span> data-toggle="popover" data-trigger="manual" data-placement="top">
</div> <span class="input-group-addon"><a href="javascript:;"><img id="captcha-image" src=""></a></span>
<p class="input-addon-desc">验证码,点击图片可更换</p> </div>
</div> <p class="input-addon-desc">验证码,点击图片可更换</p>
</div>
<div class="inputbox">
<div class="checkbox"> <div id="login-area-oath" style="display:none;">
<label><input id="remember-me" type="checkbox" value=""> 记住我12小时内无需重新登录。</label> <div class="inputbox">
</div> <div class="input-group input-group-lg">
</div> <span class="input-group-addon"><i class="fa fa-google fa-fw"></i></span>
<input id="oath-code" type="text" class="form-control" placeholder="谷歌身份验证器动态验证码">
<div class="inputbox"> </div>
<button id="btn-login-account" class="btn btn-primary btn-lg btn-block">登 录</button> </div>
</div> </div>
</div>
<div> <div class="inputbox">
<p id="login_message" class="op_box" style="display:none;"></p> <div class="checkbox">
</div> <label><input id="remember-me" type="checkbox" value=""> 记住我12小时内无需重新登录。</label>
</div>
</div> </div>
</div>
</div> <div class="inputbox">
<button id="btn-login" class="btn btn-primary btn-lg btn-block">登 录</button>
</div>
</div>
<div>
<p id="message" class="op_box" style="display:none;"></p>
</div>
</div>
</div>
</div>

View File

@ -101,18 +101,19 @@
<div class="dropdown"> <div class="dropdown">
<a class="title" href="#" id="user-profile" data-target="#" data-toggle="dropdown" role="button" <a class="title" href="#" id="user-profile" data-target="#" data-toggle="dropdown" role="button"
aria-haspopup="true" aria-expanded="false"> aria-haspopup="true" aria-expanded="false">
<span class="name">${ current_user['nick_name'] }</span> <span class="name">${ current_user['name'] }</span>
<span class="role"> <span class="role">
%if current_user['type'] == 100: %if current_user['type'] == 100:
平台管理员 系统管理员
%else: %else:
普通用户 运维人员
%endif %endif
<i class="fa fa-caret-right"></i></span> <i class="fa fa-caret-right"></i></span>
</a> </a>
<ul class="dropdown-menu dropdown-menu-right"> <ul class="dropdown-menu dropdown-menu-right">
<li><a href="/pwd" id="btn-logout">修改密码</a></li> ## <li><a href="/pwd" id="btn-logout">修改密码</a></li>
<li><a href="/auth/logout" id="btn-logout">安全退出</a></li> <li><a href="/user/personal">个人中心</a></li>
<li><a href="/auth/logout">安全退出</a></li>
</ul> </ul>
</div> </div>

View File

@ -93,7 +93,7 @@
ywl.ajax_post_json('/maintenance/rpc', {cmd: 'upgrade_db'}, ywl.ajax_post_json('/maintenance/rpc', {cmd: 'upgrade_db'},
function (ret) { function (ret) {
console.log('upgrade-db:', ret); console.log('upgrade-db:', ret);
if (ret.code == 0) { if (ret.code === 0) {
var cb_stack = CALLBACK_STACK.create(); var cb_stack = CALLBACK_STACK.create();
cb_stack cb_stack
@ -112,7 +112,7 @@
ywl.get_task_ret = function (cb_stack, cb_args) { ywl.get_task_ret = function (cb_stack, cb_args) {
var task_id = cb_args.task_id || 0; var task_id = cb_args.task_id || 0;
if (task_id == 0) { if (task_id === 0) {
console.log('task-id', task_id); console.log('task-id', task_id);
return; return;
} }
@ -120,7 +120,7 @@
ywl.ajax_post_json('/maintenance/rpc', {cmd: 'get_task_ret', 'tid': task_id}, ywl.ajax_post_json('/maintenance/rpc', {cmd: 'get_task_ret', 'tid': task_id},
function (ret) { function (ret) {
console.log('get_task_ret:', ret); console.log('get_task_ret:', ret);
if (ret.code == 0) { if (ret.code === 0) {
// show step progress. // show step progress.
var steps = ret.data.steps; var steps = ret.data.steps;
@ -130,14 +130,20 @@
var icon_class = ''; var icon_class = '';
var err_class = ''; var err_class = '';
for(var i = 0; i < steps.length; ++i) { for(var i = 0; i < steps.length; ++i) {
if(steps[i].stat == 0) if(steps[i].code !== 0) {
err_class = ' class="error"';
icon_class = 'fa-times-circle';
}
else {
err_class = '';
icon_class = 'fa-check'; icon_class = 'fa-check';
}
if(steps[i].stat === 0)
;//icon_class = 'fa-check';
else else
icon_class = 'fa-cog fa-spin'; icon_class = 'fa-cog fa-spin';
if(steps[i].code != 0)
err_class = ' class="error"';
else
err_class = '';
html.push('<p'); html.push('<p');
html.push(err_class); html.push(err_class);
html.push('><i class="fa '); html.push('><i class="fa ');

View File

@ -33,6 +33,10 @@
<div class="page-content"> <div class="page-content">
<div class="box">
<img src="/test/oath-code?">
</div>
<div class="box"> <div class="box">
<h1>这是一级标题This is H1.</h1> <h1>这是一级标题This is H1.</h1>
<h2>这是二级标题This is H2.</h2> <h2>这是二级标题This is H2.</h2>

View File

@ -0,0 +1,222 @@
<%!
page_title_ = '个人中心'
page_menu_ = ['personal']
page_id_ = 'personal'
%>
<%inherit file="../page_base.mako"/>
<%block name="extend_js">
<script type="text/javascript" src="${ static_url('js/ui/teleport.js') }"></script>
<script type="text/javascript" src="${ static_url('js/ui/user/personal.js') }"></script>
</%block>
<%block name="breadcrumb">
<ol class="breadcrumb">
<li><i class="fa fa-pencil-square-o fa-fw"></i> ${self.attr.page_title_}</li>
</ol>
</%block>
<%block name="extend_css">
<style type="text/css">
.table {
width: auto;
}
.table .key {
text-align: right;
}
.table .value {
text-align: left;
font-weight: bold;
}
.oath-code {
font-family: Consolas, Lucida Console, Monaco, Courier, 'Courier New', monospace;
font-size: 18px;
line-height: 26px;
font-weight: bold;
color: #559f47;
}
</style>
</%block>
## Begin Main Body.
<div class="page-content">
<div class="box">
<!-- Nav tabs -->
<ul class="nav nav-tabs">
<li class="active"><a href="#info" data-toggle="tab">个人信息</a></li>
<li><a href="#password" data-toggle="tab">修改密码</a></li>
<li><a href="#oath" data-toggle="tab">身份认证器</a></li>
</ul>
<!-- Tab panes -->
<div class="tab-content">
<div class="tab-pane active" id="info">
<table class="table">
<tr>
<td class="key">登录名:</td>
<td class="value">${user['name']}</td>
</tr>
<tr>
<td class="key">姓名:</td>
<td class="value">${user['nick_name']}</td>
</tr>
<tr>
<td class="key">邮箱:</td>
<td class="value">-</td>
</tr>
<tr>
<td class="key">手机:</td>
<td class="value">-</td>
</tr>
<tr>
<td class="key">注册时间:</td>
<td class="value">-</td>
</tr>
<tr>
<td class="key">上次登录时间:</td>
<td class="value">-</td>
</tr>
</table>
</div>
<div class="tab-pane" id="password">
<div class="input-group input-group-sm" style="margin-top: 10px">
<span class="input-group-addon" style="width:90px;">当前密码:</span>
<input type="password" class="form-control" id="current-password">
</div>
<div class="input-group input-group-sm" style="margin-top: 10px">
<span class="input-group-addon" style="width:90px;">新密码:</span>
<input type="password" class="form-control" id="new-password-1">
</div>
<div class="input-group input-group-sm" style="margin-top: 10px">
<span class="input-group-addon" style="width:90px;">重复新密码:</span>
<input type="password" class="form-control" id="new-password-2">
</div>
<div style="margin-top:20px;">
<a href="javascript:;" id="btn-modify-password" class="btn btn-sm btn-primary"><i class="fa fa-check fa-fw"></i> 确认修改</a>
</div>
</div>
<div class="tab-pane" id="oath">
<p>请在你的手机上安装身份验证器,然后在验证器中添加你的登录账号。</p>
<div id="oath-app-download-box" style="display:none;">
<p>选择你喜欢的身份验证器,扫描二维码进行安装:</p>
<div class="row">
<div class="col-md-2">
<p style="text-align: center;">
<i class="fa fa-apple"></i> iOSApple Store<br/>
<img src="${ static_url('img/qrcode/google-oath-appstore.png') }"><br/>
Google身份验证器
</p>
</div>
<div class="col-md-2">
<p style="text-align: center;">
<i class="fa fa-android"></i> Android百度手机助手<br/>
<img src="${ static_url('img/qrcode/google-oath-baidu.png') }"><br/>
Google身份验证器
</p>
</div>
<div class="col-md-2">
<p style="text-align: center;">
<i class="fa fa-android"></i> AndroidGoogle Play<br/>
<img src="${ static_url('img/qrcode/google-oath-googleplay.png') }"><br/>
Google身份验证器
</p>
</div>
<div class="col-md-2">
<p style="text-align: center;">
<i class="fa fa-apple"></i> iOSApple Store<br/>
<img src="${ static_url('img/qrcode/xiaomi-oath-appstore.png') }"><br/>
小米安全令牌
</p>
</div>
<div class="col-md-2">
<p style="text-align: center;">
<i class="fa fa-android"></i> Android小米应用商店<br/>
<img src="${ static_url('img/qrcode/xiaomi-oath-xiaomi.png') }"><br/>
小米安全令牌
</p>
</div>
</div>
</div>
<p style="margin-top:5px;"><a href="javascript:;" id="toggle-oath-download">显示下载地址 <i class="fa fa-angle-down"></i></a></p>
<hr/>
<p>要验证已经绑定的身份验证器,可在下面的输入框中输入验证器器上显示的动态验证码,然后点击验证。</p>
<p>如果验证失败,请注意检查您的身份验证器的时间与服务器时间是否一致,如果两者时间偏差超过两分钟则无法验证通过!</p>
<div style="width:360px;">
<div class="input-group input-group-sm" style="margin-top: 10px">
<span class="input-group-addon">动态验证码6位数字</span>
<input type="text" class="form-control" id="oath-code">
<div class="input-group-btn">
<button id="btn-verify-oath-code" class="btn btn-primary"><i class="fa fa-check"></i> 验证</button>
</div>
</div>
</div>
<hr/>
<p>要将你的登录账号添加到身份验证器中,请点击下面的“绑定身份验证器”按钮。</p>
<p>
<button id="btn-reset-oath-code" class="btn btn-sm btn-success"><i class="fa fa-refresh"></i> 绑定身份验证器</button>
</p>
</div>
</div>
</div>
</div>
<%block name="extend_content">
<div class="modal fade" id="dialog-reset-oath-code" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">绑定身份验证器</h3>
</div>
<div class="modal-body">
<p>请在手机上打开身份验证器,点击增加账号按钮,然后选择“扫描条形码”并扫描下面的二维码来完成账号绑定。</p>
<p style="text-align: center;"><img id="oath-secret-qrcode" src=""></p>
<p>如果无法扫描二维码,则可以选择“手动输入验证码”,设置一个容易记忆的账号名称,并确保“基于时间”一项是选中的,然后在“密钥”一项中输入下列密钥:</p>
<div class="row">
<div class="col-sm-4" style="text-align:right;">
<span style="line-height:25px;">密钥:</span>
</div>
<div class="col-sm-8">
<span class="oath-code"><span id="tmp-oath-secret"></span></span>
</div>
</div>
<hr/>
<p>然后请在下面的动态验证码输入框中输入身份验证器提供的6位数字</p>
<div class="row">
<div class="col-sm-4" style="text-align:right;">
<span style="line-height:34px;">动态验证码:</span>
</div>
<div class="col-sm-4">
<input type="text" class="form-control" id="oath-code-verify">
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-primary" id="btn-verify-oath-and-save"><i class="fa fa-check fa-fw"></i> 验证动态验证码并完成绑定</button>
<button type="button" class="btn btn-sm btn-default" data-dismiss="modal"><i class="fa fa-close fa-fw"></i> 取消</button>
</div>
</div>
</div>
</div>
</%block>