Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ The project follows a modular architecture with separate backend services and fr
### Backend Setup
1. Clone the repository:
```
git clone https://github.com/yourusername/afyaquik.git
git clone https://github.com/brianbrix/afyaquik.git
cd afyaquik
```

Expand Down
3 changes: 3 additions & 0 deletions afyaquik-frontend/admin/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ import BillingItemEdit from "./billing/items/BillingItemEdit";
import CurrencyList from "./billing/currencies/CurrencyList";
import CurrencyCreate from "./billing/currencies/CurrencyCreate";
import CurrencyEdit from "./billing/currencies/CurrencyEdit";
import PasswordResetRequestList from "./users/PasswordResetRequestList";
import PasswordResetRequestAction from "./users/PasswordResetRequestAction";

const App = () => (
<AuthGuard requiredRoles={['ADMIN', 'SUPERADMIN']}>
Expand All @@ -68,6 +70,7 @@ const App = () => (
<Resource name="drugForms" list={DrugFormList} create={DrugFormCreate} edit={DrugFormEdit} />
<Resource name="billingItems" list={BillingItemList} create={BillingItemCreate} edit={BillingItemEdit} />
<Resource name="currencies" list={CurrencyList} create={CurrencyCreate} edit={CurrencyEdit} />
<Resource name="passwordResetRequests" list={PasswordResetRequestList} edit={PasswordResetRequestAction} />
</Admin>
</AuthGuard>
);
Expand Down
5 changes: 4 additions & 1 deletion afyaquik-frontend/admin/src/stations/StationCreate.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { Create, SimpleForm, TextInput } from 'react-admin';
import {CheckboxGroupInput, Create, ReferenceArrayInput, SimpleForm, TextInput} from 'react-admin';

const StationCreate = () => (
<Create>
<SimpleForm>
<TextInput source="name" />
<ReferenceArrayInput source="allowedRoles" reference="roles">
<CheckboxGroupInput optionText="name" optionValue="name" />
</ReferenceArrayInput>
</SimpleForm>
</Create>
);
Expand Down
5 changes: 4 additions & 1 deletion afyaquik-frontend/admin/src/stations/StationEdit.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { Edit, SimpleForm, TextInput } from 'react-admin';
import {CheckboxGroupInput, Edit, ReferenceArrayInput, SimpleForm, TextInput} from 'react-admin';

const StationEdit = () => (
<Edit>
<SimpleForm>
<TextInput source="id" disabled />
<TextInput source="name" />
<ReferenceArrayInput source="allowedRoles" reference="roles">
<CheckboxGroupInput optionText="name" optionValue="name" />
</ReferenceArrayInput>
</SimpleForm>
</Edit>
);
Expand Down
21 changes: 21 additions & 0 deletions afyaquik-frontend/admin/src/users/PasswordResetRequestAction.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import {
Edit,
SimpleForm,
TextInput,
PasswordInput
} from 'react-admin';
import {confirmPasswordValidator, passwordValidator} from "../utils";



const PasswordResetRequestAction = () => (
<Edit>
<SimpleForm>
<TextInput source="id" disabled />
<TextInput source="username" disabled />
<PasswordInput source="password" required={true} validate={passwordValidator} />
<PasswordInput source="confirmPassword" required={true} validate={confirmPasswordValidator} />
</SimpleForm>
</Edit>
);
export default PasswordResetRequestAction;
17 changes: 17 additions & 0 deletions afyaquik-frontend/admin/src/users/PasswordResetRequestList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {List, Datagrid, TextField, EmailField, EditButton, BooleanField, TextInput, SelectInput} from 'react-admin';

const UserFilters = [
<TextInput label="Search by username" source="username" alwaysOn />,
];
const PasswordResetRequestList = () => (
<List filters={UserFilters}>
<Datagrid rowClick="edit">
<TextField source="id" />
<TextField source="username" />
<TextField source="status" />
<EditButton />
</Datagrid>
</List>
);

export default PasswordResetRequestList;
53 changes: 35 additions & 18 deletions afyaquik-frontend/admin/src/users/UserCreate.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,37 @@
import { Create, SimpleForm, TextInput, ReferenceArrayInput, CheckboxGroupInput,BooleanInput } from 'react-admin';

const UserCreate = () => (
<Create>
<SimpleForm>
<TextInput source="username" />
<TextInput source="firstName" required={true} />
<TextInput source="secondName" required={true} />
<TextInput source="lastName" required={true} />
<ReferenceArrayInput source="roles" reference="roles">
<CheckboxGroupInput optionText="name" optionValue="name" />
</ReferenceArrayInput>
<TextInput source="email" required={true}/>
<TextInput source="password" type="password" />
<BooleanInput source="enabled" label="Enabled" />
</SimpleForm>
</Create>
);
import {
Create,
SimpleForm,
TextInput,
ReferenceArrayInput,
CheckboxGroupInput,
BooleanInput,
PasswordInput
} from 'react-admin';
import {generatePassword, passwordValidator} from "../utils";
import {useState} from "react";

const UserCreate = () => {
const [generatedPassword, setGeneratedPassword] = useState("");
return (
<Create>
<SimpleForm>
<TextInput source="username" />
<TextInput source="firstName" required={true} />
<TextInput source="secondName" required={true} />
<TextInput source="lastName" required={true} />
<ReferenceArrayInput source="roles" reference="roles">
<CheckboxGroupInput optionText="name" optionValue="name" />
</ReferenceArrayInput>
<TextInput source="email" required={true}/>
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<PasswordInput source="password" type="password" validate={passwordValidator} defaultValue={generatedPassword} />
<button type="button" className="btn btn-outline-secondary btn-sm" onClick={() => setGeneratedPassword(generatePassword())}>
Generate Password
</button>
</div>
<BooleanInput source="enabled" label="Enabled" />
</SimpleForm>
</Create>
);
};
export default UserCreate;
24 changes: 21 additions & 3 deletions afyaquik-frontend/admin/src/users/UserEdit.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
import { Edit, SimpleForm, TextInput, ReferenceArrayInput, CheckboxGroupInput,BooleanInput } from 'react-admin';
import {
Edit,
SimpleForm,
TextInput,
ReferenceArrayInput,
CheckboxGroupInput,
BooleanInput,
PasswordInput
} from 'react-admin';
import {generatePassword, passwordValidator} from "../utils";
import {useState} from "react";

const UserEdit = () => (
const UserEdit = () => {
const [generatedPassword, setGeneratedPassword] = useState("");
return (
<Edit>
<SimpleForm>
<TextInput source="id" disabled />
Expand All @@ -9,6 +21,12 @@ const UserEdit = () => (
<TextInput source="secondName" required={true} />
<TextInput source="lastName" required={true} />
<TextInput source="email" required={true} />
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<PasswordInput source="password" type="password" validate={passwordValidator} defaultValue={generatedPassword} />
<button type="button" className="btn btn-outline-secondary btn-sm" onClick={() => setGeneratedPassword(generatePassword())}>
Generate Password
</button>
</div>
<BooleanInput source="available" />
<ReferenceArrayInput source="roles" reference="roles">
<CheckboxGroupInput optionText="name" optionValue="name" />
Expand All @@ -19,6 +37,6 @@ const UserEdit = () => (
<BooleanInput source="enabled" label="Enabled" />
</SimpleForm>
</Edit>
);
)};

export default UserEdit;
25 changes: 25 additions & 0 deletions afyaquik-frontend/admin/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export const generatePassword = () => {
const length = 12;
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;:,.<>?";
let password = "";
for (let i = 0, n = charset.length; i < length; ++i) {
password += charset.charAt(Math.floor(Math.random() * n));
}
return password;
};

export const passwordValidator = (value: string) => {
if (!value) return 'Password is required';
if (value.length < 8) return 'Password must be at least 8 characters';
if (!/[A-Z]/.test(value)) return 'Password must contain an uppercase letter';
if (!/[a-z]/.test(value)) return 'Password must contain a lowercase letter';
if (!/[0-9]/.test(value)) return 'Password must contain a number';
if (!/[!@#$%^&*(),.?":{}|<>]/.test(value)) return 'Password must contain a special character';
return undefined;
};

export const confirmPasswordValidator = (value: string, allValues: any) => {
if (!value) return 'Confirm password is required';
if (value !== allValues.password) return 'Passwords do not match';
return undefined;
};
9 changes: 6 additions & 3 deletions afyaquik-frontend/auth/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import {Routes, Route, HashRouter} from 'react-router-dom';
import {Routes, Route} from 'react-router-dom';
import LoginPage from './pages/LoginPage';
import { AuthProvider } from './components/AuthProvider';
import {AuthGuard, Header, ToastProvider} from "@afyaquik/shared";
import {Header, ToastProvider} from "@afyaquik/shared";
import HomePage from "./pages/HomePage";
import ProfilePage from "./pages/ProfilePage";
import React from "react";
import ForgotPasswordPage from "./pages/ForgotPasswordPage";

export default function App() {
return (
Expand All @@ -13,6 +14,8 @@ export default function App() {
<Route path="/login" element={<LoginPage />} />
<Route path="/home" element={<HomePage />} />
<Route path="/" element={<HomePage />} />
<Route path="/profile" element={<ProfilePage />} />
<Route path="/forgot-password" element={<ForgotPasswordPage />} />
</Routes>
</ToastProvider>

Expand Down
51 changes: 51 additions & 0 deletions afyaquik-frontend/auth/src/pages/ForgotPasswordPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { apiRequest } from "@afyaquik/shared";
import React, { useState } from "react";

const ForgotPasswordPage = () => {
const [username, setUsername] = useState("");
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError(null);
setSuccess(false);
try {
await apiRequest("/password-reset/request", {
method: "POST",
body: { username },
});
setSuccess(true);
} catch {
setError("Failed to request password reset. Please check your username.");
}
setLoading(false);
};

return (
<div>
<h1>Reset Password</h1>
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="username">Username</label>
<input
id="username"
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
/>
</div>
{error && <p style={{ color: "red" }}>{error}</p>}
{success && <p style={{ color: "green" }}>Check your email for reset link!</p>}
<button type="submit" disabled={loading}>
{loading ? "Loading..." : "Request Password Reset"}
</button>
</form>
</div>
);
};

export default ForgotPasswordPage;
18 changes: 17 additions & 1 deletion afyaquik-frontend/auth/src/pages/LoginPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { authService } from '../utils/authService';
import {sendNotification} from "@afyaquik/shared";
import {apiRequest, sendNotification} from "@afyaquik/shared";

export default function LoginPage() {
const [username, setUsername] = useState('');
Expand Down Expand Up @@ -53,6 +53,13 @@ export default function LoginPage() {
setError('Please select a role to continue.');
return;
}
apiRequest(`/roles/byName/${selectedRole}`,{method:'GET'})
.then(response=>
{
localStorage.setItem('allowedStations', response.stations)
localStorage.setItem('formattedStations', response.stations.join('||'))
}
)

localStorage.setItem('currentRole', selectedRole);

Expand Down Expand Up @@ -127,6 +134,15 @@ export default function LoginPage() {
</>
)}
</form>
<div className="mt-3 text-center">
<a
href="/client/auth/index.html#/forgot-password"
className="text-primary"
style={{ cursor: 'pointer', textDecoration: 'underline' }}
>
Forgot password?
</a>
</div>
</div>
);
}
67 changes: 67 additions & 0 deletions afyaquik-frontend/auth/src/pages/ProfilePage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React, { useEffect, useState } from "react";
import { apiRequest, StepForm, StepConfig, useToast } from "@afyaquik/shared";
import { Button } from "react-bootstrap";

const ProfilePage = () => {
const [profile, setProfile] = useState<any>({});
const [loading, setLoading] = useState(true);
const [editing, setEditing] = useState(false);
const { showToast } = useToast();

useEffect(() => {
apiRequest("/users/me", { method: "GET" })
.then((data) => setProfile(data))
.finally(() => setLoading(false));
}, []);

const formConfig: StepConfig[] = [
{
label: "Edit Profile",
fields: [
{ name: "username", label: "Username", type: "text", disabled: true },
{ name: "firstName", label: "First Name", type: "text", required: true },
{ name: "lastName", label: "Last Name", type: "text", required: true },
{ name: "email", label: "Email", type: "email", required: true },
{ name: "phone", label: "Phone", type: "text" },
],
},
];

if (loading) return <div>Loading...</div>;

return (
<div>
{!editing ? (
<div>
<h2>Profile</h2>
<p><b>Username:</b> {profile.username}</p>
<p><b>First Name:</b> {profile.firstName}</p>
<p><b>Last Name:</b> {profile.lastName}</p>
<p><b>Email:</b> {profile.email}</p>
<p><b>Phone:</b> {profile.phone}</p>
<Button variant="primary" onClick={() => setEditing(true)}>
Edit Profile
</Button>
</div>
) : (
<StepForm
config={formConfig}
defaultValues={profile}
onSubmit={(data) => {
apiRequest(`/users/${profile.id}`, { method: "PUT", body: data })
.then((updated) => {
setProfile(updated);
setEditing(false);
showToast("Profile updated successfully.", "success");
})
.catch(() => showToast("Failed to update profile.", "error"));
}}
submitButtonLabel="Save Changes"
/>
)}
</div>
);
};

export default ProfilePage;

Loading