KEYCLOAK BABE

main
Lowlights 2024-03-19 17:07:56 +06:00
parent fd83b11c4a
commit f928306d15
22 changed files with 194502 additions and 149 deletions

Binary file not shown.

194202
debug.log

File diff suppressed because it is too large Load Diff

View File

@ -51,7 +51,7 @@ INSTALLED_APPS = [
'mozilla_django_oidc', 'mozilla_django_oidc',
] ]
SESSION_COOKIE_AGE = 86400 # 24 часа в секундах SESSION_COOKIE_AGE = 86400
SESSION_ENGINE = 'django.contrib.sessions.backends.db' SESSION_ENGINE = 'django.contrib.sessions.backends.db'
SITE_ID = 1 SITE_ID = 1
@ -165,8 +165,8 @@ KEYCLOAK_CONFIG = {
'REALM': 'Harmony', 'REALM': 'Harmony',
'CLIENT_ID': 'admin-cli', 'CLIENT_ID': 'admin-cli',
'CLIENT_SECRET': 'wOVphEiLVBS1AlNKRpaQpD4yQh5Wm3TJ', 'CLIENT_SECRET': 'wOVphEiLVBS1AlNKRpaQpD4yQh5Wm3TJ',
'CALLBACK_URL': 'http://127.0.0.1:8000/products/', 'CALLBACK_URL': 'http://127.0.0.1:8000/callback/',
'POST_LOGOUT_REDIRECT_URI': 'http://127.0.0.1:8000/', 'LOGOUT_REDIRECT_URL': 'http://127.0.0.1:8000/admin_logout_user/',
} }
@ -179,19 +179,24 @@ LOGGING = {
'class': 'logging.FileHandler', 'class': 'logging.FileHandler',
'filename': 'debug.log', 'filename': 'debug.log',
}, },
'console': {
'level': 'INFO',
'class': 'logging.StreamHandler',
},
}, },
'loggers': { 'loggers': {
'django': { 'django': {
'handlers': ['file'], 'handlers': ['file', 'console'],
'level': 'DEBUG', 'level': 'DEBUG',
'propagate': True, 'propagate': True,
}, },
}, },
} }
AUTHENTICATION_BACKENDS = [ AUTHENTICATION_BACKENDS = [
# ... # ...
'django.contrib.auth.backends.ModelBackend', # Стандартный бэкенд для работы с моделью User
'users.keycloak_backend.KeycloakBackend',
'allauth.account.auth_backends.AuthenticationBackend', 'allauth.account.auth_backends.AuthenticationBackend',
# ... # ...
] ]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

After

Width:  |  Height:  |  Size: 1.9 MiB

View File

@ -70,7 +70,7 @@
color: #bb7bff; color: #bb7bff;
} }
.cart-container{ .cart-container{
background-color: rgba(203, 92, 255, 0.62); /* Прозрачный серый фон */ background-color: rgba(222, 155, 255, 0.62); /* Прозрачный серый фон */
padding: 30px; /* Отступ для создания пространства вокруг .cart-page */ padding: 30px; /* Отступ для создания пространства вокруг .cart-page */
border-radius: 15px; /* Скруглённые углы для согласованности с .cart-page */ border-radius: 15px; /* Скруглённые углы для согласованности с .cart-page */
box-shadow: 0 6px 12px rgb(136, 5, 255); /* Небольшое тень для дополнительной глубины */ box-shadow: 0 6px 12px rgb(136, 5, 255); /* Небольшое тень для дополнительной глубины */
@ -93,7 +93,7 @@
/* Дополнительный стиль для общей цены, чтобы выделить её ещё сильнее */ /* Дополнительный стиль для общей цены, чтобы выделить её ещё сильнее */
.total .num { .total .num {
background-color: rgb(203, 92, 255); /* Слегка прозрачный золотой фон */ background-color: rgb(192, 69, 255); /* Слегка прозрачный золотой фон */
border-radius: 5px; border-radius: 5px;
padding: 5px 10px; padding: 5px 10px;
margin-right: 5px; margin-right: 5px;
@ -102,7 +102,7 @@
/* Стиль для кнопок действий с использованием эзотерических цветов и эффектов */ /* Стиль для кнопок действий с использованием эзотерических цветов и эффектов */
.button.light, .button.checkout { .button.light, .button.checkout {
background: linear-gradient(145deg, #8a00a9, #71008f); background: linear-gradient(145deg, #efb4ff, #f0c1ff);
color: white; color: white;
padding: 10px 20px; padding: 10px 20px;
border-radius: 30px; border-radius: 30px;

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 MiB

View File

@ -0,0 +1,6 @@
const audio = document.querySelector('#audio');
const btnka = document.querySelector('#btnka');
btnka.addEventListener('click', () => {
audio.paused ? audio.play() : audio.pause();
});

View File

@ -57,12 +57,18 @@
</a> </a>
<ul class="dropdown-menu" aria-labelledby="navbarDropdown"> <ul class="dropdown-menu" aria-labelledby="navbarDropdown">
<li>
<li><a class="dropdown-item text-white" href="#">Личный кабинет</a></li> <a class="dropdown-item text-white" href="{% url 'main:profile' %}">Личный кабинет</a>
<li><a class="dropdown-item text-white" href="{% url 'keycloak_login' %}">Войти</a></li> <a class="dropdown-item text-white" href="{% url 'main:admin_logout_user' %}">Выйти</a>
<li><a class="dropdown-item text-white" href="{% url 'keycloak_logout' %}">Выйти</a></li>
<a class="dropdown-item text-white" href="{% url 'keycloak_login' %}">Войти</a>
</li>
</ul>
</ul>
</li> </li>
</ul> </ul>
@ -71,10 +77,10 @@
</nav> </nav>
</header> </header>
<main class="d-flex flex-column flex-md-row align-items-center justify-content-around"> <main class="d-flex flex-column flex-md-row align-items-center justify-content-around">
<img alt="backimg" src="{% static '/deps/images/bg-image2.png' %}" class="fixed-top z-0 vw-100 vh-100 object-fit-cover"/> <img alt="backimg" src="{% static '/deps/images/фон2.png' %}" class="fixed-top z-0 vw-100 vh-100 object-fit-cover"/>
<div class="button-container d-flex flex-column justify-content-around align-items-center "> <div class="button-container d-flex flex-column justify-content-around align-items-center ">
<a href="/" class="button m-2" id="shop-button"> <a href="/products" class="button m-2" id="shop-button">
<div class="icon-shop"></div> <div class="icon-shop"></div>
</a> </a>
<a href="{% url 'main:about' %}" class="button m-2" id="about-button"> <a href="{% url 'main:about' %}" class="button m-2" id="about-button">

View File

@ -1,11 +1,39 @@
{% extends "main/base.html" %}
{% load static %} {% load static %}
<!DOCTYPE html>
<html lang="en"> {% block content %}
<head> <div class="row">
<meta charset="UTF-8"> <div class="container mt-5">
<title>Title</title> <div class="row justify-content-center">
</head> <!-- Профиль с данными пользователя -->
<body> <div class="col-md-5 position-relative">
<h1>CALLBACK</h1> <div class="bg-white p-4 mb-4 mx-2 rounded custom-shadow">
</body> <h3 class="text-center mb-4">Профиль пользователя</h3>
</html> <form action="#" method="post" enctype="multipart/form-data">
{% csrf_token %}
<div class="row">
<div class="col-md-12 mb-3 text-center">
<img src="{% static 'deps/img/no_image.jpg' %}" alt="Аватар пользователя" class="img-fluid rounded-circle" style="max-width: 150px;">
<input type="file" class="form-control mt-3" id="id_image" name="image" accept="image/*">
</div>
<div class="mb-3">
<strong>Имя:</strong> {{ userinfo.given_name|default:'Не указано' }}
</div>
<div class="mb-3">
<strong>Фамилия:</strong> {{ userinfo.family_name|default:'Не указано' }}
</div>
<div class="mb-3">
<strong>Имя пользователя:</strong> {{ userinfo.preferred_username|default:'Не указано' }}
</div>
<div class="mb-3">
<strong>Email:</strong> {{ userinfo.email|default:'Не указано' }}
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,11 @@
{% load static %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>LOGOUT</h1>
</body>
</html>

View File

@ -1,119 +1,43 @@
{% extends "main/base.html" %} {% extends "main/base.html" %}
{% load static %} {% load static %}
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="container mt-5"> <div class="container mt-5">
<div class="row justify-content-center"> <div class="row justify-content-center">
<!-- Профиль с данными пользователя --> <!-- Профиль с данными пользователя -->
<div class="col-md-5"> <div class="col-md-5 position-relative">
<div class=" bg-white p-4 mb-4 mx-2 rounded custom-shadow"> <div class="bg-white p-4 mb-4 mx-2 rounded custom-shadow">
<h3 class="text-center mb-4">Профиль пользователя</h3> <h3 class="text-center mb-4">Профиль пользователя</h3>
<form action="#" method="post" enctype="multipart/form-data"> <form action="#" method="post" enctype="multipart/form-data">
{% csrf_token %} {% csrf_token %}
<div class="row"> <div class="row">
<div class="col-md-12 mb-3 text-center"> <div class="col-md-12 mb-3 text-center">
<img src="{% static 'deps/img/no_image.jpg' %}" alt="Аватар пользователя" class="img-fluid rounded-circle" style="max-width: 150px;">
<img src="{% static "deps/img/no_image.jpg" %}" <input type="file" class="form-control mt-3" id="id_image" name="image" accept="image/*">
alt="Аватар пользователя" class="img-fluid rounded-circle"
style="max-width: 150px;">
<input type="file" class="form-control mt-3" id="id_image"
name='image'
accept="image/*">
</div> </div>
<div class="col-md-12 mb-3"> <div class="col-md-12 mb-3">
<label for="id_first_name" class="form-label">Имя*</label> <label for="id_first_name" class="form-label">Имя*</label>
<input type="text" class="form-control" id="id_first_name" <input type="text" class="form-control" id="id_first_name" name="first_name" placeholder="Введите ваше имя" value="{{ userinfo.given_name|default:'' }}" required>
name="first_name"
placeholder="Введите ваше имя"
value="{{ userinfo.firstName }}"
required>
</div> </div>
<div class="col-md-12 mb-3"> <div class="col-md-12 mb-3">
<label for="id_last_name" class="form-label">Фамилия*</label> <label for="id_last_name" class="form-label">Фамилия*</label>
<input type="text" class="form-control" id="id_last_name" <input type="text" class="form-control" id="id_last_name" name="last_name" placeholder="Введите вашу фамилию" value="{{ userinfo.family_name|default:'' }}" required>
name="last_name"
placeholder="Введите вашу фамилию"
value="{{ userinfo.lastName }}"
required>
</div> </div>
<div class="col-md-12 mb-3"> <div class="col-md-12 mb-3">
<label for="id_username" class="form-label">Имя пользователя*</label> <label for="id_username" class="form-label">Имя пользователя*</label>
<input type="text" class="form-control" id="id_username" <input type="text" class="form-control" id="id_username" name="username" placeholder="Введите ваше имя пользователя" value="{{ userinfo.preferred_username|default:'' }}" required>
name="username"
placeholder="Введите ваше имя пользователя"
value="{{ userinfo.username }}"
required>
</div> </div>
<div class="col-md-12 mb-3"> <div class="col-md-12 mb-3">
<label for="id_email" class="form-label">Email*</label> <label for="id_email" class="form-label">Email*</label>
<input type="email" class="form-control" id="id_email" <input type="email" class="form-control" id="id_email" name="email" placeholder="Введите ваш email" value="{{ userinfo.email|default:'' }}" required>
name="email"
placeholder="Введите ваш email *youremail@example.com"
value="{{ userinfo.email }}"
required>
</div> </div>
</div> </div>
<button type="submit" class="btn btn-dark">Сохранить</button> <button type="submit" class="btn btn-dark">Сохранить</button>
</form> </form>
</div> </div>
</div> </div>
<!-- Оформленные заказы -->
<div class="col-md-12">
<div class=" bg-white p-4 mb-4 mx-2 rounded custom-shadow">
<h3 class="text-center mb-4">Мои заказы</h3>
<!-- Разметка заказов -->
<div class="container">
<div class="accordion" id="accordionExample">
{% for order in orders %}
<div class="accordion-item">
<h2 class="accordion-header" id="heading{{ order.id }}">
<button class="accordion-button {% if order != orders.0 %}collapsed{% endif %}" type="button" data-bs-toggle="collapse" data-bs-target="#collapse{{ order.id }}" aria-expanded="false" aria-controls="collapse{{ order.id }}">
Заказ № {{ order.id }} - {{ order.created_timestamp }} | Статус: <strong class="mx-2">{{order.requires_delivery}}</strong>
</button>
</h2>
<div id="collapse{{ order.id }}" class="accordion-collapse collapse {% if order == orders.0 %}show{% endif %}" aria-labelledby="heading{{ order.id }}" data-bs-parent="#accordionExample">
<div class="accordion-body">
<table class="table table-dark table-hover">
<thead>
<tr>
<th>Товар</th>
<th>Количество</th>
<th>Цена</th>
<th>Общая стоимость</th>
</tr>
</thead>
<tbody>
{% for item in order.orderitem_set.all %}
<tr>
<td><a class="text-white" href="{% url 'goods:product' item.product.slug %}">{{ item.product.name }}</a></td>
<td>{{ item.quantity }}</td>
<td>{{ item.price }}</td>
<td>{{ item.products_price }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -4,12 +4,14 @@ from . import views
app_name = 'main' app_name = 'main'
urlpatterns = [ urlpatterns = [
path('admin_logout_user/',views.admin_logout_user,name = 'admin_logout_user'),
path('callback/', views.callback, name='callback'), path('callback/', views.callback, name='callback'),
path('profile/',views.profile,name='profile'),
path('', views.product_list, name='product_list'),
path('about/', views.about, name='about'), path('about/', views.about, name='about'),
path('products/', views.product_list, name='product_list'),
path('<slug:category_slug>/', views.product_list, path('products/', views.callback, name='product_list'),
path('<slug:category_slug>/', views.callback,
name='product_list_by_category'), name='product_list_by_category'),
path('<int:id>/<slug:slug>/', views.product_detail, path('<int:id>/<slug:slug>/', views.product_detail,
name='product_detail'), name='product_detail'),

View File

@ -1,26 +1,20 @@
import json import requests
from django.shortcuts import render,get_object_or_404 from django.shortcuts import render,get_object_or_404
from django.http import HttpResponse, JsonResponse from django.http import HttpResponse, HttpResponseRedirect
import logging
from djangoProject1 import settings
from .models import Category, Product from .models import Category, Product
from cart.forms import CartAddProductForm from cart.forms import CartAddProductForm
from jose import jwt
from django.contrib import messages
YOUR_REDIRECT_URI = 'http://127.0.0.1:8000/callback/'
def home(request): def home(request):
# Ваш код для отображения главной страницы # Ваш код для отображения главной страницы
return render(request, 'main/base.html') return render(request, 'main/base.html')
def product_list(request, category_slug=None):
category = None
categories = Category.objects.all()
products = Product.objects.filter(available=True)
if category_slug:
category = get_object_or_404(Category,
slug=category_slug)
products = products.filter(category=category)
return render(request,'main/product/list.html',
{'category': category,
'categories': categories,
'products': products})
def product_detail(request, id, slug): def product_detail(request, id, slug):
product = get_object_or_404(Product, product = get_object_or_404(Product,
@ -38,5 +32,173 @@ def product_detail(request, id, slug):
# Функция для страницы "О нас" # Функция для страницы "О нас"
def about(request): def about(request):
return render(request, 'main/about.html') return render(request, 'main/about.html')
def callback(request):
return render(request,'main/callback.html') def exchange_code_for_token(authorization_code, redirect_uri):
token_endpoint = 'https://auth.myterior.kz/realms/Harmony/protocol/openid-connect/token'
client_id = 'admin-cli'
client_secret = 'wOVphEiLVBS1AlNKRpaQpD4yQh5Wm3TJ'
payload = {
'grant_type': 'authorization_code',
'code': authorization_code,
'client_id': client_id,
'client_secret': client_secret,
'redirect_uri': redirect_uri,
}
response = requests.post(token_endpoint, data=payload)
print(response.status_code)
if response.status_code == 200:
tokens = response.json()
access_token = tokens.get('access_token')
return access_token
else:
print("Failed to exchange code: HTTP status", response.status_code)
print("Response body:", response.text)
raise Exception('Failed to exchange authorization code for tokens')
def get_user_info(access_token):
userinfo_endpoint = 'https://auth.myterior.kz/realms/Harmony/protocol/openid-connect/userinfo'
headers = {'Authorization': f'Bearer {access_token}'}
response = requests.get(userinfo_endpoint, headers=headers)
if response.status_code == 200:
return response.json()
else:
print("Failed to fetch user info: HTTP status", response.status_code)
return None
import jwt
def decode_access_token(access_token):
payload = jwt.decode(access_token, options={"verify_signature": False})
return payload
def callback(request,category_slug=None):
#Получение кода авторизации из запроса
context = {}
authorization_code = request.GET.get('code')
if authorization_code:
try:
access_token = exchange_code_for_token(authorization_code, YOUR_REDIRECT_URI)
user_info = decode_access_token(access_token)
request.session['user_info'] = user_info
context['userinfo'] = request.session.get('user_info', {})
except Exception as e:
context['error'] = str(e)
else:
context['error'] = "Authorization code not found."
category = None
categories = Category.objects.all()
products = Product.objects.filter(available=True)
if category_slug:
category = get_object_or_404(Category,
slug=category_slug)
products = products.filter(category=category)
return render(request, 'main/product/list.html',{'category': category,
'categories': categories,
'products': products})
from django.shortcuts import render, redirect
def profile(request):
if request.method == 'POST':
user_info = request.session.get('user_info', {})
user_info['given_name'] = request.POST.get('first_name', user_info.get('given_name', ''))
user_info['family_name'] = request.POST.get('last_name', user_info.get('family_name', ''))
user_info['preferred_username'] = request.POST.get('username', user_info.get('preferred_username', ''))
user_info['email'] = request.POST.get('email', user_info.get('email', ''))
request.session['user_info'] = user_info
return render(request, 'main/callback.html')
else:
context = {'userinfo': request.session.get('user_info', {})}
return render(request, 'main/callback.html', context)
# Настройте логирование
logger = logging.getLogger(__name__)
def get_keycloak_admin_token():
data = {
'client_id': settings.KEYCLOAK_CONFIG['CLIENT_ID'],
'client_secret': settings.KEYCLOAK_CONFIG['CLIENT_SECRET'],
'grant_type': 'client_credentials',
}
response = requests.post(
f"{settings.KEYCLOAK_CONFIG['SERVER_URL']}/realms/{settings.KEYCLOAK_CONFIG['REALM']}/protocol/openid-connect/token",
data=data
)
return response.json()['access_token']
def get_user_sessions(admin_token, user_id):
headers = {'Authorization': f'Bearer {admin_token}'}
response = requests.get(
f"{settings.KEYCLOAK_CONFIG['SERVER_URL']}/admin/realms/{settings.KEYCLOAK_CONFIG['REALM']}/users/{user_id}/sessions",
headers=headers
)
if response.status_code == 200:
return response.json()
else:
logger.error(f"Error fetching user sessions: {response.status_code}")
return []
def logout_user(admin_token,session_id):
headers = {
'Authorization': f'Bearer {admin_token}',
}
response = requests.delete(
f"{settings.KEYCLOAK_CONFIG['SERVER_URL']}/admin/realms/{settings.KEYCLOAK_CONFIG['REALM']}/sessions/{session_id}",
headers=headers
)
return response.status_code == 204
def get_user_id_from_session(request):
user_info = request.session.get('user_info', {})
user_id = user_info.get('sub')
if isinstance(user_id, str):
return user_id
else:
logger.error(f"Invalid user ID format in session: {user_id}")
return None
def admin_logout_user(request):
user_id = get_user_id_from_session(request)
admin_token = get_keycloak_admin_token()
if not admin_token:
logger.error("Can't obtain admin token.")
messages.error(request, "Can't obtain admin token for logout.")
return redirect(settings.KEYCLOAK_CONFIG['LOGOUT_REDIRECT_URL'])
sessions = get_user_sessions(admin_token, user_id)
if sessions is None or not sessions:
logger.error("Can't find user sessions.")
messages.error(request, "No active sessions found for user.")
return render(request,'main/logout.html')
all_sessions_logged_out = True
for session in sessions:
if not logout_user(admin_token, session['id']):
all_sessions_logged_out = False
logger.error(f"Failed to log out session {session['id']}")
if all_sessions_logged_out:
logger.info("User successfully logged out from all Keycloak sessions.")
messages.success(request, "Successfully logged out.")
else:
messages.warning(request, "Partial logout. Some sessions may still be active.")
request.session.flush() # Clear Django session
return render(request,'main/logout.html')

23
users/keycloak_backend.py Normal file
View File

@ -0,0 +1,23 @@
from django.contrib.auth.backends import BaseBackend
from django.contrib.auth.models import User
from main.views import decode_access_token
class KeycloakBackend(BaseBackend):
def authenticate(self, request, token=None):
# Ваш код для проверки токена через Keycloak и извлечения информации о пользователе
user_info = decode_access_token(token)
if user_info:
user, created = User.objects.get_or_create(username=user_info['preferred_username'],
defaults={'first_name': user_info.get('given_name', ''),
'last_name': user_info.get('family_name', ''),
'email': user_info.get('email', '')})
return user
return None
def get_user(self, user_id):
try:
return User.objects.get(pk=user_id)
except User.DoesNotExist:
return None

View File

@ -6,6 +6,6 @@ from . import views
urlpatterns = [ urlpatterns = [
path('login/', views.keycloak_login, name='keycloak_login'), path('login/', views.keycloak_login, name='keycloak_login'),
path('keycloak_logout/', views.keycloak_logout, name='keycloak_logout'),
path('products/', views.keycloak_redirect, name='keycloak_redirect'), path('products/', views.keycloak_redirect, name='keycloak_redirect'),
] ]

View File

@ -1,11 +1,11 @@
import json
import logging import logging
from django.http import JsonResponse
from django.shortcuts import redirect from django.shortcuts import redirect
from django.conf import settings from django.conf import settings
from keycloak import KeycloakOpenID from keycloak import KeycloakOpenID
from urllib.parse import urlencode, urljoin from urllib.parse import urlencode
from django.contrib.auth import logout from django.contrib.auth import logout
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -26,22 +26,6 @@ def keycloak_login(request):
def keycloak_logout(request):
logout(request)
keycloak_server_url = settings.KEYCLOAK_CONFIG['SERVER_URL']
realm_name = settings.KEYCLOAK_CONFIG['REALM']
redirect_uri = settings.KEYCLOAK_CONFIG['POST_LOGOUT_REDIRECT_URI']
params = {'redirect_uri': redirect_uri}
keycloak_logout_url = (
f'{keycloak_server_url}/realms/{realm_name}/protocol/openid-connect/logout'
f'?{urlencode(params)}'
)
return redirect(keycloak_logout_url)
from django.http import HttpResponse from django.http import HttpResponse
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods