Skip to content

Commit

Permalink
Merge pull request #350 from wizarrrr/develop
Browse files Browse the repository at this point in the history
release v4 beta
  • Loading branch information
PhantomMantis authored Apr 11, 2024
2 parents 80e95dc + 642b3cd commit 4b6c722
Show file tree
Hide file tree
Showing 63 changed files with 1,201 additions and 2,090 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -441,3 +441,7 @@ poetry.toml

# End of https://www.toptal.com/developers/gitignore/api/pycharm,flask,python
testing/core
apps/wizarr-backend-next/
apps/wizarr-backend-old/
database/
.sentryclirc
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
## [3.5.1](https://github.com/wizarrrr/wizarr/compare/v3.5.0...v3.5.1) (2023-11-17)


### Bug Fixes

* 🐝 beta-ci workflow ([6044747](https://github.com/wizarrrr/wizarr/commit/60447477eb1dc932013e986db4e91a8c827311cf))
* 🐝 Jellyfin users now can have uppercase usernames ([1539c56](https://github.com/wizarrrr/wizarr/commit/1539c56b617c3fce0d17877c960a73fd8e6a1aac))
* 🔧 release branch identification logic and improve error handling ([e963e0b](https://github.com/wizarrrr/wizarr/commit/e963e0beec90c5d476231e1e149451480342c5d7))
* 🩹 add formkit stories ([e5cd97b](https://github.com/wizarrrr/wizarr/commit/e5cd97ba0c7c9dcf79ab8c59d888e8e8a326671f))
* 🩹 fix workflow issue ([268aeeb](https://github.com/wizarrrr/wizarr/commit/268aeeb33e6e8fb79c81dfe74b0868fbb7128e1b))
* 🩹 software lifecycle issue ([15b1edf](https://github.com/wizarrrr/wizarr/commit/15b1edf21ca43086346555f8afa2ac375f303e86))
* 🚑 beta-ci workflow ([bc7d834](https://github.com/wizarrrr/wizarr/commit/bc7d834f83736bf762cb0315c19c6badd9b2fc89))
* 🚑 software lifecycle issue ([ac55841](https://github.com/wizarrrr/wizarr/commit/ac55841b892653d62eda2b3951bf04527b1b4349))
* 🆘 EMERGENCY FIX FOR VERSION 🆘 ([7570ec1](https://github.com/wizarrrr/wizarr/commit/7570ec14d327bfad4000184732f4e329df6e0448))

## [3.5.1](https://github.com/wizarrrr/wizarr/compare/v3.5.0...v3.5.1) (2023-11-17)


### Bug Fixes

* 🐝 beta-ci workflow ([6044747](https://github.com/wizarrrr/wizarr/commit/60447477eb1dc932013e986db4e91a8c827311cf))
Expand Down
35 changes: 35 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Contributing Guide

Wizarr is proud to be an open-source project. We welcome any contributions made by the community to the project.

## Getting Started

We highly recommend joining our Discord before beginning your work. There, you can discuss with the development team about what you'd like to contribute to verify it is not already in progress and avoid duplicate work.

## Prerequisites

- Python 3.10+
- pip
- npm
- node

## Guidelines

We require following conventional commit guidelines in relation to commit messages and branch naming.

[Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/)

[Branch and Commit Conventions](https://dev.to/varbsan/a-simplified-convention-for-naming-branches-and-commits-in-git-il4)

## Contributing to Wizarr

We highly recommend using VSCode as your IDE. There is a code workspace already created to assist in organizing the project.

1. Fork the repository in GitHub
2. Clone your forked repository with `git clone [email protected]:<YOUR_USERNAME>/wizarr.git`
3. Move into the directory `cd wizarr`
4. Run the script to setup your local environment: `./scripts/setup-build-environment.sh`
5. Use VSCode and open the `develop.code-workspace` file under File -> Open Workspace from File, or type `code develop.code-workspace` in your terminal.
6. Inside the Nx Console panel of VSCode, you have access to the project targets. Run the build target for both wizarr-backend and wizarr-frontend. Then run the serve target to begin your work.
7. Visit http://127.0.0.1:5173 (Frontend) and http://127.0.0.1:5000 (Backend) to see your changes in realtime.
8. Create a new branch from 'develop' following conventions, commit your work, and open a PR against the 'develop' branch when you are ready for the team to review your contribution.
2 changes: 0 additions & 2 deletions apps/wizarr-backend/wizarr_backend/api/routes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
from .jellyfin_api import api as jellyfin_api
from .healthcheck_api import api as healthcheck_api
from .server_api import api as server_api
from .membership_api import api as membership_api

authorizations = {
"jsonWebToken": {
Expand Down Expand Up @@ -128,7 +127,6 @@ def handle_request_exception(error):
api.add_namespace(users_api)
api.add_namespace(utilities_api)
api.add_namespace(webhooks_api)
api.add_namespace(membership_api)

# Potentially remove this if it becomes unstable
# api.add_namespace(live_notifications_api)
Expand Down
17 changes: 17 additions & 0 deletions apps/wizarr-backend/wizarr_backend/api/routes/accounts_api.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from app.models.wizarr.accounts import AccountsModel
from flask import request
from flask_jwt_extended import jwt_required, current_user
from flask_restx import Namespace, Resource
Expand Down Expand Up @@ -120,3 +121,19 @@ def delete(self, account_id: str) -> tuple[dict[str, str], int]:
"""Delete an account"""
delete_account(account_id)
return {"message": "Account deleted"}, 200

@api.route("/change_password")
@api.route("/change_password/", doc=False)
class ChangePassword(Resource):
"""API resource for changing the user's password"""

method_decorators = [jwt_required()]

@api.doc(description="Change the user's password")
@api.response(200, "Password changed")
@api.response(401, "Invalid password")
@api.response(500, "Internal server error")
def post(self):
"""Change the user's password"""
#get the current user's id
return AccountsModel.change_password(request), 200
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,4 @@ class Logout(Resource):
@api.response(500, "Internal server error")
def post(self):
"""Logout the currently logged in user"""
return AuthenticationModel.logout_user()
return AuthenticationModel.logout_user()
61 changes: 0 additions & 61 deletions apps/wizarr-backend/wizarr_backend/api/routes/membership_api.py

This file was deleted.

28 changes: 27 additions & 1 deletion apps/wizarr-backend/wizarr_backend/app/models/wizarr/accounts.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from schematics.exceptions import DataError, ValidationError
from schematics.models import Model
from schematics.types import DateTimeType, EmailType, StringType, BooleanType
from werkzeug.security import generate_password_hash
from werkzeug.security import generate_password_hash, check_password_hash

from app.models.database.accounts import Accounts

Expand Down Expand Up @@ -108,3 +108,29 @@ def update_account(self, account: Accounts):
# Set the attributes of the updated account to the model
for key, value in model_to_dict(account).items():
setattr(self, key, value)


# ANCHOR - Chnage password for user
def change_password(self):
old_password = self.form.get("old_password")
new_password = self.form.get("new_password")
username = self.form.get("username")
# get account by username
account = Accounts.get_or_none(Accounts.username == username)

# Create password policy based on environment variables or defaults
policy = PasswordPolicy.from_names(length=min_password_length, uppercase=min_password_uppercase, numbers=min_password_numbers, special=min_password_special)

# Check if the password is strong enough
if len(policy.test(new_password)) > 0:
raise ValidationError("Password is not strong enough")

# First, check if the old_password matches the account's current password
if not check_password_hash(account.password, old_password):
raise ValidationError("Old password does not match.")

# Next update the password on account
account.password = generate_password_hash(new_password, method="scrypt")
account.save()
return True

Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,6 @@ def validate_password(self, _, value):
if not check_password_hash(self._user.password, value):
raise ValidationError("Invalid Username or Password")


# ANCHOR - Perform migration of old passwords
def _migrate_password(self):
# Migrate to scrypt from sha 256
Expand Down
31 changes: 20 additions & 11 deletions apps/wizarr-backend/wizarr_backend/helpers/plex.py
Original file line number Diff line number Diff line change
Expand Up @@ -284,9 +284,17 @@ def sync_plex_users(server_api_key: Optional[str] = None, server_url: Optional[s
# If plex_users.id is not in database_users.token, add user to database
for plex_user in plex_users:
if str(plex_user.id) not in [str(database_user.token) for database_user in database_users]:
create_user(username=plex_user.username, token=plex_user.id, email=plex_user.email)
info(f"User {plex_user.username} successfully imported to database")

create_user(username=plex_user.title, token=plex_user.id, email=plex_user.email)
info(f"User {plex_user.title} successfully imported to database")

# Handle Plex Managed/Guest users.
# Update DB username to Plex user title.
# This value is the same as username for normal accounts.
# For managed accounts without a public username,
# this value is set to 'Guest' or local username
elif str(plex_user.username) == "" and plex_user.email is None:
create_user(username=plex_user.title, token=plex_user.id, email=plex_user.email)
info(f"Managed User {plex_user.title} successfully updated to database")

# If database_users.token is not in plex_users.id, remove user from database
for database_user in database_users:
Expand Down Expand Up @@ -317,14 +325,15 @@ def get_plex_profile_picture(user_id: str, server_api_key: Optional[str] = None,
# Get the user
user = get_plex_user(user_id=user_id, server_api_key=server_api_key, server_url=server_url)

try:
# Get the profile picture from Plex
url = user.thumb
response = get(url=url, timeout=30)
except RequestException:
# Backup profile picture using ui-avatars.com if Jellyfin fails
username = f"{user.username}&length=1" if user else "ERROR&length=60&font-size=0.28"
response = get(url=f"https://ui-avatars.com/api/?uppercase=true&name={username}", timeout=30)
if str(user.email) != "":
try:
# Get the profile picture from Plex
url = user.thumb
response = get(url=url, timeout=30)
except RequestException:
# Backup profile picture using ui-avatars.com if Jellyfin fails
username = f"{user.username}&length=1" if user else "ERROR&length=60&font-size=0.28"
response = get(url=f"https://ui-avatars.com/api/?uppercase=true&name={username}", timeout=30)

# Raise exception if either Jellyfin or ui-avatars.com fails
if response.status_code != 200:
Expand Down
9 changes: 7 additions & 2 deletions apps/wizarr-backend/wizarr_backend/helpers/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,9 +155,14 @@ def create_user(**kwargs) -> Users:
form = UsersModel(**kwargs)
user_model = form.model_dump()

#
# Lookup by token to fix Issue #322 and #352
# https://github.com/wizarrrr/wizarr/issues/322
# https://github.com/wizarrrr/wizarr/issues/352
#
# If user already exists raise error (maybe change this to update user)
if get_user_by_username(form.username, verify=False) is not None:
user: Users = Users.update(**user_model).where(Users.username == form.username)
if get_user_by_token(form.token, verify=False) is not None:
user: Users = Users.update(**user_model).where(Users.token == form.token).execute()
else:
user: Users = Users.create(**user_model)

Expand Down
80 changes: 52 additions & 28 deletions apps/wizarr-frontend/src/api/authentication.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { errorToast, infoToast } from "../ts/utils/toasts";
import { errorToast, infoToast, successToast } from "../ts/utils/toasts";
import { startAuthentication, startRegistration } from "@simplewebauthn/browser";

import type { APIUser } from "@/types/api/auth/User";
import type { Membership } from "@/types/api/membership";
import type { RegistrationResponseJSON } from "@simplewebauthn/typescript-types";
import type { WebAuthnError } from "@simplewebauthn/browser/dist/types/helpers/webAuthnError";
import { useAuthStore } from "@/stores/auth";
Expand All @@ -14,6 +13,7 @@ class Auth {
// Local toast functions
private errorToast = errorToast;
private infoToast = infoToast;
private successToast = successToast;

// Router and axios instance
private router = useRouter();
Expand Down Expand Up @@ -72,36 +72,10 @@ class Auth {
authStore.setAccessToken(token);
authStore.setRefreshToken(refresh_token);

// Handle membership update
const membership = await this.handleMembershipUpdate();
userStore.setMembership(membership);

// Reset the user data
return { user, token };
}

/**
* Handle Membership Update
* This function is used to handle membership updates
*/
async handleMembershipUpdate(): Promise<Membership | null> {
// Get the membership from the database
const response = await this.axios
.get("/api/membership", {
disableErrorToast: true,
disableInfoToast: true,
})
.catch(() => null);

// Check if the response is successful
if (response?.status != 200) {
return null;
}

// Get the membership from the response
return response.data;
}

/**
* Get the current user
* This method is used to get the current user
Expand Down Expand Up @@ -220,6 +194,56 @@ class Auth {
return this.handleAuthData(response.data.auth.user, response.data.auth.token, response.data.auth.refresh_token);
}

/**
* Change Password
* This method is change the password of the user
*
* @param old_password The old password of the user
* @param new_password The new password of the user
*
* @returns The response from the server
*/
async changePassword(old_password?: string, new_password?: string) {
const userStore = useUserStore();

// verify if the user is authenticated
if (!this.isAuthenticated()) {
this.errorToast("User is not authenticated");
return;
}

// check if old assword is correct
const username = userStore.user?.username;

if (old_password) this.old_password = old_password;
if (new_password) this.new_password = new_password;
if (username) this.username = username;

// Create a form data object
const formData = new FormData();

// Add the username, password and remember_me to the form data
formData.append("old_password", this.old_password);
formData.append("new_password", this.new_password);
formData.append("username", this.username);

// send request to server to change password
await this.axios
.post("/api/accounts/change_password", formData)
.then((res) => {
this.successToast("Password changed successfully");
this.infoToast("Logging out in 5 seconds...");
setTimeout(() => {
this.logout();
}, 5000);
return res;
})
.catch(() => {
this.errorToast("Failed to change password, please try again");
return;
});
}

/**
* Logout of the application
* This method is used to logout the user
Expand Down
Loading

0 comments on commit 4b6c722

Please sign in to comment.