diff --git a/app/Enums/ActivityLog.php b/app/Enums/ActivityLog.php
index a4216b81..9697d829 100644
--- a/app/Enums/ActivityLog.php
+++ b/app/Enums/ActivityLog.php
@@ -8,4 +8,7 @@ class ActivityLog
public const USER_API_TOKEN_GENERATED = 'user.api_token_regenerated';
public const USER_API_TOKEN_REVOKED = 'user.api_token_revoked';
+
+ public const SYSTEM_API_TOKEN_GENERATED = 'system.api_token_regenerated';
+ public const SYSTEM_API_TOKEN_REVOKED = 'system.api_token_revoked';
}
diff --git a/app/Enums/ApiToken.php b/app/Enums/ApiToken.php
index 40f223af..34b7dd43 100644
--- a/app/Enums/ApiToken.php
+++ b/app/Enums/ApiToken.php
@@ -6,4 +6,44 @@ class ApiToken
{
public const ABILITY_USER_ACCESS = 'user_access';
public const ABILITY_SYSTEM_ACCESS = 'system_access';
+ public const ABILITY_SYSTEM_ACCESS_PRIVATE = 'system_access_private';
+
+ public const ABILITY_LINKS_READ = 'links.read';
+ public const ABILITY_LINKS_CREATE = 'links.create';
+ public const ABILITY_LINKS_UPDATE = 'links.update';
+ public const ABILITY_LINKS_DELETE = 'links.delete';
+
+ public const ABILITY_LISTS_READ = 'lists.read';
+ public const ABILITY_LISTS_CREATE = 'lists.create';
+ public const ABILITY_LISTS_UPDATE = 'lists.update';
+ public const ABILITY_LISTS_DELETE = 'lists.delete';
+
+ public const ABILITY_TAGS_READ = 'tags.read';
+ public const ABILITY_TAGS_CREATE = 'tags.create';
+ public const ABILITY_TAGS_UPDATE = 'tags.update';
+ public const ABILITY_TAGS_DELETE = 'tags.delete';
+
+ public const ABILITY_NOTES_READ = 'notes.read';
+ public const ABILITY_NOTES_CREATE = 'notes.create';
+ public const ABILITY_NOTES_UPDATE = 'notes.update';
+ public const ABILITY_NOTES_DELETE = 'notes.delete';
+
+ public static array $systemTokenAbilities = [
+ self::ABILITY_LINKS_READ,
+ self::ABILITY_LINKS_CREATE,
+ self::ABILITY_LINKS_UPDATE,
+ self::ABILITY_LINKS_DELETE,
+ self::ABILITY_LISTS_READ,
+ self::ABILITY_LISTS_CREATE,
+ self::ABILITY_LISTS_UPDATE,
+ self::ABILITY_LISTS_DELETE,
+ self::ABILITY_TAGS_READ,
+ self::ABILITY_TAGS_CREATE,
+ self::ABILITY_TAGS_UPDATE,
+ self::ABILITY_TAGS_DELETE,
+ self::ABILITY_NOTES_READ,
+ self::ABILITY_NOTES_CREATE,
+ self::ABILITY_NOTES_UPDATE,
+ self::ABILITY_NOTES_DELETE,
+ ];
}
diff --git a/app/Helper/functions.php b/app/Helper/functions.php
index 5da7bcfd..c2dec7ee 100644
--- a/app/Helper/functions.php
+++ b/app/Helper/functions.php
@@ -122,6 +122,10 @@ function getPaginationLimit(): mixed
$default = config('linkace.default.pagination');
+ if (auth()->id() === 0) {
+ return $default;
+ }
+
if (request()->is('guest/*')) {
return guestsettings('listitem_count') ?: $default;
}
diff --git a/app/Http/Controllers/API/LinkCheckController.php b/app/Http/Controllers/API/LinkCheckController.php
index 2818dfcc..92038a99 100644
--- a/app/Http/Controllers/API/LinkCheckController.php
+++ b/app/Http/Controllers/API/LinkCheckController.php
@@ -13,6 +13,8 @@ public function __invoke(Request $request): JsonResponse
{
$searchedUrl = $request->input('url', false);
+ $this->authorize('viewAny', Link::class);
+
if (!$searchedUrl) {
return response()->json(['linksFound' => false]);
}
diff --git a/app/Http/Controllers/API/LinkController.php b/app/Http/Controllers/API/LinkController.php
index bb0d2286..7111d3ba 100644
--- a/app/Http/Controllers/API/LinkController.php
+++ b/app/Http/Controllers/API/LinkController.php
@@ -2,6 +2,7 @@
namespace App\Http\Controllers\API;
+use App\Enums\ApiToken;
use App\Http\Controllers\Controller;
use App\Http\Controllers\Traits\ChecksOrdering;
use App\Http\Requests\Models\LinkStoreRequest;
@@ -18,7 +19,7 @@ class LinkController extends Controller
public function __construct()
{
$this->allowedOrderBy = Link::$allowOrderBy;
- $this->authorizeResource(Link::class, 'link');
+ $this->authorizeResource(Link::class . 'Api', 'link');
}
public function index(Request $request): JsonResponse
@@ -29,7 +30,7 @@ public function index(Request $request): JsonResponse
$this->checkOrdering();
$links = Link::query()
- ->visibleForUser()
+ ->visibleForUser(privateSystemAccess: $request->user()->tokenCan(ApiToken::ABILITY_SYSTEM_ACCESS_PRIVATE))
->orderBy($this->orderBy, $this->orderDir)
->paginate(getPaginationLimit());
diff --git a/app/Http/Controllers/API/ListController.php b/app/Http/Controllers/API/ListController.php
index cc5568a7..8d1ce2af 100644
--- a/app/Http/Controllers/API/ListController.php
+++ b/app/Http/Controllers/API/ListController.php
@@ -18,7 +18,7 @@ class ListController extends Controller
public function __construct()
{
$this->allowedOrderBy = LinkList::$allowOrderBy;
- $this->authorizeResource(LinkList::class, 'list');
+ $this->authorizeResource(LinkList::class . 'Api', 'list');
}
public function index(Request $request): JsonResponse
diff --git a/app/Http/Controllers/API/NoteController.php b/app/Http/Controllers/API/NoteController.php
index cd0c3bbc..bd04e712 100644
--- a/app/Http/Controllers/API/NoteController.php
+++ b/app/Http/Controllers/API/NoteController.php
@@ -14,7 +14,7 @@ class NoteController extends Controller
{
public function __construct()
{
- $this->authorizeResource(Note::class, 'note');
+ $this->authorizeResource(Note::class . 'Api', 'note');
}
public function store(NoteStoreRequest $request): JsonResponse
diff --git a/app/Http/Controllers/API/TagController.php b/app/Http/Controllers/API/TagController.php
index 7be96a36..ac978364 100644
--- a/app/Http/Controllers/API/TagController.php
+++ b/app/Http/Controllers/API/TagController.php
@@ -18,7 +18,7 @@ class TagController extends Controller
public function __construct()
{
$this->allowedOrderBy = Tag::$allowOrderBy;
- $this->authorizeResource(Tag::class, 'tag');
+ $this->authorizeResource(Tag::class . 'Api', 'tag');
}
public function index(Request $request): JsonResponse
diff --git a/app/Http/Controllers/Admin/ApiTokenController.php b/app/Http/Controllers/Admin/ApiTokenController.php
new file mode 100644
index 00000000..b52fe835
--- /dev/null
+++ b/app/Http/Controllers/Admin/ApiTokenController.php
@@ -0,0 +1,65 @@
+ User::getSystemUser()->tokens()->get(),
+ ]);
+ }
+
+ public function show(PersonalAccessToken $token)
+ {
+ return view('admin.api-tokens.show', [
+ 'token' => $token,
+ ]);
+ }
+
+ public function store(CreateSystemApiTokenRequest $request): RedirectResponse
+ {
+ $abilities = $request->validated('abilities');
+
+ if ($request->get('private_access', false)) {
+ $abilities[] = ApiToken::ABILITY_SYSTEM_ACCESS_PRIVATE;
+ } else {
+ $abilities[] = ApiToken::ABILITY_SYSTEM_ACCESS;
+ }
+
+ $token = User::getSystemUser()->createToken($request->validated('token_name'), $abilities);
+
+ activity()
+ ->by($request->user())
+ ->withProperty('token_id', $token->accessToken->id)
+ ->log(ActivityLog::SYSTEM_API_TOKEN_GENERATED);
+
+ session()->flash('new_token', $token->plainTextToken);
+
+ return redirect()->route('system.api-tokens.show', ['api_token' => $token->accessToken]);
+ }
+
+ public function destroy(Request $request, PersonalAccessToken $token): RedirectResponse
+ {
+ $this->authorize('delete', $token);
+
+ $token->delete();
+
+ activity()
+ ->by($request->user())
+ ->log(ActivityLog::SYSTEM_API_TOKEN_REVOKED);
+
+ flash()->warning(trans('auth.api_tokens.revoke_successful'));
+ return redirect()->route('system.api-tokens.index');
+ }
+}
diff --git a/app/Http/Middleware/Authenticate.php b/app/Http/Middleware/Authenticate.php
index 7d97c7c1..34a0382a 100644
--- a/app/Http/Middleware/Authenticate.php
+++ b/app/Http/Middleware/Authenticate.php
@@ -11,6 +11,10 @@ public function handle($request, Closure $next, ...$guards)
{
$this->authenticate($request, $guards);
+ if (!$request->is('api/*') && $request->user()->isSystemUser()) {
+ abort(403, trans('user.system_user_locked'));
+ }
+
if ($request->user()->isBlocked()) {
abort(403, trans('user.block_warning'));
}
diff --git a/app/Http/Requests/Admin/CreateSystemApiTokenRequest.php b/app/Http/Requests/Admin/CreateSystemApiTokenRequest.php
new file mode 100644
index 00000000..30eb1063
--- /dev/null
+++ b/app/Http/Requests/Admin/CreateSystemApiTokenRequest.php
@@ -0,0 +1,34 @@
+ [
+ 'required',
+ 'alpha_dash',
+ 'min:3',
+ 'max:100',
+ Rule::unique('personal_access_tokens', 'name')->where(function (Builder $query) {
+ return $query->whereNull('tokenable_id');
+ }),
+ ],
+ 'abilities' => [
+ 'required',
+ new ApiTokenAbilityRule(),
+ ],
+ 'private_access' => [
+ 'sometimes',
+ 'accepted',
+ ],
+ ];
+ }
+}
diff --git a/app/Models/ScopesVisibility.php b/app/Models/ScopesVisibility.php
index 00aac7f4..a1c52799 100644
--- a/app/Models/ScopesVisibility.php
+++ b/app/Models/ScopesVisibility.php
@@ -7,12 +7,16 @@
trait ScopesVisibility
{
- public function scopeVisibleForUser(Builder $query, int $userId = null): Builder
+ public function scopeVisibleForUser(Builder $query, int $userId = null, bool $privateSystemAccess = false): Builder
{
if (is_null($userId) && auth()->check()) {
$userId = auth()->id();
}
+ if ($userId === 0) {
+ return $privateSystemAccess ? $query : $query->whereNot('visibility', ModelAttribute::VISIBILITY_PRIVATE);
+ }
+
// Entity must be either public or internal, or have a private status together with the matching user id
return $query->where(function (Builder $query) use ($userId) {
$query->where('visibility', ModelAttribute::VISIBILITY_PUBLIC)
diff --git a/app/Models/User.php b/app/Models/User.php
index a3d7197e..89a5483a 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -2,6 +2,7 @@
namespace App\Models;
+use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
@@ -77,11 +78,26 @@ class User extends Authenticatable implements Auditable
public array $auditModifiers = [];
+ /*
+ * ========================================================================
+ * SCOPES
+ */
+
+ public function scopeNotSystem(Builder $query): Builder
+ {
+ return $query->whereNot('id', 0);
+ }
+
/*
* ========================================================================
* METHODS
*/
+ public static function getSystemUser(): User
+ {
+ return self::whereId(0)->first();
+ }
+
public function isBlocked(): bool
{
return $this->blocked_at !== null;
@@ -91,4 +107,9 @@ public function isCurrentlyLoggedIn(): bool
{
return $this->is(auth()->user());
}
+
+ public function isSystemUser(): bool
+ {
+ return $this->id === 0;
+ }
}
diff --git a/app/Policies/Api/LinkApiPolicy.php b/app/Policies/Api/LinkApiPolicy.php
new file mode 100644
index 00000000..d1fd2e92
--- /dev/null
+++ b/app/Policies/Api/LinkApiPolicy.php
@@ -0,0 +1,65 @@
+isSystemUser()) {
+ return $user->tokenCan(ApiToken::ABILITY_LINKS_READ);
+ }
+ return true;
+ }
+
+ public function view(User $user, Link $link): bool
+ {
+ if ($user->isSystemUser()) {
+ $canViewPrivate = $user->tokenCan(ApiToken::ABILITY_SYSTEM_ACCESS_PRIVATE);
+ return $link->is_private ? $canViewPrivate : $user->tokenCan(ApiToken::ABILITY_LINKS_READ);
+ }
+ return $this->userCanAccessLink($user, $link);
+ }
+
+ public function create(User $user): bool
+ {
+ return true;
+ }
+
+ public function update(User $user, Link $link): bool
+ {
+ return $this->userCanAccessLink($user, $link);
+ }
+
+ public function delete(User $user, Link $link): bool
+ {
+ return $link->user->is($user);
+ }
+
+ public function restore(User $user, Link $link): bool
+ {
+ return $link->user->is($user);
+ }
+
+ public function forceDelete(User $user, Link $link): bool
+ {
+ return $link->user->is($user);
+ }
+
+ // Link must be either owned by user, or be not private
+ protected function userCanAccessLink(User $user, Link $link): bool
+ {
+ if ($link->user_id === $user->id) {
+ return true;
+ }
+ return $link->visibility !== ModelAttribute::VISIBILITY_PRIVATE;
+ }
+}
diff --git a/app/Policies/Api/LinkListApiPolicy.php b/app/Policies/Api/LinkListApiPolicy.php
new file mode 100644
index 00000000..e37e8cde
--- /dev/null
+++ b/app/Policies/Api/LinkListApiPolicy.php
@@ -0,0 +1,57 @@
+userCanAccessList($user, $list);
+ }
+
+ public function create(User $user): bool
+ {
+ return true;
+ }
+
+ public function update(User $user, LinkList $list): bool
+ {
+ return $this->userCanAccessList($user, $list);
+ }
+
+ public function delete(User $user, LinkList $list): bool
+ {
+ return $list->user->is($user);
+ }
+
+ public function restore(User $user, LinkList $list): bool
+ {
+ return $list->user->is($user);
+ }
+
+ public function forceDelete(User $user, LinkList $list): bool
+ {
+ return $list->user->is($user);
+ }
+
+ // Link must be either owned by user, or be not private
+ protected function userCanAccessList(User $user, LinkList $list): bool
+ {
+ if ($list->user_id === $user->id) {
+ return true;
+ }
+ return $list->visibility !== ModelAttribute::VISIBILITY_PRIVATE;
+ }
+}
diff --git a/app/Policies/Api/NoteApiPolicy.php b/app/Policies/Api/NoteApiPolicy.php
new file mode 100644
index 00000000..074b47aa
--- /dev/null
+++ b/app/Policies/Api/NoteApiPolicy.php
@@ -0,0 +1,57 @@
+userCanAccessNote($user, $note);
+ }
+
+ public function create(User $user): bool
+ {
+ return true;
+ }
+
+ public function update(User $user, Note $note): bool
+ {
+ return $this->userCanAccessNote($user, $note);
+ }
+
+ public function delete(User $user, Note $note): bool
+ {
+ return $note->user->is($user);
+ }
+
+ public function restore(User $user, Note $note): bool
+ {
+ return $note->user->is($user);
+ }
+
+ public function forceDelete(User $user, Note $note): bool
+ {
+ return $note->user->is($user);
+ }
+
+ // Link must be either owned by user, or be not private
+ protected function userCanAccessNote(User $user, Note $note): bool
+ {
+ if ($note->user_id === $user->id) {
+ return true;
+ }
+ return $note->visibility !== ModelAttribute::VISIBILITY_PRIVATE;
+ }
+}
diff --git a/app/Policies/Api/TagApiPolicy.php b/app/Policies/Api/TagApiPolicy.php
new file mode 100644
index 00000000..edbe81b4
--- /dev/null
+++ b/app/Policies/Api/TagApiPolicy.php
@@ -0,0 +1,57 @@
+userCanAccessTag($user, $tag);
+ }
+
+ public function create(User $user): bool
+ {
+ return true;
+ }
+
+ public function update(User $user, Tag $tag): bool
+ {
+ return $this->userCanAccessTag($user, $tag);
+ }
+
+ public function delete(User $user, Tag $tag): bool
+ {
+ return $tag->user->is($user);
+ }
+
+ public function restore(User $user, Tag $tag): bool
+ {
+ return $tag->user->is($user);
+ }
+
+ public function forceDelete(User $user, Tag $tag): bool
+ {
+ return $tag->user->is($user);
+ }
+
+ // Link must be either owned by user, or be not private
+ protected function userCanAccessTag(User $user, Tag $tag): bool
+ {
+ if ($tag->user_id === $user->id) {
+ return true;
+ }
+ return $tag->visibility !== ModelAttribute::VISIBILITY_PRIVATE;
+ }
+}
diff --git a/app/Policies/ApiTokenPolicy.php b/app/Policies/ApiTokenPolicy.php
index 2e45532c..c31cf42d 100644
--- a/app/Policies/ApiTokenPolicy.php
+++ b/app/Policies/ApiTokenPolicy.php
@@ -2,6 +2,7 @@
namespace App\Policies;
+use App\Enums\Role;
use App\Models\User;
use Illuminate\Auth\Access\HandlesAuthorization;
use Laravel\Sanctum\PersonalAccessToken;
@@ -32,7 +33,7 @@ public function update(User $user, PersonalAccessToken $personalAccessToken): bo
public function delete(User $user, PersonalAccessToken $personalAccessToken): bool
{
- return $personalAccessToken->tokenable->is($user);
+ return $personalAccessToken->tokenable->is($user) || $user->hasRole(Role::ADMIN);
}
public function restore(User $user, PersonalAccessToken $personalAccessToken): bool
diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php
index e60c53b8..0a9541e5 100644
--- a/app/Providers/AuthServiceProvider.php
+++ b/app/Providers/AuthServiceProvider.php
@@ -6,6 +6,10 @@
use App\Models\LinkList;
use App\Models\Note;
use App\Models\Tag;
+use App\Policies\Api\LinkApiPolicy;
+use App\Policies\Api\LinkListApiPolicy;
+use App\Policies\Api\NoteApiPolicy;
+use App\Policies\Api\TagApiPolicy;
use App\Policies\ApiTokenPolicy;
use App\Policies\LinkListPolicy;
use App\Policies\LinkPolicy;
@@ -27,6 +31,10 @@ class AuthServiceProvider extends ServiceProvider
Note::class => NotePolicy::class,
Tag::class => TagPolicy::class,
PersonalAccessToken::class => ApiTokenPolicy::class,
+ Link::class . 'Api' => LinkApiPolicy::class,
+ LinkList::class . 'Api' => LinkListApiPolicy::class,
+ Note::class . 'Api' => NoteApiPolicy::class,
+ Tag::class . 'Api' => TagApiPolicy::class,
];
/**
diff --git a/app/Rules/ApiTokenAbilityRule.php b/app/Rules/ApiTokenAbilityRule.php
new file mode 100644
index 00000000..7fe84a01
--- /dev/null
+++ b/app/Rules/ApiTokenAbilityRule.php
@@ -0,0 +1,48 @@
+id();
+ return self::$userId ?: auth()->id();
}
public static function defaults(): array
diff --git a/database/factories/LinkFactory.php b/database/factories/LinkFactory.php
index b4391c61..9dcdf75c 100644
--- a/database/factories/LinkFactory.php
+++ b/database/factories/LinkFactory.php
@@ -18,7 +18,7 @@ class LinkFactory extends Factory
public function definition(): array
{
return [
- 'user_id' => User::first()->id ?? User::factory(),
+ 'user_id' => User::notSystem()->first()->id ?? User::factory(),
'url' => $this->faker->url(),
'title' => $this->faker->boolean(70)
? $this->faker->words(random_int(2, 5), true)
diff --git a/database/factories/LinkListFactory.php b/database/factories/LinkListFactory.php
index 7cdd4588..9413e76b 100644
--- a/database/factories/LinkListFactory.php
+++ b/database/factories/LinkListFactory.php
@@ -18,7 +18,7 @@ class LinkListFactory extends Factory
public function definition(): array
{
return [
- 'user_id' => User::first()->id ?? User::factory(),
+ 'user_id' => User::notSystem()->first()->id ?? User::factory(),
'name' => ucwords($this->faker->words(random_int(2, 5), true)),
'description' => random_int(0, 1) ? $this->faker->sentences(random_int(1, 2), true) : null,
'visibility' => ModelAttribute::VISIBILITY_PUBLIC,
diff --git a/database/factories/NoteFactory.php b/database/factories/NoteFactory.php
index 5dcbd9fd..b8f5a18c 100644
--- a/database/factories/NoteFactory.php
+++ b/database/factories/NoteFactory.php
@@ -19,7 +19,7 @@ class NoteFactory extends Factory
public function definition(): array
{
return [
- 'user_id' => User::first()->id ?? User::factory(),
+ 'user_id' => User::notSystem()->first()->id ?? User::factory(),
'link_id' => Link::first()->id ?? Link::factory(),
'note' => $this->faker->sentences(random_int(1, 5), true),
'visibility' => ModelAttribute::VISIBILITY_PUBLIC,
diff --git a/database/factories/TagFactory.php b/database/factories/TagFactory.php
index f8e1c4a7..5206efe0 100644
--- a/database/factories/TagFactory.php
+++ b/database/factories/TagFactory.php
@@ -18,7 +18,7 @@ class TagFactory extends Factory
public function definition(): array
{
return [
- 'user_id' => User::first()->id ?? User::factory(),
+ 'user_id' => User::notSystem()->first()->id ?? User::factory(),
'name' => $this->faker->words(random_int(2, 3), true),
'visibility' => ModelAttribute::VISIBILITY_PUBLIC,
];
diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php
index 846f822c..5a9c2eb8 100644
--- a/database/factories/UserFactory.php
+++ b/database/factories/UserFactory.php
@@ -2,9 +2,10 @@
namespace Database\Factories;
+use App\Actions\Settings\SetDefaultSettingsForUser;
+use App\Enums\Role;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
-use Illuminate\Support\Str;
class UserFactory extends Factory
{
@@ -21,4 +22,12 @@ public function definition(): array
'password' => '$2y$10$9.preebMjZ.8obdvk5ZVdOCw7Cq1EJm6i1B1RJevxCXYW0lUiwDJG', // secretpassword
];
}
+
+ public function configure(): UserFactory
+ {
+ return $this->afterCreating(function (User $user) {
+ (new SetDefaultSettingsForUser($user))->up();
+ $user->assignRole(Role::USER);
+ });
+ }
}
diff --git a/database/migrations/2022_06_23_112431_migrate_user_data.php b/database/migrations/2022_06_23_112431_migrate_user_data.php
index 2139f81d..6c24e710 100644
--- a/database/migrations/2022_06_23_112431_migrate_user_data.php
+++ b/database/migrations/2022_06_23_112431_migrate_user_data.php
@@ -10,7 +10,10 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Artisan;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Schema;
+use Illuminate\Support\Str;
class MigrateUserData extends Migration
{
@@ -27,6 +30,7 @@ public function up()
$this->addUserRoles();
$this->migrateApiTokens();
+ $this->createSystemUser();
}
protected function migrateLinkVisibility(): void
@@ -140,4 +144,17 @@ public function migrateApiTokens(): void
$table->dropColumn('api_token');
});
}
+
+ public function createSystemUser(): void
+ {
+ User::forceCreate([
+ 'id' => '0',
+ 'name' => 'System',
+ 'email' => 'system@localhost',
+ 'password' => Hash::make(Str::random(128)),
+ 'two_factor_secret' => encrypt(Str::random(128)),
+ ]);
+
+ DB::table('users')->where('email', 'system@localhost')->update(['id' => 0]);
+ }
}
diff --git a/database/settings/2022_06_22_124112_migrate_existing_settings.php b/database/settings/2022_06_22_124112_migrate_existing_settings.php
index 62f06741..f4dfdce7 100644
--- a/database/settings/2022_06_22_124112_migrate_existing_settings.php
+++ b/database/settings/2022_06_22_124112_migrate_existing_settings.php
@@ -16,7 +16,7 @@ public function up(): void
$this->migrateSystemSettings();
$this->migrateGuestSettings();
- $this->migrateUserSettings();
+ $this->migrateAllUserSettings();
}
protected function migrateSystemSettings(): void
@@ -60,85 +60,93 @@ protected function migrateGuestSettings()
$this->migrator->add('guest.share_xing', (bool)$this->sysSettings->get('guest_share_xing', false));
}
- protected function migrateUserSettings(): void
+ protected function migrateAllUserSettings(): void
{
+ foreach (DB::table('users')->pluck('id') as $userId) {
+ $this->migrateUserSettings($userId);
+ }
+ }
+
+ protected function migrateUserSettings(int $userId): void
+ {
+ $id = 'user-' . $userId;
$this->migrator->add(
- 'user-1.timezone',
+ $id . '.timezone',
$this->userSettings->get('timezone', 'UTC')
);
$this->migrator->add(
- 'user-1.date_format',
+ $id . '.date_format',
$this->userSettings->get('date_format', config('linkace.default.date_format'))
);
$this->migrator->add(
- 'user-1.time_format',
+ $id . '.time_format',
$this->userSettings->get('time_format', config('linkace.default.time_format'))
);
$this->migrator->add(
- 'user-1.locale',
+ $id . '.locale',
$this->userSettings->get('locale', config('app.fallback_locale'))
);
$this->migrator->add(
- 'user-1.profile_is_public',
+ $id . '.profile_is_public',
(bool)$this->sysSettings->get('system_guest_access', false)
);
$this->migrator->add(
- 'user-1.links_default_visibility',
+ $id . '.links_default_visibility',
$this->userSettings->get('links_private_default', false)
? ModelAttribute::VISIBILITY_PRIVATE : ModelAttribute::VISIBILITY_PUBLIC
);
$this->migrator->add(
- 'user-1.notes_default_visibility',
+ $id . '.notes_default_visibility',
$this->userSettings->get('notes_private_default', false)
? ModelAttribute::VISIBILITY_PRIVATE : ModelAttribute::VISIBILITY_PUBLIC
);
$this->migrator->add(
- 'user-1.lists_default_visibility',
+ $id . '.lists_default_visibility',
$this->userSettings->get('lists_private_default', false)
? ModelAttribute::VISIBILITY_PRIVATE : ModelAttribute::VISIBILITY_PUBLIC
);
$this->migrator->add(
- 'user-1.tags_default_visibility',
+ $id . '.tags_default_visibility',
$this->userSettings->get('tags_private_default', false)
? ModelAttribute::VISIBILITY_PRIVATE : ModelAttribute::VISIBILITY_PUBLIC
);
$this->migrator->add(
- 'user-1.archive_backups_enabled',
+ $id . '.archive_backups_enabled',
(bool)$this->userSettings->get('archive_backups_enabled', true)
);
$this->migrator->add(
- 'user-1.archive_private_backups_enabled',
+ $id . '.archive_private_backups_enabled',
(bool)$this->userSettings->get('archive_private_backups_enabled', true)
);
- $this->migrator->add('user-1.listitem_count', (int)$this->userSettings->get('listitem_count', 24));
- $this->migrator->add('user-1.darkmode_setting', (int)$this->userSettings->get('darkmode_setting', 2));
- $this->migrator->add('user-1.link_display_mode', (int)$this->userSettings->get('link_display_mode', 1));
- $this->migrator->add('user-1.links_new_tab', (bool)$this->userSettings->get('links_new_tab', false));
- $this->migrator->add('user-1.markdown_for_text', (bool)$this->userSettings->get('markdown_for_text', true));
+ $this->migrator->add($id . '.listitem_count', (int)$this->userSettings->get('listitem_count', 24));
+ $this->migrator->add($id . '.darkmode_setting', (int)$this->userSettings->get('darkmode_setting', 2));
+ $this->migrator->add($id . '.link_display_mode', (int)$this->userSettings->get('link_display_mode', 1));
+ $this->migrator->add($id . '.links_new_tab', (bool)$this->userSettings->get('links_new_tab', false));
+ $this->migrator->add($id . '.markdown_for_text', (bool)$this->userSettings->get('markdown_for_text', true));
- $this->migrator->add('user-1.share_email', (bool)$this->userSettings->get('share_email', true));
- $this->migrator->add('user-1.share_buffer', (bool)$this->userSettings->get('share_buffer', true));
- $this->migrator->add('user-1.share_evernote', (bool)$this->userSettings->get('share_evernote', true));
- $this->migrator->add('user-1.share_facebook', (bool)$this->userSettings->get('share_facebook', true));
- $this->migrator->add('user-1.share_flipboard', (bool)$this->userSettings->get('share_flipboard', true));
- $this->migrator->add('user-1.share_hackernews', (bool)$this->userSettings->get('share_hackernews', true));
- $this->migrator->add('user-1.share_linkedin', (bool)$this->userSettings->get('share_linkedin', true));
- $this->migrator->add('user-1.share_mastodon', (bool)$this->userSettings->get('share_mastodon', true));
- $this->migrator->add('user-1.share_pinterest', (bool)$this->userSettings->get('share_pinterest', true));
- $this->migrator->add('user-1.share_pocket', (bool)$this->userSettings->get('share_pocket', true));
- $this->migrator->add('user-1.share_reddit', (bool)$this->userSettings->get('share_reddit', true));
- $this->migrator->add('user-1.share_skype', (bool)$this->userSettings->get('share_skype', true));
- $this->migrator->add('user-1.share_sms', (bool)$this->userSettings->get('share_sms', true));
- $this->migrator->add('user-1.share_telegram', (bool)$this->userSettings->get('share_telegram', true));
- $this->migrator->add('user-1.share_trello', (bool)$this->userSettings->get('share_trello', true));
- $this->migrator->add('user-1.share_tumblr', (bool)$this->userSettings->get('share_tumblr', true));
- $this->migrator->add('user-1.share_twitter', (bool)$this->userSettings->get('share_twitter', true));
- $this->migrator->add('user-1.share_wechat', (bool)$this->userSettings->get('share_wechat', true));
- $this->migrator->add('user-1.share_whatsapp', (bool)$this->userSettings->get('share_whatsapp', true));
- $this->migrator->add('user-1.share_xing', (bool)$this->userSettings->get('share_xing', true));
+ $this->migrator->add($id . '.share_email', (bool)$this->userSettings->get('share_email', true));
+ $this->migrator->add($id . '.share_buffer', (bool)$this->userSettings->get('share_buffer', true));
+ $this->migrator->add($id . '.share_evernote', (bool)$this->userSettings->get('share_evernote', true));
+ $this->migrator->add($id . '.share_facebook', (bool)$this->userSettings->get('share_facebook', true));
+ $this->migrator->add($id . '.share_flipboard', (bool)$this->userSettings->get('share_flipboard', true));
+ $this->migrator->add($id . '.share_hackernews', (bool)$this->userSettings->get('share_hackernews', true));
+ $this->migrator->add($id . '.share_linkedin', (bool)$this->userSettings->get('share_linkedin', true));
+ $this->migrator->add($id . '.share_mastodon', (bool)$this->userSettings->get('share_mastodon', true));
+ $this->migrator->add($id . '.share_pinterest', (bool)$this->userSettings->get('share_pinterest', true));
+ $this->migrator->add($id . '.share_pocket', (bool)$this->userSettings->get('share_pocket', true));
+ $this->migrator->add($id . '.share_reddit', (bool)$this->userSettings->get('share_reddit', true));
+ $this->migrator->add($id . '.share_skype', (bool)$this->userSettings->get('share_skype', true));
+ $this->migrator->add($id . '.share_sms', (bool)$this->userSettings->get('share_sms', true));
+ $this->migrator->add($id . '.share_telegram', (bool)$this->userSettings->get('share_telegram', true));
+ $this->migrator->add($id . '.share_trello', (bool)$this->userSettings->get('share_trello', true));
+ $this->migrator->add($id . '.share_tumblr', (bool)$this->userSettings->get('share_tumblr', true));
+ $this->migrator->add($id . '.share_twitter', (bool)$this->userSettings->get('share_twitter', true));
+ $this->migrator->add($id . '.share_wechat', (bool)$this->userSettings->get('share_wechat', true));
+ $this->migrator->add($id . '.share_whatsapp', (bool)$this->userSettings->get('share_whatsapp', true));
+ $this->migrator->add($id . '.share_xing', (bool)$this->userSettings->get('share_xing', true));
}
}
diff --git a/lang/en_US/auth.php b/lang/en_US/auth.php
index 3f79a5b3..5074e3ef 100644
--- a/lang/en_US/auth.php
+++ b/lang/en_US/auth.php
@@ -32,10 +32,21 @@
'api_tokens.generate' => 'Generate a new API Token',
'api_tokens.generate_short' => 'Generate Token',
'api_tokens.generate_help' => 'API tokens are used to authenticate yourself when using the LinkAce API.',
- 'api_tokens.generated_successfully' => 'Your API token was generated successfully: :token
',
+ 'api_tokens.generated_successfully' => 'The API token was generated successfully: :token
',
'api_tokens.generated_help' => 'Please store this token in a safe place. It is not possible to recover your token if you lose it.',
'api_tokens.name' => 'Token name',
'api_tokens.name_help' => 'Choose a name for your token. The name can only contain alpha-numeric characters, dashes, and underscores. Helpful if you want to create separate tokens for different use cases or applications.',
+
+ 'api_token_system' => 'System API Token',
+ 'api_tokens_system' => 'System API Tokens',
+ 'api_tokens.generate_help_system' => 'API tokens are used to access the LinkAce API from other applications or scripts. By default, only public or internal data is accessible, but tokens can be granted additional access to private data if needed.',
+ 'api_tokens.private_access' => 'Token can access private data',
+ 'api_tokens.private_access_help' => 'The token access and change private links, lists, tags and notes of any user based on the specified abilities.',
+ 'api_tokens.abilities' => 'Token abilities',
+ 'api_tokens.abilities_select' => 'Select token abilities...',
+ 'api_tokens.abilities_help' => 'Select all abilities a token can have. Abilities cannot be changed later.',
+ 'api_tokens.ability_private_access' => 'Token can access private data',
+
'api_tokens.revoke' => 'Revoke token',
'api_tokens.revoke_confirm' => 'Do you really want to revoke this token? This step cannot be undone and the token cannot be recovered.',
'api_tokens.revoke_successful' => 'The token was revoked successfully.',
diff --git a/lang/en_US/linkace.php b/lang/en_US/linkace.php
index 0f064366..0bb357fe 100644
--- a/lang/en_US/linkace.php
+++ b/lang/en_US/linkace.php
@@ -40,6 +40,7 @@
'block' => 'Block',
'unblock' => 'Unblock',
'unblocked' => 'Unblocked',
+ 'details' => 'Details',
'menu' => 'Menu',
'entries' => 'Entries',
diff --git a/lang/en_US/user.php b/lang/en_US/user.php
index 42ef1207..8292a078 100644
--- a/lang/en_US/user.php
+++ b/lang/en_US/user.php
@@ -25,6 +25,8 @@
'restore_confirmation' => 'Do you really want to restore this User?',
'restore_successful' => 'The user :username was restored successfully.',
+ 'system_user_locked' => 'The system user cannot login like a regular user. Please login with your personal account.',
+
'action_not_allowed_on_user' => 'This action cannot be performed on the selected user.',
'history_deleted' => 'User :name
was deleted',
diff --git a/lang/en_US/validation.php b/lang/en_US/validation.php
index 863ba06f..534f4fd0 100644
--- a/lang/en_US/validation.php
+++ b/lang/en_US/validation.php
@@ -128,6 +128,9 @@
'visibility' => [
'visibility' => 'The Visibility must bei either 1 (public), 2 (internal) or 3 (private).',
],
+ 'api_token_ability' => [
+ 'api_token_ability' => 'The API token must at least have one ability from the predefined token abilities.',
+ ],
],
/*
diff --git a/resources/assets/js/components/SimpleSelect.js b/resources/assets/js/components/SimpleSelect.js
index aeef6cbc..b518b3dc 100644
--- a/resources/assets/js/components/SimpleSelect.js
+++ b/resources/assets/js/components/SimpleSelect.js
@@ -8,9 +8,14 @@ TomSelect.define('input_autogrow', TomSelect_input_autogrow);
export default class SimpleSelect {
constructor ($el) {
- new TomSelect($el, {
+ let options = {
plugins: ['caret_position', 'input_autogrow'],
create: false
- });
+ };
+ if (typeof $el.dataset.selectConfig !== 'undefined') {
+ const additionalOptions = JSON.parse($el.dataset.selectConfig);
+ options = {...options, ...additionalOptions};
+ }
+ new TomSelect($el, options);
}
}
diff --git a/resources/views/admin/api-tokens/index.blade.php b/resources/views/admin/api-tokens/index.blade.php
new file mode 100644
index 00000000..c2ccdcef
--- /dev/null
+++ b/resources/views/admin/api-tokens/index.blade.php
@@ -0,0 +1,125 @@
+@extends('layouts.app')
+
+@section('content')
+
+ @if(session()->has('new_token'))
+
+ + @lang('auth.api_tokens.generated_successfully', ['token' => session()->get('new_token')]) + +
+@lang('auth.api_tokens.generated_help')
+@lang('auth.api_tokens.name') | +@lang('linkace.created_at') | +@lang('linkace.last_used') | ++ |
---|---|---|---|
{{ $token->name }} | +{{ $token->created_at }} | +{{ $token->last_used ?: trans('linkace.never_used') }} | ++ + | +
@lang('auth.api_tokens.no_tokens_found') | +
@lang('auth.api_tokens.generate_help_system')
+ + + ++ + @lang('auth.api_tokens.generated_successfully', ['token' => session()->get('new_token')]) + +
+@lang('auth.api_tokens.generated_help')
+@lang('auth.api_tokens.abilities'):
+@lang('auth.api_tokens.ability_private_access'): {{ in_array(\App\Enums\ApiToken::ABILITY_SYSTEM_ACCESS_PRIVATE, $token->abilities) ? trans('linkace.yes') : trans('linkace.no') }}
+