first commit

This commit is contained in:
JaguarJack
2022-12-05 23:01:12 +08:00
commit 0024080c28
322 changed files with 27698 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
<?php
namespace Modules\User\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Http\Request;
use Illuminate\Queue\SerializesModels;
class Login
{
use Dispatchable, InteractsWithSockets, SerializesModels;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct(
public Request $request,
public string $token
) {
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Modules\User\Http\Controllers;
use Catch\Base\CatchController as Controller;
use Catch\Exceptions\FailedException;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Event;
use Modules\User\Events\Login;
class AuthController extends Controller
{
/**
* @param Request $request
* @return array
*/
public function login(Request $request)
{
$token = Auth::guard(getGuardName())->attempt($request->only(['email', 'password']));
Event::dispatch(new Login($request, $token));
if (! $token) {
throw new FailedException('登录失败!请检查邮箱或者密码');
}
return compact('token');
}
/**
* logout
*
* @return bool
*/
public function logout()
{
// Auth::guard(Helper::getGuardName())->logout();
return true;
}
}

View File

@@ -0,0 +1,113 @@
<?php
namespace Modules\User\Http\Controllers;
use Catch\Base\CatchController as Controller;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Http\Request;
use Modules\User\Models\LogLogin;
use Modules\User\Models\Users;
class UserController extends Controller
{
public function __construct(
protected readonly Users $user
) {
}
/**
* get list
*
* @return mixed
*/
public function index()
{
return $this->user->getList();
}
/**
* store
*
* @param Request $request
* @return false|mixed
*/
public function store(Request $request)
{
return $this->user->storeBy($request->all());
}
/**
* show
*
* @param $id
* @return mixed
*/
public function show($id)
{
return $this->user->firstBy($id)->makeHidden('password');
}
/**
* update
*
* @param $id
* @param Request $request
* @return mixed
*/
public function update($id, Request $request)
{
return $this->user->updateBy($id, $request->all());
}
/**
* destroy
*
* @param $id
* @return bool|null
*/
public function destroy($id)
{
return $this->user->deleteBy($id);
}
/**
* enable
*
* @param $id
* @return bool
*/
public function enable($id)
{
return $this->user->disOrEnable($id);
}
/**
* online user
*
* @return Authenticatable
*/
public function online(Request $request)
{
/* @var Users $user */
$user = $this->getLoginUser();
if ($request->isMethod('post')) {
return $user->updateBy($user->id, $request->all());
}
return $user;
}
/**
* login log
* @param LogLogin $logLogin
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
* @throws \Psr\Container\ContainerExceptionInterface
* @throws \Psr\Container\NotFoundExceptionInterface
*/
public function loginLog(LogLogin $logLogin)
{
return $logLogin->getUserLogBy($this->getLoginUser()->email);
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace Modules\User\Listeners;
use Catch\Enums\Status;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Str;
use Illuminate\Support\Stringable;
use Modules\User\Events\Login as Event;
use Modules\User\Models\LogLogin;
use Modules\User\Models\Users;
class Login
{
/**
* Handle the event.
*
* @param Event $event
* @return void
*/
public function handle(Event $event): void
{
$request = $event->request;
$this->log($request, (bool) $event->token);
if ($event->token) {
/* @var Users $user */
$user = Auth::guard(getGuardName())->user();
$user->login_ip = $request->ip();
$user->login_at = time();
$user->remember_token = $event->token;
$user->save();
}
}
/**
* login log
*
* @param Request $request
* @param int $isSuccess
* @return void
*/
protected function log(Request $request, int $isSuccess): void
{
LogLogin::insert([
'account' => $request->get('email'),
'login_ip' => $request->ip(),
'browser' => $this->getBrowserFrom(Str::of($request->userAgent())),
'platform' => $this->getPlatformFrom(Str::of($request->userAgent())),
'login_at' => time(),
'status' => $isSuccess ? Status::Enable : Status::Disable
]);
}
/**
* get platform
*
* @param Stringable $userAgent
* @return string
*/
protected function getBrowserFrom(Stringable $userAgent): string
{
return match (true) {
$userAgent->contains('MSIE', true) => 'IE',
$userAgent->contains('Firefox', true) => 'Firefox',
$userAgent->contains('Chrome', true) => 'Chrome',
$userAgent->contains('Opera', true) => 'Opera',
$userAgent->contains('Safari', true) => 'Safari',
default => 'unknown'
};
}
/**
* get os name
*
* @param Stringable $userAgent
* @return string
*/
protected function getPlatformFrom(Stringable $userAgent): string
{
return match (true) {
$userAgent->contains('win', true) => 'Windows',
$userAgent->contains('mac', true) => 'Mac OS',
$userAgent->contains('linux', true) => 'Linux',
$userAgent->contains('iphone', true) => 'iphone',
$userAgent->contains('android', true) => 'Android',
default => 'unknown'
};
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Modules\User\Models;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Model;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class LogLogin extends Model
{
protected $table = 'log_login';
public $timestamps = false;
protected $fillable = [
'id', 'account', 'login_ip', 'browser', 'platform', 'login_at', 'status',
];
protected $casts = [
'login_at' => 'datetime:Y-m-d H:i'
];
/**
*
* @param string $email
* @return LengthAwarePaginator
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function getUserLogBy(string $email): LengthAwarePaginator
{
return self::query()->where('account', $email)
->paginate(request()->get('limit', 10));
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace Modules\User\Models;
use Catch\Base\CatchModel as Model;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Tymon\JWTAuth\Contracts\JWTSubject;
use Illuminate\Auth\Authenticatable;
/**
* @property int $id
* @property string $username
* @property string $email
* @property string $avatar
* @property string $password
* @property int $creator_id
* @property int $status
* @property string $login_ip
* @property int $login_at
* @property int $created_at
* @property int $updated_at
* @property string $remember_token
*/
class Users extends Model implements AuthenticatableContract, JWTSubject
{
use Authenticatable;
protected $fillable = [
'id', 'username', 'email', 'avatar', 'password', 'remember_token', 'creator_id', 'status', 'login_ip', 'login_at', 'created_at', 'updated_at', 'deleted_at'
];
/**
* @var array|string[]
*/
public array $searchable = [
'username' => 'like',
'email' => 'like',
'status' => '='
];
/**
* @var string
*/
protected $table = 'users';
/**
* @var array|string[]
*/
protected array $form = ['username', 'email', 'password'];
/**
*
* @return mixed
*/
public function getJWTIdentifier(): mixed
{
return $this->getKey();
}
/**
* Return a key value array, containing any custom claims to be added to the JWT.
*
* @return array
*/
public function getJWTCustomClaims(): array
{
return [];
}
/**
* password
*
* @return Attribute
*/
protected function password(): Attribute
{
return new Attribute(
// get: fn($value) => '',
set: fn ($value) => bcrypt($value),
);
}
/**
* update
* @param $id
* @param array $data
* @return mixed
*/
public function updateBy($id, array $data): mixed
{
if (isset($data['password']) && ! $data['password']) {
unset($data['password']);
}
return parent::updateBy($id, $data);
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Modules\User\Providers;
use Catch\CatchAdmin;
use Catch\Providers\CatchModuleServiceProvider;
use Modules\User\Events\Login;
use Modules\User\Listeners\Login as LoginListener;
class UserServiceProvider extends CatchModuleServiceProvider
{
protected array $events = [
Login::class => LoginListener::class
];
/**
* route path
*
* @return string|array
*/
public function routePath(): string|array
{
// TODO: Implement path() method.
return CatchAdmin::getModuleRoutePath('user');
}
public function registerEvents(array $events): void
{
parent::registerEvents($events); // TODO: Change the autogenerated stub
}
}

View File

@@ -0,0 +1,54 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class () extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->increments('id');
$table->string('username')->comment('昵称');
$table->string('password')->comment('密码');
$table->string('email')->comment('邮箱');
$table->string('avatar')->comment('头像');
$table->string('remember_token', 1000)->comment('token');
$table->integer('creator_id');
$table->status();
$table->string('login_ip')->comment('登录IP');
$table->integer('login_at')->comment('登录时间');
$table->createdAt();
$table->updatedAt();
$table->deletedAt();
$table->comment('用户表');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
}
};

View File

@@ -0,0 +1,40 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class () extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::create('log_login', function (Blueprint $table) {
$table->increments('id');
$table->string('account')->comment('登录账户');
$table->string('login_ip')->comment('登录的IP');
$table->string('browser')->comment('浏览器');
$table->string('platform')->comment('平台');
$table->integer('login_at')->comment('平台');
$table->status();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
}
};

15
modules/User/route.php Normal file
View File

@@ -0,0 +1,15 @@
<?php
use Illuminate\Support\Facades\Route;
use Modules\User\Http\Controllers\AuthController;
use Modules\User\Http\Controllers\UserController;
// login route
Route::post('login', [AuthController::class, 'login'])->withoutMiddleware(config('catch.route.middlewares'));
Route::post('logout', [AuthController::class, 'logout']);
// users route
Route::apiResource('users', UserController::class);
Route::put('users/enable/{id}', [UserController::class, 'enable']);
Route::match(['post', 'get'], 'user/online', [UserController::class, 'online']);
Route::get('user/login/log', [UserController::class, 'loginLog']);

View File

@@ -0,0 +1,26 @@
import { RouteRecordRaw } from 'vue-router'
// @ts-ignore
const router: RouteRecordRaw[] = [
{
path: '/users',
component: () => import('/admin/layout/index.vue'),
meta: { title: '用户管理', icon: 'user' },
children: [
{
path: 'index',
name: 'users',
meta: { title: '账号管理', icon: 'home' },
component: () => import('./user/index.vue'),
},
{
path: 'center',
name: 'center',
meta: { title: '个人中心', icon: 'home' },
component: () => import('./user/center.vue'),
},
],
},
]
export default router

View File

@@ -0,0 +1,44 @@
<template>
<div class="flex flex-col sm:flex-row dark:bg-regal-dark w-full">
<el-card shadow="never" class="w-full sm:w-[35rem]">
<template #header>
<div class="card-header">
<span>个人资料</span>
</div>
</template>
<div class="flex flex-col w-full">
<div class="w-full">
<Profile />
</div>
</div>
</el-card>
<el-tabs v-model="activeName" class="pl-3 pr-3 bg-white dark:bg-regal-dark mt-2 sm:mt-0 w-full ml-2">
<el-tab-pane label="登录日志" name="login_log">
<LoginLog />
</el-tab-pane>
<el-tab-pane label="操作日志" name="operation_log">
<OperateLog />
</el-tab-pane>
</el-tabs>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import Profile from './components/profile.vue'
import LoginLog from './components/loginLog.vue'
import OperateLog from './components/operateLog.vue'
const activeName = ref('login_log')
</script>
<style scoped>
.el-tabs {
--el-tabs-header-height: 62px !important;
}
.el-tabs .el-tabs__item {
font-size: 18px !important;
}
</style>

View File

@@ -0,0 +1,45 @@
<template>
<el-table :data="tableData" class="mt-3" v-loading="loading">
<el-table-column prop="account" label="账户" width="150px" />
<el-table-column prop="browser" label="浏览器" width="100px" />
<el-table-column prop="platform" label="平台" width="100px" />
<el-table-column prop="login_ip" label="IP" width="120px" />
<el-table-column prop="status" label="状态" width="100px">
<template #default="scope">
<el-tag type="success" v-if="scope.row.status === 1">成功</el-tag>
<el-tag type="danger" v-else>失败</el-tag>
</template>
</el-table-column>
<el-table-column prop="login_at" label="登录时间" />
</el-table>
<div class="pt-2 pb-2 flex justify-end">
<el-pagination
background
v-if="total > query.limit"
layout="total,sizes,prev, pager,next"
:current-page="query.page"
:page-size="query.limit"
@current-change="changePage"
@size-change="changeLimit"
:total="total"
:page-sizes="[10, 20, 30, 50]"
/>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted } from 'vue'
import { useGetList } from '/admin/composables/curd/useGetList'
const api = 'user/login/log'
const { data, query, search, changePage, changeLimit, loading } = useGetList(api)
onMounted(() => search())
const tableData = computed(() => data.value?.data)
const total = computed(() => data.value?.total)
</script>
<style scoped></style>

View File

@@ -0,0 +1,43 @@
<template>
<el-table :data="tableData" class="mt-3" v-loading="loading">
<el-table-column prop="username" label="用户名" width="180" />
<el-table-column prop="avatar" label="头像" width="180" />
<el-table-column prop="email" label="邮箱" />
<el-table-column prop="status" label="状态">
<template #default="scope">
<Status v-model="scope.row.status" :id="scope.row.id" :api="api" />
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" />
</el-table>
<div class="pt-2 pb-2 flex justify-end">
<el-pagination
background
v-if="total > query.limit"
layout="total,sizes,prev, pager,next"
:current-page="query.page"
:page-size="query.limit"
@current-change="changePage"
@size-change="changeLimit"
:total="total"
:page-sizes="[10, 20, 30, 50]"
/>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted } from 'vue'
import { useGetList } from '/admin/composables/curd/useGetList'
const api = 'users'
const { data, query, search, reset, changePage, changeLimit, loading } = useGetList(api)
onMounted(() => search())
const tableData = computed(() => data.value?.data)
const total = computed(() => data.value?.total)
</script>
<style scoped></style>

View File

@@ -0,0 +1,88 @@
<template>
<el-form :model="profile" ref="form" v-loading="loading" label-position="top">
<el-upload
class="w-28 h-28 rounded-full mx-auto"
action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15"
:show-file-list="false"
:on-success="handleAvatarSuccess"
:before-upload="beforeAvatarUpload"
>
<img src="https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg" class="h-28 rounded-full" />
</el-upload>
<el-form-item
label="昵称"
prop="username"
class="mt-2"
:rules="[
{
required: true,
message: '昵称必须填写',
},
]"
>
<el-input v-model="profile.username" placeholder="请填写昵称" />
</el-form-item>
<el-form-item
label="邮箱"
prop="email"
:rules="[
{
required: true,
message: '邮箱必须填写',
},
{
type: 'email',
message: '邮箱格式不正确',
},
]"
>
<el-input v-model="profile.email" placeholder="请填写邮箱" />
</el-form-item>
<el-form-item
label="密码"
prop="password"
:rules="[
{
pattern: /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{6,20}$/,
message: '必须包含大小写字母和数字的组合可以使用特殊字符长度在6-20之间',
},
]"
>
<el-input v-model="profile.password" type="password" show-password placeholder="请输入密码" />
</el-form-item>
<div class="flex justify-center">
<el-button type="primary" @click="submitForm(form)">{{ $t('system.update') }}</el-button>
</div>
</el-form>
</template>
<script lang="ts" setup>
import { onMounted, ref, unref } from 'vue'
import { useCreate } from '/admin/composables/curd/useCreate'
import http from '/admin/support/http'
interface profile {
avatar: string
username: string
email: string
password: string
}
const profile = ref<profile>(
Object.assign({
avatar: '',
username: '',
email: '',
password: '',
}),
)
onMounted(() => {
http.get('user/online').then(r => {
profile.value.username = r.data.data.username
profile.value.avatar = r.data.data.avatar
profile.value.email = r.data.data.email
})
})
const { form, loading, submitForm } = useCreate('user/online', null, profile)
</script>

View File

@@ -0,0 +1,82 @@
<template>
<el-form :model="formData" label-width="120px" ref="form" v-loading="loading" class="pr-4">
<el-form-item
label="昵称"
prop="username"
:rules="[
{
required: true,
message: '昵称必须填写',
},
]"
>
<el-input v-model="formData.username" placeholder="请填写昵称" />
</el-form-item>
<el-form-item
label="邮箱"
prop="email"
:rules="[
{
required: true,
message: '邮箱必须填写',
},
{
type: 'email',
message: '邮箱格式不正确',
},
]"
>
<el-input v-model="formData.email" placeholder="请填写邮箱" />
</el-form-item>
<el-form-item label="密码" prop="password" :rules="passwordRules">
<el-input v-model="formData.password" type="password" show-password placeholder="请输入密码" />
</el-form-item>
<div class="flex justify-end">
<el-button type="primary" @click="submitForm(form)">{{ $t('system.confirm') }}</el-button>
</div>
</el-form>
</template>
<script lang="ts" setup>
import { useCreate } from '/admin/composables/curd/useCreate'
import { useShow } from '/admin/composables/curd/useShow'
import { onMounted, watch, ref } from 'vue'
const props = defineProps({
primary: String | Number,
api: String,
})
const passwordRules = [
{
required: true,
message: '密码必须填写',
},
{
pattern: /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{6,20}$/,
message: '必须包含大小写字母和数字的组合可以使用特殊字符长度在6-20之间',
},
]
if (props.primary) {
passwordRules.shift()
}
const { formData, form, loading, submitForm, isClose } = useCreate(props.api, props.primary)
const emit = defineEmits(['close'])
watch(isClose, function (value) {
if (value) {
emit('close')
}
})
onMounted(() => {
if (props.primary) {
useShow(props.api, props.primary).then(r => {
formData.value = r.data
})
}
})
</script>

View File

@@ -0,0 +1,104 @@
<template>
<div>
<div class="w-full min-h-0 bg-white dark:bg-regal-dark pl-5 pt-5 pr-5 rounded-lg">
<el-form :inline="true">
<el-form-item label="用户名">
<el-input v-model="query.username" clearable />
</el-form-item>
<el-form-item label="邮箱">
<el-input v-model="query.email" clearable />
</el-form-item>
<el-form-item label="状态">
<Select v-model="query.status" clearable api="status" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="search()">
<Icon name="magnifying-glass" class="w-4 mr-1 -ml-1" />
搜索
</el-button>
<el-button @click="reset()">
<Icon name="arrow-path" class="w-4 mr-1 -ml-1" />
重置
</el-button>
</el-form-item>
</el-form>
</div>
<div class="pl-2 pr-2 bg-white dark:bg-regal-dark rounded-lg mt-4">
<div class="pt-5 pl-2">
<Add @click="show(null)" />
</div>
<el-table :data="tableData" class="mt-3" v-loading="loading">
<el-table-column prop="username" label="用户名" width="180" />
<el-table-column prop="avatar" label="头像" width="180" />
<el-table-column prop="email" label="邮箱" />
<el-table-column prop="status" label="状态">
<template #default="scope">
<Status v-model="scope.row.status" :id="scope.row.id" :api="api" />
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间" />
<el-table-column label="操作" width="200">
<template #default="scope">
<Update @click="show(scope.row.id)" />
<Destroy @click="destroy(api, scope.row.id)" />
</template>
</el-table-column>
</el-table>
<div class="pt-2 pb-2 flex justify-end">
<el-pagination
background
layout="total,sizes,prev, pager,next"
:current-page="query.page"
:page-size="query.limit"
@current-change="changePage"
@size-change="changeLimit"
:total="total"
:page-sizes="[10, 20, 30, 50]"
/>
</div>
</div>
<Dialog v-model="visible" :title="title" destroy-on-close>
<Create @close="close" :primary="id" :api="api" />
</Dialog>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, ref, watch } from 'vue'
import Create from './create.vue'
import { useGetList } from '/admin/composables/curd/useGetList'
import { useDestroy } from '/admin/composables/curd/useDestroy'
import { useEnabled } from '/admin/composables/curd/useEnabled'
import { t } from '/admin/support/helper'
const visible = ref<boolean>(false)
const id = ref(null)
const api = 'users'
const title = ref<string>('')
const { data, query, search, reset, changePage, changeLimit, loading } = useGetList(api)
const { destroy, isDeleted } = useDestroy()
onMounted(() => search())
const tableData = computed(() => data.value?.data)
const total = computed(() => data.value?.total)
const close = () => {
visible.value = false
reset()
}
const show = primary => {
title.value = primary ? t('system.edit') : t('system.add')
id.value = primary
visible.value = true
}
watch(isDeleted, function () {
isDeleted.value = false
reset()
})
</script>