Merge pull request #17492 from ischooluw/17448-feature-notes-api-endpoints

Fixes #17448: feat(api) - API endpoint for Adding Ad-Hoc Notes to Assets
This commit is contained in:
snipe
2025-08-26 12:40:52 +01:00
committed by GitHub
4 changed files with 227 additions and 0 deletions

View File

@@ -0,0 +1,95 @@
<?php
namespace App\Http\Controllers\Api;
use App\Helpers\Helper;
use App\Http\Controllers\Controller;
use App\Models\Actionlog;
use App\Models\Asset;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Log;
/**
* This class controls all API actions related to notes for
* the Snipe-IT Asset Management application.
*/
class NotesController extends Controller
{
/**
* Retrieve a list of manual notes (action logs) for a given asset.
*
* Checks authorization to view assets, attempts to find the asset by ID,
* and fetches related action log entries of type 'note added', including
* user information for each note. Returns a JSON response with the notes or errors.
*
* @param \Illuminate\Http\Request $request The incoming HTTP request.
* @param Asset $asset The ID of the asset whose notes to retrieve.
* @return \Illuminate\Http\JsonResponse
*/
public function index(Asset $asset): JsonResponse
{
$this->authorize('view', $asset);
// Get the manual notes for the asset
$notes = ActionLog::with('user:id,username')
->where('item_type', Asset::class)
->where('item_id', $asset->id)
->where('action_type', 'note added')
->orderBy('created_at', 'desc')
->get(['id', 'created_at', 'note', 'created_by', 'item_id', 'item_type', 'action_type', 'target_id', 'target_type']);
$notesArray = $notes->map(function ($note) {
return [
'id' => $note->id,
'created_at' => $note->created_at,
'note' => $note->note,
'created_by' => $note->created_by,
'username' => $note->user?->username, // adding the username
'item_id' => $note->item_id,
'item_type' => $note->item_type,
'action_type' => $note->action_type,
];
});
// Return a success response
return response()->json(Helper::formatStandardApiResponse('success', ['notes' => $notesArray, 'asset_id' => $asset->id]));
}
/**
* Store a manual note on a specified asset and log the action.
*
* Checks authorization for updating assets, validates the presence of the 'note',
* attempts to find the asset by ID, and creates a new ActionLog entry if successful.
* Returns JSON responses indicating success or failure with appropriate HTTP status codes.
*
* @param \Illuminate\Http\Request $request The incoming HTTP request containing the 'note'.
* @param Asset $asset The ID of the asset to attach the note to.
* @return \Illuminate\Http\JsonResponse
*/
public function store(Request $request, Asset $asset): JsonResponse
{
$this->authorize('update', $asset);
if ($request->input('note', '') == '') {
return response()->json(Helper::formatStandardApiResponse('error', null, trans('validation.required', ['attribute' => 'note'])), 422);
}
// Create the note
$logaction = new ActionLog();
$logaction->item_type = get_class($asset);
$logaction->created_by = Auth::id();
$logaction->item_id = $asset->id;
$logaction->note = $request->input('note', '');
if ($logaction->logaction('note added')) {
// Return a success response
return response()->json(Helper::formatStandardApiResponse('success', ['note' => $logaction->note, 'item_id' => $asset->id], trans('general.note_added')));
}
// Return an error response if something went wrong
return response()->json(Helper::formatStandardApiResponse('error', null, 'Something went wrong'), 500);
}
}

View File

@@ -84,6 +84,28 @@ class ActionlogFactory extends Factory
});
}
/**
* This sets up an ActionLog representing a manually added note tied to an Asset,
* with an optional User as the creator. If no User is provided, one is generated.
*
* @param User|null $user Optional user to associate as the creator of the note.
* @return \Illuminate\Database\Eloquent\Factories\Factory<ActionLog>
*/
public function assetNote(?User $user=null)
{
return $this
->state(function () use ($user) {
return [
'action_type' => 'note added',
'item_type' => Asset::class,
'target_type' => 'asset',
'note' => 'Factory-generated manual note',
'created_by' => $user?->id ?? User::factory(),
];
})
->for($user ?? User::factory(), 'user');
}
public function licenseCheckoutToUser()
{
return $this->state(function () {

View File

@@ -841,6 +841,28 @@ Route::group(['prefix' => 'v1', 'middleware' => ['api', 'api-throttle:api']], fu
); // end asset models API routes
/**
* Asset notes API routes
*/
Route::group(['prefix' => 'notes'], function () {
Route::post(
'{asset}/store',
[
Api\NotesController::class,
'store'
]
)->name('api.notes.store');
Route::get(
'{asset}/index',
[
Api\NotesController::class,
'index'
]
)->name('api.notes.index');
}
); // end asset notes API routes
/**
* Settings API routes

View File

@@ -0,0 +1,88 @@
<?php
namespace Tests\Feature\Assets\Api;
use App\Models\Actionlog;
use App\Models\Asset;
use App\Models\User;
use Tests\TestCase;
class AssetNotesTest extends TestCase
{
public function testThatANonExistentAssetIdReturnsError()
{
$this->actingAsForApi(User::factory()->editAssets()->create())
->postJson(route('api.notes.store', ['asset' => 123456789]))
->assertStatusMessageIs('error');
}
public function testRequiresPermissionToAddNoteToAssetAsset()
{
$asset = Asset::factory()->create();
$this->actingAsForApi(User::factory()->create())
->postJson(route('api.notes.store', $asset), [
'note' => 'test'
])
->assertForbidden();
}
public function testAssetNoteIsSaved()
{
$asset = Asset::factory()->create();
$this->actingAsForApi(User::factory()->editAssets()->create())
->postJson(route('api.notes.store', $asset), [
'note' => 'This is a test note.'
])
->assertStatusMessageIs('success')
->assertJson([
'messages' => trans('general.note_added'),
'payload' => [
'note' => 'This is a test note.',
'item_id' => e($asset->id),
],
])
->assertStatus(200);
$note = ActionLog::where('item_id', $asset->id)
->where('action_type', 'note added')
->first();
$this->assertNotNull($note, 'The note was not saved in the database.');
$this->assertEquals('This is a test note.', $note->note, 'The note content does not match.');
}
public function testAssetNotesAreRetrievable()
{
$asset = Asset::factory()->create();
$user = User::factory()->viewAssets()->create();
$assetNote = Actionlog::factory()
->assetNote($user)
->create([
'item_id' => $asset->id,
'note' => 'This is a test note.',
]);
$this->actingAsForApi($user)
->getJson(route('api.notes.index', $asset))
->assertOk()
->assertJson([
'messages' => null,
'payload' => [
'notes' => [
[
'note' => 'This is a test note.',
'created_by' => $assetNote->created_by,
'username' => $user->username,
'item_id' => $assetNote->item_id,
'item_type' => Asset::class,
'action_type' => 'note added',
]
]
],
]);
}
}