├── .gitattributes ├── .gitignore ├── README.md ├── assets ├── style.css └── style.less ├── composer.json ├── config └── user-activity.php ├── gulpfile.js ├── migrations └── 2020_11_20_100001_create_log_table.php ├── previews └── preview.png ├── routes └── web.php ├── src ├── Console │ ├── UserActivityDelete.php │ └── UserActivityInstall.php ├── Controllers │ └── ActivityController.php ├── EventServiceProvider.php ├── Listeners │ ├── LockoutListener.php │ └── LoginListener.php ├── Models │ └── Log.php ├── ServiceProvider.php └── Traits │ └── Loggable.php └── views ├── index.blade.php └── partials └── script.blade.php /.gitattributes: -------------------------------------------------------------------------------- 1 | * linguist-vendored 2 | *.php linguist-vendored=true 3 | *.html linguist-vendored=false 4 | *.css linguist-vendored=false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /*.txt 2 | /vendor 3 | /.idea 4 | /node_modules 5 | /composer.lock 6 | /package-lock.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Laravel User Activity

2 |

3 | 4 | 5 | 6 | 7 |

8 | 9 |

Easily monitor your user activity with beautiful responsive & easy user-interface!

10 | 11 | ![Image description](previews/preview.png) 12 | 13 | ## Documentation 14 | Checkout features & full documentation of [Laravel User Activity](https://laravelarticle.com/laravel-user-activity) 15 | 16 | ## Other Packages 17 | - [Laravel H](https://github.com/haruncpi/laravel-h) - A helper package for Laravel Framework. 18 | - [Laravel ID generator](https://github.com/haruncpi/laravel-id-generator) - A laravel package for custom database ID generation. 19 | - [Laravel Simple Filemanager](https://github.com/haruncpi/laravel-simple-filemanager) - A simple filemanager for Laravel. 20 | - [Laravel Option Framework](https://github.com/haruncpi/laravel-option-framework) - Option framework for Laravel. 21 | 22 | ### Change Log 23 | 24 | v1.0.6 25 | - Default user model `App\Models\User` to support laravel >=8 26 | - Carbon date parse instead of $dates cast. 27 | 28 | v1.0.4 29 | - Completely enable or disable logging by `activated` config value 30 | - Added Base model logging compatibility 31 | 32 | v1.0.3 33 | - Minor improvements 34 | 35 | v1.0.2 36 | - Create log type added 37 | - User model configuration 38 | - UI ajax loading indicator -------------------------------------------------------------------------------- /assets/style.css: -------------------------------------------------------------------------------- 1 | body{margin:0;padding:0;background:#f4f4f4;font-family:sans-serif}.btn{text-decoration:none;background:white;padding:5px 12px;border-radius:25px}header{min-height:30px;display:flex;justify-content:space-between;align-items:center;padding:15px;background:#2e2e2f;position:fixed;left:0;right:0;top:0;box-shadow:0 1px 3px rgba(0,0,0,0.12),0 1px 2px rgba(0,0,0,0.24)}header a{color:#333}header .btn_clear_all{background:#de4f4f;color:#fff}header .name{font-size:25px;font-weight:500;color:white;position:relative}header .name span:nth-child(1){position:absolute;font-size:13px;left:28px;top:-3px}header .name span:nth-child(3){position:absolute;left:28px;top:7px;font-size:22px}.letter_a{background:#FFC107;border-radius:4px;color:#333;padding:0px 3px;margin-right:3px;font-weight:600}.content{margin-top:65px;padding:15px;background:#fff;min-height:100px}.content select,.content input,.content button{box-sizing:border-box;min-height:28px;max-height:28px;min-width:120px;border:1px solid #ddd;border-radius:4px;padding:2px 5px}.content input{padding:2px 7px}.top_content{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px}.top_content .top_content_left{display:flex}.top_content .top_content_left .log_filter{display:flex;align-items:center;margin-left:15px}.top_content .top_content_left .log_filter .log_type_item{margin-right:4px;background:#eae9e9;max-height:20px;font-size:11px;box-sizing:border-box;padding:4px 6px;cursor:pointer}.top_content .top_content_left .log_filter .log_type_item.active{background:#2f2e2f;color:white!important}.top_content .top_content_left .log_filter .log_type_item.clear{background:#607D8B;color:white}.top_content .top_content_right{display:flex}.top_content .top_content_right .user_list_box{position:relative}.top_content .top_content_right .user_list_box #user_list{box-shadow:0 1px 3px rgba(0,0,0,0.12),0 1px 2px rgba(0,0,0,0.24);position:absolute;z-index:10;max-height:200px;border:1px solid #f4f4f4;max-width:100%;min-width:100%;overflow:hidden;box-sizing:border-box;background:white;top:45px;overflow-y:scroll}.top_content .top_content_right .user_list_box #user_list::-webkit-scrollbar{width:3px}.top_content .top_content_right .user_list_box #user_list .single_user{margin-bottom:8px;cursor:pointer;padding:7px}.top_content .top_content_right .user_list_box #user_list .single_user:hover{background:#f4f4f4}.top_content .top_content_right .user_list_box #user_list .single_user p{margin:0}.top_content .top_content_right .user_list_box #user_list .single_user p span{font-size:12px}.top_content .top_content_right .btn_filter{min-width:70px;background:#FFC107;color:#2b2b28;cursor:pointer;border:1px solid #af8300}.top_content .top_content_right .btn_filter_active{background:#F44336;color:#fff;border:1px solid #a51208}.top_content .top_content_right .btn_reset{min-width:70px;cursor:pointer}.top_content .top_content_right .filter_item{margin-right:5px;display:flex;flex-direction:column}.top_content .top_content_right .filter_item label{font-size:13px;margin-bottom:3px;color:#555}.action_column button{min-width:70px;cursor:pointer}.action_column .btn_delete{background:#ef5b50;color:white}.log_data_wrapper{position:relative;min-height:calc(100vh - 205px);overflow-y:auto}.log_data_wrapper::-webkit-scrollbar{width:2px}.log_data_wrapper .loader{position:absolute;z-index:10;top:0;left:0;background:rgba(0,0,0,0.5);width:100%;height:100%;display:flex;align-items:center;justify-content:center;color:#fff}footer{background:#f4f4f4;display:flex;justify-content:space-between;box-sizing:border-box;padding:10px}footer .footer_right .btn{background:#ef5b50;color:white;cursor:pointer}table{border:1px solid #ccc;border-collapse:collapse;margin:0;padding:0;width:100%}table tr{border:1px solid #e8e8e8;padding:5px}table tr:hover{background:#f4f4f4}thead tr td{background:#717171;color:#fff}table th,table td{padding:6px 6px;font-size:15px;color:#666}table th{font-size:14px;letter-spacing:1px;text-transform:uppercase}.lbl_table{margin-top:4px;display:inline-block;margin-left:3px;color:#777}.field_cell{background:#f4f4f4;width:150px}.changed{background:antiquewhite}.edit_badge{width:33px;display:inline-block;text-align:center}@media screen and (max-width:700px){.top_content{flex-direction:column}.top_content .top_content_left{flex-direction:column}.top_content .log_filter{flex-wrap:wrap}.top_content .log_filter .log_type_item{margin-bottom:3px}}.popup_wrapper{position:fixed;top:0;left:0;width:100%;height:100vh;background:rgba(0,0,0,0.5)}.popup{width:50%;background:#fff;position:absolute;margin:0 auto;left:0;right:0;top:20%;z-index:200;box-shadow:1px 0 20px rgba(0,0,0,0.19),0 6px 6px rgba(0,0,0,0.23);max-height:calc(100vh - 35%);overflow-x:hidden;overflow-y:scroll}.popup::-webkit-scrollbar{width:3px}.popup .header{display:flex;background:whitesmoke;justify-content:space-between;height:35px;align-items:center;border-bottom:1px solid #ddd}.popup .header .title{margin-left:15px;font-weight:700}.popup .header .close{width:30px;background:#333;color:#fff;text-align:center;line-height:35px;border-bottom:1px solid #333;cursor:pointer}.popup .popup_content{min-height:100px;width:100%;padding:15px}.popup .popup_content .form-group{margin-bottom:10px}.popup .popup_content label{display:block;color:#777}.popup .popup_content input{border:0;padding:5px;border:1px solid #ddd;width:100%;margin-top:5px}.popup .popup_content input:focus{outline:none;border-bottom:1px solid #666}.popup .footer{padding:0 15px;display:flex;background:whitesmoke;justify-content:space-between;height:40px;align-items:center;border-top:1px solid #ddd}.pagination_wrapper{display:flex;justify-content:flex-end}.pagination_wrapper ul.pagination{display:flex;padding:0;margin-right:5px}.pagination_wrapper ul.pagination li{list-style-type:none}.pagination_wrapper ul.pagination li a{padding:5px 8px;background:#f4f4f4;border:1px solid #ddd;display:inline-block;text-decoration:none;color:#000;margin-right:3px}.pagination_wrapper ul.pagination li a:hover{background:#222;color:#fff}.pagination_wrapper ul.pagination li .active{background:#222;color:#fff}.pagination_wrapper ul.pagination li.active a{background:#222;color:#fff}@media screen and (max-width:600px){.top_content{flex-direction:column-reverse}.top_content_right{flex-wrap:wrap}.top_content_right .filter_item{min-width:48%;margin-bottom:8px}.top_content_right .full_width_param{min-width:98%;max-width:98%}.pagination_wrapper{display:flex;justify-content:flex-start;overflow-x:scroll}.btn{font-size:13px}.dt_box,.selected_date{text-align:center}.responsive_table{max-width:100%;overflow-x:auto}.popup{width:96%!important}.popup .popup_content table{width:93% !important}.popup .popup_content table td{width:96%}table{border:0}table thead{display:none}table tr{border-bottom:2px solid #ddd;display:block;margin-bottom:10px}table td{border-bottom:1px dotted #ccc;display:block;font-size:15px}table td:last-child{border-bottom:0}table td:before{content:attr(data-label);float:left;font-weight:bold;text-transform:uppercase}}.badge{padding:3px 8px;-webkit-border-radius:25px;-moz-border-radius:25px;border-radius:25px;font-size:12px}.badge.primary{background:#4ba4ea;color:white}.badge.info{background:#6bb5b5;color:#fff}.badge.warning{background:#f7be57}.badge.critical{background:#de4f4f;color:#fff}.badge.emergency{background:#ff6060;color:white}.badge.notice{background:bisque}.badge.debug{background:#8e8c8c;color:white}.badge.alert{background:#4ba4ea;color:white}.badge.error{background:#c36a6a;color:white}.text_center{text-align:center}.text_right{text-align:right}.text_left{text-align:left}.text_light{color:#888}.spinner{margin:100px auto 0;width:70px;text-align:center}.spinner>div{width:18px;height:18px;background-color:#fff;border-radius:100%;display:inline-block;-webkit-animation:sk-bouncedelay 1.4s infinite ease-in-out both;animation:sk-bouncedelay 1.4s infinite ease-in-out both}.spinner .bounce1{-webkit-animation-delay:-0.32s;animation-delay:-0.32s}.spinner .bounce2{-webkit-animation-delay:-0.16s;animation-delay:-0.16s}@-webkit-keyframes sk-bouncedelay{0%,80%,100%{-webkit-transform:scale(0)}40%{-webkit-transform:scale(1)}}@keyframes sk-bouncedelay{0%,80%,100%{-webkit-transform:scale(0);transform:scale(0)}40%{-webkit-transform:scale(1);transform:scale(1)}} -------------------------------------------------------------------------------- /assets/style.less: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | padding: 0; 4 | background: #f4f4f4; 5 | font-family: sans-serif; 6 | } 7 | .btn { 8 | text-decoration: none; 9 | background: white; 10 | padding: 5px 12px; 11 | border-radius: 25px; 12 | } 13 | 14 | header { 15 | min-height: 30px; 16 | display: flex; 17 | justify-content: space-between; 18 | align-items: center; 19 | padding: 15px; 20 | background: #2e2e2f; 21 | //background: #3F51B5; 22 | position: fixed; 23 | left: 0; 24 | right: 0; 25 | top: 0; 26 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 27 | a{ 28 | color: #333; 29 | } 30 | .btn_clear_all{ 31 | background: #de4f4f; 32 | color: #fff; 33 | } 34 | } 35 | 36 | header .name { 37 | font-size: 25px; 38 | font-weight: 500; 39 | color: white; 40 | position: relative; 41 | span:nth-child(1){ 42 | position: absolute; 43 | font-size: 13px; 44 | left: 28px; 45 | top: -3px; 46 | } 47 | 48 | span:nth-child(3){ 49 | position: absolute; 50 | left: 28px; 51 | top: 7px; 52 | font-size: 22px; 53 | } 54 | } 55 | 56 | .letter_a{ 57 | background: #FFC107; 58 | border-radius: 4px; 59 | color: #333; 60 | padding: 0px 3px; 61 | margin-right: 3px; 62 | font-weight: 600; 63 | } 64 | 65 | .content { 66 | margin-top: 65px; 67 | padding: 15px; 68 | background: #fff; 69 | min-height: 100px; 70 | 71 | select,input,button{ 72 | box-sizing:border-box; 73 | min-height: 28px; 74 | max-height: 28px; 75 | min-width: 120px; 76 | border: 1px solid #ddd; 77 | border-radius: 4px; 78 | padding: 2px 5px; 79 | } 80 | input{ 81 | padding: 2px 7px; 82 | } 83 | } 84 | 85 | .top_content { 86 | display: flex; 87 | justify-content: space-between; 88 | align-items: center; 89 | margin-bottom: 10px; 90 | 91 | .top_content_left { 92 | display: flex; 93 | 94 | .log_filter { 95 | display: flex; 96 | align-items: center; 97 | margin-left: 15px; 98 | 99 | .log_type_item { 100 | margin-right: 4px; 101 | background: #eae9e9; 102 | max-height: 20px; 103 | font-size: 11px; 104 | box-sizing: border-box; 105 | padding: 4px 6px; 106 | cursor: pointer; 107 | } 108 | 109 | .log_type_item.active { 110 | background: #2f2e2f; 111 | color: white!important; 112 | } 113 | 114 | .log_type_item.clear { 115 | background: #607D8B; 116 | color: white; 117 | } 118 | } 119 | } 120 | 121 | .top_content_right { 122 | display: flex; 123 | 124 | .user_list_box{ 125 | position: relative; 126 | 127 | 128 | 129 | #user_list{ 130 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); 131 | position: absolute; 132 | z-index: 10; 133 | max-height: 200px; 134 | border: 1px solid #f4f4f4; 135 | max-width: 100%; 136 | min-width: 100%; 137 | overflow: hidden; 138 | box-sizing: border-box; 139 | background: white; 140 | top: 45px; 141 | overflow-y: scroll; 142 | 143 | &::-webkit-scrollbar { 144 | width: 3px; 145 | } 146 | 147 | .single_user{ 148 | margin-bottom: 8px; 149 | cursor: pointer; 150 | padding: 7px; 151 | &:hover{ 152 | background: #f4f4f4; 153 | } 154 | p{ 155 | margin: 0; 156 | span{ 157 | font-size: 12px; 158 | } 159 | } 160 | } 161 | 162 | } 163 | } 164 | .btn_filter{ 165 | min-width: 70px; 166 | background: #FFC107; 167 | color: #2b2b28; 168 | cursor: pointer; 169 | border: 1px solid #af8300; 170 | } 171 | .btn_filter_active{ 172 | background: #F44336; 173 | color: #fff; 174 | border: 1px solid #a51208; 175 | } 176 | .btn_reset{ 177 | min-width: 70px; 178 | cursor: pointer; 179 | } 180 | .filter_item{ 181 | margin-right: 5px; 182 | display: flex; 183 | flex-direction: column; 184 | label{ 185 | font-size: 13px; 186 | margin-bottom: 3px; 187 | color: #555; 188 | } 189 | } 190 | } 191 | } 192 | 193 | .action_column{ 194 | button{ 195 | min-width: 70px; 196 | cursor: pointer; 197 | } 198 | .btn_delete{ 199 | background: #ef5b50; 200 | color: white; 201 | } 202 | } 203 | 204 | .log_data_wrapper{ 205 | position: relative; 206 | min-height: calc(100vh - 205px); 207 | overflow-y: auto; 208 | &::-webkit-scrollbar { 209 | width: 2px; 210 | } 211 | 212 | .loader{ 213 | position: absolute; 214 | z-index: 10; 215 | top: 0; 216 | left:0; 217 | background: fade(#000,50%); 218 | width: 100%; 219 | height: 100%; 220 | display: flex; 221 | align-items: center; 222 | justify-content: center; 223 | color: #fff; 224 | } 225 | } 226 | footer{ 227 | background: #f4f4f4; 228 | display: flex; 229 | justify-content: space-between; 230 | box-sizing: border-box; 231 | padding: 10px; 232 | 233 | .footer_right{ 234 | .btn{ 235 | background: #ef5b50; 236 | color: white; 237 | cursor: pointer; 238 | } 239 | } 240 | } 241 | table { 242 | border: 1px solid #ccc; 243 | border-collapse: collapse; 244 | margin: 0; 245 | padding: 0; 246 | width: 100%; 247 | } 248 | 249 | table tr { 250 | border: 1px solid #e8e8e8; 251 | padding: 5px; 252 | &:hover{ 253 | background:#f4f4f4; 254 | } 255 | } 256 | 257 | thead tr td { 258 | background: #717171; 259 | color: #fff; 260 | } 261 | 262 | table th, 263 | table td { 264 | padding: 6px 6px; 265 | font-size: 15px; 266 | color: #666; 267 | } 268 | 269 | table th { 270 | font-size: 14px; 271 | letter-spacing: 1px; 272 | text-transform: uppercase; 273 | } 274 | 275 | .lbl_table{ 276 | margin-top: 4px; 277 | display: inline-block; 278 | margin-left: 3px; 279 | color: #777; 280 | } 281 | .field_cell{ 282 | background: #f4f4f4; 283 | width: 150px; 284 | } 285 | .changed{ 286 | background: antiquewhite; 287 | } 288 | .edit_badge{ 289 | width: 33px; 290 | display: inline-block; 291 | text-align: center; 292 | } 293 | 294 | @media screen and (max-width: 700px) { 295 | .top_content{ 296 | flex-direction: column; 297 | .top_content_left{ 298 | flex-direction: column; 299 | } 300 | .log_filter{ 301 | flex-wrap: wrap; 302 | .log_type_item{ 303 | margin-bottom: 3px; 304 | } 305 | } 306 | } 307 | } 308 | 309 | .popup_wrapper{ 310 | position: fixed; 311 | top: 0; 312 | left: 0; 313 | width: 100%; 314 | height: 100vh; 315 | background: fade(#000,50%); 316 | } 317 | 318 | .popup{ 319 | width: 50%; 320 | 321 | background: #fff; 322 | position: absolute; 323 | margin: 0 auto; 324 | left: 0; 325 | right: 0; 326 | top: 20%; 327 | z-index: 200; 328 | box-shadow: 1px 0px 20px rgba(0, 0, 0, 0.19), 0 6px 6px rgba(0, 0, 0, 0.23); 329 | max-height: calc(100vh - 35%); 330 | overflow-x: hidden; 331 | overflow-y: scroll; 332 | 333 | &::-webkit-scrollbar { 334 | width: 3px; 335 | } 336 | 337 | .header{ 338 | display: flex; 339 | background: whitesmoke; 340 | justify-content: space-between; 341 | height: 35px; 342 | align-items: center; 343 | border-bottom: 1px solid #ddd; 344 | .title{ 345 | margin-left: 15px; 346 | font-weight: 700; 347 | } 348 | .close{ 349 | width: 30px; 350 | background: #333; 351 | color: #fff; 352 | text-align: center; 353 | line-height: 35px; 354 | border-bottom: 1px solid #333; 355 | cursor: pointer; 356 | } 357 | } 358 | .popup_content{ 359 | min-height: 100px; 360 | width: 100%; 361 | padding: 15px; 362 | 363 | .form-group{ 364 | margin-bottom: 10px; 365 | } 366 | label{ 367 | display: block; 368 | color: #777; 369 | } 370 | input{ 371 | border: 0; 372 | padding: 5px; 373 | border: 1px solid #ddd; 374 | width: 100%; 375 | margin-top: 5px; 376 | &:focus{ 377 | outline: none; 378 | border-bottom: 1px solid #666; 379 | } 380 | } 381 | } 382 | .footer{ 383 | padding: 0 15px; 384 | display: flex; 385 | background: whitesmoke; 386 | justify-content: space-between; 387 | height: 40px; 388 | align-items: center; 389 | border-top: 1px solid #ddd; 390 | } 391 | } 392 | 393 | .pagination_wrapper{ 394 | display: flex; 395 | justify-content: flex-end; 396 | ul.pagination{ 397 | display: flex; 398 | padding: 0; 399 | margin-right: 5px; 400 | li{ 401 | list-style-type: none; 402 | 403 | a{ 404 | padding: 5px 8px; 405 | background: #f4f4f4; 406 | border: 1px solid #ddd; 407 | display: inline-block; 408 | text-decoration: none; 409 | color: #000; 410 | margin-right: 3px; 411 | &:hover{ 412 | background: #222; 413 | color: #fff; 414 | } 415 | } 416 | .active{ 417 | background: #222; 418 | color: #fff; 419 | } 420 | } 421 | li.active a{ 422 | background: #222; 423 | color: #fff; 424 | } 425 | } 426 | } 427 | 428 | @media screen and (max-width: 600px) { 429 | 430 | .top_content{ 431 | flex-direction: column-reverse; 432 | } 433 | .top_content_right{ 434 | flex-wrap: wrap; 435 | .filter_item{ 436 | min-width: 48%; 437 | margin-bottom: 8px; 438 | } 439 | .full_width_param{ 440 | min-width: 98%; 441 | max-width: 98%; 442 | } 443 | } 444 | .pagination_wrapper{ 445 | display: flex; 446 | justify-content: flex-start; 447 | overflow-x: scroll; 448 | } 449 | .btn { 450 | font-size: 13px; 451 | } 452 | 453 | .dt_box, .selected_date { 454 | text-align: center; 455 | } 456 | 457 | .responsive_table { 458 | max-width: 100%; 459 | overflow-x: auto; 460 | } 461 | 462 | .popup{ 463 | width: 96%!important; 464 | .popup_content{ 465 | table{ 466 | width: 93% !important; 467 | td{ 468 | width: 96%; 469 | } 470 | } 471 | } 472 | } 473 | 474 | table { 475 | border: 0; 476 | } 477 | 478 | table thead { 479 | display: none; 480 | } 481 | 482 | table tr { 483 | border-bottom: 2px solid #ddd; 484 | display: block; 485 | margin-bottom: 10px; 486 | } 487 | 488 | table td { 489 | border-bottom: 1px dotted #ccc; 490 | display: block; 491 | font-size: 15px; 492 | } 493 | 494 | table td:last-child { 495 | border-bottom: 0; 496 | } 497 | 498 | table td:before { 499 | content: attr(data-label); 500 | float: left; 501 | font-weight: bold; 502 | text-transform: uppercase; 503 | } 504 | } 505 | 506 | .badge { 507 | padding: 3px 8px; 508 | -webkit-border-radius: 25px; 509 | -moz-border-radius: 25px; 510 | border-radius: 25px; 511 | font-size: 12px; 512 | } 513 | 514 | .badge.primary { 515 | background: #4ba4ea; 516 | color: white; 517 | } 518 | 519 | .badge.info { 520 | background: #6bb5b5; 521 | color: #fff; 522 | } 523 | 524 | .badge.warning { 525 | background: #f7be57; 526 | } 527 | 528 | .badge.critical { 529 | background: #de4f4f; 530 | color: #fff; 531 | } 532 | 533 | .badge.emergency { 534 | background: #ff6060; 535 | color: white; 536 | } 537 | 538 | .badge.notice { 539 | background: bisque; 540 | } 541 | 542 | .badge.debug { 543 | background: #8e8c8c; 544 | color: white; 545 | } 546 | 547 | .badge.alert { 548 | background: #4ba4ea; 549 | color: white; 550 | } 551 | 552 | .badge.error { 553 | background: #c36a6a; 554 | color: white; 555 | } 556 | 557 | .text_center{ 558 | text-align: center; 559 | } 560 | .text_right{ 561 | text-align: right; 562 | } 563 | .text_left{ 564 | text-align: left; 565 | } 566 | .text_light{ 567 | color: #888; 568 | } 569 | 570 | //animation 571 | .spinner { 572 | margin: 100px auto 0; 573 | width: 70px; 574 | text-align: center; 575 | } 576 | 577 | .spinner > div { 578 | width: 18px; 579 | height: 18px; 580 | background-color: #fff; 581 | 582 | border-radius: 100%; 583 | display: inline-block; 584 | -webkit-animation: sk-bouncedelay 1.4s infinite ease-in-out both; 585 | animation: sk-bouncedelay 1.4s infinite ease-in-out both; 586 | } 587 | 588 | .spinner .bounce1 { 589 | -webkit-animation-delay: -0.32s; 590 | animation-delay: -0.32s; 591 | } 592 | 593 | .spinner .bounce2 { 594 | -webkit-animation-delay: -0.16s; 595 | animation-delay: -0.16s; 596 | } 597 | 598 | @-webkit-keyframes sk-bouncedelay { 599 | 0%, 80%, 100% { -webkit-transform: scale(0) } 600 | 40% { -webkit-transform: scale(1.0) } 601 | } 602 | 603 | @keyframes sk-bouncedelay { 604 | 0%, 80%, 100% { 605 | -webkit-transform: scale(0); 606 | transform: scale(0); 607 | } 40% { 608 | -webkit-transform: scale(1.0); 609 | transform: scale(1.0); 610 | } 611 | } -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "haruncpi/laravel-user-activity", 3 | "description": "Monitor user activity easily!", 4 | "license": "cc-by-4.0", 5 | "keywords": [ 6 | "laravel", 7 | "user", 8 | "activity" 9 | ], 10 | "type": "library", 11 | "authors": [ 12 | { 13 | "name": "MD.HARUN-UR-RASHID", 14 | "email": "harun.cox@gmail.com" 15 | } 16 | ], 17 | "minimum-stability": "dev", 18 | "prefer-stable": true, 19 | "require": { 20 | "php": ">=5.6" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "Haruncpi\\LaravelUserActivity\\": "src" 25 | } 26 | }, 27 | "scripts": { 28 | "phpunit": "phpunit" 29 | }, 30 | "extra": { 31 | "laravel": { 32 | "providers": [ 33 | "Haruncpi\\LaravelUserActivity\\ServiceProvider" 34 | ] 35 | } 36 | }, 37 | "config": { 38 | "preferred-install": "dist", 39 | "sort-packages": true, 40 | "optimize-autoloader": true 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /config/user-activity.php: -------------------------------------------------------------------------------- 1 | true, // active/inactive all logging 5 | 'middleware' => ['web', 'auth'], 6 | 'route_path' => 'admin/user-activity', 7 | 'admin_panel_path' => 'admin/dashboard', 8 | 'delete_limit' => 7, // default 7 days 9 | 10 | 'model' => [ 11 | 'user' => "App\Models\User" 12 | ], 13 | 14 | 'log_events' => [ 15 | 'on_create' => false, 16 | 'on_edit' => true, 17 | 'on_delete' => true, 18 | 'on_login' => true, 19 | 'on_lockout' => true 20 | ] 21 | ]; 22 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var inject = require('gulp-inject'); 3 | var print = require('gulp-print').default; 4 | 5 | gulp.task('inject-style', function () { 6 | gulp.watch(['./assets/style.css'], function (file) { 7 | return gulp.src('./views/index.blade.php') 8 | .pipe(inject(gulp.src(['./assets/style.css']), { 9 | starttag: '', 10 | transform: function (filepath, file) { 11 | return ``; 12 | } 13 | })) 14 | .pipe(print(function (file) { 15 | return "Processing " + file; 16 | })) 17 | .pipe(gulp.dest('./views')); 18 | }); 19 | }); 20 | 21 | gulp.task('default', gulp.series('inject-style')); -------------------------------------------------------------------------------- /migrations/2020_11_20_100001_create_log_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | $table->bigInteger('user_id')->unsigned(); 19 | $table->dateTime('log_date'); 20 | $table->string('table_name',50)->nullable(); 21 | $table->string('log_type',50); 22 | $table->longText('data'); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | * @return void 30 | */ 31 | public function down() 32 | { 33 | Schema::dropIfExists('logs'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /previews/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/haruncpi/laravel-user-activity/5055fd4a03edc39a1eea8d2d1cbdef3ed007da52/previews/preview.png -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | '\Haruncpi\LaravelUserActivity\Controllers', 4 | 'middleware' => config('user-activity.middleware') 5 | ], function () { 6 | Route::get(config('user-activity.route_path'), 'ActivityController@getIndex'); 7 | Route::post(config('user-activity.route_path'), 'ActivityController@handlePostRequest'); 8 | }); -------------------------------------------------------------------------------- /src/Console/UserActivityDelete.php: -------------------------------------------------------------------------------- 1 | argument('delete_limit'); 37 | switch (strtolower(trim($deleteLimit))) { 38 | case 'all': 39 | Log::truncate(); 40 | $this->info("All log data deleted!"); 41 | break; 42 | default: 43 | if (is_numeric($deleteLimit)) { 44 | Log::whereRaw('log_date < NOW() - INTERVAL ? DAY', [$deleteLimit])->delete(); 45 | $this->info("Successfully deleted log data older than $deleteLimit days"); 46 | } else { 47 | $dayLimit = config('user-activity.delete_limit'); 48 | Log::whereRaw('log_date < NOW() - INTERVAL ? DAY', [$dayLimit])->delete(); 49 | $this->info("Successfully deleted log data older than $dayLimit days"); 50 | } 51 | } 52 | 53 | } 54 | 55 | 56 | } 57 | -------------------------------------------------------------------------------- /src/Console/UserActivityInstall.php: -------------------------------------------------------------------------------- 1 | confirm("user-activity.php config file already exist. Do you want to overwrite?"); 46 | if ($confirm) { 47 | $this->publishConfig(); 48 | $this->info("config overwrite finished"); 49 | } else { 50 | $this->info("skipped config publish"); 51 | } 52 | } else { 53 | $this->publishConfig(); 54 | $this->info("config published"); 55 | } 56 | 57 | 58 | //migration 59 | if (File::exists(database_path("migrations/$migrationFile"))) { 60 | $confirm = $this->confirm("migration file already exist. Do you want to overwrite?"); 61 | if ($confirm) { 62 | $this->publishMigration(); 63 | $this->info("migration overwrite finished"); 64 | } else { 65 | $this->info("skipped migration publish"); 66 | } 67 | } else { 68 | $this->publishMigration(); 69 | $this->info("migration published"); 70 | } 71 | 72 | $this->line('-----------------------------'); 73 | if (!Schema::hasTable('logs')) { 74 | $this->call('migrate'); 75 | } else { 76 | $this->error('logs table already exist in your database. migration not run successfully'); 77 | } 78 | 79 | } 80 | 81 | private function publishConfig() 82 | { 83 | $this->call('vendor:publish', [ 84 | '--provider' => "Haruncpi\LaravelUserActivity\ServiceProvider", 85 | '--tag' => 'config', 86 | '--force' => true 87 | ]); 88 | } 89 | 90 | private function publishMigration() 91 | { 92 | $this->call('vendor:publish', [ 93 | '--provider' => "Haruncpi\LaravelUserActivity\ServiceProvider", 94 | '--tag' => 'migrations', 95 | '--force' => true 96 | ]); 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /src/Controllers/ActivityController.php: -------------------------------------------------------------------------------- 1 | userInstance = $userInstance; 18 | } 19 | 20 | private function handleData(Request $request) 21 | { 22 | $this->validate($request, [ 23 | 'action' => 'required|string', 24 | 'user_id' => 'sometimes|numeric', 25 | 'log_type' => 'sometimes|string', 26 | 'table' => 'sometimes|string', 27 | 'from_date' => 'sometimes|date_format:Y-m-d', 28 | 'to_date' => 'sometimes|date_format:Y-m-d' 29 | ]); 30 | 31 | $data = Log::with('user')->orderBy('id', 'desc'); 32 | if ($request->has('user_id')) { 33 | $data = $data->where('user_id', request('user_id')); 34 | } 35 | if ($request->has('log_type')) { 36 | $data = $data->where('log_type', request('log_type')); 37 | } 38 | if ($request->has('table')) { 39 | $data = $data->where('table_name', request('table')); 40 | } 41 | if ($request->has('from_date') && $request->has('to_date')) { 42 | $from = request('from_date') . " 00:00:00"; 43 | $to = request('to_date') . " 23:59:59"; 44 | $data = $data->whereBetween('log_date', [$from, $to]); 45 | } 46 | 47 | return $data->paginate(10); 48 | } 49 | 50 | private function handleCurrentData(Request $request) 51 | { 52 | $this->validate($request, [ 53 | 'table' => 'required|string', 54 | 'id' => 'required', 55 | 'log_id' => 'required|numeric' 56 | ]); 57 | 58 | $table = request('table'); 59 | $id = request('id'); 60 | $logId = request('log_id'); 61 | $currentData = DB::table($table)->find($id); 62 | if ($currentData) { 63 | $editHistory = Log::with('user') 64 | ->orderBy('log_date', 'desc') 65 | ->whereNotIn('id', [$logId]) 66 | ->where(['table_name' => $table, 'log_type' => 'edit']) 67 | ->whereRaw('data like ?', array('%"id":"' . $id . '"%'))->get(); 68 | return ['current_data' => $currentData, 'edit_history' => $editHistory]; 69 | } 70 | return []; 71 | } 72 | 73 | private function handleUserAutocomplete(Request $request) 74 | { 75 | $this->validate($request, [ 76 | 'user' => 'required|string|max:50' 77 | ]); 78 | 79 | $user = request('user'); 80 | return $this->userInstance::select('id', 'name', 'email') 81 | ->where('name', 'like', '%' . $user . '%') 82 | ->orWhere('id', $user) 83 | ->limit(10)->get(); 84 | } 85 | 86 | public function getIndex(Request $request) 87 | { 88 | 89 | if ($request->has('action')) { 90 | $action = $request->get('action'); 91 | switch ($action) { 92 | case 'data': 93 | return response()->json($this->handleData($request)); 94 | break; 95 | 96 | case 'current_data': 97 | return response()->json($this->handleCurrentData($request)); 98 | break; 99 | 100 | case 'user_autocomplete': 101 | return response()->json($this->handleUserAutocomplete($request)); 102 | break; 103 | } 104 | } 105 | 106 | $connection = config('database.default'); 107 | $driver = DB::connection($connection)->getDriverName(); 108 | switch ($driver) { 109 | case 'pgsql': 110 | $sql = sprintf( 111 | "SELECT table_name FROM information_schema.tables where table_schema = '%s' ORDER BY table_schema,table_name;", 112 | DB::connection($connection)->getConfig('schema') ?: 'public' 113 | ); 114 | $all = array_map('current', DB::select($sql)); 115 | break; 116 | case 'sqlite': 117 | $sql = "SELECT name as table_name FROM sqlite_master WHERE type='table' ORDER BY name"; 118 | $all = array_map('current', DB::select($sql)); 119 | break; 120 | default: 121 | $all = array_map('current', DB::select('SHOW TABLES')); 122 | } 123 | 124 | $exclude = ['failed_jobs', 'password_resets', 'migrations', 'logs']; 125 | $tables = array_diff($all, $exclude); 126 | 127 | return view('LaravelUserActivity::index', ['tables' => $tables]); 128 | 129 | } 130 | 131 | public function handlePostRequest(Request $request) 132 | { 133 | if ($request->has('action')) { 134 | $action = $request->get('action'); 135 | switch ($action) { 136 | case 'delete': 137 | $dayLimit = config('user-activity.delete_limit'); 138 | Log::whereRaw('log_date < NOW() - INTERVAL ? DAY', [$dayLimit])->delete(); 139 | return ['success' => true, 'message' => "Successfully deleted log data older than $dayLimit days"]; 140 | break; 141 | } 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/EventServiceProvider.php: -------------------------------------------------------------------------------- 1 | [ 16 | LoginListener::class 17 | ], 18 | Lockout::class => [ 19 | LockoutListener::class 20 | ] 21 | ]; 22 | 23 | public function boot() 24 | { 25 | parent::boot(); 26 | } 27 | } -------------------------------------------------------------------------------- /src/Listeners/LockoutListener.php: -------------------------------------------------------------------------------- 1 | request = $request; 16 | 17 | $userInstance = config('user-activity.model.user'); 18 | if(!empty($userInstance)) $this->userInstance = $userInstance; 19 | } 20 | 21 | 22 | public function handle($event) 23 | { 24 | if (!config('user-activity.log_events.on_lockout', false) 25 | || !config('user-activity.activated', true)) return; 26 | 27 | if (!$event->request->has('email')) return; 28 | $user = $this->userInstance::where('email', $event->request->input('email'))->first(); 29 | if (!$user) return; 30 | 31 | 32 | $data = [ 33 | 'ip' => $this->request->ip(), 34 | 'user_agent' => $this->request->userAgent() 35 | ]; 36 | 37 | DB::table('logs')->insert([ 38 | 'user_id' => $user->id, 39 | 'log_date' => date('Y-m-d H:i:s'), 40 | 'table_name' => '', 41 | 'log_type' => 'lockout', 42 | 'data' => json_encode($data) 43 | ]); 44 | 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Listeners/LoginListener.php: -------------------------------------------------------------------------------- 1 | request = $request; 14 | } 15 | 16 | public function handle(Login $event) 17 | { 18 | if (!config('user-activity.log_events.on_login', false) 19 | || !config('user-activity.activated', true)) return; 20 | 21 | $user = $event->user; 22 | $dateTime = date('Y-m-d H:i:s'); 23 | 24 | $data = [ 25 | 'ip' => $this->request->ip(), 26 | 'user_agent' => $this->request->userAgent() 27 | ]; 28 | 29 | DB::table('logs')->insert([ 30 | 'user_id' => $user->id, 31 | 'log_date' => $dateTime, 32 | 'table_name' => '', 33 | 'log_type' => 'login', 34 | 'data' => json_encode($data) 35 | ]); 36 | } 37 | } -------------------------------------------------------------------------------- /src/Models/Log.php: -------------------------------------------------------------------------------- 1 | userInstance = $userInstance; 16 | } 17 | 18 | public function getDateHumanizeAttribute() 19 | { 20 | return Carbon::parse($this->attributes['log_date'])->diffForHumans(); 21 | } 22 | 23 | public function getJsonDataAttribute() 24 | { 25 | return json_decode($this->data,true); 26 | } 27 | 28 | public function user() 29 | { 30 | return $this->belongsTo($this->userInstance); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 20 | self::CONFIG_PATH => config_path('user-activity.php') 21 | ], 'config'); 22 | 23 | $this->publishes([ 24 | self::MIGRATION_PATH => database_path('migrations') 25 | ], 'migrations'); 26 | } 27 | 28 | public function boot() 29 | { 30 | $this->publish(); 31 | 32 | $this->loadRoutesFrom(self::ROUTE_PATH . '/web.php'); 33 | $this->loadViewsFrom(self::VIEW_PATH, 'LaravelUserActivity'); 34 | } 35 | 36 | public function register() 37 | { 38 | $this->mergeConfigFrom( 39 | self::CONFIG_PATH, 40 | 'user-activity' 41 | ); 42 | 43 | $this->app->register(EventServiceProvider::class); 44 | $this->commands([UserActivityInstall::class, UserActivityDelete::class]); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/Traits/Loggable.php: -------------------------------------------------------------------------------- 1 | check() || $model->excludeLogging || !config('user-activity.activated', true)) return; 14 | if ($logType == 'create') $originalData = json_encode($model); 15 | else { 16 | if (version_compare(app()->version(), '7.0.0', '>=')) 17 | $originalData = json_encode($model->getRawOriginal()); // getRawOriginal available from Laravel 7.x 18 | else 19 | $originalData = json_encode($model->getOriginal()); 20 | } 21 | 22 | $tableName = $model->getTable(); 23 | $dateTime = date('Y-m-d H:i:s'); 24 | $userId = auth()->user()->id; 25 | 26 | DB::table(self::$logTable)->insert([ 27 | 'user_id' => $userId, 28 | 'log_date' => $dateTime, 29 | 'table_name' => $tableName, 30 | 'log_type' => $logType, 31 | 'data' => $originalData 32 | ]); 33 | } 34 | 35 | public static function bootLoggable() 36 | { 37 | if (config('user-activity.log_events.on_edit', false)) { 38 | self::updated(function ($model) { 39 | self::logToDb($model, 'edit'); 40 | }); 41 | } 42 | 43 | 44 | if (config('user-activity.log_events.on_delete', false)) { 45 | self::deleted(function ($model) { 46 | self::logToDb($model, 'delete'); 47 | }); 48 | } 49 | 50 | 51 | if (config('user-activity.log_events.on_create', false)) { 52 | self::created(function ($model) { 53 | self::logToDb($model, 'create'); 54 | }); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /views/index.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | User Activity 7 | 8 | 10 | 11 | 406 | 407 | 408 | 409 | 410 | 411 | 412 |
413 |
414 | user 415 | A ctivity 416 |
417 |
418 | Goto Admin Panel 419 | Doc 420 |
421 |
422 |
423 |
424 |
425 |

Showing @{{response.from}} to @{{response.to}} of @{{response.total}} records

426 |
427 | 428 |
429 |
430 | 431 | 436 | 437 |
438 | 439 |
440 |

@{{ user.name }}
441 | @{{ user.email }} 442 |

443 |
444 | 445 |
446 | 447 |
448 |
449 | 450 | 451 | 456 |
457 |
458 | 459 | 464 |
465 |
466 | 467 | 468 |
469 |
470 | 471 | 472 |
473 |
474 | 475 |
476 |
477 | 480 |
481 |
482 |
483 | 484 |
485 |
486 |
487 |
488 |
489 |
490 |
491 |
492 |
493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 509 | 522 | 523 | 527 | 530 | 531 |
IDDATELOG TYPEDONE BYACTION
@{{ log.id }} 507 | @{{ log.log_date }} - @{{log.dateHumanize}} 508 | 510 | @{{log.log_type}} 512 | 513 | @{{log.log_type}} 514 | to @{{log.table_name}} 515 | @{{log.log_type}} 516 | 517 | from @{{ log.table_name }} 519 | 520 | @{{log.log_type}} 521 | 524 | @{{ log.user.name }}
525 | @{{ log.user.email }} 526 |
528 | 529 |
532 | 533 |
534 |
543 |
544 |
545 | 546 |
547 |
548 | 555 | 556 | 652 | @include('LaravelUserActivity::partials.script') 653 |
654 | 655 | 656 | -------------------------------------------------------------------------------- /views/partials/script.blade.php: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------