Compare commits
10 Commits
1065da076d
...
31e0bd6eac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31e0bd6eac | ||
|
|
a55ccc63c6 | ||
|
|
755181c328 | ||
|
|
95c5b0814a | ||
|
|
a34eadb51f | ||
|
|
6be48846b0 | ||
|
|
c0810f047b | ||
|
|
a172cdeb37 | ||
|
|
73a5cd4e2f | ||
|
|
de8b599364 |
8
Makefile
8
Makefile
@@ -1,8 +1,8 @@
|
||||
deploy: public/build/manifest.json
|
||||
scp raspigate:/var/www/ticcat/.env .env.production
|
||||
scp raspiweb:/var/www/ticcat/.env .env.production
|
||||
pnpm run build --mode prod
|
||||
ssh raspigate 'cd /var/www/ticcat && git pull origin main && make install'
|
||||
scp -r public/build raspigate:/var/www/ticcat/public
|
||||
ssh raspiweb 'cd /var/www/ticcat && git pull origin main && make install'
|
||||
scp -r public/build raspiweb:/var/www/ticcat/public
|
||||
|
||||
install: vendor/autoload.php
|
||||
php artisan down
|
||||
@@ -11,7 +11,7 @@ install: vendor/autoload.php
|
||||
php artisan up
|
||||
|
||||
vendor/autoload.php: composer.lock
|
||||
php composer.phar install
|
||||
composer install
|
||||
touch vendor/autoload.php
|
||||
|
||||
public/build/manifest.json: package.json
|
||||
|
||||
@@ -53,7 +53,7 @@ class TimeTrackerController extends Controller
|
||||
*/
|
||||
public function show(TimeTracker $timeTracker)
|
||||
{
|
||||
//
|
||||
return response()->json(new TimeTrackerResource($timeTracker->load('toDo')));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -19,7 +19,7 @@ class ToDoResource extends JsonResource
|
||||
'user_id' => $this->user_id,
|
||||
'name' => $this->name,
|
||||
'description' => $this->description,
|
||||
'checked' => $this->checked,
|
||||
'checked' => $this->checked?->format('Y-m-d H:i:s'),
|
||||
'duration' => $this->duration,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -15,9 +15,7 @@ class Reset extends Mailable
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*/
|
||||
public function __construct(private readonly string $token)
|
||||
{
|
||||
}
|
||||
public function __construct(private readonly string $token) {}
|
||||
|
||||
/**
|
||||
* Get the message envelope.
|
||||
@@ -37,7 +35,7 @@ class Reset extends Mailable
|
||||
return new Content(
|
||||
markdown: 'mails.reset_password',
|
||||
with: [
|
||||
'link' => config('gpao.front_url').'/reset/'.$this->token,
|
||||
'link' => config('app.url').'/reset/'.$this->token,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,10 @@ class ToDo extends Model
|
||||
'description',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'checked' => 'datetime',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"keywords": ["laravel", "framework"],
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.1",
|
||||
"php": "^8.3",
|
||||
"guzzlehttp/guzzle": "^7.2",
|
||||
"laravel/framework": "^10.10",
|
||||
"laravel/sanctum": "^3.3",
|
||||
|
||||
1731
composer.lock
generated
1731
composer.lock
generated
File diff suppressed because it is too large
Load Diff
38
package.json
38
package.json
@@ -8,26 +8,26 @@
|
||||
"lint-fix": "eslint ./ --fix"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^18.2.48",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.1",
|
||||
"@typescript-eslint/parser": "^7.0.1",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"axios": "^1.6.4",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-plugin-react": "^7.33.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-tailwindcss": "^3.14.0",
|
||||
"laravel-vite-plugin": "^1.0.0",
|
||||
"postcss": "^8.4.33",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.0"
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "^7.18.0",
|
||||
"@typescript-eslint/parser": "^7.18.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"axios": "^1.7.8",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-plugin-react": "^7.37.2",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-tailwindcss": "^3.17.5",
|
||||
"laravel-vite-plugin": "^1.0.6",
|
||||
"postcss": "^8.4.49",
|
||||
"tailwindcss": "^3.4.15",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^5.4.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-router-dom": "^6.21.3"
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-router-dom": "^6.28.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@
|
||||
<env name="APP_ENV" value="testing"/>
|
||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||
<env name="CACHE_DRIVER" value="redis"/>
|
||||
<!-- <env name="DB_CONNECTION" value="sqlite"/> -->
|
||||
<!-- <env name="DB_DATABASE" value=":memory:"/> -->
|
||||
<env name="DB_CONNECTION" value="sqlite"/>
|
||||
<env name="DB_DATABASE" value=":memory:"/>
|
||||
<env name="MAIL_MAILER" value="array"/>
|
||||
<env name="PULSE_ENABLED" value="false"/>
|
||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||
|
||||
4822
pnpm-lock.yaml
generated
4822
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -9,8 +9,6 @@ const Header = () => {
|
||||
const {authUser} = useAuthUser()
|
||||
const location = useLocation()
|
||||
|
||||
console.log(authUser)
|
||||
|
||||
return <header className="bg-blue-700 text-xl text-white">
|
||||
<div className="flex justify-between px-5 py-3">
|
||||
<div>
|
||||
|
||||
@@ -30,8 +30,9 @@ export const AuthUserProvider = ({children}: PropsWithChildren) => {
|
||||
} catch (error) {
|
||||
// @ts-expect-error 401 error to redirect
|
||||
if (error.response.status === 401) {
|
||||
const page = window.location.pathname.split('/')[1]
|
||||
if (!['connexion', 'sinscrire', 'reset'].includes(page)) {
|
||||
console.info('no user login')
|
||||
if (!['/connexion', '/sinscrire'].includes(window.location.pathname)) {
|
||||
window.location.href = '/connexion'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,9 +14,13 @@ const useAxiosTools = (isLoading = false) => {
|
||||
|
||||
const errorCatch = (error: Error|AxiosError|unknown) => {
|
||||
if (axios.isAxiosError(error)) {
|
||||
(error.response?.status === 422)
|
||||
? displayFormErrors(error)
|
||||
: setError(error.response?.data.message || error.message)
|
||||
if (error.response?.status === 422) {
|
||||
displayFormErrors(error)
|
||||
} else if (error.response?.status === 401) {
|
||||
console.log('not authorise')
|
||||
} else {
|
||||
setError(error.response?.data.message || error.message)
|
||||
}
|
||||
} else if (error instanceof Error) {
|
||||
setError(error.message)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ interface TrackerProps {
|
||||
|
||||
export const TrackerProvider = ({children}: PropsWithChildren) => {
|
||||
const [currentTimeTracker, setCurrentTimeTracker] = useState<timeTracker|null>(null)
|
||||
const {axiosGet, axiosPost, axiosDelete} = useAxiosTools()
|
||||
const {axiosGet, axiosPost, axiosDelete, errorCatch} = useAxiosTools()
|
||||
|
||||
useEffect(() => {
|
||||
fetchCurrentTimeTracker()
|
||||
@@ -25,7 +25,7 @@ export const TrackerProvider = ({children}: PropsWithChildren) => {
|
||||
const res = await axiosGet(`/api/time-trackers/user`)
|
||||
setCurrentTimeTracker(res.data)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
errorCatch(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,11 @@ const Login = () => {
|
||||
placeholder="******"
|
||||
value={password}
|
||||
onChange={event => setPassword(event.target.value)}>Mot de passe</Field>
|
||||
|
||||
<Field type="hidden"
|
||||
value={1}
|
||||
name="form_info" onChange={() => setPassword('')}/>
|
||||
|
||||
<button type="submit" className="btn-primary mt-5 block w-full text-lg">Valider</button>
|
||||
<Link to="/mot-de-passe-oubliee" className="mt-2 inline-block">Mot de passe oublié ?</Link>
|
||||
</form>
|
||||
|
||||
@@ -3,12 +3,10 @@ import Field from "../../components/Field"
|
||||
import axios from "axios"
|
||||
import {useNavigate} from "react-router-dom"
|
||||
import Card from "../../components/Card"
|
||||
import useAuthUser from "../../hooks/AuthUser"
|
||||
|
||||
const Register = () => {
|
||||
|
||||
const navigate = useNavigate()
|
||||
const {setAuthUser} = useAuthUser()
|
||||
const [name, setName] = useState('')
|
||||
const [email, setEmail] = useState('')
|
||||
const [password, setPassword] = useState('')
|
||||
@@ -17,9 +15,8 @@ const Register = () => {
|
||||
event.preventDefault()
|
||||
try {
|
||||
await axios.get('/sanctum/csrf-cookie')
|
||||
const res = await axios.post('/api/register', {name, email, password})
|
||||
setAuthUser(res.data)
|
||||
navigate('/')
|
||||
await axios.post('/api/register', {name, email, password})
|
||||
navigate('/connexion')
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ import Register from "./Auth/Register"
|
||||
import ToDoShow from "./ToDos/ToDoShow"
|
||||
import TimeTrackersIndex from "./TimeTrackersIndex"
|
||||
import {env} from "../utilities/env"
|
||||
import Reset from "./Auth/Reset"
|
||||
import ForgotPassword from "./Auth/ForgotPassword"
|
||||
|
||||
const Router = () => {
|
||||
|
||||
@@ -26,6 +28,8 @@ const Router = () => {
|
||||
<Route path="/connexion" element={<Login />} />
|
||||
{env.VITE_REGISTER_DISABLED === 'false' && <Route path="/sinscrire" element={<Register />} />}
|
||||
<Route path="/todos/:id" element={<ToDoShow />} />
|
||||
<Route path="/mot-de-passe-oubliee" element={<ForgotPassword />} />
|
||||
<Route path="/reset/:token" element={<Reset />} />
|
||||
<Route path="/times" element={<TimeTrackersIndex />} />
|
||||
</Routes>
|
||||
</Suspense>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
// @ts-expect-error use import var
|
||||
export const env: envProps = import.meta.env
|
||||
|
||||
|
||||
interface envProps {
|
||||
BASE_URL: string,
|
||||
DEV: boolean,
|
||||
|
||||
7
resources/lang/fr/validation.php
Normal file
7
resources/lang/fr/validation.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'exists' => 'Le champ :attribute est invalide.',
|
||||
'invalid_credentials' => 'Identifiant ou mot de passe incorrect.',
|
||||
'password_rules' => 'Le mot de passe doit contenir des lettres majuscules et minuscules, des chiffres et au moins 8 caractères.',
|
||||
];
|
||||
@@ -25,8 +25,8 @@ test('invalid credential return an error', function () {
|
||||
])
|
||||
->assertStatus(422)
|
||||
->assertJson([
|
||||
'message' => 'Invalid credentials.',
|
||||
'errors' => ['form_info' => 'Invalid credentials.'],
|
||||
'message' => 'Identifiant ou mot de passe incorrect.',
|
||||
'errors' => ['form_info' => 'Identifiant ou mot de passe incorrect.'],
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -201,8 +201,8 @@ test('reset action need specific credentials', function () {
|
||||
])
|
||||
->assertStatus(422)
|
||||
->assertJson([
|
||||
'message' => 'The selected token is invalid.',
|
||||
'errors' => ['token' => ['The selected token is invalid.']],
|
||||
'message' => 'Le champ token est invalide.',
|
||||
'errors' => ['token' => ['Le champ token est invalide.']],
|
||||
]);
|
||||
|
||||
DB::table('password_reset_tokens')
|
||||
@@ -221,7 +221,7 @@ test('reset action need specific credentials', function () {
|
||||
])
|
||||
->assertStatus(422)
|
||||
->assertJson([
|
||||
'message' => 'Invalid credentials.',
|
||||
'errors' => ['form_info' => 'Invalid credentials.'],
|
||||
'message' => 'Identifiant ou mot de passe incorrect.',
|
||||
'errors' => ['form_info' => 'Identifiant ou mot de passe incorrect.'],
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -54,6 +54,27 @@ test('user has no content response if not current time tracker', function () {
|
||||
->assertNoContent();
|
||||
});
|
||||
|
||||
test('user can retrieve a time tracker', function () {
|
||||
Sanctum::actingAs($user = User::factory()->create());
|
||||
$toDo = ToDo::factory()->create(['user_id' => $user->id, 'checked' => null]);
|
||||
$timeTracker = $toDo->timeTrackers()->create([
|
||||
'start_at' => now()->subMinutes(20),
|
||||
'end_at' => now()->subMinutes(10),
|
||||
]);
|
||||
|
||||
$this->get('/api/time-trackers/'.$timeTracker->id)
|
||||
->assertOk()
|
||||
->assertJson([
|
||||
'id' => $timeTracker->id,
|
||||
'start_at' => now()->subMinutes(20),
|
||||
'end_at' => now()->subMinutes(10),
|
||||
'to_do' => [
|
||||
'id' => $toDo->id,
|
||||
'user_id' => $user->id,
|
||||
],
|
||||
]);
|
||||
});
|
||||
|
||||
test('user can stop current time tracker', function () {
|
||||
Sanctum::actingAs($user = User::factory()->create());
|
||||
$toDo = ToDo::factory()->create(['user_id' => $user->id, 'checked' => null]);
|
||||
@@ -115,3 +136,18 @@ test('user can update a time tracker', function () {
|
||||
|
||||
expect($toDo->refresh())->duration->toBe(660);
|
||||
});
|
||||
|
||||
test('user can retrieve time trackers of a todo', function () {
|
||||
Sanctum::actingAs($user = User::factory()->create());
|
||||
$toDo = ToDo::factory()->create(['user_id' => $user->id, 'duration' => 600, 'checked' => null]);
|
||||
for ($i = 10; $i > 0; $i--) {
|
||||
$toDo->timeTrackers()->create([
|
||||
'start_at' => now()->subMinutes(20 + $i * 10),
|
||||
'end_at' => now()->subMinutes(10 + $i * 10),
|
||||
]);
|
||||
}
|
||||
|
||||
$this->get("/api/todos/{$toDo->id}/time-trackers")
|
||||
->assertOk()
|
||||
->assertJsonCount(10);
|
||||
});
|
||||
|
||||
@@ -11,11 +11,10 @@ beforeEach(function () {
|
||||
test('an user can add todo', function () {
|
||||
Sanctum::actingAs($this->user);
|
||||
|
||||
$response = $this->post('api/todos', [
|
||||
$this->post('api/todos', [
|
||||
'name' => 'Test',
|
||||
]);
|
||||
|
||||
$response->assertStatus(201)
|
||||
])
|
||||
->assertStatus(201)
|
||||
->assertJson([
|
||||
'user_id' => $this->user->id,
|
||||
'name' => 'Test',
|
||||
@@ -25,7 +24,8 @@ test('an user can add todo', function () {
|
||||
expect($this->user->toDos)
|
||||
->toHaveCount(1)
|
||||
->first()->name->toBe('Test')
|
||||
->first()->checked->toBeNull();
|
||||
->first()->checked->toBeNull()
|
||||
->first()->user->id->toBe($this->user->id);
|
||||
});
|
||||
|
||||
test('an user can retrieve his to dos', function () {
|
||||
@@ -76,9 +76,8 @@ test('an user can retrieve a to do', function () {
|
||||
|
||||
$toDo = $toDos[rand(0, 9)];
|
||||
|
||||
$response = $this->get('api/todos/'.$toDo->id);
|
||||
|
||||
$response->assertOk()
|
||||
$this->get('api/todos/'.$toDo->id)
|
||||
->assertOk()
|
||||
->assertJson([
|
||||
'id' => $toDo->id,
|
||||
'user_id' => $toDo->user_id,
|
||||
@@ -96,9 +95,8 @@ test('an user can update a to do', function () {
|
||||
|
||||
$toDo = $toDos[rand(0, 9)];
|
||||
|
||||
$response = $this->put('api/todos/'.$toDo->id, ['name' => 'update test']);
|
||||
|
||||
$response->assertOk()
|
||||
$this->put('api/todos/'.$toDo->id, ['name' => 'update test'])
|
||||
->assertOk()
|
||||
->assertJson([
|
||||
'id' => $toDo->id,
|
||||
'user_id' => $toDo->user_id,
|
||||
@@ -108,6 +106,15 @@ test('an user can update a to do', function () {
|
||||
|
||||
expect(ToDo::find($toDo->id))
|
||||
->name->toBe('update test');
|
||||
|
||||
$this->put('api/todos/'.$toDo->id, ['checked' => true])
|
||||
->assertOk()
|
||||
->assertJson([
|
||||
'id' => $toDo->id,
|
||||
'user_id' => $toDo->user_id,
|
||||
'name' => 'update test',
|
||||
'checked' => now()->format('Y-m-d H:i:s'),
|
||||
]);
|
||||
});
|
||||
|
||||
test('an user can delete a to do', function () {
|
||||
@@ -119,9 +126,8 @@ test('an user can delete a to do', function () {
|
||||
|
||||
$toDo = $toDos[rand(0, 9)];
|
||||
|
||||
$response = $this->delete('api/todos/'.$toDo->id);
|
||||
|
||||
$response->assertNoContent();
|
||||
$this->delete('api/todos/'.$toDo->id)
|
||||
->assertNoContent();
|
||||
|
||||
expect(ToDo::all())->toHaveCount(9);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user