Google auth provided
This commit is contained in:
parent
c9e8e5386f
commit
00ad7ddef1
1
.gitignore
vendored
1
.gitignore
vendored
@ -11,3 +11,4 @@
|
|||||||
static/css/*.css
|
static/css/*.css
|
||||||
static/css/*.css.map
|
static/css/*.css.map
|
||||||
.env
|
.env
|
||||||
|
/flask_session/
|
22
app.py
22
app.py
@ -7,12 +7,16 @@ import random
|
|||||||
import asyncio
|
import asyncio
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import requests
|
import requests
|
||||||
|
from authlib.integrations.flask_client import OAuth
|
||||||
|
from auth import auth_bp, init_oauth
|
||||||
|
from config import Config, configure_oauth
|
||||||
|
from flask_session import Session
|
||||||
from PIL import Image as PILImage
|
from PIL import Image as PILImage
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
from werkzeug.exceptions import BadRequest
|
from werkzeug.exceptions import BadRequest
|
||||||
from werkzeug.security import generate_password_hash, check_password_hash
|
from werkzeug.security import generate_password_hash, check_password_hash
|
||||||
from werkzeug.utils import secure_filename
|
from werkzeug.utils import secure_filename
|
||||||
from flask import Flask, abort, render_template, redirect, url_for, request, flash, session, jsonify
|
from flask import Flask, abort, render_template, redirect, url_for, request, session, jsonify
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
from flask_bcrypt import Bcrypt
|
from flask_bcrypt import Bcrypt
|
||||||
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
|
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
|
||||||
@ -34,9 +38,12 @@ from auth import auth_bp
|
|||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
csrf = CSRFProtect(app)
|
csrf = CSRFProtect(app)
|
||||||
|
|
||||||
app.config.from_object(Config)
|
app.config.from_object(Config)
|
||||||
|
|
||||||
|
oauth = configure_oauth(app)
|
||||||
|
|
||||||
|
init_oauth(oauth)
|
||||||
|
|
||||||
db.init_app(app)
|
db.init_app(app)
|
||||||
bcrypt.init_app(app)
|
bcrypt.init_app(app)
|
||||||
login_manager = LoginManager(app)
|
login_manager = LoginManager(app)
|
||||||
@ -44,7 +51,16 @@ login_manager.login_view = 'auth.login'
|
|||||||
|
|
||||||
@login_manager.user_loader
|
@login_manager.user_loader
|
||||||
def load_user(user_id):
|
def load_user(user_id):
|
||||||
return User.query.get(int(user_id))
|
return db.session.get(User, int(user_id))
|
||||||
|
|
||||||
|
from flask_session import Session
|
||||||
|
|
||||||
|
app.config['SESSION_TYPE'] = 'filesystem'
|
||||||
|
app.config['SESSION_FILE_DIR'] = './flask_session'
|
||||||
|
app.config['SESSION_PERMANENT'] = False
|
||||||
|
app.config['SESSION_USE_SIGNER'] = True
|
||||||
|
app.config['SECRET_KEY'] = Config.SECRET_KEY
|
||||||
|
Session(app)
|
||||||
|
|
||||||
register_admin_routes(app)
|
register_admin_routes(app)
|
||||||
app.register_blueprint(upload_bp)
|
app.register_blueprint(upload_bp)
|
||||||
|
87
auth.py
87
auth.py
@ -1,15 +1,24 @@
|
|||||||
from flask import Blueprint, render_template, redirect, url_for, request
|
from flask import Blueprint, render_template, redirect, url_for, request, session
|
||||||
from flask_login import login_user, logout_user, login_required, current_user
|
from flask_login import login_user, logout_user, login_required, current_user
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
from models import db, User
|
from models import db, User
|
||||||
|
import uuid
|
||||||
from utils import get_client_ip
|
from utils import get_client_ip
|
||||||
from models import RegistrationForm, LoginForm, PasswordField, RecaptchaField, SubmitField
|
from models import RegistrationForm, LoginForm, PasswordField, RecaptchaField, SubmitField
|
||||||
from flask_bcrypt import Bcrypt
|
from flask_bcrypt import Bcrypt
|
||||||
from wtforms.validators import DataRequired, Length, EqualTo
|
from wtforms.validators import DataRequired, Length, EqualTo
|
||||||
from config import Config
|
from config import Config
|
||||||
|
from authlib.integrations.flask_client import OAuth # type: ignore
|
||||||
auth_bp = Blueprint('auth', __name__)
|
auth_bp = Blueprint('auth', __name__)
|
||||||
bcrypt = Bcrypt()
|
bcrypt = Bcrypt()
|
||||||
|
|
||||||
|
|
||||||
|
google = None
|
||||||
|
|
||||||
|
def init_oauth(oauth):
|
||||||
|
global google
|
||||||
|
google = oauth.google
|
||||||
|
|
||||||
password = PasswordField('Password', validators=[DataRequired(), Length(min=6)])
|
password = PasswordField('Password', validators=[DataRequired(), Length(min=6)])
|
||||||
confirm_password = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')])
|
confirm_password = PasswordField('Confirm Password', validators=[DataRequired(), EqualTo('password')])
|
||||||
recaptcha = RecaptchaField()
|
recaptcha = RecaptchaField()
|
||||||
@ -20,11 +29,15 @@ def register():
|
|||||||
form = RegistrationForm()
|
form = RegistrationForm()
|
||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
hashed_password = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
|
|
||||||
ip_address = get_client_ip()
|
ip_address = get_client_ip()
|
||||||
|
existing_user = User.query.filter_by(ip_address=ip_address).first()
|
||||||
|
|
||||||
|
if existing_user:
|
||||||
|
return render_template('register.html', form=form, recaptcha_key=Config.RECAPTCHA_PUBLIC_KEY)
|
||||||
|
|
||||||
|
hashed_password = bcrypt.generate_password_hash(form.password.data).decode('utf-8')
|
||||||
username = form.username.data.lower()
|
username = form.username.data.lower()
|
||||||
|
|
||||||
existing_user = User.query.filter_by(ip_address=ip_address).first()
|
|
||||||
user = User(username=username, encrypted_password=hashed_password, ip_address=ip_address)
|
user = User(username=username, encrypted_password=hashed_password, ip_address=ip_address)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -46,21 +59,24 @@ def login():
|
|||||||
|
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
user = User.query.filter_by(username=form.username.data).first()
|
user = User.query.filter_by(username=form.username.data).first()
|
||||||
if user and user.check_password(form.password.data):
|
if user:
|
||||||
login_user(user)
|
|
||||||
if user.ip_address is None:
|
if user.google_id:
|
||||||
ip_address = get_client_ip()
|
return redirect(url_for('auth.login_google'))
|
||||||
user.ip_address = ip_address
|
|
||||||
db.session.commit()
|
if user.check_password(form.password.data):
|
||||||
return redirect(url_for('profile', username=user.username))
|
login_user(user)
|
||||||
|
if user.ip_address is None:
|
||||||
|
ip_address = get_client_ip()
|
||||||
|
user.ip_address = ip_address
|
||||||
|
db.session.commit()
|
||||||
|
return redirect(url_for('profile', username=user.username))
|
||||||
|
|
||||||
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||||
return render_template('login-modal.html', form=form, recaptcha_key=Config.RECAPTCHA_PUBLIC_KEY)
|
return render_template('login-modal.html', form=form, recaptcha_key=Config.RECAPTCHA_PUBLIC_KEY)
|
||||||
|
|
||||||
return render_template('login.html', form=form, recaptcha_key=Config.RECAPTCHA_PUBLIC_KEY)
|
return render_template('login.html', form=form, recaptcha_key=Config.RECAPTCHA_PUBLIC_KEY)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@auth_bp.route('/register-modal')
|
@auth_bp.route('/register-modal')
|
||||||
def register_modal():
|
def register_modal():
|
||||||
form = RegistrationForm()
|
form = RegistrationForm()
|
||||||
@ -76,3 +92,50 @@ def login_modal():
|
|||||||
def logout():
|
def logout():
|
||||||
logout_user()
|
logout_user()
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
@auth_bp.route('/login/google')
|
||||||
|
def login_google():
|
||||||
|
|
||||||
|
base_domain = request.host_url.rstrip('/')
|
||||||
|
redirect_uri = f"{base_domain}/auth/google/callback"
|
||||||
|
|
||||||
|
state = str(uuid.uuid4())
|
||||||
|
nonce = str(uuid.uuid4())
|
||||||
|
session['oauth_state'] = state
|
||||||
|
session['oauth_nonce'] = nonce
|
||||||
|
|
||||||
|
return google.authorize_redirect(redirect_uri, state=state, nonce=nonce)
|
||||||
|
|
||||||
|
@auth_bp.route('/auth/google/callback')
|
||||||
|
def authorize_google():
|
||||||
|
state = session.pop('oauth_state', None)
|
||||||
|
nonce = session.pop('oauth_nonce', None)
|
||||||
|
|
||||||
|
if not state or state != request.args.get('state'):
|
||||||
|
return "Error: Invalid state parameter", 400
|
||||||
|
|
||||||
|
token = google.authorize_access_token()
|
||||||
|
|
||||||
|
try:
|
||||||
|
user_info = google.parse_id_token(token, nonce=nonce)
|
||||||
|
except Exception as e:
|
||||||
|
return "Error: Invalid ID token", 400
|
||||||
|
|
||||||
|
google_id = user_info['sub']
|
||||||
|
user = User.query.filter_by(google_id=google_id).first()
|
||||||
|
if user:
|
||||||
|
|
||||||
|
login_user(user)
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
username = f"user_{uuid.uuid4().hex[:8]}"
|
||||||
|
user = User(
|
||||||
|
username=username,
|
||||||
|
google_id=google_id,
|
||||||
|
ip_address=get_client_ip()
|
||||||
|
)
|
||||||
|
db.session.add(user)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
login_user(user)
|
||||||
|
return redirect(url_for('index'))
|
21
config.py
21
config.py
@ -1,5 +1,6 @@
|
|||||||
import os
|
import os
|
||||||
from dotenv import load_dotenv, find_dotenv
|
from dotenv import load_dotenv, find_dotenv
|
||||||
|
from authlib.integrations.flask_client import OAuth
|
||||||
|
|
||||||
dotenv_path = find_dotenv()
|
dotenv_path = find_dotenv()
|
||||||
load_dotenv(dotenv_path, override=True)
|
load_dotenv(dotenv_path, override=True)
|
||||||
@ -10,6 +11,9 @@ class Config:
|
|||||||
RECAPTCHA_PUBLIC_KEY = os.getenv('RECAPTCHA_PUBLIC_KEY')
|
RECAPTCHA_PUBLIC_KEY = os.getenv('RECAPTCHA_PUBLIC_KEY')
|
||||||
RECAPTCHA_PRIVATE_KEY = os.getenv('RECAPTCHA_PRIVATE_KEY')
|
RECAPTCHA_PRIVATE_KEY = os.getenv('RECAPTCHA_PRIVATE_KEY')
|
||||||
SQLALCHEMY_DATABASE_URI = os.getenv('SQLALCHEMY_DATABASE_URI')
|
SQLALCHEMY_DATABASE_URI = os.getenv('SQLALCHEMY_DATABASE_URI')
|
||||||
|
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||||
|
BASE_DOMAIN = os.getenv('BASE_DOMAIN', 'http://localhost:5000')
|
||||||
|
GOOGLE_REDIRECT_URI = f"{BASE_DOMAIN}/auth/google/callback"
|
||||||
UPLOAD_FOLDER = {
|
UPLOAD_FOLDER = {
|
||||||
'images': 'static/arts/',
|
'images': 'static/arts/',
|
||||||
'arts': 'static/arts/',
|
'arts': 'static/arts/',
|
||||||
@ -25,3 +29,20 @@ class Config:
|
|||||||
ALLOWED_VIDEO_EXTENSIONS = {'mp4', 'avi', 'mov'}
|
ALLOWED_VIDEO_EXTENSIONS = {'mp4', 'avi', 'mov'}
|
||||||
MAX_IMAGE_SIZE = 15 * 1024 * 1024
|
MAX_IMAGE_SIZE = 15 * 1024 * 1024
|
||||||
MAX_VIDEO_SIZE = 10 * 1024 * 1024 * 1024
|
MAX_VIDEO_SIZE = 10 * 1024 * 1024 * 1024
|
||||||
|
|
||||||
|
GOOGLE_CLIENT_ID = os.getenv('GOOGLE_CLIENT_ID')
|
||||||
|
GOOGLE_CLIENT_SECRET = os.getenv('GOOGLE_CLIENT_SECRET')
|
||||||
|
GOOGLE_REDIRECT_URI = os.getenv('GOOGLE_REDIRECT_URI', 'http://localhost:5000/auth/google/callback')
|
||||||
|
|
||||||
|
def configure_oauth(app):
|
||||||
|
oauth = OAuth(app)
|
||||||
|
oauth.register(
|
||||||
|
name='google',
|
||||||
|
client_id=Config.GOOGLE_CLIENT_ID,
|
||||||
|
client_secret=Config.GOOGLE_CLIENT_SECRET,
|
||||||
|
server_metadata_url='https://accounts.google.com/.well-known/openid-configuration',
|
||||||
|
client_kwargs={
|
||||||
|
'scope': 'openid profile'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return oauth
|
@ -54,7 +54,8 @@ class Comments(db.Model):
|
|||||||
class User(db.Model, UserMixin):
|
class User(db.Model, UserMixin):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
username = db.Column(db.String(20), unique=True, nullable=False)
|
username = db.Column(db.String(20), unique=True, nullable=False)
|
||||||
encrypted_password = db.Column(db.String(60), nullable=False)
|
encrypted_password = db.Column(db.String(60), nullable=True)
|
||||||
|
google_id = db.Column(db.String(255), unique=True, nullable=True)
|
||||||
ip_address = db.Column(db.String(15), nullable=True)
|
ip_address = db.Column(db.String(15), nullable=True)
|
||||||
avatar_file = db.Column(db.String(50), nullable=True)
|
avatar_file = db.Column(db.String(50), nullable=True)
|
||||||
banner_file = db.Column(db.String(50), nullable=True)
|
banner_file = db.Column(db.String(50), nullable=True)
|
||||||
@ -65,7 +66,9 @@ class User(db.Model, UserMixin):
|
|||||||
return f'<User {self.username}>'
|
return f'<User {self.username}>'
|
||||||
|
|
||||||
def check_password(self, password):
|
def check_password(self, password):
|
||||||
return bcrypt.check_password_hash(self.encrypted_password, password)
|
if self.encrypted_password:
|
||||||
|
return bcrypt.check_password_hash(self.encrypted_password, password)
|
||||||
|
return False
|
||||||
|
|
||||||
class Image(db.Model):
|
class Image(db.Model):
|
||||||
__tablename__ = 'image'
|
__tablename__ = 'image'
|
||||||
|
File diff suppressed because it is too large
Load Diff
1
static/css/styles.scss.map
Normal file
1
static/css/styles.scss.map
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"version":3,"sourceRoot":"","sources":["styles.css"],"names":[],"mappings":"AAAQ;AACA;AACR;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;AAAA;EAEE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;AACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;AAAA;EAEE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;AAAA;EAEE;;;AAGF;AAAA;EAEE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;AAAA;EAEE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;AAAA;EAEE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;AACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;AACA;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;AACA;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;;;AAEF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAGF","file":"styles.scss"}
|
14
static/icons/google.svg
Normal file
14
static/icons/google.svg
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<svg width="17" height="17" viewBox="0 0 17 17" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0_4_95)">
|
||||||
|
<path d="M5.64829 0.937973C4.10051 1.4991 2.76572 2.56415 1.83996 3.97667C0.9142 5.38919 0.446273 7.07474 0.504909 8.78572C0.563545 10.4967 1.14565 12.143 2.16573 13.4826C3.18581 14.8223 4.59009 15.7849 6.17231 16.2288C7.45505 16.5747 8.79898 16.5899 10.0886 16.2731C11.2568 15.9989 12.3369 15.4123 13.223 14.5707C14.1453 13.6682 14.8148 12.5199 15.1594 11.2495C15.534 9.86798 15.6006 8.41511 15.3542 7.00247H8.15102V10.1251H12.3226C12.2393 10.6232 12.0606 11.0985 11.7973 11.5227C11.5341 11.9469 11.1916 12.3113 10.7905 12.5939C10.2811 12.9461 9.70687 13.183 9.10467 13.2896C8.5007 13.4069 7.88121 13.4069 7.27724 13.2896C6.6651 13.1573 6.08602 12.8933 5.57688 12.5143C4.75895 11.9092 4.1448 11.0496 3.82207 10.0581C3.49387 9.04807 3.49387 7.95306 3.82207 6.94302C4.0518 6.23503 4.43157 5.59041 4.93305 5.05727C5.50692 4.43596 6.23346 3.99184 7.03296 3.77365C7.83246 3.55545 8.67402 3.57161 9.46531 3.82035C10.0835 4.01865 10.6487 4.36512 11.1161 4.83215C11.5864 4.34311 12.056 3.85281 12.5247 3.36124C12.7668 3.09691 13.0306 2.84522 13.269 2.57456C12.5557 1.88082 11.7184 1.341 10.805 0.986033C9.14183 0.354909 7.32199 0.337948 5.64829 0.937973Z" fill="black"/>
|
||||||
|
<path d="M5.85226 0.908712C7.53202 0.349179 9.3586 0.364569 11.0281 0.95232C11.945 1.28535 12.785 1.79082 13.5 2.43972C13.2571 2.69194 13.0008 2.92766 12.753 3.17281C12.2817 3.62932 11.8108 4.08426 11.3403 4.53763C10.8712 4.10241 10.3038 3.77954 9.68342 3.59475C8.88946 3.36214 8.04481 3.34621 7.24214 3.54871C6.43947 3.75121 5.70979 4.16432 5.13316 4.7427C4.62983 5.23953 4.24865 5.84024 4.01807 6.5L1.5 4.60834C2.40132 2.8741 3.96189 1.54754 5.85226 0.908712Z" fill="#E33629"/>
|
||||||
|
<path d="M0.635417 6.45951C0.757101 5.77897 0.95912 5.11993 1.23607 4.5L3.5 6.48292C3.20384 7.46712 3.20384 8.53411 3.5 9.51831C2.74572 10.1756 1.99108 10.8362 1.23607 11.5C0.542753 9.94264 0.331303 8.16822 0.635417 6.45951Z" fill="#F8BD00"/>
|
||||||
|
<path d="M8.50059 7.5H16.3413C16.6095 8.80657 16.537 10.1503 16.1292 11.4281C15.7541 12.6032 15.0254 13.6652 14.0215 14.5C13.1402 13.8894 12.255 13.2834 11.3737 12.6728C11.8106 12.4111 12.1835 12.0737 12.4701 11.681C12.7567 11.2882 12.951 10.8481 13.0414 10.387H8.50059C8.49927 9.42547 8.50059 8.46273 8.50059 7.5Z" fill="#587DBD"/>
|
||||||
|
<path d="M1.5 12.3961C2.34258 11.7672 3.18474 11.1352 4.02651 10.5C4.35217 11.4247 4.97155 12.2262 5.79616 12.7899C6.31049 13.1416 6.89492 13.3858 7.51219 13.5068C8.12042 13.6163 8.74429 13.6163 9.35253 13.5068C9.95898 13.4075 10.5373 13.1866 11.0503 12.8583C11.8656 13.4738 12.6846 14.0846 13.5 14.7001C12.6077 15.4851 11.52 16.0324 10.3434 16.2885C9.04468 16.5839 7.69125 16.5697 6.39945 16.2472C5.37776 15.9833 4.42343 15.518 3.59628 14.8806C2.72079 14.2081 2.00573 13.3606 1.5 12.3961Z" fill="#319F43"/>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_4_95">
|
||||||
|
<rect width="16" height="16" fill="white" transform="translate(0.5 0.5)"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2.9 KiB |
@ -15,7 +15,6 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Делегируем клики для открытия логина/регистрации внутри любых модалок
|
|
||||||
document.addEventListener('click', function (e) {
|
document.addEventListener('click', function (e) {
|
||||||
const target = e.target.closest('[data-action]');
|
const target = e.target.closest('[data-action]');
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
|
@ -3,7 +3,14 @@
|
|||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
<div class="form-inner-container">
|
<div class="form-inner-container">
|
||||||
<p class="login-form-title">ВХОД</p>
|
<p class="login-form-title">ВХОД</p>
|
||||||
|
<div class="social-login-container">
|
||||||
|
<a href="{{ url_for('auth.login_google') }}" class="google-login-button">
|
||||||
|
<div class="google-button-content">
|
||||||
|
<img src="/static/icons/google.svg" alt="Google Icon" class="google-icon">
|
||||||
|
<span class="google-button-text">Войти с аккаунтом Google</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
<div class="modal-login-input-container">
|
<div class="modal-login-input-container">
|
||||||
{{ form.username(class_="modal-login-text-input", placeholder="Имя пользователя") }}
|
{{ form.username(class_="modal-login-text-input", placeholder="Имя пользователя") }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,6 +3,14 @@
|
|||||||
{{ form.hidden_tag() }}
|
{{ form.hidden_tag() }}
|
||||||
<div class="form-inner-container">
|
<div class="form-inner-container">
|
||||||
<p class="reg-form-title">РЕГИСТРАЦИЯ</p>
|
<p class="reg-form-title">РЕГИСТРАЦИЯ</p>
|
||||||
|
<div class="social-login-container">
|
||||||
|
<a href="{{ url_for('auth.login_google') }}" class="google-login-button">
|
||||||
|
<div class="google-button-content">
|
||||||
|
<img src="/static/icons/google.svg" alt="Google Icon" class="google-icon">
|
||||||
|
<span class="google-button-text">Войти с аккаунтом Google</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="modal-register-input-container">
|
<div class="modal-register-input-container">
|
||||||
{{ form.username(class_="modal-register-text-input", placeholder="Имя пользователя") }}
|
{{ form.username(class_="modal-register-text-input", placeholder="Имя пользователя") }}
|
||||||
|
@ -12,31 +12,31 @@
|
|||||||
|
|
||||||
.subnav-container {
|
.subnav-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 50px;
|
height: 3rem; /* Используем rem вместо пикселей */
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: #3C3882;
|
background-color: #3C3882;
|
||||||
padding: 0 20px;
|
padding: 0 1.25rem; /* Относительные отступы */
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 112px;
|
top: 7rem; /* Используем rem для позиции */
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 50px;
|
height: 100%; /* Высота кнопки равна высоте контейнера */
|
||||||
background-color: #3C3882;
|
background-color: #3C3882;
|
||||||
border: none;
|
border: none;
|
||||||
font-size: 16px;
|
font-size: 1rem; /* Относительный размер шрифта */
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: #8784C9;
|
color: #8784C9;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 15px 0;
|
padding: 0.9375rem 0; /* Относительные отступы */
|
||||||
}
|
}
|
||||||
|
|
||||||
.button:not(:last-child) {
|
.button:not(:last-child) {
|
||||||
margin-right: 20px;
|
margin-right: 1.25rem; /* Относительный отступ между кнопками */
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user