Add authentications log

This commit is contained in:
Bubka 2024-04-15 00:44:18 +02:00
parent 1bc55f5535
commit a6745c28a6
25 changed files with 3944 additions and 4712 deletions

File diff suppressed because it is too large Load Diff

View File

@ -4,6 +4,7 @@
use App\Api\v1\Requests\UserManagerPromoteRequest;
use App\Api\v1\Requests\UserManagerStoreRequest;
use App\Api\v1\Resources\UserAuthentication;
use App\Api\v1\Resources\UserManagerResource;
use App\Http\Controllers\Controller;
use App\Models\User;
@ -193,8 +194,7 @@ public function promote(UserManagerPromoteRequest $request, User $user)
{
$this->authorize('promote', $user);
if ($user->promoteToAdministrator($request->validated('is_admin')))
{
if ($user->promoteToAdministrator($request->validated('is_admin'))) {
$user->save();
Log::info(sprintf('User ID #%s set is_admin=%s for User ID #%s', $request->user()->id, $user->isAdministrator(), $user->id));
@ -206,6 +206,24 @@ public function promote(UserManagerPromoteRequest $request, User $user)
], 403);
}
/**
* Get the user's authentication logs
*
* @return \Illuminate\Http\JsonResponse
*/
public function authentications(Request $request, User $user)
{
$this->authorize('view', $user);
$validated = $this->validate($request, [
'limit' => 'sometimes|numeric',
]);
$authentications = $request->has('limit') ? $user->authentications->take($validated['limit']) : $user->authentications;
return UserAuthentication::collection($authentications);
}
/**
* Get the broker to be used during password reset.
*

View File

@ -0,0 +1,63 @@
<?php
namespace App\Api\v1\Resources;
use Carbon\CarbonInterface;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Carbon;
use Jenssegers\Agent\Agent;
/**
* @property mixed $id
* @property string $name
* @property string $email
* @property string $oauth_provider
* @property \Illuminate\Support\Collection<array-key, mixed> $preferences
* @property string $is_admin
*/
class UserAuthentication extends JsonResource
{
/**
* A user agent parser instance.
*
* @var mixed
*/
protected $agent;
/**
* Create a new resource instance.
*
* @param mixed $resource
* @return void
*/
public function __construct($resource)
{
$this->agent = new Agent();
$this->agent->setUserAgent($resource->user_agent);
parent::__construct($resource);
}
/**
* Transform the resource into an array.
*
* @param \Illuminate\Http\Request $request
* @return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
'ip_address' => $this->ip_address,
'user_agent' => $this->user_agent,
'browser' => $this->agent->browser(),
'platform' => $this->agent->platform(),
'device' => $this->agent->deviceType(),
'login_at' => Carbon::parse($this->login_at)->toDayDateTimeString(),
'login_successful' => $this->login_successful,
'duration' => $this->logout_at
? Carbon::parse($this->logout_at)->diffForHumans(Carbon::parse($this->login_at), ['syntax' => CarbonInterface::DIFF_ABSOLUTE])
: null,
];
}
}

View File

@ -70,6 +70,7 @@ protected function flushDB() : void
DB::table('groups')->delete();
DB::table('users')->delete();
DB::table('options')->delete();
DB::table(config('authentication-log.table_name'))->delete();
$this->line('Database cleaned');
}

View File

@ -6,7 +6,6 @@
use App\Http\Controllers\Controller;
use App\Http\Requests\UserDeleteRequest;
use App\Http\Requests\UserUpdateRequest;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Log;

View File

@ -12,6 +12,7 @@
use Illuminate\Support\Str;
use Laragear\WebAuthn\WebAuthnAuthentication;
use Laravel\Passport\HasApiTokens;
use Rappasoft\LaravelAuthenticationLog\Traits\AuthenticationLoggable;
/**
* App\Models\User
@ -42,6 +43,7 @@
*/
class User extends Authenticatable implements WebAuthnAuthenticatable
{
use AuthenticationLoggable;
use HasApiTokens, HasFactory, Notifiable;
use WebAuthnAuthentication, WebAuthnManageCredentials;
@ -81,7 +83,7 @@ class User extends Authenticatable implements WebAuthnAuthenticatable
* These are extra user-defined events observers may subscribe to.
*/
protected $observables = [
'demoting'
'demoting',
];
/**
@ -108,7 +110,7 @@ public function isAdministrator()
/**
* Grant administrator permissions to the user.
*/
public function promoteToAdministrator(bool $promote = true): bool
public function promoteToAdministrator(bool $promote = true) : bool
{
if ($promote == false && $this->fireModelEvent('demoting') === false) {
return false;

View File

@ -0,0 +1,49 @@
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use Jenssegers\Agent\Agent;
use Rappasoft\LaravelAuthenticationLog\Models\AuthenticationLog;
class SignedInWithNewDevice extends Notification implements ShouldQueue
{
use Queueable;
public AuthenticationLog $authenticationLog;
/**
* A user agent parser instance.
*
* @var mixed
*/
protected $agent;
public function __construct(AuthenticationLog $authenticationLog)
{
$this->authenticationLog = $authenticationLog;
$this->agent = new Agent();
$this->agent->setUserAgent($authenticationLog->user_agent);
}
public function via($notifiable)
{
return $notifiable->notifyAuthenticationLogVia();
}
public function toMail($notifiable)
{
return (new MailMessage())
->subject(__('notifications.new_device.subject'))
->markdown('emails.newDevice', [
'account' => $notifiable,
'time' => $this->authenticationLog->login_at,
'ipAddress' => $this->authenticationLog->ip_address,
'browser' => $this->agent->browser(),
'platform' => $this->agent->platform(),
]);
}
}

View File

@ -26,6 +26,7 @@
"google/protobuf": "^3.21",
"guzzlehttp/guzzle": "^7.2",
"jackiedo/dotenv-editor": "^2.1",
"jenssegers/agent": "^2.6",
"khanamiryan/qrcode-detector-decoder": "^2.0.2",
"laragear/webauthn": "^2.0",
"laravel/framework": "^10.10",
@ -34,6 +35,7 @@
"laravel/tinker": "^2.8",
"laravel/ui": "^4.2",
"paragonie/constant_time_encoding": "^2.6",
"rappasoft/laravel-authentication-log": "^4.0",
"socialiteproviders/manager": "^4.4",
"spatie/eloquent-sortable": "^4.0.1",
"spomky-labs/otphp": "^11.0"
@ -101,4 +103,4 @@
"vendor/bin/phpunit --coverage-html tests/Coverage/"
]
}
}
}

270
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "bce4feb20f25403dd63ac68e2e56f84a",
"content-hash": "360f86c3dabb2c352bac0d34bb749d1f",
"packages": [
{
"name": "brick/math",
@ -1859,6 +1859,141 @@
},
"time": "2022-03-07T20:28:08+00:00"
},
{
"name": "jaybizzle/crawler-detect",
"version": "v1.2.117",
"source": {
"type": "git",
"url": "https://github.com/JayBizzle/Crawler-Detect.git",
"reference": "6785557f03d0fa9e2205352ebae9a12a4484cc8e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/JayBizzle/Crawler-Detect/zipball/6785557f03d0fa9e2205352ebae9a12a4484cc8e",
"reference": "6785557f03d0fa9e2205352ebae9a12a4484cc8e",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"require-dev": {
"phpunit/phpunit": "^4.8|^5.5|^6.5|^9.4"
},
"type": "library",
"autoload": {
"psr-4": {
"Jaybizzle\\CrawlerDetect\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Mark Beech",
"email": "m@rkbee.ch",
"role": "Developer"
}
],
"description": "CrawlerDetect is a PHP class for detecting bots/crawlers/spiders via the user agent",
"homepage": "https://github.com/JayBizzle/Crawler-Detect/",
"keywords": [
"crawler",
"crawler detect",
"crawler detector",
"crawlerdetect",
"php crawler detect"
],
"support": {
"issues": "https://github.com/JayBizzle/Crawler-Detect/issues",
"source": "https://github.com/JayBizzle/Crawler-Detect/tree/v1.2.117"
},
"time": "2024-03-19T22:51:22+00:00"
},
{
"name": "jenssegers/agent",
"version": "v2.6.4",
"source": {
"type": "git",
"url": "https://github.com/jenssegers/agent.git",
"reference": "daa11c43729510b3700bc34d414664966b03bffe"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/jenssegers/agent/zipball/daa11c43729510b3700bc34d414664966b03bffe",
"reference": "daa11c43729510b3700bc34d414664966b03bffe",
"shasum": ""
},
"require": {
"jaybizzle/crawler-detect": "^1.2",
"mobiledetect/mobiledetectlib": "^2.7.6",
"php": ">=5.6"
},
"require-dev": {
"php-coveralls/php-coveralls": "^2.1",
"phpunit/phpunit": "^5.0|^6.0|^7.0"
},
"suggest": {
"illuminate/support": "Required for laravel service providers"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
},
"laravel": {
"providers": [
"Jenssegers\\Agent\\AgentServiceProvider"
],
"aliases": {
"Agent": "Jenssegers\\Agent\\Facades\\Agent"
}
}
},
"autoload": {
"psr-4": {
"Jenssegers\\Agent\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jens Segers",
"homepage": "https://jenssegers.com"
}
],
"description": "Desktop/mobile user agent parser with support for Laravel, based on Mobiledetect",
"homepage": "https://github.com/jenssegers/agent",
"keywords": [
"Agent",
"browser",
"desktop",
"laravel",
"mobile",
"platform",
"user agent",
"useragent"
],
"support": {
"issues": "https://github.com/jenssegers/agent/issues",
"source": "https://github.com/jenssegers/agent/tree/v2.6.4"
},
"funding": [
{
"url": "https://github.com/jenssegers",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/jenssegers/agent",
"type": "tidelift"
}
],
"time": "2020-06-13T08:05:20+00:00"
},
{
"name": "khanamiryan/qrcode-detector-decoder",
"version": "2.0.2",
@ -3593,6 +3728,68 @@
],
"time": "2024-03-23T07:42:40+00:00"
},
{
"name": "mobiledetect/mobiledetectlib",
"version": "2.8.45",
"source": {
"type": "git",
"url": "https://github.com/serbanghita/Mobile-Detect.git",
"reference": "96aaebcf4f50d3d2692ab81d2c5132e425bca266"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/serbanghita/Mobile-Detect/zipball/96aaebcf4f50d3d2692ab81d2c5132e425bca266",
"reference": "96aaebcf4f50d3d2692ab81d2c5132e425bca266",
"shasum": ""
},
"require": {
"php": ">=5.0.0"
},
"require-dev": {
"phpunit/phpunit": "~4.8.36"
},
"type": "library",
"autoload": {
"psr-0": {
"Detection": "namespaced/"
},
"classmap": [
"Mobile_Detect.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Serban Ghita",
"email": "serbanghita@gmail.com",
"homepage": "http://mobiledetect.net",
"role": "Developer"
}
],
"description": "Mobile_Detect is a lightweight PHP class for detecting mobile devices. It uses the User-Agent string combined with specific HTTP headers to detect the mobile environment.",
"homepage": "https://github.com/serbanghita/Mobile-Detect",
"keywords": [
"detect mobile devices",
"mobile",
"mobile detect",
"mobile detector",
"php mobile detect"
],
"support": {
"issues": "https://github.com/serbanghita/Mobile-Detect/issues",
"source": "https://github.com/serbanghita/Mobile-Detect/tree/2.8.45"
},
"funding": [
{
"url": "https://github.com/serbanghita",
"type": "github"
}
],
"time": "2023-11-07T21:57:25+00:00"
},
{
"name": "monolog/monolog",
"version": "3.5.0",
@ -5238,6 +5435,77 @@
],
"time": "2023-11-08T05:53:05+00:00"
},
{
"name": "rappasoft/laravel-authentication-log",
"version": "v4.0.0",
"source": {
"type": "git",
"url": "https://github.com/rappasoft/laravel-authentication-log.git",
"reference": "a916caaa979b1d18d679d8b063325fe547f691c6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/rappasoft/laravel-authentication-log/zipball/a916caaa979b1d18d679d8b063325fe547f691c6",
"reference": "a916caaa979b1d18d679d8b063325fe547f691c6",
"shasum": ""
},
"require": {
"illuminate/contracts": "^10.0|^11.0",
"php": "^8.1",
"spatie/laravel-package-tools": "^1.4.3"
},
"require-dev": {
"nunomaduro/collision": "^6.0",
"orchestra/testbench": "^7.0",
"pestphp/pest": "^1.21",
"pestphp/pest-plugin-laravel": "^1.2",
"spatie/laravel-ray": "^1.29",
"vimeo/psalm": "^4.20"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"Rappasoft\\LaravelAuthenticationLog\\LaravelAuthenticationLogServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"Rappasoft\\LaravelAuthenticationLog\\": "src",
"Rappasoft\\LaravelAuthenticationLog\\Database\\Factories\\": "database/factories"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Anthony Rappa",
"email": "rappa819@gmail.com",
"role": "Developer"
}
],
"description": "Log user authentication details and send new device notifications.",
"homepage": "https://github.com/rappasoft/laravel-authentication-log",
"keywords": [
"laravel",
"laravel-authentication-log",
"rappasoft"
],
"support": {
"issues": "https://github.com/rappasoft/laravel-authentication-log/issues",
"source": "https://github.com/rappasoft/laravel-authentication-log/tree/v4.0.0"
},
"funding": [
{
"url": "https://github.com/rappasoft",
"type": "github"
}
],
"time": "2024-03-30T01:12:44+00:00"
},
{
"name": "socialiteproviders/manager",
"version": "v4.5.1",

View File

@ -0,0 +1,61 @@
<?php
return [
// The database table name
// You can change this if the database keys get too long for your driver
'table_name' => 'authentication_log',
// The database connection where the authentication_log table resides. Leave empty to use the default
'db_connection' => null,
// The events the package listens for to log
'events' => [
'login' => \Illuminate\Auth\Events\Login::class,
'failed' => \Illuminate\Auth\Events\Failed::class,
'logout' => \Illuminate\Auth\Events\Logout::class,
'logout-other-devices' => \Illuminate\Auth\Events\OtherDeviceLogout::class,
],
'listeners' => [
'login' => \Rappasoft\LaravelAuthenticationLog\Listeners\LoginListener::class,
'failed' => \Rappasoft\LaravelAuthenticationLog\Listeners\FailedLoginListener::class,
'logout' => \Rappasoft\LaravelAuthenticationLog\Listeners\LogoutListener::class,
'logout-other-devices' => \Rappasoft\LaravelAuthenticationLog\Listeners\OtherDeviceLogoutListener::class,
],
'notifications' => [
'new-device' => [
// Send the NewDevice notification
'enabled' => env('NEW_DEVICE_NOTIFICATION', true),
// Use torann/geoip to attempt to get a location
'location' => false,
// The Notification class to send
'template' => \App\Notifications\SignedInWithNewDevice::class,
],
'failed-login' => [
// Send the FailedLogin notification
'enabled' => env('FAILED_LOGIN_NOTIFICATION', false),
// Use torann/geoip to attempt to get a location
'location' => false,
// The Notification class to send
'template' => \Rappasoft\LaravelAuthenticationLog\Notifications\FailedLogin::class,
],
],
// When the clean-up command is run, delete old logs greater than `purge` days
// Don't schedule the clean-up command if you want to keep logs forever.
'purge' => 365,
// If you are behind an CDN proxy, set 'behind_cdn.http_header_field' to the corresponding http header field of your cdn
// For cloudflare you can have look at: https://developers.cloudflare.com/fundamentals/get-started/reference/http-request-headers/
// 'behind_cdn' => [
// 'http_header_field' => 'HTTP_CF_CONNECTING_IP' // used by Cloudflare
// ],
// If you are not a cdn user, use false
'behind_cdn' => false,
];

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create(config('authentication-log.table_name'), function (Blueprint $table) {
$table->id();
$table->morphs('authenticatable');
$table->string('ip_address', 45)->nullable();
$table->text('user_agent')->nullable();
$table->timestamp('login_at')->nullable();
$table->boolean('login_successful')->default(false);
$table->timestamp('logout_at')->nullable();
$table->boolean('cleared_by_user')->default(false);
$table->json('location')->nullable();
});
}
public function down(): void
{
Schema::dropIfExists(config('authentication-log.table_name'));
}
};

View File

@ -0,0 +1,90 @@
<script setup>
import SearchBox from '@/components/SearchBox.vue'
import { useNotifyStore } from '@/stores/notify'
import userService from '@/services/userService'
import { FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
const notify = useNotifyStore()
const props = defineProps({
userId: [Number, String],
lastOnly: Boolean,
showSearch: Boolean
})
const authentications = ref([])
const isFetching = ref(false)
const searched = ref('')
const visibleAuthentications = computed(() => {
return authentications.value.filter(authentication => {
return JSON.stringify(authentication)
.toString()
.toLowerCase()
.includes(searched.value);
})
})
onMounted(() => {
getAuthentications()
})
/**
* Gets user authentication logs
*/
function getAuthentications() {
isFetching.value = true
let limit = props.lastOnly ? 3 : false
userService.getauthentications(props.userId, limit, {returnError: true})
.then(response => {
authentications.value = response.data
})
.catch(error => {
notify.error(error)
})
.finally(() => {
isFetching.value = false
})
}
const deviceIcon = (device) => {
switch (device) {
case "phone":
return 'mobile-screen'
case "tablet":
return 'tablet-screen-button'
default:
return 'display'
}
}
</script>
<template>
<SearchBox v-if="props.showSearch" v-model:keyword="searched" :hasNoBackground="true" />
<div v-if="visibleAuthentications.length > 0">
<div v-for="authentication in visibleAuthentications" :key="authentication.id" class="list-item is-size-6 is-size-7-mobile has-text-grey is-flex is-justify-content-space-between">
<div>
<div >
<span v-if="authentication.login_successful" v-html="$t('admin.successful_login_on', { login_at: authentication.login_at })" />
<span v-else v-html="$t('admin.failed_login_on', { login_at: authentication.login_at })" />
</div>
<div>
{{ $t('commons.IP') }}: <span class="has-text-grey-light">{{ authentication.ip_address }}</span> - {{ $t('commons.browser') }}: <span class="has-text-grey-light">{{ authentication.browser }}</span> - {{ $t('commons.operating_system_short') }}: <span class="has-text-grey-light">{{ authentication.platform }}</span>
</div>
</div>
<div class="is-align-self-center has-text-grey-darker">
<font-awesome-layers class="fa-2x">
<FontAwesomeIcon :icon="['fas', deviceIcon(authentication.device)]" transform="grow-6" fixed-width />
<FontAwesomeIcon :icon="['fas', authentication.login_successful ? 'check' : 'times']" :transform="'shrink-7' + (authentication.device == 'desktop' ? ' up-2' : '')" fixed-width :class="authentication.login_successful ? 'has-text-success-dark' : 'has-text-danger-dark'" />
</font-awesome-layers>
</div>
</div>
</div>
<div v-else-if="authentications.length == 0" class="mt-4">
{{ $t('commons.no_entry_yet') }}
</div>
<div v-else class="mt-5 pl-3">
{{ $t('commons.no_result') }}
</div>
</template>

View File

@ -48,6 +48,9 @@ import {
faChevronRight,
faSlash,
faAlignLeft,
faMobileScreen,
faTabletScreenButton,
faDisplay,
} from '@fortawesome/free-solid-svg-icons'
import {
@ -110,7 +113,10 @@ library.add(
faOpenid,
faPaperPlane,
faSlash,
faAlignLeft
faAlignLeft,
faMobileScreen,
faTabletScreenButton,
faDisplay
);
export default FontAwesomeIcon

View File

@ -39,6 +39,7 @@ const router = createRouter({
{ path: '/admin/users', name: 'admin.users', component: () => import('../views/admin/Users.vue'), meta: { middlewares: [authGuard, adminOnly], watchedByKicker: true, showAbout: true } },
{ path: '/admin/users/create', name: 'admin.createUser', component: () => import('../views/admin/users/Create.vue'), meta: { middlewares: [authGuard, adminOnly], watchedByKicker: true, showAbout: true } },
{ path: '/admin/users/:userId/manage', name: 'admin.manageUser', component: () => import('../views/admin/users/Manage.vue'), meta: { middlewares: [authGuard, adminOnly], watchedByKicker: true, showAbout: true }, props: true },
{ path: '/admin/logs/:userId/access', name: 'admin.logs.access', component: () => import('../views/admin/logs/Access.vue'), meta: { middlewares: [authGuard, adminOnly], watchedByKicker: true, showAbout: true }, props: true },
{ path: '/login', name: 'login', component: () => import('../views/auth/Login.vue'), meta: { middlewares: [skipIfAuthProxy, setReturnTo], showAbout: true } },
{ path: '/register', name: 'register', component: () => import('../views/auth/Register.vue'), meta: { middlewares: [skipIfAuthProxy, noRegistration, setReturnTo], showAbout: true } },

View File

@ -128,5 +128,14 @@ export default {
revokeWebauthnCredentials(id, config = {}) {
return apiClient.delete('/users/' + id + '/credentials', { ...config })
},
/**
* Get user's authentication logs
*
* @returns promise
*/
getauthentications(id, limit, config = {}) {
return apiClient.get('/users/' + id + '/authentications' + (limit ? '?limit=' + limit : ''), { ...config })
},
}

View File

@ -9,6 +9,7 @@ export const useBusStore = defineStore({
decodedUri: null,
inManagementMode: false,
editedGroupName: null,
username: null,
}
},

View File

@ -0,0 +1,46 @@
<script setup>
import AccessLogViewer from '@/components/AccessLogViewer.vue'
import userService from '@/services/userService'
import { useNotifyStore } from '@/stores/notify'
import { useBusStore } from '@/stores/bus'
const bus = useBusStore()
const router = useRouter()
onMounted(async () => {
getUser()
})
const props = defineProps({
userId: [Number, String]
})
const username = ref(bus.username ?? '')
/**
* Gets the user from backend
*/
function getUser() {
userService.getById(props.userId, {returnError: true})
.then(response => {
username.value = response.data.info.name
})
}
</script>
<template>
<ResponsiveWidthWrapper>
<h1 class="title has-text-grey-dark">
{{ $t('titles.admin.logs.access') }}
</h1>
<div class="block is-size-7-mobile">
{{ $t('admin.access_log_legend_for_user', { username: username }) }} (#{{ props.userId }})
</div>
<AccessLogViewer :userId="props.userId" :lastOnly="false" :showSearch="true" />
<!-- footer -->
<VueFooter :showButtons="true">
<ButtonBackCloseCancel :returnTo="{ name: 'admin.manageUser', params: { userId: props.userId }}" action="close" />
</VueFooter>
</ResponsiveWidthWrapper>
</template>

View File

@ -1,14 +1,16 @@
<script setup>
import CopyButton from '@/components/CopyButton.vue'
import AccessLogViewer from '@/components/AccessLogViewer.vue'
import userService from '@/services/userService'
import { useNotifyStore } from '@/stores/notify'
import { UseColorMode } from '@vueuse/components'
import { useUserStore } from '@/stores/user'
import { useBusStore } from '@/stores/bus'
const notify = useNotifyStore()
const router = useRouter()
const route = useRoute()
const user = useUserStore()
const bus = useBusStore()
const isFetching = ref(false)
const managedUser = ref(null)
@ -31,6 +33,7 @@
userService.getById(props.userId, {returnError: true})
.then(response => {
managedUser.value = response.data
bus.username = managedUser.value.info.name
})
.catch(error => {
notify.error(error)
@ -284,12 +287,19 @@
</li>
</ul>
</div>
<!-- logs -->
<h2 class="title is-4 has-text-grey-light">{{ $t('admin.logs') }}</h2>
<div class="block is-size-6 is-size-7-mobile has-text-grey">
{{ $t('admin.registered_on_date', { date: managedUser.info.created_at }) }} - {{ $t('admin.last_seen_on_date', { date: managedUser.info.last_seen_at }) }}
</div>
<div class="block">
<ul class="is-size-6 is-size-7-mobile">
<li>{{ $t('admin.registered_on_date', { date: managedUser.info.created_at }) }}</li>
<li>{{ $t('admin.last_seen_on_date', { date: managedUser.info.last_seen_at }) }}</li>
</ul>
<h3 class="title is-6 has-text-grey-light mb-0">{{ $t('admin.last_accesses') }}</h3>
<AccessLogViewer :userId="props.userId" :lastOnly="true" />
</div>
<div class="block is-size-6 is-size-7-mobile has-text-grey">
{{ $t('admin.access_log_has_more_entries') }} <router-link id="lnkFullLogs" :to="{ name: 'admin.logs.access', params: { userId: props.userId }}" >
{{ $t('admin.see_full_log') }}
</router-link>
</div>
<!-- danger zone -->
<h2 class="title is-4 has-text-danger">{{ $t('admin.danger_zone') }}</h2>

View File

@ -68,6 +68,13 @@
'check_now' => 'Check now',
'view_on_github' => 'View on Github',
'x_is_available' => ':version is available',
'successful_login_on' => 'Successful login on <span class="has-text-grey-light">:login_at</span>',
'failed_login_on' => 'Failed login on <span class="has-text-grey-light">:login_at</span>',
'last_accesses' => 'Last accesses',
'see_full_log' => 'See full log',
'browser_on_platform' => ':browser on :platform',
'access_log_has_more_entries' => 'The access log is likely to contain more entries.',
'access_log_legend_for_user' => 'Full access log for user :username',
'forms' => [
'use_encryption' => [
'label' => 'Protect sensitive data',

View File

@ -81,4 +81,14 @@
'information' => 'Information',
'send' => 'Send',
'optimize' => 'Optimize',
'duration' => 'Duration',
'from' => 'From',
'using' => 'Using',
'IP' => 'IP',
'browser' => 'Browser',
'operating_system_short' => 'OS',
'no_entry_yet' => 'No entry yet',
'time' => 'Time',
'ip_address' => 'IP Address',
'device' => 'Device',
];

View File

@ -14,10 +14,17 @@
*/
'hello' => 'Hello',
'hello_user' => 'Hello :username,',
'regards' => 'Regards',
'test_email_settings' => [
'subject' => '2FAuth test email',
'reason' => 'You are receiving this email because you requested a test email to validate the email settings of your 2FAuth instance.',
'success' => 'Good news, it works :)'
],
'new_device' => [
'subject' => 'New connection to 2FAuth',
'resume' => 'A new device has just connected to your 2FAuth account.',
'connection_details' => 'Here are the details of this connection',
'recommandations' => 'If this was you, you can ignore this alert. If you suspect any suspicious activity on your account, please change your password.'
],
];

View File

@ -51,5 +51,8 @@
'users' => 'Users management',
'createUser' => 'Create user',
'manageUser' => 'Manage user',
'logs' => [
'access' => 'Access log'
]
]
];

View File

@ -0,0 +1,17 @@
@component('mail::message')
@lang('notifications.hello_user', ['username' => $account->name])
<br/><br/>
**@lang('notifications.new_device.resume')**<br/>
@lang('notifications.new_device.connection_details'):
<x-mail::panel>
@lang('commons.time'): **{{ $time->toCookieString() }}**<br/>
@lang('commons.ip_address'): **{{ $ipAddress }}**<br/>
@lang('commons.device'): **@lang('admin.browser_on_platform', ['browser' => $browser, 'platform' => $platform])**<br/>
</x-mail::panel>
@lang('notifications.new_device.recommandations')<br/>
@lang('notifications.regards'),<br/>
{{ config('app.name') }}
@endcomponent

View File

@ -60,6 +60,7 @@
* Routes protected by the api authentication guard and restricted to administrators
*/
Route::group(['middleware' => ['auth:api-guard', 'admin']], function () {
Route::get('users/{user}/authentications', [UserManagerController::class, 'authentications'])->name('users.authentications');
Route::patch('users/{user}/password/reset', [UserManagerController::class, 'resetPassword'])->name('users.password.reset');
Route::patch('users/{user}/promote', [UserManagerController::class, 'promote'])->name('users.promote');
Route::delete('users/{user}/pats', [UserManagerController::class, 'revokePATs'])->name('users.revoke.pats');

View File

@ -17,6 +17,10 @@
use Illuminate\Support\Facades\Route;
use Laravel\Passport\Http\Controllers\PersonalAccessTokenController;
// use App\Models\User;
// use App\Notifications\SignedInWithNewDevice;
// use Rappasoft\LaravelAuthenticationLog\Models\AuthenticationLog;
/*
|--------------------------------------------------------------------------
| Web Routes
@ -89,6 +93,12 @@
return csrf_token();
});
// Route::get('/notification', function () {
// $user = User::find(1);
// return (new SignedInWithNewDevice(AuthenticationLog::find(9)))
// ->toMail($user);
// });
/**
* Route for the main landing view
*/