Skip to content

Commit

Permalink
Allow updating resident login information
Browse files Browse the repository at this point in the history
  • Loading branch information
Serious-senpai committed Nov 2, 2024
1 parent 9dfa869 commit cf8b5f1
Show file tree
Hide file tree
Showing 7 changed files with 199 additions and 18 deletions.
29 changes: 27 additions & 2 deletions app/resident_manager/lib/src/models/residents.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,10 @@ class Resident extends PublicInfo {
required ApplicationState state,
required PersonalInfo info,
}) async {
final headers = {"content-type": "application/json"};
final response = await state.post(
state.loggedInAsAdmin ? "/api/v1/admin/residents/update" : "/api/v1/residents/update",
queryParameters: {"id": id.toString()},
headers: headers,
headers: {"content-type": "application/json"},
body: json.encode(info.toJson()),
);
final result = json.decode(utf8.decode(response.bodyBytes));
Expand All @@ -69,6 +68,32 @@ class Resident extends PublicInfo {
return Result(result["code"], null);
}

Future<Result?> updateAuthorization({
required ApplicationState state,
required String newUsername,
required String oldPassword,
required String newPassword,
}) async {
final response = await state.post(
"/api/v1/residents/update-authorization",
headers: {"content-type": "application/json"},
body: json.encode(
{
"new_username": newUsername,
"old_password": oldPassword,
"new_password": newPassword,
},
),
);
final result = json.decode(utf8.decode(response.bodyBytes));

if (response.statusCode == 200) {
return Result(0, Resident.fromJson(result["data"]));
}

return Result(result["code"], null);
}

/// Deletes a resident.
///
/// This method deletes a resident from the database.
Expand Down
11 changes: 10 additions & 1 deletion app/resident_manager/lib/src/translations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ class AppLocale {
static const String Settings = "Settings";
static const String ComingSoon = "ComingSoon";
static const String EditPersonalInfo = "EditPersonalInfo";
static const String OldPassword = "OldPassword";
static const String NewPassword = "NewPassword";
static const String RetypeNewPassword = "RetypeNewPassword";
static const String GeneralInformation = "GeneralInformation";
Expand All @@ -72,7 +73,9 @@ class AppLocale {
static const String InvalidMotorbikesCount = "InvalidMotorbikesCount";
static const String MissingCarsCount = "MissingCarsCount";
static const String InvalidCarsCount = "InvalidCarsCount";
static const String SaveGeneralInformation = "SaveGeneralInformations";
static const String SaveGeneralInformation = "SaveGeneralInformation";
static const String SaveAuthorizationInformation = "SaveAuthorizationInformation";
static const String Successful = "Successful";

// Error codes
static const String Error0 = "Error0";
Expand Down Expand Up @@ -156,6 +159,7 @@ class AppLocale {
Settings: "Settings",
ComingSoon: "Coming soon",
EditPersonalInfo: "Edit personal information",
OldPassword: "Old password",
NewPassword: "New password",
RetypeNewPassword: "Retype new password",
GeneralInformation: "General information",
Expand All @@ -167,6 +171,8 @@ class AppLocale {
MissingCarsCount: "Missing cars count",
InvalidCarsCount: "Cars count must be in range 0-255",
SaveGeneralInformation: "Save general information",
SaveAuthorizationInformation: "Save authorization information",
Successful: "Successful",

// Error codes
Error0: "Operation completed successfully.",
Expand Down Expand Up @@ -251,6 +257,7 @@ class AppLocale {
Settings: "Cài đặt",
ComingSoon: "Sắp ra mắt",
EditPersonalInfo: "Chỉnh sửa thông tin cá nhân",
OldPassword: "Mật khẩu cũ",
NewPassword: "Mật khẩu mới",
RetypeNewPassword: "Nhập lại mật khẩu mới",
GeneralInformation: "Thông tin cơ bản",
Expand All @@ -262,6 +269,8 @@ class AppLocale {
MissingCarsCount: "Thiếu số lượng ô tô",
InvalidCarsCount: "Số lượng ô tô trong phạm vi 0-255",
SaveGeneralInformation: "Lưu thông tin cơ bản",
SaveAuthorizationInformation: "Lưu thông tin đăng nhập",
Successful: "Thành công",

// Error codes
Error0: "Thao tác thành công.",
Expand Down
8 changes: 3 additions & 5 deletions app/resident_manager/lib/src/widgets/home.dart
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,9 @@ class HomePageState extends AbstractCommonState<HomePage> with CommonStateMixin<
),
Expanded(
child: TextButton.icon(
icon: const Icon(Icons.settings_outlined),
label: Text(AppLocale.Settings.getString(context)),
onPressed: () {
// TODO: Implement this
},
icon: const Icon(Icons.construction_outlined),
label: Text(AppLocale.ComingSoon.getString(context)),
onPressed: null,
),
),
Expanded(
Expand Down
111 changes: 104 additions & 7 deletions app/resident_manager/lib/src/widgets/personal_info.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import "package:flutter_localization/flutter_localization.dart";

import "common.dart";
import "state.dart";
import "../routes.dart";
import "../translations.dart";
import "../utils.dart";
import "../models/info.dart";
Expand Down Expand Up @@ -43,6 +44,7 @@ class PersonalInfoPageState extends AbstractCommonState<PersonalInfoPage> with C
final _phone = TextEditingController();
final _email = TextEditingController();
final _username = TextEditingController();
final _oldPassword = TextEditingController();
final _newPassword = TextEditingController();
final _newPasswordRetype = TextEditingController();

Expand All @@ -52,7 +54,8 @@ class PersonalInfoPageState extends AbstractCommonState<PersonalInfoPage> with C
Widget _generalNotification = const SizedBox.square(dimension: 0);
Widget _authNotification = const SizedBox.square(dimension: 0);

final _actionLock = Lock();
final _generalLock = Lock();
final _authLock = Lock();

@override
void initState() {
Expand Down Expand Up @@ -182,6 +185,7 @@ class PersonalInfoPageState extends AbstractCommonState<PersonalInfoPage> with C
),
),
_generalNotification,
const SizedBox.square(dimension: 5),
Row(
children: [
Expanded(
Expand All @@ -194,10 +198,10 @@ class PersonalInfoPageState extends AbstractCommonState<PersonalInfoPage> with C
AppLocale.SaveGeneralInformation.getString(context),
style: const TextStyle(color: Colors.yellow),
),
onPressed: _actionLock.locked
onPressed: _generalLock.locked
? null
: () async {
await _actionLock.run(
await _generalLock.run(
() async {
_generalNotification = Builder(
builder: (context) => Text(
Expand Down Expand Up @@ -228,7 +232,12 @@ class PersonalInfoPageState extends AbstractCommonState<PersonalInfoPage> with C
);
} else {
state.resident = result.data;
_generalNotification = const SizedBox.square(dimension: 0);
_generalNotification = Builder(
builder: (context) => Text(
AppLocale.Successful.getString(context),
style: const TextStyle(color: Colors.blue),
),
);
}
} catch (e) {
await showToastSafe(msg: context.mounted ? AppLocale.ConnectionError.getString(context) : AppLocale.ConnectionError);
Expand Down Expand Up @@ -274,14 +283,29 @@ class PersonalInfoPageState extends AbstractCommonState<PersonalInfoPage> with C
validator: (value) => usernameValidator(context, required: true, value: value),
),
),
_InfoCard(
child: TextFormField(
controller: _oldPassword,
decoration: InputDecoration(
contentPadding: const EdgeInsets.all(8.0),
label: FieldLabel(
AppLocale.OldPassword.getString(context),
required: true,
style: const TextStyle(color: Colors.black),
),
),
obscureText: true,
// No need to validate old password
// validator: (value) => passwordValidator(context, required: false, value: value),
),
),
_InfoCard(
child: TextFormField(
controller: _newPassword,
decoration: InputDecoration(
contentPadding: const EdgeInsets.all(8.0),
label: FieldLabel(
AppLocale.NewPassword.getString(context),
required: true,
style: const TextStyle(color: Colors.black),
),
),
Expand All @@ -296,7 +320,6 @@ class PersonalInfoPageState extends AbstractCommonState<PersonalInfoPage> with C
contentPadding: const EdgeInsets.all(8.0),
label: FieldLabel(
AppLocale.RetypeNewPassword.getString(context),
required: true,
style: const TextStyle(color: Colors.black),
),
),
Expand All @@ -318,11 +341,85 @@ class PersonalInfoPageState extends AbstractCommonState<PersonalInfoPage> with C
child: Column(
children: [
Text(
AppLocale.GeneralInformation.getString(context),
AppLocale.AuthorizationInformation.getString(context),
style: const TextStyle(fontWeight: FontWeight.bold),
),
...List<Widget>.from(items.map((item) => Row(children: [item]))),
_authNotification,
const SizedBox.square(dimension: 5),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
icon: const Icon(
Icons.edit_outlined,
color: Colors.yellow,
),
label: Text(
AppLocale.SaveAuthorizationInformation.getString(context),
style: const TextStyle(color: Colors.yellow),
),
onPressed: _authLock.locked
? null
: () async {
await _authLock.run(
() async {
_authNotification = Builder(
builder: (context) => Text(
AppLocale.Loading.getString(context),
style: const TextStyle(color: Colors.blue),
),
);
refresh();

try {
final result = await state.resident?.updateAuthorization(
state: state,
newUsername: _username.text,
oldPassword: _oldPassword.text,

// If the user doesn't fill out the new password, keep the old one
newPassword: _newPassword.text.isEmpty ? _oldPassword.text : _newPassword.text,
);

if (result == null || result.code != 0) {
_authNotification = Builder(
builder: (context) => Text(
AppLocale.errorMessage(result?.code ?? -1).getString(context),
style: const TextStyle(color: Colors.red),
),
);
} else {
// Authorization info updated. Logout.
await state.deauthorize();
if (context.mounted) {
Navigator.popUntil(context, (route) => route.isFirst);
await Navigator.pushReplacementNamed(context, ApplicationRoute.login);
}
}
} catch (e) {
await showToastSafe(msg: context.mounted ? AppLocale.ConnectionError.getString(context) : AppLocale.ConnectionError);
_authNotification = Builder(
builder: (context) => Text(
AppLocale.ConnectionError.getString(context),
style: const TextStyle(color: Colors.red),
),
);

if (!(e is SocketException || e is TimeoutException)) {
rethrow;
}
} finally {
refresh();
}
},
);
},
style: ElevatedButton.styleFrom(backgroundColor: Colors.purple),
),
),
],
),
],
),
);
Expand Down
6 changes: 3 additions & 3 deletions server/v1/models/residents.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ class Resident(PublicInfo, HashedAuthorization):
Each object of this class corresponds to a database row."""

async def update_authorization(self, username: str, password: str) -> Result[None]:
async def update_authorization(self, username: str, password: str) -> Result[Optional[Resident]]:
if not validate_username(username):
return Result(code=105, data=None)

Expand Down Expand Up @@ -70,12 +70,12 @@ async def update_authorization(self, username: str, password: str) -> Result[Non
try:
row = await cursor.fetchone()
if row is not None:
return Result(data=None)
return Result(data=Resident.from_row(row))

except pyodbc.ProgrammingError:
pass

return Result(code=301, data=None)
return Result(code=107, data=None)

@classmethod
def from_row(cls, row: Any) -> Resident:
Expand Down
1 change: 1 addition & 0 deletions server/v1/routes/residents/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
from .me import *
from .update_authorization import *
from .update import *
51 changes: 51 additions & 0 deletions server/v1/routes/residents/update_authorization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from __future__ import annotations

from typing import Annotated, Optional

import pydantic
from fastapi import Depends, Response, status

from ...app import api_v1
from ...models import Resident, Result
from ...utils import check_password


__all__ = ("residents_update_authorization",)


class _Payload(pydantic.BaseModel):
new_username: Annotated[str, pydantic.Field(description="The new authorization username")]
old_password: Annotated[str, pydantic.Field(description="The old authorization password")]
new_password: Annotated[str, pydantic.Field(description="The new authorization password")]


@api_v1.post(
"/residents/update-authorization",
name="Residents authorization info update",
description="Update authorization information of a resident",
tags=["resident"],
responses={
status.HTTP_200_OK: {
"description": "The operation completed successfully",
"model": Result[Resident],
},
status.HTTP_400_BAD_REQUEST: {
"description": "Incorrect authorization data",
"model": Result[None],
},
},
)
async def residents_update_authorization(
resident: Annotated[Result[Optional[Resident]], Depends(Resident.from_token)],
response: Response,
payload: _Payload,
) -> Result[Optional[Resident]]:
if resident.data is None or not check_password(payload.old_password, hashed=resident.data.hashed_password):
response.status_code = status.HTTP_400_BAD_REQUEST
return Result(code=402, data=None)

result = await resident.data.update_authorization(payload.new_username, payload.new_password)
if result.data is None:
response.status_code = status.HTTP_400_BAD_REQUEST

return result

0 comments on commit cf8b5f1

Please sign in to comment.