Skip to content

Commit

Permalink
✨ Allow changing password
Browse files Browse the repository at this point in the history
Fixes #171
  • Loading branch information
pajowu committed Jun 27, 2023
1 parent 4e9756e commit 7eded68
Show file tree
Hide file tree
Showing 7 changed files with 253 additions and 12 deletions.
43 changes: 43 additions & 0 deletions backend/openapi-schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,20 @@ components:
- file
title: Body_create_document_api_v1_documents__post
type: object
ChangePasswordRequest:
properties:
new_password:
minLength: 6
title: New Password
type: string
old_password:
title: Old Password
type: string
required:
- old_password
- new_password
title: ChangePasswordRequest
type: object
CreateUser:
properties:
password:
Expand Down Expand Up @@ -816,6 +830,35 @@ paths:
$ref: '#/components/schemas/HTTPValidationError'
description: Validation Error
summary: Mark Failed
/api/v1/users/change_password/:
post:
operationId: change_password_api_v1_users_change_password__post
parameters:
- in: header
name: authorization
required: true
schema:
title: Authorization
type: string
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ChangePasswordRequest'
required: true
responses:
'200':
content:
application/json:
schema: {}
description: Successful Response
'422':
content:
application/json:
schema:
$ref: '#/components/schemas/HTTPValidationError'
description: Validation Error
summary: Change Password
/api/v1/users/create/:
post:
operationId: create_user_req_api_v1_users_create__post
Expand Down
10 changes: 10 additions & 0 deletions backend/transcribee_backend/models/user.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import datetime
import uuid

from pydantic import BaseModel, ConstrainedStr
from sqlmodel import Column, DateTime, Field, Relationship, SQLModel


Expand Down Expand Up @@ -41,3 +42,12 @@ class UserToken(UserTokenBase, table=True):
valid_until: datetime.datetime = Field(
sa_column=Column(DateTime(timezone=True), nullable=False)
)


class PasswordConstrainedStr(ConstrainedStr):
min_length = 6


class ChangePasswordRequest(BaseModel):
old_password: str
new_password: PasswordConstrainedStr
23 changes: 23 additions & 0 deletions backend/transcribee_backend/routers/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@
from transcribee_backend.auth import (
NotAuthorized,
authorize_user,
change_user_password,
create_user,
generate_user_token,
get_user_token,
)
from transcribee_backend.db import get_session
from transcribee_backend.exceptions import UserAlreadyExists
from transcribee_backend.models import CreateUser, User, UserBase, UserToken
from transcribee_backend.models.user import ChangePasswordRequest

user_router = APIRouter()

Expand Down Expand Up @@ -52,3 +54,24 @@ def read_user(
statement = select(User).where(User.id == token.user_id)
user = session.exec(statement).one()
return {"username": user.username}


@user_router.post("/change_password/")
def change_password(
body: ChangePasswordRequest,
session: Session = Depends(get_session),
token: UserToken = Depends(get_user_token),
):
try:
authorized_user = authorize_user(
session=session, username=token.user.username, password=body.old_password
)
except NotAuthorized:
raise HTTPException(403)

user = change_user_password(
session=session,
username=authorized_user.username,
new_password=body.new_password,
)
return {"username": user.username}
6 changes: 6 additions & 0 deletions frontend/src/api/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ export const login = fetcher
.path('/api/v1/users/login/')
.method('post', 'application/json')
.create();

export const changePassword = fetcher
.path('/api/v1/users/change_password/')
.method('post', 'application/json')
.create();

export const getMe = fetcher.path('/api/v1/users/me/').method('get').create();

const useGetMeWithRetry = makeSwrHook('getMe', getMe);
Expand Down
37 changes: 25 additions & 12 deletions frontend/src/common/top_bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import { storeAuthToken } from '../api';
import { useGetMe } from '../api/user';
import { primitiveWithClassname } from '../styled';
import { BiUser } from 'react-icons/bi';
import { IconButton, PrimaryButton } from '../components/button';
import { IconButton, PrimaryButton, SecondaryButton } from '../components/button';
import { Popup } from '../components/popup';
import { showModal } from '../components/modal';
import { ChangePasswordModal } from '../components/change_password';

export const TopBar = primitiveWithClassname('div', 'mb-8 flex items-center gap-4 justify-between');
export const TopBarTitle = primitiveWithClassname('h2', 'text-xl font-bold');
Expand All @@ -24,17 +26,28 @@ export function MeMenu() {

return (
<>
<div className="pb-4">hello, {data?.username}</div>
<PrimaryButton
onClick={() => {
storeAuthToken(undefined);
mutate();
navigate('/');
window.location.reload();
}}
>
Logout
</PrimaryButton>
<div className="flex flex-col gap-y-4">
<div>hello, {data?.username}</div>
<SecondaryButton
onClick={() => {
showModal(
<ChangePasswordModal label="Change Password" onClose={() => showModal(null)} />,
);
}}
>
Change Password
</SecondaryButton>
<PrimaryButton
onClick={() => {
storeAuthToken(undefined);
mutate();
navigate('/');
window.location.reload();
}}
>
Logout
</PrimaryButton>
</div>
</>
);
}
108 changes: 108 additions & 0 deletions frontend/src/components/change_password.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { ComponentProps, useState } from 'react';
import { PrimaryButton } from './button';
import { FormControl, Input } from './form';
import { Modal } from './modal';
import { useForm, FieldValues, SubmitHandler } from 'react-hook-form';
import { changePassword } from '../api/user';

export function ChangePasswordModal({
onClose,
...props
}: {
onClose: () => void;
} & ComponentProps<typeof Modal>) {
const [errorMessage, setErrorMessage] = useState<string | null>(null);

const {
register,
handleSubmit,
formState: { errors },
getValues,
} = useForm<FieldValues>();

const submitHandler: SubmitHandler<FieldValues> = async (data) => {
setErrorMessage(null); // clear general error when hitting submit

try {
await changePassword({
new_password: data.new_password,
old_password: data.old_password,
});
onClose();
} catch (e) {
let message = 'An unknown error occcured.';

if (e instanceof changePassword.Error) {
const error = e.getActualType();
console.log('error', e);
if (error.status === 422) {
if (error.data.detail) {
message = error.data.detail.map((x) => x.msg).join(' ');
}
} else if (error.status === 403) {
message = 'Incorrect old password.';
}
}

setErrorMessage(message);
}
};

return (
<Modal {...props} onClose={onClose}>
<form
onSubmit={handleSubmit(submitHandler, () => {
setErrorMessage(null); // clear general error when hitting submit
})}
>
<div className="flex flex-col gap-6">
<FormControl label="Old Password" error={errors.old_password?.message?.toString()}>
<Input
autoFocus
{...register('old_password', { required: 'This field is required.' })}
type="password"
/>
</FormControl>
<FormControl label="New Password" error={errors.new_password?.message?.toString()}>
<Input
{...register('new_password', {
minLength: {
value: 6,
message: 'Password needs to be at least 6 characters long.',
},
required: 'This field is required.',
})}
type="password"
/>
</FormControl>
<FormControl
label="New Password (again)"
error={errors.confirm_new_password?.message?.toString()}
>
<Input
{...register('confirm_new_password', {
minLength: {
value: 6,
message: 'Password needs to be at least 6 characters long.',
},
required: 'This field is required',
validate: (value) =>
value === getValues('new_password') || 'The new passwords must be the same',
})}
type="password"
/>
</FormControl>

{errorMessage && (
<div className="block bg-red-100 px-2 py-2 rounded text-center text-red-700">
{errorMessage}
</div>
)}
<div className="block">
<PrimaryButton type="submit">Login</PrimaryButton>
</div>
</div>
</form>
</Modal>
);
}
38 changes: 38 additions & 0 deletions frontend/src/openapi-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ export interface paths {
/** Mark Failed */
post: operations["mark_failed_api_v1_tasks__task_id__mark_failed__post"];
};
"/api/v1/users/change_password/": {
/** Change Password */
post: operations["change_password_api_v1_users_change_password__post"];
};
"/api/v1/users/create/": {
/** Create User Req */
post: operations["create_user_req_api_v1_users_create__post"];
Expand Down Expand Up @@ -136,6 +140,13 @@ export interface components {
/** Name */
name: string;
};
/** ChangePasswordRequest */
ChangePasswordRequest: {
/** New Password */
new_password: string;
/** Old Password */
old_password: string;
};
/** CreateUser */
CreateUser: {
/** Password */
Expand Down Expand Up @@ -687,6 +698,33 @@ export interface operations {
};
};
};
/** Change Password */
change_password_api_v1_users_change_password__post: {
parameters: {
header: {
authorization: string;
};
};
requestBody: {
content: {
"application/json": components["schemas"]["ChangePasswordRequest"];
};
};
responses: {
/** @description Successful Response */
200: {
content: {
"application/json": Record<string, never>;
};
};
/** @description Validation Error */
422: {
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
/** Create User Req */
create_user_req_api_v1_users_create__post: {
requestBody: {
Expand Down

0 comments on commit 7eded68

Please sign in to comment.