From 8bb529d5eefec11360e7aacb287120d6427793bd Mon Sep 17 00:00:00 2001 From: Bobby Date: Sun, 15 Dec 2024 19:11:48 -0500 Subject: user stats --- apps/blog/views.py | 8 +- ignis/views.py | 2 +- internal/weblog_utilities.py | 9 +- middleware/userprofilemiddleware.py | 2 +- services/users/__init__.py | 0 services/users/accountFunctions.py | 59 ++++ services/users/admin.py | 7 + services/users/apps.py | 6 + services/users/forms.py | 279 +++++++++++++++++++ services/users/mail_send.py | 75 ++++++ services/users/migrations/0001_initial.py | 29 ++ .../migrations/0002_userprofile_email_verified.py | 18 ++ services/users/migrations/0003_captchastore.py | 22 ++ ...aptchastore_id_alter_captchastore_csrf_token.py | 22 ++ ...aptchastore_id_alter_captchastore_csrf_token.py | 24 ++ .../users/migrations/0006_delete_captchastore.py | 16 ++ services/users/migrations/0007_captchastore.py | 22 ++ ...8_remove_userprofile_gravatar_email_and_more.py | 22 ++ .../users/migrations/0009_delete_captchastore.py | 16 ++ .../migrations/0010_userprofile_blinkie_url.py | 17 ++ services/users/migrations/0011_tokenstore.py | 42 +++ .../migrations/0012_alter_tokenstore_expires.py | 17 ++ services/users/migrations/__init__.py | 0 services/users/models.py | 36 +++ .../templates/email_change_verification_email.html | 27 ++ services/users/templates/reset_password_email.html | 25 ++ services/users/templates/verification_email.html | 26 ++ services/users/tests.py | 3 + services/users/tokens.py | 45 ++++ services/users/urls.py | 31 +++ services/users/views.py | 300 +++++++++++++++++++++ static/css/shared/core.css | 65 ++++- static/css/shared/login-area.css | 4 +- static/images/core/icons/coin.png | Bin 0 -> 5172 bytes templates/en/core/home.html | 2 +- templates/ja/core/home.html | 1 + templates/shared/right_sidebar.html | 42 +++ thatcomputerscientist/settings.py | 4 +- thatcomputerscientist/urls.py | 1 + users/__init__.py | 0 users/accountFunctions.py | 48 ---- users/admin.py | 7 - users/apps.py | 6 - users/forms.py | 279 ------------------- users/mail_send.py | 75 ------ users/migrations/0001_initial.py | 29 -- .../migrations/0002_userprofile_email_verified.py | 18 -- users/migrations/0003_captchastore.py | 22 -- ...aptchastore_id_alter_captchastore_csrf_token.py | 22 -- ...aptchastore_id_alter_captchastore_csrf_token.py | 24 -- users/migrations/0006_delete_captchastore.py | 16 -- users/migrations/0007_captchastore.py | 22 -- ...8_remove_userprofile_gravatar_email_and_more.py | 22 -- users/migrations/0009_delete_captchastore.py | 16 -- users/migrations/0010_userprofile_blinkie_url.py | 17 -- users/migrations/0011_tokenstore.py | 42 --- users/migrations/0012_alter_tokenstore_expires.py | 17 -- users/migrations/__init__.py | 0 users/models.py | 36 --- .../templates/email_change_verification_email.html | 27 -- users/templates/reset_password_email.html | 25 -- users/templates/verification_email.html | 26 -- users/tests.py | 3 - users/tokens.py | 45 ---- users/urls.py | 31 --- users/views.py | 300 --------------------- 66 files changed, 1311 insertions(+), 1190 deletions(-) create mode 100644 services/users/__init__.py create mode 100644 services/users/accountFunctions.py create mode 100644 services/users/admin.py create mode 100644 services/users/apps.py create mode 100644 services/users/forms.py create mode 100644 services/users/mail_send.py create mode 100644 services/users/migrations/0001_initial.py create mode 100644 services/users/migrations/0002_userprofile_email_verified.py create mode 100644 services/users/migrations/0003_captchastore.py create mode 100644 services/users/migrations/0004_remove_captchastore_id_alter_captchastore_csrf_token.py create mode 100644 services/users/migrations/0005_captchastore_id_alter_captchastore_csrf_token.py create mode 100644 services/users/migrations/0006_delete_captchastore.py create mode 100644 services/users/migrations/0007_captchastore.py create mode 100644 services/users/migrations/0008_remove_userprofile_gravatar_email_and_more.py create mode 100644 services/users/migrations/0009_delete_captchastore.py create mode 100644 services/users/migrations/0010_userprofile_blinkie_url.py create mode 100644 services/users/migrations/0011_tokenstore.py create mode 100644 services/users/migrations/0012_alter_tokenstore_expires.py create mode 100644 services/users/migrations/__init__.py create mode 100644 services/users/models.py create mode 100644 services/users/templates/email_change_verification_email.html create mode 100644 services/users/templates/reset_password_email.html create mode 100644 services/users/templates/verification_email.html create mode 100644 services/users/tests.py create mode 100644 services/users/tokens.py create mode 100644 services/users/urls.py create mode 100644 services/users/views.py create mode 100644 static/images/core/icons/coin.png delete mode 100644 users/__init__.py delete mode 100644 users/accountFunctions.py delete mode 100644 users/admin.py delete mode 100644 users/apps.py delete mode 100644 users/forms.py delete mode 100644 users/mail_send.py delete mode 100644 users/migrations/0001_initial.py delete mode 100644 users/migrations/0002_userprofile_email_verified.py delete mode 100644 users/migrations/0003_captchastore.py delete mode 100644 users/migrations/0004_remove_captchastore_id_alter_captchastore_csrf_token.py delete mode 100644 users/migrations/0005_captchastore_id_alter_captchastore_csrf_token.py delete mode 100644 users/migrations/0006_delete_captchastore.py delete mode 100644 users/migrations/0007_captchastore.py delete mode 100644 users/migrations/0008_remove_userprofile_gravatar_email_and_more.py delete mode 100644 users/migrations/0009_delete_captchastore.py delete mode 100644 users/migrations/0010_userprofile_blinkie_url.py delete mode 100644 users/migrations/0011_tokenstore.py delete mode 100644 users/migrations/0012_alter_tokenstore_expires.py delete mode 100644 users/migrations/__init__.py delete mode 100644 users/models.py delete mode 100644 users/templates/email_change_verification_email.html delete mode 100644 users/templates/reset_password_email.html delete mode 100644 users/templates/verification_email.html delete mode 100644 users/tests.py delete mode 100644 users/tokens.py delete mode 100644 users/urls.py delete mode 100644 users/views.py diff --git a/apps/blog/views.py b/apps/blog/views.py index fe9bc41f..07022263 100644 --- a/apps/blog/views.py +++ b/apps/blog/views.py @@ -23,15 +23,15 @@ from haystack.query import SearchQuerySet from user_agents import parse from apps.administration.models import Announcement -from users.accountFunctions import verify_token -from users.forms import ( +from services.users.accountFunctions import verify_token +from services.users.forms import ( RegisterForm, ResetPasswordForm, UpdateUserDetailsForm, ForgotPasswordForm, ) -from users.models import UserProfile -from users.tokens import CaptchaTokenGenerator +from services.users.models import UserProfile +from services.users.tokens import CaptchaTokenGenerator from .context_processors import ( add_excerpt, diff --git a/ignis/views.py b/ignis/views.py index aa9a87d5..f681f64d 100644 --- a/ignis/views.py +++ b/ignis/views.py @@ -9,7 +9,7 @@ from django.views.decorators.csrf import csrf_exempt from PIL import Image from apps.blog.models import Post -from users.tokens import CaptchaTokenGenerator +from services.users.tokens import CaptchaTokenGenerator from .models import PostImage, RepositoryTitle diff --git a/internal/weblog_utilities.py b/internal/weblog_utilities.py index a6eede4b..db4fc5d5 100644 --- a/internal/weblog_utilities.py +++ b/internal/weblog_utilities.py @@ -1,6 +1,8 @@ from apps.blog.models import Post, Comment from bs4 import BeautifulSoup +AUTHOR_USERNAME = "bobby" + def add_excerpt(post, lang="en"): if lang == "ja": @@ -14,7 +16,6 @@ def add_excerpt(post, lang="en"): if len(excerpt) >= 1000: break - print(excerpt) return excerpt @@ -23,8 +24,10 @@ def add_num_comments(post): return num_comments -def recent_weblogs(lang="en"): - recent_posts = Post.objects.filter(is_public=True).order_by("-date")[:5] +def recent_weblogs(lang="en", amount=3): + recent_posts = Post.objects.filter( + is_public=True, author__username=AUTHOR_USERNAME + ).order_by("-date")[:amount] for post in recent_posts: post.excerpt = add_excerpt(post, lang) post.num_comments = add_num_comments(post) diff --git a/middleware/userprofilemiddleware.py b/middleware/userprofilemiddleware.py index 25e43c14..dc6c8bac 100644 --- a/middleware/userprofilemiddleware.py +++ b/middleware/userprofilemiddleware.py @@ -1,5 +1,5 @@ from django.utils.deprecation import MiddlewareMixin -from users.models import UserProfile +from services.users.models import UserProfile class UserProfileMiddleware(MiddlewareMixin): diff --git a/services/users/__init__.py b/services/users/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/services/users/accountFunctions.py b/services/users/accountFunctions.py new file mode 100644 index 00000000..036b549a --- /dev/null +++ b/services/users/accountFunctions.py @@ -0,0 +1,59 @@ +import secrets +import uuid + +from django.utils import timezone + +from services.users.models import TokenStore, UserProfile + + +def generate_token(): + uid = uuid.uuid4().hex + token = secrets.token_urlsafe(32) + print(uid, token) + return uid, token + + +def store_token(token_type, user, email=None): + previous_tokens = TokenStore.objects.filter(user=user, token_type=token_type) + if previous_tokens.exists(): + previous_tokens.delete() + uid, token = generate_token() + token_store = TokenStore.objects.create( + user=user, + email=email if email is not None else user.email, + uid=uid, + token=token, + token_type=token_type, + expires=timezone.now() + timezone.timedelta(minutes=30), + ) + token_store.save() + return uid, token + + +def verify_token(token_type, uid, token, hold_verification=False): + try: + token_store = TokenStore.objects.get( + token_type=token_type, uid=uid, token=token + ) + if ( + token_store.expires > timezone.now() + and not token_store.verified + and token_store.token_type == token_type + and token_store.uid == uid + and token_store.token == token + ): + + if hold_verification: + return token_store + token_store.verified = True + + if token_type == "verifyemail": + UserProfile.objects.filter(user=token_store.user).update( + email_verified=True + ) + + token_store.save() + + return token_store + except TokenStore.DoesNotExist: + return None diff --git a/services/users/admin.py b/services/users/admin.py new file mode 100644 index 00000000..0f1d5fdb --- /dev/null +++ b/services/users/admin.py @@ -0,0 +1,7 @@ +from django.contrib import admin + +# Register your models here. +from .models import TokenStore, UserProfile + +admin.site.register(UserProfile) +admin.site.register(TokenStore) diff --git a/services/users/apps.py b/services/users/apps.py new file mode 100644 index 00000000..dafff467 --- /dev/null +++ b/services/users/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class UsersConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "services.users" diff --git a/services/users/forms.py b/services/users/forms.py new file mode 100644 index 00000000..39d503fc --- /dev/null +++ b/services/users/forms.py @@ -0,0 +1,279 @@ +# Registration form + +import string +from random import choice + +from django import forms +from django.contrib.auth.models import User +from django.template.loader import render_to_string +from django.utils.html import strip_tags + +from apps.blog.context_processors import avatar_list +from services.users.models import UserProfile + +from .accountFunctions import store_token +from .mail_send import send_email + + +class RegisterForm(forms.Form): + username = forms.CharField( + label="Username", + max_length=30, + min_length=4, + required=True, + widget=forms.TextInput( + attrs={"placeholder": "Username", "autocomplete": "off"} + ), + ) + email = forms.EmailField( + label="Email", + max_length=255, + required=True, + widget=forms.EmailInput(attrs={"placeholder": "Email", "autocomplete": "off"}), + ) + password1 = forms.CharField( + label="Password", + min_length=8, + required=True, + widget=forms.PasswordInput(attrs={"placeholder": "Password"}), + ) + password2 = forms.CharField( + label="Password (again)", + widget=forms.PasswordInput(attrs={"placeholder": "Password (again)"}), + min_length=8, + required=True, + ) + captcha = forms.CharField( + label="Captcha", + max_length=6, + min_length=6, + required=True, + widget=forms.TextInput(attrs={"placeholder": "Captcha", "autocomplete": "off"}), + ) + expected_captcha = None + protected_usernames = [ + "admin", + "administrator", + "root", + "thatcomputerscientist", + "skippy", + "system", + "test", + "user", + "webmaster", + "www", + "postmaster", + "hostmaster", + "info", + "support", + "anonymous", + "guest", + "nobody", + "someone", + "moderator", + "moderators", + "mods", + "crvs", + ] + allowed_chars = string.ascii_letters + string.digits + + def __init__(self, *args, **kwargs): + if "expected_captcha" in kwargs: + self.expected_captcha = kwargs.pop("expected_captcha") + super().__init__(*args, **kwargs) + + def clean(self): + cleaned_data = super().clean() + password1 = cleaned_data.get("password1") + password2 = cleaned_data.get("password2") + captcha = cleaned_data.get("captcha") + if password1 and password2: + if password1 != password2: + raise forms.ValidationError("Passwords do not match.") + if len(password1) < 8: + raise forms.ValidationError("Password must be at least 8 characters long.") + if str.lower(captcha) != str.lower(self.expected_captcha): + raise forms.ValidationError("Captcha does not match.") + if User.objects.filter(username=cleaned_data.get("username")).exists(): + raise forms.ValidationError( + "Username not available. Please choose another." + ) + if cleaned_data.get("username").lower() in self.protected_usernames: + raise forms.ValidationError( + "Username not available. Please choose another." + ) + for char in cleaned_data.get("username"): + if char not in self.allowed_chars: + raise forms.ValidationError( + "Username contains invalid characters. Only A-Z, a-z, and 0-9 are allowed." + ) + if User.objects.filter(email=cleaned_data.get("email")).exists(): + raise forms.ValidationError( + "Email already exists. Please login if this account is yours." + ) + return cleaned_data + + def save(self, request): + user = User.objects.create_user( + username=self.cleaned_data.get("username").lower(), + email=self.cleaned_data.get("email").lower(), + password=self.cleaned_data.get("password1"), + ) + user.save() + user_profile = UserProfile.objects.create(user=user) + avatar_dir = choice(list(avatar_list().keys())) + avatar_file = choice(avatar_list()[avatar_dir]) + user_profile.avatar_url = avatar_dir + "/" + avatar_file.replace(".gif", "") + user_profile.save() + + uid, token = store_token(token_type="verifyemail", user=user, email=user.email) + + # Send verification email + subject = "Verify your email address" + message = render_to_string( + "verification_email.html", + { + "user": user.username if user.first_name is None else user.first_name, + "site_name": "Shifoo", + "uid": uid, + "token": token, + "protocol": "https://" if request.is_secure() else "http://", + "domain": request.get_host(), + }, + ) + # message = strip_tags(message) + # send_mail(subject, message, 'Shifoo <' + settings.EMAIL_HOST_USER + '>', [user.email], fail_silently=False) + if send_email( + sender="noreply@thatcomputerscientist.com", + sender_name="Shifoo", + recipient=user.email, + subject=subject, + body_html=message, + body_text=message, + ): + return user + else: + return user + + +class ForgotPasswordForm(forms.Form): + email = forms.EmailField(label="Email", required=True) + + def clean(self): + cleaned_data = super().clean() + return cleaned_data + + def save(self, request): + email = self.cleaned_data.get("email") + user = User.objects.get(email=email) + uid, token = store_token( + token_type="resetpassword", user=user, email=user.email + ) + subject = "Reset your password" + message = render_to_string( + "reset_password_email.html", + { + "user": user.username if user.first_name is None else user.first_name, + "site_name": "Shifoo", + "uid": uid, + "token": token, + "protocol": "https://" if request.is_secure() else "http://", + "domain": request.get_host(), + }, + ) + # message = strip_tags(message) + if send_email( + sender="noreply@thatcomputerscientist.com", + sender_name="Shifoo", + recipient=user.email, + subject=subject, + body_html=message, + body_text=message, + ): + return user + else: + raise forms.ValidationError("Failed to send email.") + + +class ResetPasswordForm(forms.Form): + password1 = forms.CharField( + label="New Password", widget=forms.PasswordInput, min_length=8 + ) + password2 = forms.CharField( + label="New Password (again)", widget=forms.PasswordInput, min_length=8 + ) + + def clean(self): + cleaned_data = super().clean() + password1 = cleaned_data.get("password1") + password2 = cleaned_data.get("password2") + if password1 and password2: + if password1 != password2: + raise forms.ValidationError("Passwords do not match.") + if len(password1) < 8: + raise forms.ValidationError("Password must be at least 8 characters long.") + return cleaned_data + + def save(self, user): + user.set_password(self.cleaned_data.get("password1")) + user.save() + return user + + +class UpdateUserDetailsForm(forms.Form): + first_name = forms.CharField( + label="First name", + max_length=30, + required=False, + widget=forms.TextInput(attrs={"placeholder": "First name"}), + ) + last_name = forms.CharField( + label="Last name", + max_length=30, + required=False, + widget=forms.TextInput(attrs={"placeholder": "Last name"}), + ) + location = forms.CharField( + label="Location", + max_length=30, + required=False, + widget=forms.TextInput(attrs={"placeholder": "Location"}), + ) + bio = forms.CharField( + label="Bio", + max_length=500, + required=False, + widget=forms.Textarea(attrs={"placeholder": "Bio"}), + ) + is_public = forms.ChoiceField( + label="Activity Visibility", + choices=((True, "Public"), (False, "Private")), + widget=forms.RadioSelect, + ) + email_public = forms.ChoiceField( + label="Email Visibility", + choices=((True, "Public"), (False, "Private")), + widget=forms.RadioSelect, + ) + + def __init__(self, *args, **kwargs): + self.user = kwargs.pop("user") + super().__init__(*args, **kwargs) + + def clean(self): + cleaned_data = super().clean() + return cleaned_data + + def save(self): + self.user.first_name = self.cleaned_data.get("first_name") + self.user.last_name = self.cleaned_data.get("last_name") + self.user.save() + + user_profile = UserProfile.objects.get(user=self.user) + user_profile.location = self.cleaned_data.get("location") + user_profile.bio = self.cleaned_data.get("bio") + user_profile.is_public = self.cleaned_data.get("is_public") + user_profile.email_public = self.cleaned_data.get("email_public") + user_profile.save() + + return (self.user, user_profile) diff --git a/services/users/mail_send.py b/services/users/mail_send.py new file mode 100644 index 00000000..df837a32 --- /dev/null +++ b/services/users/mail_send.py @@ -0,0 +1,75 @@ +# python script for sending SMTP configuration with Oracle Cloud Infrastructure Email Delivery +import email.utils +import smtplib +import ssl +from email.message import EmailMessage + +from django.conf import settings + + +def send_email(sender, sender_name, recipient, subject, body_html, body_text): + # Replace sender@example.com with your "From" address. + # This address must be verified. + # this is the approved sender email + SENDER = sender + SENDERNAME = sender_name + + # Replace recipient@example.com with a "To" address. If your account + # is still in the sandbox, this address must be verified. + RECIPIENT = recipient + + # Replace the USERNAME_SMTP value with your Email Delivery SMTP username. + USERNAME_SMTP = settings.USERNAME_SMTP + + # Put the PASSWORD value from your Email Delivery SMTP password into the following file. + PASSWORD_SMTP = settings.PASSWORD_SMTP + + # If you're using Email Delivery in a different region, replace the HOST value with an appropriate SMTP endpoint. + # Use port 25 or 587 to connect to the SMTP endpoint. + HOST = settings.EMAIL_HOST + PORT = settings.EMAIL_PORT + + # The subject line of the email. + SUBJECT = subject + + # The email body for recipients with non-HTML email clients. + BODY_TEXT = body_text + + # The HTML body of the email. + BODY_HTML = body_html + + # create message container + msg = EmailMessage() + msg["Subject"] = SUBJECT + msg["From"] = email.utils.formataddr((SENDERNAME, SENDER)) + msg["To"] = RECIPIENT + + # make the message multi-part alternative, making the content the first part + msg.add_alternative(BODY_TEXT, subtype="text") + # this adds the additional part to the message + # According to RFC 2046, the last part of a multipart message, in this case + # the HTML message, is best and preferred. + msg.add_alternative(BODY_HTML, subtype="html") + + # Try to send the message. + try: + server = smtplib.SMTP(HOST, PORT) + server.ehlo() + # most python runtimes default to a set of trusted public CAs that will include the CA used by OCI Email Delivery. + # However, on platforms lacking that default (or with an outdated set of CAs), customers may need to provide a capath that includes our public CA. + server.starttls( + context=ssl.create_default_context( + purpose=ssl.Purpose.SERVER_AUTH, cafile=None, capath=None + ) + ) + # smtplib docs recommend calling ehlo() before & after starttls() + server.ehlo() + server.login(USERNAME_SMTP, PASSWORD_SMTP) + # our requirement is that SENDER is the same as From address set previously + server.sendmail(SENDER, RECIPIENT, msg.as_string()) + server.close() + # Display an error message if something goes wrong. + except Exception as e: + return e + else: + return True diff --git a/services/users/migrations/0001_initial.py b/services/users/migrations/0001_initial.py new file mode 100644 index 00000000..a8a78c6b --- /dev/null +++ b/services/users/migrations/0001_initial.py @@ -0,0 +1,29 @@ +# Generated by Django 4.0.6 on 2022-07-29 15:58 + +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='UserProfile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('location', models.CharField(blank=True, max_length=50)), + ('bio', models.TextField(blank=True)), + ('gravatar_email', models.EmailField(blank=True, max_length=254)), + ('is_public', models.BooleanField(default=False)), + ('email_public', models.BooleanField(default=False)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/services/users/migrations/0002_userprofile_email_verified.py b/services/users/migrations/0002_userprofile_email_verified.py new file mode 100644 index 00000000..ca89b815 --- /dev/null +++ b/services/users/migrations/0002_userprofile_email_verified.py @@ -0,0 +1,18 @@ +# Generated by Django 4.0.6 on 2022-07-29 18:34 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='userprofile', + name='email_verified', + field=models.BooleanField(default=False), + ), + ] diff --git a/services/users/migrations/0003_captchastore.py b/services/users/migrations/0003_captchastore.py new file mode 100644 index 00000000..9aebc0aa --- /dev/null +++ b/services/users/migrations/0003_captchastore.py @@ -0,0 +1,22 @@ +# Generated by Django 4.0.6 on 2022-09-05 22:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0002_userprofile_email_verified'), + ] + + operations = [ + migrations.CreateModel( + name='CaptchaStore', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('captcha_string', models.CharField(max_length=6)), + ('csrf_token', models.CharField(max_length=100)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + ), + ] diff --git a/services/users/migrations/0004_remove_captchastore_id_alter_captchastore_csrf_token.py b/services/users/migrations/0004_remove_captchastore_id_alter_captchastore_csrf_token.py new file mode 100644 index 00000000..1dd16bcc --- /dev/null +++ b/services/users/migrations/0004_remove_captchastore_id_alter_captchastore_csrf_token.py @@ -0,0 +1,22 @@ +# Generated by Django 4.0.6 on 2022-09-05 22:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0003_captchastore'), + ] + + operations = [ + migrations.RemoveField( + model_name='captchastore', + name='id', + ), + migrations.AlterField( + model_name='captchastore', + name='csrf_token', + field=models.CharField(max_length=100, primary_key=True, serialize=False), + ), + ] diff --git a/services/users/migrations/0005_captchastore_id_alter_captchastore_csrf_token.py b/services/users/migrations/0005_captchastore_id_alter_captchastore_csrf_token.py new file mode 100644 index 00000000..bc3d86e5 --- /dev/null +++ b/services/users/migrations/0005_captchastore_id_alter_captchastore_csrf_token.py @@ -0,0 +1,24 @@ +# Generated by Django 4.0.6 on 2022-09-05 23:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0004_remove_captchastore_id_alter_captchastore_csrf_token'), + ] + + operations = [ + migrations.AddField( + model_name='captchastore', + name='id', + field=models.BigAutoField(auto_created=True, default=1, primary_key=True, serialize=False, verbose_name='ID'), + preserve_default=False, + ), + migrations.AlterField( + model_name='captchastore', + name='csrf_token', + field=models.CharField(max_length=100), + ), + ] diff --git a/services/users/migrations/0006_delete_captchastore.py b/services/users/migrations/0006_delete_captchastore.py new file mode 100644 index 00000000..f324ff2f --- /dev/null +++ b/services/users/migrations/0006_delete_captchastore.py @@ -0,0 +1,16 @@ +# Generated by Django 4.0.6 on 2022-09-05 23:47 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0005_captchastore_id_alter_captchastore_csrf_token'), + ] + + operations = [ + migrations.DeleteModel( + name='CaptchaStore', + ), + ] diff --git a/services/users/migrations/0007_captchastore.py b/services/users/migrations/0007_captchastore.py new file mode 100644 index 00000000..15ac458a --- /dev/null +++ b/services/users/migrations/0007_captchastore.py @@ -0,0 +1,22 @@ +# Generated by Django 4.0.6 on 2022-09-05 23:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0006_delete_captchastore'), + ] + + operations = [ + migrations.CreateModel( + name='CaptchaStore', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('captcha_string', models.CharField(max_length=6)), + ('csrf_token', models.CharField(max_length=100)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + ), + ] diff --git a/services/users/migrations/0008_remove_userprofile_gravatar_email_and_more.py b/services/users/migrations/0008_remove_userprofile_gravatar_email_and_more.py new file mode 100644 index 00000000..07f33320 --- /dev/null +++ b/services/users/migrations/0008_remove_userprofile_gravatar_email_and_more.py @@ -0,0 +1,22 @@ +# Generated by Django 4.0.6 on 2022-12-28 10:05 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('users', '0007_captchastore'), + ] + + operations = [ + migrations.RemoveField( + model_name='userprofile', + name='gravatar_email', + ), + migrations.AddField( + model_name='userprofile', + name='avatar_url', + field=models.TextField(blank=True), + ), + ] diff --git a/services/users/migrations/0009_delete_captchastore.py b/services/users/migrations/0009_delete_captchastore.py new file mode 100644 index 00000000..dbde60c8 --- /dev/null +++ b/services/users/migrations/0009_delete_captchastore.py @@ -0,0 +1,16 @@ +# Generated by Django 4.1.4 on 2022-12-28 12:41 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("users", "0008_remove_userprofile_gravatar_email_and_more"), + ] + + operations = [ + migrations.DeleteModel( + name="CaptchaStore", + ), + ] diff --git a/services/users/migrations/0010_userprofile_blinkie_url.py b/services/users/migrations/0010_userprofile_blinkie_url.py new file mode 100644 index 00000000..06f5e53c --- /dev/null +++ b/services/users/migrations/0010_userprofile_blinkie_url.py @@ -0,0 +1,17 @@ +# Generated by Django 4.1.4 on 2023-04-04 01:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("users", "0009_delete_captchastore"), + ] + + operations = [ + migrations.AddField( + model_name="userprofile", + name="blinkie_url", + field=models.TextField(blank=True, default=""), + ), + ] diff --git a/services/users/migrations/0011_tokenstore.py b/services/users/migrations/0011_tokenstore.py new file mode 100644 index 00000000..f0bf9330 --- /dev/null +++ b/services/users/migrations/0011_tokenstore.py @@ -0,0 +1,42 @@ +# Generated by Django 4.1.4 on 2023-04-30 03:19 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("users", "0010_userprofile_blinkie_url"), + ] + + operations = [ + migrations.CreateModel( + name="TokenStore", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ("uid", models.TextField(unique=True)), + ("token", models.TextField(unique=True)), + ("email", models.EmailField(blank=True, max_length=254)), + ("token_type", models.CharField(max_length=50)), + ("expires", models.DateTimeField(auto_now_add=True)), + ("verified", models.BooleanField(default=False)), + ( + "user", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + ), + ] diff --git a/services/users/migrations/0012_alter_tokenstore_expires.py b/services/users/migrations/0012_alter_tokenstore_expires.py new file mode 100644 index 00000000..4b3481fd --- /dev/null +++ b/services/users/migrations/0012_alter_tokenstore_expires.py @@ -0,0 +1,17 @@ +# Generated by Django 4.1.4 on 2023-05-03 01:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("users", "0011_tokenstore"), + ] + + operations = [ + migrations.AlterField( + model_name="tokenstore", + name="expires", + field=models.DateTimeField(), + ), + ] diff --git a/services/users/migrations/__init__.py b/services/users/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/services/users/models.py b/services/users/models.py new file mode 100644 index 00000000..f11f2f46 --- /dev/null +++ b/services/users/models.py @@ -0,0 +1,36 @@ +from django.conf import settings +from django.db import models + + +# User Profile Model +class UserProfile(models.Model): + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + ) + location = models.CharField(max_length=50, blank=True) + bio = models.TextField(blank=True) + avatar_url = models.TextField(blank=True) + is_public = models.BooleanField(default=False) + email_public = models.BooleanField(default=False) + email_verified = models.BooleanField(default=False) + blinkie_url = models.TextField(blank=True, default='') + + def __str__(self): + return self.user.username + + +class TokenStore(models.Model): + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + ) + uid = models.TextField(unique=True) + token = models.TextField(unique=True) + email = models.EmailField(blank=True) + token_type = models.CharField(max_length=50) + expires = models.DateTimeField() + verified = models.BooleanField(default=False) + + def __str__(self): + return self.user.username diff --git a/services/users/templates/email_change_verification_email.html b/services/users/templates/email_change_verification_email.html new file mode 100644 index 00000000..f6f9b127 --- /dev/null +++ b/services/users/templates/email_change_verification_email.html @@ -0,0 +1,27 @@ + + + + + + + Change Email + + +

Change Your Current Email

+

Hi {{ user }},

+

+ We received a request to change your email address on {{ site_name }}. To + verify and change your email address, please click the link below. +

+ Change Email +

If the above link does not work, copy and paste the URL below into your browser:

+ {{ protocol }}{{ domain }}{% url 'users:changeemail' 'changeemail' uid token %} + +

Please ignore this email if you did not make this request.

+

Thanks,

+

Bobby from {{ site_name }}

+ + diff --git a/services/users/templates/reset_password_email.html b/services/users/templates/reset_password_email.html new file mode 100644 index 00000000..f9651a13 --- /dev/null +++ b/services/users/templates/reset_password_email.html @@ -0,0 +1,25 @@ + + + + + + + Reset Password + + +

Reset Your Current Password

+

Hi {{ user }},

+

+ We received a request to reset your password on {{ site_name }}. To reset + your password, please click the link below. +

+ Reset Password +

If the above link does not work, copy and paste the URL below into your browser:

+ {{ protocol }}{{ domain }}{% url 'blog:resetpassword' uid token %} +

Please ignore this email if you did not make this request.

+

Thanks,

+

Bobby from {{ site_name }}

+ + diff --git a/services/users/templates/verification_email.html b/services/users/templates/verification_email.html new file mode 100644 index 00000000..06e91a79 --- /dev/null +++ b/services/users/templates/verification_email.html @@ -0,0 +1,26 @@ + + + + + + + Verify Email + + +

Verify Your New Account

+

Hi {{ user }},

+

+ Thanks for registering an account on {{ site_name }}. To verify your email + address, please click the link below. +

+ Verify Email +

If the above link does not work, copy and paste the URL below into your browser:

+ {{ protocol }}{{ domain }}{% url 'users:changeemail' 'verifyemail' uid token %} +

Please ignore this email if you did not make this request.

+

Thanks,

+

Bobby from {{ site_name }}

+ + diff --git a/services/users/tests.py b/services/users/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/services/users/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/services/users/tokens.py b/services/users/tokens.py new file mode 100644 index 00000000..05d73362 --- /dev/null +++ b/services/users/tokens.py @@ -0,0 +1,45 @@ +import os + +from Crypto.Cipher import AES +from django.contrib.auth.tokens import PasswordResetTokenGenerator +from dotenv import load_dotenv +from six import text_type + +load_dotenv() + +class AccountActivationTokenGenerator(PasswordResetTokenGenerator): + def _make_hash_value(self, user, timestamp): + return ( + text_type(user.pk) + text_type(timestamp) + + text_type(user.is_active) + ) + +class EmailChangeTokenGenerator(): + def encrypt(self, email): + auth_string = os.getenv('AUTHORIZATION_STRING') + key = auth_string.encode('utf-8')[0:16] + cipher = AES.new(key, AES.MODE_CFB, key) + return cipher.encrypt(email.encode('utf-8')).hex() + + + def decrypt(self, token): + auth_string = os.getenv('AUTHORIZATION_STRING') + key = auth_string.encode('utf-8')[0:16] + cipher = AES.new(key, AES.MODE_CFB, key) + return cipher.decrypt(bytes.fromhex(token)).decode('utf-8') + + +class CaptchaTokenGenerator(): + def encrypt(self, captcha_string): + auth_string = os.getenv('AUTHORIZATION_STRING') + key = auth_string.encode('utf-8')[0:16] + cipher = AES.new(key, AES.MODE_CFB, key) + return cipher.encrypt(captcha_string.encode('utf-8')).hex() + + def decrypt(self, token): + auth_string = os.getenv('AUTHORIZATION_STRING') + key = auth_string.encode('utf-8')[0:16] + cipher = AES.new(key, AES.MODE_CFB, key) + return cipher.decrypt(bytes.fromhex(token)).decode('utf-8') + +account_activation_token = AccountActivationTokenGenerator() diff --git a/services/users/urls.py b/services/users/urls.py new file mode 100644 index 00000000..b7081e42 --- /dev/null +++ b/services/users/urls.py @@ -0,0 +1,31 @@ +from django.contrib import admin +from django.urls import path + +from . import views + +app_name = "users" +urlpatterns = [ + path("/login", views.login_user, name="login"), + path("/logout", views.logout_user, name="logout"), + path("/update", views.update_user, name="update"), + path("/changepassword", views.change_password, name="changepassword"), + path( + "/sendchangeuseremail", views.send_change_user_email, name="sendchangeuseremail" + ), + path( + "/sendverificationemail", + views.send_verification_email, + name="sendverificationemail", + ), + path("/updateavatar", views.update_avatar, name="updateavatar"), + path("/updateblinkies", views.update_blinkie, name="updateblinkie"), + path("/delete", views.delete_user, name="delete"), + path("///", views.verify_email, name="verifyemail"), + path("///", views.verify_email, name="changeemail"), + path("/resetpassword//", views.reset_password, name="resetpassword"), +] + +# Configure Admin Site +admin.site.site_header = "Shifoo Administation" +admin.site.site_title = "Shifoo" +admin.site.index_title = "Administration Area" diff --git a/services/users/views.py b/services/users/views.py new file mode 100644 index 00000000..5dba135b --- /dev/null +++ b/services/users/views.py @@ -0,0 +1,300 @@ +from django.contrib import messages +from django.contrib.auth import authenticate, login, logout, update_session_auth_hash +from django.contrib.auth.models import User +from django.http import HttpResponse, HttpResponseRedirect +from django.shortcuts import redirect, reverse +from django.template.loader import render_to_string +from django.utils.html import strip_tags + +from .accountFunctions import store_token, verify_token +from .forms import UpdateUserDetailsForm +from .mail_send import send_email +from .models import UserProfile + + +# Create your views here. +def login_user(request): + # pass + next = request.POST.get("next", "blog:home") + username = request.POST["username"] + password = request.POST["password"] + if username == "" or password == "" or username is None or password is None: + # required fields are empty + messages.error(request, "RFEERR", extra_tags="loginError") + return HttpResponseRedirect(next + "?username=" + username) + else: + # check if email is verified + user = authenticate(request, username=username, password=password) + if user is not None: + try: + email_verified = UserProfile.objects.get(user=user.pk).email_verified + except: + # user has no profile + email_verified = False + if email_verified: + login(request, user) + return HttpResponseRedirect(next) + else: + # email not verified + messages.error(request, "ENVERR", extra_tags="loginError") + return HttpResponseRedirect(next + "?username=" + username) + else: + # invalid credentials + messages.error(request, "IUOPERR", extra_tags="loginError") + return HttpResponseRedirect(next + "?username=" + username) + + +def logout_user(request): + logout(request) + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + + +def update_user(request): + user = request.user + if user is not None: + if request.method == "POST": + form = UpdateUserDetailsForm(request.POST, user=user) + if form.is_valid(): + form.save() + messages.success(request, "Profile was successfully updated!") + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + else: + messages.error( + request, "Unable to update profile! Please try again later." + ) + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + else: + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + else: + messages.error(request, "You must be logged in to update your profile!") + return redirect("blog:home") + + +def delete_user(request): + user = request.user + if user is not None: + if request.method == "POST": + password = request.POST["password"] + if user.check_password(password): + # delete user, all comments, user profile details, and all posts + user.delete() + messages.success(request, "Your account was successfully deleted!") + return redirect("blog:home") + else: + messages.error(request, "Incorrect password!") + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + else: + messages.error(request, "Unable to delete account! Please try again later.") + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + else: + messages.error(request, "You must be logged in to delete your account!") + return redirect("blog:home") + + +def update_avatar(request): + user = request.user + if user is not None: + if request.method == "POST": + user_profile = UserProfile.objects.get(user=user) + user_profile.avatar_url = request.POST["avatar"] + user_profile.save() + messages.success(request, "Avatar was successfully updated!") + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + else: + messages.error(request, "Unable to update avatar! Please try again later.") + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + else: + messages.error(request, "You must be logged in to update your avatar!") + return redirect("blog:home") + + +def update_blinkie(request): + user = request.user + if user is not None: + if request.method == "POST": + user_profile = UserProfile.objects.get(user=user) + user_profile.blinkie_url = request.POST["blinkie"] + user_profile.save() + messages.success(request, "Blinkie was successfully updated!") + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + else: + messages.error(request, "Unable to update blinkie! Please try again later.") + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + else: + messages.error(request, "You must be logged in to update your blinkie!") + return redirect("blog:home") + + +def change_password(request): + username = request.user + old_password = request.POST["oldPassword"] + new_password = request.POST["newPassword"] + confirm_password = request.POST["confirmPassword"] + if username is not None: + user = User.objects.get(username=username) + if user.check_password(old_password): + if new_password == confirm_password: + if len(new_password) < 8: + messages.error( + request, "The new password must be at least 8 characters long!" + ) + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + user.set_password(new_password) + user.save() + update_session_auth_hash(request, user) + messages.success(request, "Password was successfully changed!") + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + else: + messages.error( + request, "The new password and confirmation password do not match!" + ) + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + else: + messages.error(request, "Old password is incorrect!") + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + else: + messages.error(request, "Unable to change password! Please try again later.") + return redirect("blog:home") + + +def send_change_user_email(request): + user = request.user + new_email = request.POST["email"] + if user is not None: + # Check if the new and the old email are the same + if user.email == new_email: + messages.error(request, "New email is the same as the old one!") + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + + # check if email is already in use + if User.objects.filter(email=new_email).exists(): + messages.error(request, "Email is already in use!") + # Redirect to referrer + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + # Send verification email + subject = "Change your email address" + uid, token = store_token(token_type="changeemail", user=user, email=new_email) + + message = render_to_string( + "email_change_verification_email.html", + { + "user": user.username if user.first_name is None else user.first_name, + "site_name": "Shifoo", + "uid": uid, + "token": token, + "protocol": request.scheme + "://", + "domain": request.get_host(), + }, + ) + # message = strip_tags(message) + # send_mail(subject, message, 'That Computer Scientist <' + settings.EMAIL_HOST_USER + '>', [new_email]) + + if send_email( + sender="noreply@thatcomputerscientist.com", + sender_name="Shifoo", + recipient=new_email, + subject=subject, + body_html=message, + body_text=message, + ): + messages.success( + request, "Verification email was sent! Please check your email." + ) + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + else: + messages.error(request, "Unable to change email! Please try again later.") + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + + else: + messages.error(request, "Unable to change email! Please try again later.") + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + + +def send_verification_email(request): + # this is a post only view + if request.method == "POST": + username = request.POST.get("username") + subject = "Verify your email address" + user = User.objects.get(username=username) + uid, token = store_token(token_type="verifyemail", user=user, email=user.email) + + message = render_to_string( + "verification_email.html", + { + "user": user.username if user.first_name is None else user.first_name, + "site_name": "Shifoo", + "uid": uid, + "token": token, + "protocol": "https://" if request.is_secure() else "http://", + "domain": request.get_host(), + }, + ) + # message = strip_tags(message) + if send_email( + sender="noreply@thatcomputerscientist.com", + sender_name="Shifoo", + recipient=user.email, + subject=subject, + body_html=message, + body_text=message, + ): + messages.success(request, "VESENT", extra_tags="loginError") + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + else: + messages.error(request, "VESENDERR", extra_tags="loginError") + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + else: + messages.error(request, "VESENDERR", extra_tags="loginError") + return HttpResponseRedirect(request.META.get("HTTP_REFERER")) + + +def verify_email(request, mode, uid, token): + token_object = verify_token(mode, uid, token) + redirect_to = ( + reverse("blog:account") + "?tab=email" if mode == "changeemail" else "blog:home" + ) + success_message = ( + "Email was successfully changed!" if mode == "changeemail" else "VESUCCESS" + ) + error_message = "Unable to verify email! Please try again later." + + if token_object is not None and token_object.verified: + user = User.objects.get(pk=token_object.user_id) + user.email = token_object.email + user.save() + token_object.delete() + messages.success( + request, + success_message, + extra_tags="loginError" if mode == "verifyemail" else "", + ) + return redirect(redirect_to) + else: + messages.error(request, error_message) + return redirect(redirect_to) + + +def reset_password(request, uid, token): + mode = "resetpassword" + token_object = verify_token(mode, uid, token) + + # Token is not verified yet, but confirmed that it belongs to the user + # Now we send a form for the user to reset their password + if token_object is not None and token_object.verified: + print(token_object.user_id) + # redirect to forgotpassword/reset?uid=uid&token=token + return HttpResponseRedirect( + reverse("blog:resetpassword") + + "?uid=" + + token_object.user_id + + "&token=" + + token + ) + else: + # Token is invalid + messages.error( + request, + "Unable to reset password! Please try again later.", + extra_tags="passwordReset", + ) + return redirect("blog:forgotpassword") diff --git a/static/css/shared/core.css b/static/css/shared/core.css index 02d41657..094f9555 100644 --- a/static/css/shared/core.css +++ b/static/css/shared/core.css @@ -1,11 +1,13 @@ /* Reset and Base Styles */ -@font-face { +@import url('https://fonts.googleapis.com/css2?family=M+PLUS+Rounded+1c:wght@100;300;400;500;700;800;900&display=swap'); + +/* @font-face { font-family: 'Klee'; src: url('../../fonts/KleeOne-Regular.ttf') format('truetype'); font-weight: 400; font-style: normal; font-display: swap -} +} */ @font-face { font-family: 'SweetFairy'; @@ -39,7 +41,7 @@ body { body, textarea, input { - font-family: 'Klee'; + font-family: 'M PLUS Rounded 1c'; font-size: 12px; font-weight: 400; } @@ -259,6 +261,63 @@ html[lang='ja'] .navigation-title { margin-right: 8px; } +.user-stats-area { + margin: 12px 0px 0px 0px; + background-color: #f4f1e90f; + padding: 8px 0px; + border-radius: 8px; +} + +.stats-profile-image { + width: 120px; + height: 120px; + margin: 4px auto; + display: block; +} + +.stats-username, +.stats-bio { + margin: 4px 0px; +} + +.stats-bio { + padding: 4px 0px; + border-top: 1px solid #fff; + border-bottom: 1px solid #fff; +} + +.user-stats { + display: flex; + justify-content: space-between; + padding: 4px; + background-color: #6446858a; +} + +.user-stats:nth-child(odd) { + background-color: #54266a85; +} + +.stat-links a { + display: block; + padding: 4px; +} + +.stat-links a::before { + content: '>'; + margin-right: 4px; +} + +.stat-value { + display: flex; + align-items: center; + gap: 2px; +} + +.stat-value img { + width: 12px; + height: 12px; +} + /* Sidebar Search Bar */ #search-area { diff --git a/static/css/shared/login-area.css b/static/css/shared/login-area.css index cb3d1a07..b2e86506 100644 --- a/static/css/shared/login-area.css +++ b/static/css/shared/login-area.css @@ -152,7 +152,7 @@ .user-avatar { width: 72px; height: 72px; - border: solid 2px #8d8dff; + /* border: solid 2px #8d8dff; */ padding: 4px; position: absolute; bottom: 0; @@ -162,4 +162,6 @@ .user-avatar img { width: 100%; height: 100%; + border-radius: 50%; + } \ No newline at end of file diff --git a/static/images/core/icons/coin.png b/static/images/core/icons/coin.png new file mode 100644 index 00000000..4be6fe09 Binary files /dev/null and b/static/images/core/icons/coin.png differ diff --git a/templates/en/core/home.html b/templates/en/core/home.html index 281560c2..8075b6b1 100644 --- a/templates/en/core/home.html +++ b/templates/en/core/home.html @@ -50,7 +50,6 @@
{% include 'partials/weblog_list.html' with posts=recent_weblogs %}
-

Recent Weeb Degenerecy

{% for anime in recent_mal_activity.anime %} @@ -68,6 +67,7 @@ {% endfor %}
+
{% endblock %} {% block scripts %} diff --git a/templates/ja/core/home.html b/templates/ja/core/home.html index 20d8747e..6d566a93 100644 --- a/templates/ja/core/home.html +++ b/templates/ja/core/home.html @@ -67,6 +67,7 @@ {% endfor %} +
{% endblock %} {% block scripts %} diff --git a/templates/shared/right_sidebar.html b/templates/shared/right_sidebar.html index 10c5c96b..3a071310 100644 --- a/templates/shared/right_sidebar.html +++ b/templates/shared/right_sidebar.html @@ -45,6 +45,48 @@
+ + {% if user.is_authenticated %} +
+ {{ user.first_name }}'s Avatar +

{{ user.first_name }} {{ user.last_name }}

+

{{ user.profile.bio|linebreaksbr }}

+
+

{% if request.LANGUAGE_CODE == 'ja' %}経験値{% else %}XP{% endif %}

+

{{ user.profile.experience }}0 / 1000

+
+
+

{% if request.LANGUAGE_CODE == 'ja' %}レベル{% else %}Level{% endif %}

+

{{ user.profile.level }}1

+
+
+

{% if request.LANGUAGE_CODE == 'ja' %}残高{% else %}Balance{% endif %}

+

+ {{ user.profile.balance }}0 + Coin Icon +

+
+
+

{% if request.LANGUAGE_CODE == 'ja' %}ジャーナルエントリー{% else %}Journal Entries{% endif %}

+

{{ user.profile.journal_entries }}0

+
+
+

{% if request.LANGUAGE_CODE == 'ja' %}ジャーナル連続記録{% else %}Journal Streak{% endif %}

+

{{ user.profile.journal_streak }}0 days

+
+
+

{% if request.LANGUAGE_CODE == 'ja' %}ウェブログ投稿{% else %}Weblog Posts{% endif %}

+

{{ user.profile.weblog_posts }}0

+
+ +
+ + {% endif %} +