1
0
mirror of https://github.com/Kovah/LinkAce.git synced 2025-01-17 05:08:21 +01:00

Add public user profiles (#165)

This commit is contained in:
Kovah 2022-07-19 20:59:41 +02:00
parent 26ac19a114
commit cbebfa48eb
No known key found for this signature in database
GPG Key ID: AAAA031BA9830D7B
13 changed files with 293 additions and 25 deletions

View File

@ -23,6 +23,8 @@ class SetDefaultSettingsForUser extends SettingsMigration
$this->migrator->add($group . '.time_format', $defaults['time_format']);
$this->migrator->add($group . '.locale', $defaults['locale']);
$this->migrator->add($group . '.profile_is_public', $defaults['profile_is_public']);
$this->migrator->add($group . '.links_default_visibility', $defaults['links_default_visibility']);
$this->migrator->add($group . '.notes_default_visibility', $defaults['notes_default_visibility']);
$this->migrator->add($group . '.lists_default_visibility', $defaults['lists_default_visibility']);

View File

@ -30,15 +30,20 @@ function setupCompleted()
/**
* Shorthand for the current user settings
*
* @param string $key
* @param string $key
* @param int|null $userId
* @return mixed
*/
function usersettings(string $key = ''): mixed
function usersettings(string $key = '', ?int $userId = null): mixed
{
if (!auth()->user()) {
if (is_null($userId) && !auth()->user()) {
return null;
}
if (!is_null($userId)) {
app(UserSettings::class)::setUserId($userId);
}
if ($key === '') {
return app(UserSettings::class)->toArray();
}

View File

@ -53,8 +53,6 @@ class UserSettingsController extends Controller
*/
public function saveAppSettings(UserSettings $settings, UserSettingsUpdateRequest $request): RedirectResponse
{
$userId = $request->user()->id;
// Save all user settings or update them
$newSettings = $request->except(['_token', 'share']);
foreach ($newSettings as $key => $value) {

View File

@ -0,0 +1,54 @@
<?php
namespace App\Http\Controllers\Guest;
use App\Enums\ModelAttribute;
use App\Http\Controllers\Controller;
use App\Models\Link;
use App\Models\LinkList;
use App\Models\Note;
use App\Models\Tag;
use App\Models\User;
class UserController extends Controller
{
public function show(User $user)
{
if (usersettings('profile_is_public', $user->id) === false) {
abort(404);
}
$links = Link::byUser($user->id)
->whereVisibility(ModelAttribute::VISIBILITY_PUBLIC)
->latest()
->take(10)
->paginate(pageName: 'link_page');
$lists = LinkList::byUser($user->id)
->whereVisibility(ModelAttribute::VISIBILITY_PUBLIC)
->latest()
->take(10)
->paginate(pageName: 'link_page');
$tags = Tag::byUser($user->id)
->whereVisibility(ModelAttribute::VISIBILITY_PUBLIC)
->latest()
->take(10)
->paginate(pageName: 'link_page');
$stats = [
'total_links' => Link::byUser($user->id)->whereVisibility(ModelAttribute::VISIBILITY_PUBLIC)->count(),
'total_lists' => LinkList::byUser($user->id)->whereVisibility(ModelAttribute::VISIBILITY_PUBLIC)->count(),
'total_tags' => Tag::byUser($user->id)->whereVisibility(ModelAttribute::VISIBILITY_PUBLIC)->count(),
'total_notes' => Note::byUser($user->id)->whereVisibility(ModelAttribute::VISIBILITY_PUBLIC)->count(),
];
return view('guest.users.show', [
'user' => $user,
'links' => $links,
'lists' => $lists,
'tags' => $tags,
'stats' => $stats,
]);
}
}

View File

@ -12,6 +12,8 @@ class UserSettings extends Settings
public string $time_format;
public string $locale;
public bool $profile_is_public;
public int $links_default_visibility;
public int $notes_default_visibility;
public int $lists_default_visibility;
@ -47,16 +49,23 @@ class UserSettings extends Settings
public bool $share_whatsapp;
public bool $share_xing;
private static int $user_id = 0;
public static function group(): string
{
return 'user-' . auth()->id();
return 'user-' . self::getUserId();
}
public static function setUserId(int $user_id): void
{
self::$user_id = $user_id;
}
protected static function getUserId(): int
{
return self::$user_id ?: auth()->id();
}
/**
* Returns the default settings for users
*
* @return array{string: string|int|bool|null}
*/
public static function defaults(): array
{
return [
@ -64,6 +73,7 @@ class UserSettings extends Settings
'date_format' => config('linkace.default.date_format'),
'time_format' => config('linkace.default.time_format'),
'locale' => config('app.fallback_locale'),
'profile_is_public' => false,
'links_default_visibility' => ModelAttribute::VISIBILITY_PUBLIC,
'notes_default_visibility' => ModelAttribute::VISIBILITY_PUBLIC,
'lists_default_visibility' => ModelAttribute::VISIBILITY_PUBLIC,

View File

@ -79,6 +79,11 @@ class MigrateExistingSettings extends SettingsMigration
$this->userSettings->get('locale', config('app.fallback_locale'))
);
$this->migrator->add(
'user-1.profile_is_public',
(bool)$this->sysSettings->get('system_guest_access', false)
);
$this->migrator->add(
'user-1.links_default_visibility',
$this->userSettings->get('links_private_default', false)

View File

@ -18,7 +18,9 @@ return [
'markdown_for_text' => 'Enable Markdown for descriptions and notes',
'privacy' => 'Privacy',
'default_visibility_help' => 'Choose the default visibility for Links, Lists, Notes and Tags when adding new entries.',
'profile_privacy' => 'The following settings apply to your user profile which is visible to guests.',
'profile_is_public' => 'Profile is public',
'default_visibility_help' => 'The following settings define the default visibility for Links, Lists, Notes and Tags when adding new ones.',
'links_default_visibility' => 'Default Links visibility',
'notes_default_visibility' => 'Default Notes visibility',
'lists_default_visibility' => 'Default Lists visibility',

View File

@ -14,6 +14,33 @@ $settings = [
@lang('settings.privacy')
</h5>
<p>@lang('settings.profile_privacy')</p>
<div class="row mb-4">
<div class="col-12 col-sm-8 col-md-6">
<label class="form-label" for="profile_is_public">
@lang('settings.profile_is_public')
</label>
<select id="profile_is_public" name="profile_is_public"
class="form-select{{ $errors->has('profile_is_public') ? ' is-invalid' : '' }}">
<option value="1"
@if(usersettings('profile_is_public') === true) selected @endif>
@lang('linkace.yes')
</option>
<option value="0"
@if(usersettings('profile_is_public') === false) selected @endif>
@lang('linkace.no')
</option>
</select>
@if ($errors->has('profile_is_public'))
<p class="invalid-feedback" role="alert">
{{ $errors->first('profile_is_public') }}
</p>
@endif
</div>
</div>
<p>@lang('settings.default_visibility_help')</p>
<div class="row">

View File

@ -0,0 +1,103 @@
@extends('layouts.app')
@section('content')
<div class="card">
<div class="card-header">
@lang('user.user')
</div>
<div class="card-body">
<h2 class="mb-0">{{ $user->name }}</h2>
</div>
</div>
<div class="row">
<div class="col-12 col-md-7">
<div class="card mt-4">
<div class="card-header">
@lang('link.recent_links')
</div>
<ul class="list-group list-group-flush">
@forelse($links as $link)
<a href="{{ $link->url }}" class="list-group-item list-group-item-action one-line">
{!! $link->getIcon('me-1') !!}
{{ $link->title }}
</a>
@empty
<li class="list-group-item text-muted">
@lang('linkace.no_results_found', ['model' => trans('link.links')])
</li>
@endforelse
</ul>
</div>
</div>
<div class="col-12 col-md-5">
<div class="card mt-4">
<div class="card-header">
@lang('stats.stats')
</div>
<ul class="list-group list-group-flush">
<li class="list-group-item d-flex justify-content-between align-items-center">
<span class="me-1">@lang('stats.total_links')</span>
<span class="badge bg-secondary">{{ $stats['total_links'] }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span class="me-1">@lang('stats.total_lists')</span>
<span class="badge bg-secondary">{{ $stats['total_lists'] }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span class="me-1">@lang('stats.total_tags')</span>
<span class="badge bg-secondary">{{ $stats['total_tags'] }}</span>
</li>
<li class="list-group-item d-flex justify-content-between align-items-center">
<span class="me-1">@lang('stats.total_notes')</span>
<span class="badge bg-secondary">{{ $stats['total_notes'] }}</span>
</li>
</ul>
</div>
<div class="card mt-4">
<div class="card-header">
@lang('list.recent_lists')
</div>
<div class="card-body">
@forelse($lists as $list)
<a href="{{ route('guest.lists.show', ['list' => $list]) }}" class="btn btn-light btn-sm m-1">
{{ $list->name }}
</a>
@empty
<div class="text-muted">
@lang('linkace.no_results_found', ['model' => trans('list.lists')])
</div>
@endforelse
</div>
</div>
<div class="card mt-4">
<div class="card-header">
@lang('tag.recent_tags')
</div>
<div class="card-body">
@forelse($tags as $tag)
<a href="{{ route('guest.tags.show', ['tag' => $tag]) }}" class="btn btn-light btn-sm m-1">
{{ $tag->name }}
</a>
@empty
<div class="text-muted">
@lang('linkace.no_results_found', ['model' => trans('tag.tags')])
</div>
@endforelse
</div>
</div>
</div>
</div>
@endsection

View File

@ -62,12 +62,6 @@
</ul>
</div>
</div>
</div>
<div class="row">
<div class="col-12 col-md-7">
<div class="card mt-4">
<div class="card-header">
@lang('list.recent_lists')
@ -75,7 +69,7 @@
<div class="card-body">
@forelse($lists as $list)
<a href="{{ route('lists.show', ['list' => $list]) }}" class="btn btn-light btn-sm">
<a href="{{ route('lists.show', ['list' => $list]) }}" class="btn btn-light btn-sm m-1">
{{ $list->name }}
</a>
@empty
@ -86,9 +80,6 @@
</div>
</div>
</div>
<div class="col-12 col-md-5">
<div class="card mt-4">
<div class="card-header">
@lang('tag.recent_tags')
@ -96,7 +87,7 @@
<div class="card-body">
@forelse($tags as $tag)
<a href="{{ route('tags.show', ['tag' => $tag]) }}" class="btn btn-light btn-sm">
<a href="{{ route('tags.show', ['tag' => $tag]) }}" class="btn btn-light btn-sm m-1">
{{ $tag->name }}
</a>
@empty

View File

@ -18,6 +18,7 @@ use App\Http\Controllers\Guest\FeedController as GuestFeedController;
use App\Http\Controllers\Guest\LinkController as GuestLinkController;
use App\Http\Controllers\Guest\ListController as GuestListController;
use App\Http\Controllers\Guest\TagController as GuestTagController;
use App\Http\Controllers\Guest\UserController as GuestUserController;
use App\Http\Controllers\Models\LinkController;
use App\Http\Controllers\Models\ListController;
use App\Http\Controllers\Models\NoteController;
@ -180,6 +181,8 @@ Route::prefix('guest')->middleware(['guestaccess'])->group(function () {
Route::get('tags/feed', [GuestFeedController::class, 'tags'])->name('guest.tags.feed');
Route::get('tags/{tag}/feed', [GuestFeedController::class, 'tagLinks'])->name('guest.tags.links.feed');
Route::get('users/{user:name}', [GuestUserController::class, 'show'])->name('guest.users.show');
Route::resource('links', GuestLinkController::class)
->only(['index'])
->names([

View File

@ -14,7 +14,7 @@ class FeedControllerTest extends TestCase
{
use RefreshDatabase;
private $user;
private User $user;
protected function setUp(): void
{
@ -82,7 +82,7 @@ class FeedControllerTest extends TestCase
$tagLink = Link::factory()->create();
$unrelatedLink = Link::factory()->create();
$tagLink->tags()->sync(['tag' => $tag]);
$tagLink->tags()->sync([$tag->id]);
$response = $this->getAuthorized('tags/1/feed');

View File

@ -0,0 +1,68 @@
<?php
namespace Tests\Controller\Guest;
use App\Enums\ModelAttribute;
use App\Models\Link;
use App\Models\LinkList;
use App\Models\Tag;
use App\Models\User;
use App\Settings\SystemSettings;
use App\Settings\UserSettings;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class UserControllerTest extends TestCase
{
use RefreshDatabase;
public function testPublicUserProfile(): void
{
SystemSettings::fake(['setup_completed' => true, 'guest_access_enabled' => true]);
User::factory()->create(['name' => 'MrTestUser']);
Link::factory()->create(['url' => 'https://public.com', 'visibility' => ModelAttribute::VISIBILITY_PUBLIC]);
Link::factory()->create(['url' => 'https://internal.com', 'visibility' => ModelAttribute::VISIBILITY_INTERNAL]);
Link::factory()->create(['url' => 'https://private.com', 'visibility' => ModelAttribute::VISIBILITY_PRIVATE]);
LinkList::factory()->create(['name' => 'Public List', 'visibility' => ModelAttribute::VISIBILITY_PUBLIC]);
LinkList::factory()->create(['name' => 'Internal List', 'visibility' => ModelAttribute::VISIBILITY_INTERNAL]);
LinkList::factory()->create(['name' => 'Private List', 'visibility' => ModelAttribute::VISIBILITY_PRIVATE]);
Tag::factory()->create(['name' => 'Public Tag', 'visibility' => ModelAttribute::VISIBILITY_PUBLIC]);
Tag::factory()->create(['name' => 'Internal Tag', 'visibility' => ModelAttribute::VISIBILITY_INTERNAL]);
Tag::factory()->create(['name' => 'Private Tag', 'visibility' => ModelAttribute::VISIBILITY_PRIVATE]);
UserSettings::setUserId(1);
UserSettings::fake([
'profile_is_public' => true,
]);
$this->get('guest/users/MrTestUser')
->assertOk()
->assertSee('MrTestUser')
->assertSee('https://public.com')
->assertDontSee('https://internal.com')
->assertDontSee('https://private.com')
->assertSee('Public List')
->assertDontSee('Internal List')
->assertDontSee('Private List')
->assertSee('Public Tag')
->assertDontSee('Internal Tag')
->assertDontSee('Private Tag');
}
public function testPrivateUserProfile(): void
{
SystemSettings::fake(['setup_completed' => true, 'guest_access_enabled' => true]);
User::factory()->create(['name' => 'MrPrivateUser']);
UserSettings::fake([
'profile_is_public' => false,
]);
$this->get('guest/user/MrPrivateUser')->assertNotFound();
}
}