mirror of
https://github.com/Dadechin/Dashboard-XRoom.git
synced 2025-07-02 00:04:34 +00:00
auth and simple info
This commit is contained in:
parent
89f462bf9f
commit
3f47437822
89
.gitignore
vendored
Normal file
89
.gitignore
vendored
Normal file
|
@ -0,0 +1,89 @@
|
|||
# Created by https://www.gitignore.io
|
||||
|
||||
### OSX ###
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
|
||||
# Icon must end with two \r
|
||||
Icon
|
||||
|
||||
|
||||
# Thumbnails
|
||||
._*
|
||||
|
||||
# Files that might appear on external disk
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
|
||||
# Directories potentially created on remote AFP share
|
||||
.AppleDB
|
||||
.AppleDesktop
|
||||
Network Trash Folder
|
||||
Temporary Items
|
||||
.apdisk
|
||||
|
||||
|
||||
### Python ###
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
env/
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.coverage
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
|
||||
### Django ###
|
||||
*.log
|
||||
*.pot
|
||||
*.pyc
|
||||
__pycache__/
|
||||
local_settings.py
|
||||
|
||||
.env
|
||||
db.sqlite3
|
39
.gitlab-ci.yml
Normal file
39
.gitlab-ci.yml
Normal file
|
@ -0,0 +1,39 @@
|
|||
stages:
|
||||
- test
|
||||
- deploy
|
||||
|
||||
before_script:
|
||||
- apk update
|
||||
- apk add pkgconfig
|
||||
- apk add mariadb-connector-c-dev build-base
|
||||
|
||||
|
||||
test:
|
||||
stage: test
|
||||
image: python:3.9-alpine
|
||||
script:
|
||||
- pip install --upgrade pip
|
||||
- pip install django
|
||||
- pip install -r requirements.txt
|
||||
- pip install python-dotenv
|
||||
- python manage.py test
|
||||
|
||||
deploy:
|
||||
stage: deploy
|
||||
image: python:3.9-alpine
|
||||
before_script:
|
||||
- apk add --no-cache openssh-client
|
||||
- eval $(ssh-agent -s)
|
||||
- echo "$SSH_PRIVATE_KEY" | ssh-add -
|
||||
- ssh -o StrictHostKeyChecking=no $SSH_USER@$SERVER_IP echo "SSH-connection successful."
|
||||
script:
|
||||
- mkdir deploy
|
||||
- cp -r core deploy/
|
||||
- cp -r server deploy/
|
||||
- cp manage.py requirements.txt test.rest docker-compose.yml Dockerfile deploy/
|
||||
- ls deploy/
|
||||
- scp -r deploy $SSH_USER@$SERVER_IP:$DEPLOY_PATH/
|
||||
- ssh $SSH_USER@$SERVER_IP "cd $DEPLOY_PATH/deploy && docker compose down"
|
||||
- ssh $SSH_USER@$SERVER_IP "cd $DEPLOY_PATH/deploy && docker compose up -d"
|
||||
only:
|
||||
- main
|
19
Dockerfile
Normal file
19
Dockerfile
Normal file
|
@ -0,0 +1,19 @@
|
|||
FROM python:3.9
|
||||
|
||||
# Set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
ENV PYTHONPATH=/code:/code/CLINET_APPS
|
||||
|
||||
# Set work directory
|
||||
WORKDIR /code
|
||||
|
||||
# Update pip
|
||||
RUN pip install --upgrade pip
|
||||
|
||||
# Install dependencies
|
||||
COPY requirements.txt /code/
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
# Copy project
|
||||
COPY . /code/
|
7
REDME.md
Normal file
7
REDME.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
python3 manage.py runserver 0.0.0.0:8000
|
||||
|
||||
pm2 start "python3 manage.py runserver 0.0.0.0:8000 " --name "serverDjango"
|
||||
|
||||
pm2 start "npm run dev -- --host 0.0.0.0 " --name "vue"
|
||||
|
||||
npm run dev -- --host 0.0.0.0
|
0
core/__init__.py
Normal file
0
core/__init__.py
Normal file
11
core/admin.py
Normal file
11
core/admin.py
Normal file
|
@ -0,0 +1,11 @@
|
|||
from django.contrib import admin
|
||||
|
||||
from core.models.customer import Customer
|
||||
from core.models.role import Role
|
||||
# from .model import user
|
||||
from core.models.AssignedRule import AssignedRule
|
||||
|
||||
|
||||
admin.site.register(Role)
|
||||
admin.site.register(AssignedRule)
|
||||
admin.site.register(Customer)
|
16
core/asgi.py
Normal file
16
core/asgi.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
ASGI config for server project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.2/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
|
||||
|
||||
application = get_asgi_application()
|
47
core/migrations/0001_initial.py
Normal file
47
core/migrations/0001_initial.py
Normal file
|
@ -0,0 +1,47 @@
|
|||
# Generated by Django 5.0 on 2025-04-12 14:25
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Role',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=100, unique=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Customer',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('profile_img', models.CharField(max_length=255)),
|
||||
('profile_glb', models.CharField(max_length=255)),
|
||||
('mobile_number', models.CharField(max_length=15)),
|
||||
('verification_sms_code', models.CharField(blank=True, max_length=6)),
|
||||
('verification_email_code', models.CharField(blank=True, max_length=6)),
|
||||
('is_sms_verified', models.BooleanField(default=False)),
|
||||
('is_email_verified', models.BooleanField(default=False)),
|
||||
('sms_time_for_valid', models.DateTimeField(blank=True, null=True)),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AssignedRule',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
('role', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.role')),
|
||||
],
|
||||
),
|
||||
]
|
23
core/migrations/0002_alter_customer_profile_glb_and_more.py
Normal file
23
core/migrations/0002_alter_customer_profile_glb_and_more.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 5.0 on 2025-04-12 14:28
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='customer',
|
||||
name='profile_glb',
|
||||
field=models.CharField(blank=True, max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='customer',
|
||||
name='profile_img',
|
||||
field=models.CharField(blank=True, max_length=255),
|
||||
),
|
||||
]
|
0
core/migrations/__init__.py
Normal file
0
core/migrations/__init__.py
Normal file
9
core/models/AssignedRule.py
Normal file
9
core/models/AssignedRule.py
Normal file
|
@ -0,0 +1,9 @@
|
|||
# models.py
|
||||
from django.contrib.auth.models import User
|
||||
from .role import Role
|
||||
from django.db import models
|
||||
|
||||
class AssignedRule(models.Model):
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||
role = models.ForeignKey(Role, on_delete=models.CASCADE)
|
||||
# Add any other fields you need
|
23
core/models/customer.py
Normal file
23
core/models/customer.py
Normal file
|
@ -0,0 +1,23 @@
|
|||
|
||||
|
||||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
|
||||
class Customer(models.Model):
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||
|
||||
|
||||
mobile_number = models.CharField(max_length=15) # Adjust max length as per your needs
|
||||
profile_img = models.CharField(max_length=255,blank=True) # Adjust max length as per your needs
|
||||
profile_glb = models.CharField(max_length=255,blank=True) # Adjust max length as per your needs
|
||||
|
||||
verification_sms_code = models.CharField(blank=True,max_length=6) # Adjust max length as per your needs
|
||||
verification_email_code = models.CharField(blank=True,max_length=6) # Adjust max length as per your needs
|
||||
|
||||
is_sms_verified = models.BooleanField(default=False)
|
||||
is_email_verified = models.BooleanField(default=False)
|
||||
sms_time_for_valid = models.DateTimeField(blank=True, null=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.user.username
|
6
core/models/role.py
Normal file
6
core/models/role.py
Normal file
|
@ -0,0 +1,6 @@
|
|||
from django.db import models
|
||||
|
||||
class Role(models.Model):
|
||||
name = models.CharField(max_length=100, unique=True)
|
||||
|
||||
|
7
core/serializers/CustomerSerializer.py
Normal file
7
core/serializers/CustomerSerializer.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
from rest_framework import serializers
|
||||
from ..models.customer import Customer
|
||||
|
||||
class CustomerSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Customer
|
||||
fields = '__all__'
|
8
core/serializers/UserSerializer.py
Normal file
8
core/serializers/UserSerializer.py
Normal file
|
@ -0,0 +1,8 @@
|
|||
from rest_framework import serializers
|
||||
# from ..models.user import User
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
class UserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['id', 'username', 'email' , 'password' ]
|
0
core/serializers/__init__.py
Normal file
0
core/serializers/__init__.py
Normal file
161
core/settings.py
Normal file
161
core/settings.py
Normal file
|
@ -0,0 +1,161 @@
|
|||
"""
|
||||
Django settings for server project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 4.2.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.2/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/4.2/ref/settings/
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
import sys
|
||||
|
||||
# Load environment variables from .env file
|
||||
load_dotenv()
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = 'django-insecure-q1-xt8(+yr^6iye@sa3@miqn&(#-be96ild1s!o)wlmwqrzd3-'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
# ALLOWED_HOSTS = ['0.0.0.0']
|
||||
ALLOWED_HOSTS = ["*"]
|
||||
|
||||
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
'rest_framework',
|
||||
'rest_framework.authtoken',
|
||||
'core',
|
||||
'corsheaders'
|
||||
|
||||
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
|
||||
]
|
||||
CORS_ALLOW_ALL_ORIGINS = True # If this is used then `CORS_ALLOWED_ORIGINS` will not have any effect
|
||||
|
||||
ROOT_URLCONF = 'core.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'core.wsgi.application'
|
||||
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
|
||||
|
||||
if 'test' in sys.argv:
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'db.sqlite3',
|
||||
}
|
||||
}
|
||||
else:
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.mysql',
|
||||
'NAME': 'core1',
|
||||
'USER': 'root',
|
||||
'PASSWORD': '',
|
||||
'HOST': 'localhost',
|
||||
'PORT': '3306',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/4.2/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/4.2/howto/static-files/
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
|
||||
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
|
||||
EMAIL_HOST = 'smtp.ionos.de'
|
||||
EMAIL_PORT = 465
|
||||
EMAIL_USE_SSL = True
|
||||
EMAIL_HOST_USER = 'mail@clinet.club'
|
||||
EMAIL_HOST_PASSWORD = 'uzudzsd78786d7asd56gasdbsad'
|
||||
DEFAULT_FROM_EMAIL = 'mail@clinet.club'
|
||||
|
16
core/templates/emails/verification_email_template.html
Normal file
16
core/templates/emails/verification_email_template.html
Normal file
|
@ -0,0 +1,16 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
.verification-code {
|
||||
background-color: orange;
|
||||
color: black;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p>Hallo, Willkommen bei WZK. Ihr E-Mail-Bestätigungscode lautet:</p>
|
||||
<p class="verification-code">{{ verification_code }}</p>
|
||||
</body>
|
||||
</html>
|
764
core/templates/emails/verification_email_template2.html
Normal file
764
core/templates/emails/verification_email_template2.html
Normal file
|
@ -0,0 +1,764 @@
|
|||
<head>
|
||||
<title></title>
|
||||
<!--[if !mso]><!-- -->
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<!--<![endif]-->
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<style type="text/css">
|
||||
#outlook a {
|
||||
padding: 0;
|
||||
}
|
||||
.ReadMsgBody {
|
||||
width: 100%;
|
||||
}
|
||||
.ExternalClass {
|
||||
width: 100%;
|
||||
}
|
||||
.ExternalClass * {
|
||||
line-height: 100%;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
table,
|
||||
td {
|
||||
border-collapse: collapse;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
}
|
||||
img {
|
||||
border: 0;
|
||||
height: auto;
|
||||
line-height: 100%;
|
||||
outline: none;
|
||||
text-decoration: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
p {
|
||||
display: block;
|
||||
margin: 13px 0;
|
||||
}
|
||||
</style>
|
||||
<!--[if !mso]><!-->
|
||||
<style type="text/css">
|
||||
@media only screen and (max-width: 480px) {
|
||||
@-ms-viewport {
|
||||
width: 320px;
|
||||
}
|
||||
@viewport {
|
||||
width: 320px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<!--<![endif]-->
|
||||
<!--[if mso]>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:AllowPNG />
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
<![endif]-->
|
||||
<!--[if lte mso 11]>
|
||||
<style type="text/css">
|
||||
.outlook-group-fix {
|
||||
width: 100% !important;
|
||||
}
|
||||
</style>
|
||||
<![endif]-->
|
||||
|
||||
<!--[if !mso]><!-->
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700"
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
/>
|
||||
<style type="text/css">
|
||||
@import url(https://fonts.googleapis.com/css?family=Ubuntu:300,400,500,700);
|
||||
</style>
|
||||
<!--<![endif]-->
|
||||
<style type="text/css">
|
||||
@media only screen and (min-width: 480px) {
|
||||
.mj-column-per-100,
|
||||
* [aria-labelledby="mj-column-per-100"] {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="background: #f9f9f9">
|
||||
<div style="background-color: #f9f9f9">
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="640" align="center" style="width:640px;">
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<style type="text/css">
|
||||
html,
|
||||
body,
|
||||
* {
|
||||
-webkit-text-size-adjust: none;
|
||||
text-size-adjust: none;
|
||||
}
|
||||
a {
|
||||
color: #ffd512;
|
||||
text-decoration: none;
|
||||
}
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
<div style="margin: 0px auto; max-width: 640px; background: transparent">
|
||||
<table
|
||||
role="presentation"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
style="font-size: 0px; width: 100%; background: transparent"
|
||||
align="center"
|
||||
border="0"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
text-align: center;
|
||||
vertical-align: top;
|
||||
direction: ltr;
|
||||
font-size: 0px;
|
||||
padding: 40px 0px;
|
||||
"
|
||||
>
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td style="vertical-align:top;width:640px;">
|
||||
<![endif]-->
|
||||
<div
|
||||
aria-labelledby="mj-column-per-100"
|
||||
class="mj-column-per-100 outlook-group-fix"
|
||||
style="
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
direction: ltr;
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
"
|
||||
>
|
||||
<table
|
||||
role="presentation"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
width="100%"
|
||||
border="0"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
word-break: break-word;
|
||||
font-size: 0px;
|
||||
padding: 0px;
|
||||
"
|
||||
align="center"
|
||||
>
|
||||
<table
|
||||
role="presentation"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
style="border-collapse: collapse; border-spacing: 0px"
|
||||
align="center"
|
||||
border="0"
|
||||
></table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td></tr></table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td></tr></table>
|
||||
<![endif]-->
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="640" align="center" style="width:640px;">
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div
|
||||
style="
|
||||
max-width: 640px;
|
||||
margin: 0 auto;
|
||||
box-shadow: 0px 1px 5px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
margin: 0px auto;
|
||||
max-width: 640px;
|
||||
background: #ffd512
|
||||
url(https://wappapi.wz-kliniken.de/wzkregister/assets/login/login.jpg)
|
||||
top center / cover no-repeat;
|
||||
"
|
||||
>
|
||||
<!--[if mso | IE]>
|
||||
<v:rect xmlns:v="urn:schemas-microsoft-com:vml" fill="true" stroke="false" style="width:640px;">
|
||||
<v:fill origin="0.5, 0" position="0.5,0" type="tile" src="https://wappapi.wz-kliniken.de/wzkregister/assets/login/login.jpg" />
|
||||
<v:textbox style="mso-fit-shape-to-text:true" inset="0,0,0,0">
|
||||
<![endif]-->
|
||||
<table
|
||||
role="presentation"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
style="
|
||||
height: 230px;
|
||||
font-size: 0px;
|
||||
width: 100%;
|
||||
background: #ffd512
|
||||
url(https://wappapi.wz-kliniken.de/wzkregister/assets/login/login.jpg)
|
||||
top center / cover no-repeat;
|
||||
filter: blur(5px); /* Adjust the blur radius as needed */
|
||||
"
|
||||
align="center"
|
||||
border="0"
|
||||
background="https://wappapi.wz-kliniken.de/wzkregister/assets/login/login.jpg"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
text-align: center;
|
||||
vertical-align: top;
|
||||
direction: ltr;
|
||||
font-size: 0px;
|
||||
padding: 57px;
|
||||
"
|
||||
>
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td style="vertical-align:undefined;width:640px;">
|
||||
<![endif]-->
|
||||
<div
|
||||
style="
|
||||
cursor: auto;
|
||||
color: white;
|
||||
font-family: Whitney, Helvetica Neue, Helvetica, Arial,
|
||||
Lucida Grande, sans-serif;
|
||||
font-size: 36px;
|
||||
font-weight: 600;
|
||||
line-height: 36px;
|
||||
text-align: center;
|
||||
"
|
||||
></div>
|
||||
<!--[if mso | IE]>
|
||||
</td></tr></table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!--[if mso | IE]>
|
||||
</v:textbox>
|
||||
</v:rect>
|
||||
<![endif]-->
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td></tr></table>
|
||||
<![endif]-->
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="640" align="center" style="width:640px;">
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin: 0px auto; max-width: 640px; background: #ffffff">
|
||||
<table
|
||||
role="presentation"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
style="font-size: 0px; width: 100%; background: #ffffff"
|
||||
align="center"
|
||||
border="0"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
text-align: center;
|
||||
vertical-align: top;
|
||||
direction: ltr;
|
||||
font-size: 0px;
|
||||
padding: 40px 70px;
|
||||
"
|
||||
>
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td style="vertical-align:top;width:640px;">
|
||||
<![endif]-->
|
||||
<div
|
||||
aria-labelledby="mj-column-per-100"
|
||||
class="mj-column-per-100 outlook-group-fix"
|
||||
style="
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
direction: ltr;
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
"
|
||||
>
|
||||
<table
|
||||
role="presentation"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
width="100%"
|
||||
border="0"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
word-break: break-word;
|
||||
font-size: 0px;
|
||||
padding: 0px 0px 20px;
|
||||
"
|
||||
align="left"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
cursor: auto;
|
||||
color: #737f8d;
|
||||
font-family: Whitney, Helvetica Neue, Helvetica,
|
||||
Arial, Lucida Grande, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
text-align: left;
|
||||
"
|
||||
>
|
||||
<p>
|
||||
<!-- <img
|
||||
src="https://cdn.discordapp.com/email_assets/127c95bbea39cd4bc1ad87d1500ae27d.png"
|
||||
alt="Party Wumpus"
|
||||
title="None"
|
||||
width="500"
|
||||
style="height: auto"
|
||||
/> -->
|
||||
</p>
|
||||
|
||||
<h2
|
||||
style="
|
||||
font-family: Whitney, Helvetica Neue, Helvetica,
|
||||
Arial, Lucida Grande, sans-serif;
|
||||
font-weight: 500;
|
||||
font-size: 20px;
|
||||
color: #4f545c;
|
||||
letter-spacing: 0.27px;
|
||||
"
|
||||
>
|
||||
WZK Bestätigungscode,
|
||||
</h2>
|
||||
<p>
|
||||
Hallo, Willkommen bei WZK. Ihr
|
||||
E-Mail-Bestätigungscode lautet
|
||||
</p>
|
||||
<p>
|
||||
Before we get started, we'll need to verify your
|
||||
email.
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
word-break: break-word;
|
||||
font-size: 0px;
|
||||
padding: 10px 25px;
|
||||
"
|
||||
align="center"
|
||||
>
|
||||
<table
|
||||
role="presentation"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
style="border-collapse: separate"
|
||||
align="center"
|
||||
border="0"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
background: #ffd512;
|
||||
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
color: white;
|
||||
cursor: auto;
|
||||
padding: 15px 19px;
|
||||
"
|
||||
align="center"
|
||||
valign="middle"
|
||||
bgcolor="#7289DA"
|
||||
>
|
||||
<a
|
||||
href="#"
|
||||
style="
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
line-height: 100%;
|
||||
background: #ffd512;
|
||||
color: white;
|
||||
font-family: Ubuntu, Helvetica, Arial,
|
||||
sans-serif;
|
||||
font-size: 15px;
|
||||
font-weight: normal;
|
||||
text-transform: none;
|
||||
margin: 0px;
|
||||
"
|
||||
target="_blank"
|
||||
><p style="color: black">
|
||||
{{ verification_code }}
|
||||
</p>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td></tr></table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td></tr></table>
|
||||
<![endif]-->
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="640" align="center" style="width:640px;">
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
</div>
|
||||
<div style="margin: 0px auto; max-width: 640px; background: transparent">
|
||||
<table
|
||||
role="presentation"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
style="font-size: 0px; width: 100%; background: transparent"
|
||||
align="center"
|
||||
border="0"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
text-align: center;
|
||||
vertical-align: top;
|
||||
direction: ltr;
|
||||
font-size: 0px;
|
||||
padding: 0px;
|
||||
"
|
||||
>
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td style="vertical-align:top;width:640px;">
|
||||
<![endif]-->
|
||||
<div
|
||||
aria-labelledby="mj-column-per-100"
|
||||
class="mj-column-per-100 outlook-group-fix"
|
||||
style="
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
direction: ltr;
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
"
|
||||
>
|
||||
<table
|
||||
role="presentation"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
width="100%"
|
||||
border="0"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="word-break: break-word; font-size: 0px">
|
||||
<div style="font-size: 1px; line-height: 12px">
|
||||
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td></tr></table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td></tr></table>
|
||||
<![endif]-->
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="640" align="center" style="width:640px;">
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div
|
||||
style="
|
||||
margin: 0 auto;
|
||||
max-width: 640px;
|
||||
background: #ffffff;
|
||||
box-shadow: 0px 1px 5px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
"
|
||||
>
|
||||
<table
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
style="font-size: 0px; width: 100%; background: #ffffff"
|
||||
align="center"
|
||||
border="0"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
text-align: center;
|
||||
vertical-align: top;
|
||||
font-size: 0px;
|
||||
padding: 0px;
|
||||
"
|
||||
>
|
||||
<!--[if mso | IE]>
|
||||
<table border="0" cellpadding="0" cellspacing="0"><tr><td style="vertical-align:top;width:640px;">
|
||||
<![endif]-->
|
||||
<div
|
||||
aria-labelledby="mj-column-per-100"
|
||||
class="mj-column-per-100 outlook-group-fix"
|
||||
style="
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
direction: ltr;
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
"
|
||||
>
|
||||
<table
|
||||
role="presentation"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
width="100%"
|
||||
border="0"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
word-break: break-word;
|
||||
font-size: 0px;
|
||||
padding: 30px 70px 0px 70px;
|
||||
"
|
||||
align="center"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
cursor: auto;
|
||||
color: #43b581;
|
||||
font-family: Whitney, Helvetica Neue, Helvetica,
|
||||
Arial, Lucida Grande, sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
line-height: 16px;
|
||||
text-align: center;
|
||||
"
|
||||
>
|
||||
FUN FACT #16
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
word-break: break-word;
|
||||
font-size: 0px;
|
||||
padding: 14px 70px 30px 70px;
|
||||
"
|
||||
align="center"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
cursor: auto;
|
||||
color: #737f8d;
|
||||
font-family: Whitney, Helvetica Neue, Helvetica,
|
||||
Arial, Lucida Grande, sans-serif;
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
text-align: center;
|
||||
"
|
||||
>
|
||||
In Hearthstone, using the Hunter card Animal Companion
|
||||
against Kel'Thuzad will summon his cat Mr.
|
||||
Bigglesworth rather than the usual beasts.
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td></tr></table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td></tr></table>
|
||||
<![endif]-->
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" width="640" align="center" style="width:640px;">
|
||||
<tr>
|
||||
<td style="line-height:0px;font-size:0px;mso-line-height-rule:exactly;">
|
||||
<![endif]-->
|
||||
<div style="margin: 0px auto; max-width: 640px; background: transparent">
|
||||
<table
|
||||
role="presentation"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
style="font-size: 0px; width: 100%; background: transparent"
|
||||
align="center"
|
||||
border="0"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
text-align: center;
|
||||
vertical-align: top;
|
||||
direction: ltr;
|
||||
font-size: 0px;
|
||||
padding: 20px 0px;
|
||||
"
|
||||
>
|
||||
<!--[if mso | IE]>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0"><tr><td style="vertical-align:top;width:640px;">
|
||||
<![endif]-->
|
||||
<div
|
||||
aria-labelledby="mj-column-per-100"
|
||||
class="mj-column-per-100 outlook-group-fix"
|
||||
style="
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
direction: ltr;
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
"
|
||||
>
|
||||
<table
|
||||
role="presentation"
|
||||
cellpadding="0"
|
||||
cellspacing="0"
|
||||
width="100%"
|
||||
border="0"
|
||||
>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
word-break: break-word;
|
||||
font-size: 0px;
|
||||
padding: 0px;
|
||||
"
|
||||
align="center"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
cursor: auto;
|
||||
color: #99aab5;
|
||||
font-family: Whitney, Helvetica Neue, Helvetica,
|
||||
Arial, Lucida Grande, sans-serif;
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
"
|
||||
>
|
||||
Sent by Discord •
|
||||
<a
|
||||
href="https://blog.discordapp.com/"
|
||||
style="color: #1eb0f4; text-decoration: none"
|
||||
target="_blank"
|
||||
>check our blog</a
|
||||
>
|
||||
•
|
||||
<a
|
||||
href="https://twitter.com/discordapp"
|
||||
style="color: #1eb0f4; text-decoration: none"
|
||||
target="_blank"
|
||||
>@discordapp</a
|
||||
>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td
|
||||
style="
|
||||
word-break: break-word;
|
||||
font-size: 0px;
|
||||
padding: 0px;
|
||||
"
|
||||
align="center"
|
||||
>
|
||||
<div
|
||||
style="
|
||||
cursor: auto;
|
||||
color: #99aab5;
|
||||
font-family: Whitney, Helvetica Neue, Helvetica,
|
||||
Arial, Lucida Grande, sans-serif;
|
||||
font-size: 12px;
|
||||
line-height: 24px;
|
||||
text-align: center;
|
||||
"
|
||||
>
|
||||
444 De Haro Street, Suite 200, San Francisco, CA 94107
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td></tr></table>
|
||||
<![endif]-->
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<!--[if mso | IE]>
|
||||
</td></tr></table>
|
||||
<![endif]-->
|
||||
</div>
|
||||
</body>
|
0
core/tests/__init__.py
Normal file
0
core/tests/__init__.py
Normal file
57
core/tests/test_signup.py
Normal file
57
core/tests/test_signup.py
Normal file
|
@ -0,0 +1,57 @@
|
|||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase, APIClient
|
||||
from rest_framework import status
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework.authtoken.models import Token
|
||||
|
||||
class PatientSignUpTest(APITestCase):
|
||||
def setUp(self):
|
||||
self.client = APIClient()
|
||||
self.signup_url = reverse('signup') # Ensure you have named your URL in urls.py
|
||||
|
||||
# def test_signup_valid(self):
|
||||
# """
|
||||
# Ensure we can create a new user and patient with valid data.
|
||||
# """
|
||||
# data = {
|
||||
# 'fallnumber': '123',
|
||||
# 'username': 'newuser2',
|
||||
# 'password': 'newpassword123',
|
||||
# 'email': 'user2@example.com',
|
||||
# 'birthday': '1990-10-10',
|
||||
# 'mobile_number': '1234567890'
|
||||
# }
|
||||
# response = self.client.post(self.signup_url, data, format='json')
|
||||
# self.assertEqual(response.status_code, status.HTTP_201_CREATED)
|
||||
# self.assertIn('token', response.data)
|
||||
# self.assertEqual(User.objects.count(), 1)
|
||||
# self.assertEqual(Token.objects.count(), 1)
|
||||
|
||||
def test_signup_invalid_user_data(self):
|
||||
"""
|
||||
Ensure user data is validated.
|
||||
"""
|
||||
data = {
|
||||
'username': 'newuser', # Missing password and email
|
||||
'birthday': '2000-01-01',
|
||||
'mobile_number': '1234567890'
|
||||
}
|
||||
response = self.client.post(self.signup_url, data, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(User.objects.count(), 0)
|
||||
|
||||
def test_signup_invalid_patient_data(self):
|
||||
"""
|
||||
Ensure patient data is validated and no user is created if patient data is invalid.
|
||||
"""
|
||||
data = {
|
||||
'username': 'newuser',
|
||||
'password': 'newpassword123',
|
||||
'email': 'user@example.com',
|
||||
'birthday': 'not-a-date', # Invalid date format
|
||||
'mobile_number': '1234567890'
|
||||
}
|
||||
response = self.client.post(self.signup_url, data, format='json')
|
||||
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
self.assertEqual(User.objects.count(), 0)
|
||||
|
31
core/urls.py
Normal file
31
core/urls.py
Normal file
|
@ -0,0 +1,31 @@
|
|||
from django.urls import re_path
|
||||
from django.contrib import admin
|
||||
from django.urls import path
|
||||
|
||||
from .views import userView
|
||||
from django.urls import include, path
|
||||
|
||||
urlpatterns = [
|
||||
|
||||
path('admin/', admin.site.urls),
|
||||
|
||||
|
||||
# re_path(r'^signup/$', userView.signup, name="signup"),
|
||||
|
||||
re_path('signup', userView.signup , name="signup"),
|
||||
re_path('login', userView.login),
|
||||
re_path('getInfo', userView.getInfo),
|
||||
|
||||
re_path('sendSmsVerification', userView.sendSmsVerification),
|
||||
re_path('sendEmailVerification', userView.sendEmailVerification),
|
||||
re_path('submitEmailVerification', userView.submitEmailVerification),
|
||||
re_path('submitSmsVerification', userView.submitSmsVerification),
|
||||
|
||||
re_path('sendForgetPasswordCode', userView.sendForgetPasswordCode),
|
||||
re_path('sendCodeAndNewPassword', userView.sendCodeAndNewPassword),
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
]
|
58
core/views.py
Normal file
58
core/views.py
Normal file
|
@ -0,0 +1,58 @@
|
|||
from django.contrib.auth import get_user_model
|
||||
from rest_framework.authtoken.models import Token
|
||||
from rest_framework.decorators import api_view, authentication_classes, permission_classes
|
||||
from rest_framework.authentication import SessionAuthentication, TokenAuthentication
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
|
||||
# from .models.user import User
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from .serializers.UserSerializer import UserSerializer
|
||||
|
||||
# from .serializers.user import UserSerializer
|
||||
|
||||
|
||||
|
||||
# utils.py
|
||||
from .models.AssignedRule import AssignedRule
|
||||
|
||||
def user_has_role(user, role_name):
|
||||
return AssignedRule.objects.filter(user=user, role__name=role_name).exists()
|
||||
|
||||
|
||||
|
||||
@api_view(['POST'])
|
||||
def signup(request):
|
||||
serializer = UserSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
user = serializer.save()
|
||||
user.set_password(request.data['password'])
|
||||
user.save()
|
||||
token = Token.objects.create(user=user)
|
||||
return Response({'token': token.key, 'user': serializer.data})
|
||||
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@api_view(['POST'])
|
||||
def login(request):
|
||||
try:
|
||||
user = get_user_model().objects.get(email=request.data['email'])
|
||||
except get_user_model().DoesNotExist:
|
||||
return Response("User not found", status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
if not user.check_password(request.data['password']):
|
||||
return Response("Invalid password", status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
token, created = Token.objects.get_or_create(user=user)
|
||||
serializer = UserSerializer(user)
|
||||
return Response({'token': token.key, 'user': serializer.data})
|
||||
|
||||
@api_view(['GET'])
|
||||
@authentication_classes([SessionAuthentication, TokenAuthentication])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def test_token(request):
|
||||
if not user_has_role(request.user, 'admin'):
|
||||
return Response({'message': 'No access'}, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
return Response({'message': 'User has admin role'})
|
0
core/views/__init__.py
Normal file
0
core/views/__init__.py
Normal file
353
core/views/userView.py
Normal file
353
core/views/userView.py
Normal file
|
@ -0,0 +1,353 @@
|
|||
from pickle import TRUE
|
||||
import random
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from rest_framework.authtoken.models import Token
|
||||
from rest_framework.decorators import api_view, authentication_classes, permission_classes
|
||||
from rest_framework.authentication import SessionAuthentication, TokenAuthentication
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from core.models.customer import Customer
|
||||
from core.serializers.UserSerializer import UserSerializer
|
||||
from core.serializers.CustomerSerializer import CustomerSerializer
|
||||
|
||||
# utils.py
|
||||
from core.models.AssignedRule import AssignedRule
|
||||
|
||||
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.html import strip_tags
|
||||
from django.conf import settings
|
||||
from django.utils.dateparse import parse_date
|
||||
|
||||
|
||||
|
||||
import requests
|
||||
import json
|
||||
|
||||
def send_sms(to_number, code):
|
||||
username = "09399112092"
|
||||
password = "Dadechin123!@##!"
|
||||
from_number = "+983000505"
|
||||
pattern_code = "lgfrblbdppyn202"
|
||||
|
||||
url = "https://ippanel.com/patterns/pattern"
|
||||
to = [to_number]
|
||||
input_data = {"code": code}
|
||||
|
||||
params = {
|
||||
"username": username,
|
||||
"password": password,
|
||||
"from": from_number,
|
||||
"to": json.dumps(to),
|
||||
"input_data": json.dumps(input_data),
|
||||
"pattern_code": pattern_code
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(url, params=params, data=input_data)
|
||||
return response.text
|
||||
except Exception as e:
|
||||
return f"Error: {e}"
|
||||
|
||||
|
||||
def user_has_role(user, role_name):
|
||||
return AssignedRule.objects.filter(user=user, role__name=role_name).exists()
|
||||
|
||||
|
||||
@api_view(['POST'])
|
||||
def signup(request):
|
||||
# Check if username already exists
|
||||
if User.objects.filter(username=request.data['username']).exists():
|
||||
return Response({'username': ['A user with that username already exists.']}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Ensure mobile number is provided
|
||||
if 'mobile_number' not in request.data:
|
||||
return Response({'mobile_number': ['This field is required.']}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
# Proceed with user creation
|
||||
user_serializer = UserSerializer(data=request.data)
|
||||
if user_serializer.is_valid():
|
||||
user = user_serializer.save()
|
||||
user.set_password(request.data['password'])
|
||||
user.save()
|
||||
|
||||
customer_data = {
|
||||
'user': user.id,
|
||||
'mobile_number': request.data['mobile_number'], # Ensure mobile number is provided
|
||||
}
|
||||
customer_serializer = CustomerSerializer(data=customer_data)
|
||||
if customer_serializer.is_valid():
|
||||
customer_serializer.save()
|
||||
token = Token.objects.create(user=user)
|
||||
return Response({'token': token.key, 'user': customer_serializer.data}, status=status.HTTP_201_CREATED)
|
||||
else:
|
||||
# If customer data is invalid, delete the created user
|
||||
user.delete()
|
||||
return Response(customer_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
else:
|
||||
return Response(user_serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@api_view(['POST'])
|
||||
def sendForgetPasswordCode(request):
|
||||
try:
|
||||
|
||||
|
||||
# Retrieve the associated customer object
|
||||
customer = Customer.objects.get(mobile_number=request.data['mobile_number'])
|
||||
|
||||
|
||||
# Generate a random verification code
|
||||
verification_code = str(random.randint(100000, 999999))
|
||||
|
||||
# Update user's verification_code and sms_time_for_valid
|
||||
customer.verification_email_code = verification_code
|
||||
customer.verification_sms_code = verification_code
|
||||
customer.save()
|
||||
|
||||
sendEmail(customer.user.email,verification_code)
|
||||
|
||||
return Response({"message": ("Email verification code sent successfully. " + customer.user.email)})
|
||||
|
||||
except Customer.DoesNotExist:
|
||||
# If no customer object exists for the user, return an error response
|
||||
return Response({'error': 'No customer data found for this user'}, status=404)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
from django.contrib.auth.hashers import make_password
|
||||
|
||||
@api_view(['POST'])
|
||||
def sendCodeAndNewPassword(request):
|
||||
try:
|
||||
# Retrieve the associated customer object
|
||||
customer = Customer.objects.get(mobile_number=request.data['mobile_number'], verification_sms_code=request.data['verification_sms_code'])
|
||||
|
||||
if customer:
|
||||
# Update the user's password
|
||||
customer.user.password = make_password(request.data['password'])
|
||||
customer.user.save()
|
||||
return Response({"message": "Password has been changed successfully."})
|
||||
else:
|
||||
return Response({"message": "Wrong mobile number or verification code."})
|
||||
|
||||
except Customer.DoesNotExist:
|
||||
# If no customer object exists for the user, return an error response
|
||||
return Response({'message': 'No customer data found for this user'}, status=404)
|
||||
|
||||
|
||||
|
||||
|
||||
@api_view(['POST'])
|
||||
def login(request):
|
||||
try:
|
||||
customer = Customer.objects.get(mobile_number=request.data['mobile_number'])
|
||||
except get_user_model().DoesNotExist:
|
||||
return Response("User not found", status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
if not customer.user.check_password(request.data['password']):
|
||||
return Response("Invalid password", status=status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
token, created = Token.objects.get_or_create(user=customer.user)
|
||||
serializer = UserSerializer(customer.user)
|
||||
return Response({'token': token.key, 'user': serializer.data})
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
@authentication_classes([SessionAuthentication, TokenAuthentication])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def sendSmsVerification(request):
|
||||
# Retrieve the current user
|
||||
user = request.user
|
||||
|
||||
# Assuming a OneToOneField relation between User and Customer
|
||||
try:
|
||||
# Retrieve the associated customer object
|
||||
customer = Customer.objects.get(user=user)
|
||||
|
||||
|
||||
# Generate a random verification code
|
||||
verification_code = str(random.randint(100000, 999999))
|
||||
|
||||
# Update user's verification_code and sms_time_for_valid
|
||||
customer.verification_sms_code = verification_code
|
||||
customer.sms_time_for_valid = datetime.now() + timedelta(minutes=10) # Set validity time to now + 10 minutes
|
||||
customer.save()
|
||||
|
||||
sms_response = send_sms(customer.mobile_number, verification_code)
|
||||
|
||||
# Send SMS with verification code (implement this part according to your SMS service provider's API)
|
||||
# send_sms(user.mobile_number, verification_code) # You need to implement this function
|
||||
|
||||
return Response({
|
||||
"message": "SMS verification code sent successfully.",
|
||||
"sms_response": sms_response
|
||||
})
|
||||
|
||||
except Customer.DoesNotExist:
|
||||
# If no customer object exists for the user, return an error response
|
||||
return Response({'error': 'No customer data found for this user'}, status=404)
|
||||
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
@authentication_classes([SessionAuthentication, TokenAuthentication])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def getInfo(request):
|
||||
# Retrieve the current user
|
||||
user = request.user
|
||||
|
||||
# Assuming a OneToOneField relation between User and Customer
|
||||
try:
|
||||
# Retrieve the associated customer object
|
||||
customer = Customer.objects.get(user=user)
|
||||
|
||||
# Serialize the customer data
|
||||
serializer = CustomerSerializer(customer)
|
||||
|
||||
# Return the serialized customer data
|
||||
return Response(serializer.data)
|
||||
except Customer.DoesNotExist:
|
||||
return Response({'error': 'No customer data found for this user'}, status=404)
|
||||
|
||||
# If no customer object exists for the user, return an error response
|
||||
|
||||
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
@authentication_classes([SessionAuthentication, TokenAuthentication])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def sendEmailVerification(request):
|
||||
# Retrieve the current user
|
||||
user = request.user
|
||||
|
||||
# Assuming a OneToOneField relation between User and Customer
|
||||
try:
|
||||
# Retrieve the associated customer object
|
||||
customer = Customer.objects.get(user=user)
|
||||
|
||||
|
||||
# Generate a random verification code
|
||||
verification_code = str(random.randint(100000, 999999))
|
||||
|
||||
# Update user's verification_code and sms_time_for_valid
|
||||
customer.verification_email_code = verification_code
|
||||
customer.save()
|
||||
|
||||
sendEmail(user.email,verification_code)
|
||||
|
||||
return Response({"message": ("Email verification code sent successfully. " + user.email)})
|
||||
|
||||
except Customer.DoesNotExist:
|
||||
# If no customer object exists for the user, return an error response
|
||||
return Response({'error': 'No customer data found for this user'}, status=404)
|
||||
|
||||
|
||||
|
||||
|
||||
def sendEmail(email, verification_code):
|
||||
subject = 'WZK Bestätigungscode'
|
||||
context = {'verification_code': verification_code}
|
||||
|
||||
# Render HTML content from a template
|
||||
# html_content = render_to_string('core/templates/emails/email_template.html', context)
|
||||
html_content = render_to_string('emails/verification_email_template2.html', context)
|
||||
|
||||
# Create a plain text version of the email (optional)
|
||||
text_content = strip_tags(html_content)
|
||||
|
||||
email_from = settings.DEFAULT_FROM_EMAIL
|
||||
recipient_list = [email]
|
||||
|
||||
try:
|
||||
# Create an EmailMultiAlternatives object
|
||||
msg = EmailMultiAlternatives(subject, text_content, email_from, recipient_list)
|
||||
|
||||
# Attach the HTML content to the email
|
||||
msg.attach_alternative(html_content, "text/html")
|
||||
|
||||
# Send the email
|
||||
msg.send()
|
||||
|
||||
print("Email has been sent! ")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error sending email: {e}")
|
||||
return False
|
||||
|
||||
|
||||
|
||||
|
||||
@api_view(['POST'])
|
||||
@authentication_classes([SessionAuthentication, TokenAuthentication])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def submitEmailVerification(request):
|
||||
try:
|
||||
user = request.user
|
||||
|
||||
customer = Customer.objects.get(user=user)
|
||||
|
||||
if customer.verification_email_code == request.data['verification_email_code'] :
|
||||
customer.is_email_verified = True
|
||||
customer.save()
|
||||
return Response({"message": "Email Verified!"})
|
||||
# else :
|
||||
# return Response({"message": customer.verification_email_code})
|
||||
|
||||
|
||||
|
||||
except get_user_model().DoesNotExist:
|
||||
return Response("Not verified 1", status=status.HTTP_404_NOT_FOUND)
|
||||
return Response("Not verified ", status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
||||
|
||||
|
||||
@api_view(['POST'])
|
||||
@authentication_classes([SessionAuthentication, TokenAuthentication])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def submitSmsVerification(request):
|
||||
try:
|
||||
user = request.user
|
||||
|
||||
customer = Customer.objects.get(user=user)
|
||||
|
||||
if customer.verification_sms_code == request.data['verification_sms_code'] :
|
||||
customer.is_sms_verified = True
|
||||
customer.save()
|
||||
return Response({"message": "SMS Verified!"})
|
||||
# else :
|
||||
# return Response({"message": customer.verification_email_code})
|
||||
|
||||
|
||||
|
||||
except get_user_model().DoesNotExist:
|
||||
return Response("Not verified 1", status=status.HTTP_404_NOT_FOUND)
|
||||
return Response("Not verified ", status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@api_view(['GET'])
|
||||
@authentication_classes([SessionAuthentication, TokenAuthentication])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def test_token(request):
|
||||
if not user_has_role(request.user, 'admin'):
|
||||
return Response({'message': 'No access'}, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
return Response({'message': 'User has admin role'})
|
16
core/wsgi.py
Normal file
16
core/wsgi.py
Normal file
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
WSGI config for server project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/4.2/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
|
||||
|
||||
application = get_wsgi_application()
|
30
docker-compose.yml
Normal file
30
docker-compose.yml
Normal file
|
@ -0,0 +1,30 @@
|
|||
version: "3.6"
|
||||
|
||||
services:
|
||||
db:
|
||||
image: mysql:latest
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: CLINET999zztt
|
||||
MYSQL_DATABASE: mysql_clinet
|
||||
MYSQL_USER: clinet_dbadmin_pg
|
||||
MYSQL_PASSWORD: CLINET999zztt
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql_test_newplatform/data
|
||||
|
||||
web:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
command: sh -c "sleep 20 && python manage.py runserver 0.0.0.0:55"
|
||||
volumes:
|
||||
- .:/code
|
||||
ports:
|
||||
- "55:55"
|
||||
depends_on:
|
||||
- db
|
||||
environment:
|
||||
DJANGO_SETTINGS_MODULE: core.settings
|
||||
MYSQL_UNIX_PORT: /var/run/mysqld/mysqld.sock
|
||||
|
||||
volumes:
|
||||
mysql_data:
|
22
manage.py
Normal file
22
manage.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
7
requirements.txt
Normal file
7
requirements.txt
Normal file
|
@ -0,0 +1,7 @@
|
|||
djangorestframework==3.15.1
|
||||
django-cors-headers==4.3.1
|
||||
pymysql==1.1.0
|
||||
python-dotenv==1.0.1
|
||||
mysqlclient==2.1.1
|
||||
|
||||
py -m pip install Django==5.2
|
49
server/views.py
Normal file
49
server/views.py
Normal file
|
@ -0,0 +1,49 @@
|
|||
from rest_framework.decorators import api_view, authentication_classes, permission_classes
|
||||
from rest_framework.authentication import SessionAuthentication, TokenAuthentication
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.contrib.auth.models import User
|
||||
from rest_framework.authtoken.models import Token
|
||||
# from models.user import User
|
||||
# from .models.serializers import UserSerializer
|
||||
|
||||
from .models.user import UserSerializer
|
||||
|
||||
|
||||
# utils.py
|
||||
from .models.AssignedRule import AssignedRule
|
||||
|
||||
def user_has_role(user, role_name):
|
||||
return AssignedRule.objects.filter(user=user, role__name=role_name).exists()
|
||||
|
||||
|
||||
|
||||
@api_view(['POST'])
|
||||
def signup(request):
|
||||
serializer = UserSerializer(data=request.data)
|
||||
if serializer.is_valid():
|
||||
serializer.save()
|
||||
user = User.objects.get(username=request.data['username'])
|
||||
user.set_password(request.data['password'])
|
||||
user.save()
|
||||
token = Token.objects.create(user=user)
|
||||
return Response({'token': token.key, 'user': serializer.data})
|
||||
return Response(serializer.errors, status=status.HTTP_200_OK)
|
||||
|
||||
@api_view(['POST'])
|
||||
def login(request):
|
||||
user = get_object_or_404(User, username=request.data['username'])
|
||||
if not user.check_password(request.data['password']):
|
||||
return Response("missing user", status=status.HTTP_404_NOT_FOUND)
|
||||
token, created = Token.objects.get_or_create(user=user)
|
||||
serializer = UserSerializer(user)
|
||||
return Response({'token': token.key, 'user': serializer.data})
|
||||
|
||||
@api_view(['GET'])
|
||||
@authentication_classes([SessionAuthentication, TokenAuthentication])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def test_token(request):
|
||||
return Response("passed!")
|
71
test.rest
Normal file
71
test.rest
Normal file
|
@ -0,0 +1,71 @@
|
|||
# https://marketplace.visualstudio.com/items?itemName=humao.rest-client
|
||||
|
||||
POST http://127.0.0.1:8000/signup
|
||||
Content-Type: application/json
|
||||
|
||||
{ "username": "adam3", "password": "Pass1234!", "email": "adam2@mail.com" , "mobile_number":"09140086509" }
|
||||
|
||||
###
|
||||
|
||||
POST http://127.0.0.1:8000/login
|
||||
Content-Type: application/json
|
||||
|
||||
{ "mobile_number":"09140086509", "password": "Pass1234!" }
|
||||
|
||||
###
|
||||
|
||||
GET http://127.0.0.1:8000/sendSmsVerification
|
||||
Content-Type: application/json
|
||||
Authorization: token cb8c2ef7913df31085e749398f22da5b43f419b2
|
||||
|
||||
###
|
||||
|
||||
POST http://127.0.0.1:8000/submitSmsVerification
|
||||
Content-Type: application/json
|
||||
Authorization: token cb8c2ef7913df31085e749398f22da5b43f419b2
|
||||
|
||||
{ "verification_sms_code": "807806" }
|
||||
|
||||
###
|
||||
|
||||
GET http://127.0.0.1:8000/test_token
|
||||
Content-Type: application/json
|
||||
Authorization: token c362581117e209735d412226e54596867e370892
|
||||
# Authorization: token 53e2b003a92e22aca85c95088a438ece8d9a5dfb
|
||||
|
||||
|
||||
###
|
||||
|
||||
GET http://127.0.0.1:8000/getInfo
|
||||
Content-Type: application/json
|
||||
Authorization: token 3d5ab31449b6a075e3967559526d5e31977431a1
|
||||
# Authorization: token 53e2b003a92e22aca85c95088a438ece8d9a5dfb
|
||||
|
||||
|
||||
###
|
||||
GET http://127.0.0.1:8000/templatequestions
|
||||
Content-Type: application/json
|
||||
Authorization: token c362581117e209735d412226e54596867e370892
|
||||
|
||||
|
||||
###
|
||||
GET http://127.0.0.1:8000/templatequestionscgm
|
||||
Content-Type: application/json
|
||||
Authorization: token c362581117e209735d412226e54596867e370892
|
||||
|
||||
|
||||
###
|
||||
GET http://127.0.0.1:8000/templatepages
|
||||
Content-Type: application/json
|
||||
Authorization: token c362581117e209735d412226e54596867e370892
|
||||
|
||||
|
||||
###
|
||||
|
||||
POST http://127.0.0.1:8000/savetemplateform
|
||||
Content-Type: application/json
|
||||
Authorization: token c362581117e209735d412226e54596867e370892
|
||||
|
||||
{ "question_id": "1", "answer_text": "answer1" }
|
||||
|
||||
###
|
Loading…
Reference in New Issue
Block a user