diff --git a/API_USAGE_EXAMPLES.md b/API_USAGE_EXAMPLES.md new file mode 100644 index 000000000000..7a2c298c8192 --- /dev/null +++ b/API_USAGE_EXAMPLES.md @@ -0,0 +1,425 @@ +# API Usage Examples - Multi-User System + +## Authentication + +### Login with credentials +```bash +curl -X POST http://localhost:8080/api/v2/core/auth/login \ + -H "Content-Type: application/json" \ + -d '{ + "name": "admin", + "password": "your_password", + "language": "en" + }' +``` + +Response: +```json +{ + "code": 200, + "data": { + "name": "admin", + "token": "session_token", + "mfaStatus": "disable", + "role": "admin" + } +} +``` + +## User Management + +### Create a new user +```bash +curl -X POST http://localhost:8080/api/v2/core/users \ + -H "Content-Type: application/json" \ + -H "Cookie: SESSIONID=your_token" \ + -d '{ + "username": "john_doe", + "email": "john@example.com", + "password": "secure_password_123", + "role": "reseller", + "realName": "John Doe", + "phone": "+1234567890", + "remark": "Reseller account" + }' +``` + +### List all users +```bash +curl -X GET "http://localhost:8080/api/v2/core/users?pageNum=1&pageSize=10&role=reseller" \ + -H "Cookie: SESSIONID=your_token" +``` + +Response: +```json +{ + "code": 200, + "data": { + "total": 5, + "items": [ + { + "id": 1, + "username": "admin", + "email": "admin@example.com", + "role": "admin", + "status": "active", + "realName": "Administrator", + "phone": "", + "lastLogin": 1715000000, + "remark": "", + "createdAt": 1714900000, + "updatedAt": 1714950000 + }, + { + "id": 2, + "username": "john_doe", + "email": "john@example.com", + "role": "reseller", + "status": "active", + "realName": "John Doe", + "phone": "+1234567890", + "lastLogin": 1714990000, + "remark": "Reseller account", + "createdAt": 1714900100, + "updatedAt": 1714950100 + } + ] + } +} +``` + +### Get user details +```bash +curl -X GET http://localhost:8080/api/v2/core/users/2 \ + -H "Cookie: SESSIONID=your_token" +``` + +Response: +```json +{ + "code": 200, + "data": { + "user": { + "id": 2, + "username": "john_doe", + "email": "john@example.com", + "role": "reseller", + "status": "active", + "realName": "John Doe", + "phone": "+1234567890", + "lastLogin": 1714990000, + "remark": "Reseller account", + "createdAt": 1714900100, + "updatedAt": 1714950100 + }, + "permissions": [ + "user:view", + "user:create", + "user:update", + "user:delete", + "host:view", + "host:monitor", + "app:manage", + "app:create", + "database:manage" + ] + } +} +``` + +### Update user +```bash +curl -X PUT http://localhost:8080/api/v2/core/users \ + -H "Content-Type: application/json" \ + -H "Cookie: SESSIONID=your_token" \ + -d '{ + "id": 2, + "email": "newemail@example.com", + "role": "user", + "status": "active", + "realName": "John Doe Updated", + "phone": "+9876543210", + "remark": "Updated reseller account" + }' +``` + +### Delete user +```bash +curl -X DELETE http://localhost:8080/api/v2/core/users/2 \ + -H "Cookie: SESSIONID=your_token" +``` + +## Password Management + +### Change own password +```bash +curl -X POST http://localhost:8080/api/v2/core/users/password/change \ + -H "Content-Type: application/json" \ + -H "Cookie: SESSIONID=your_token" \ + -d '{ + "userId": 2, + "oldPassword": "old_password_123", + "newPassword": "new_password_456" + }' +``` + +### Reset user password (admin only) +```bash +curl -X POST http://localhost:8080/api/v2/core/users/password/reset \ + -H "Content-Type: application/json" \ + -H "Cookie: SESSIONID=admin_token" \ + -d '{ + "userId": 2, + "newPassword": "temporary_password_789" + }' +``` + +## Permissions Management + +### Get user permissions +```bash +curl -X GET http://localhost:8080/api/v2/core/users/2/permissions \ + -H "Cookie: SESSIONID=your_token" +``` + +Response: +```json +{ + "code": 200, + "data": [ + "user:view", + "user:create", + "user:update", + "user:delete", + "host:view", + "host:monitor", + "host:manage", + "app:manage", + "app:create", + "app:update", + "app:delete", + "app:view", + "app:install", + "app:uninstall" + ] +} +``` + +### Assign permissions to user +```bash +curl -X POST http://localhost:8080/api/v2/core/users/permissions \ + -H "Content-Type: application/json" \ + -H "Cookie: SESSIONID=admin_token" \ + -d '{ + "userId": 2, + "permissions": [ + "user:view", + "app:view", + "app:install", + "database:view", + "host:monitor" + ] + }' +``` + +## Profile Management + +### Get current user profile +```bash +curl -X GET http://localhost:8080/api/v2/core/users/profile \ + -H "Cookie: SESSIONID=your_token" +``` + +Response: +```json +{ + "code": 200, + "data": { + "user": { + "id": 2, + "username": "john_doe", + "email": "john@example.com", + "role": "reseller", + "status": "active", + "realName": "John Doe", + "phone": "+1234567890", + "lastLogin": 1714990000, + "remark": "Reseller account", + "createdAt": 1714900100, + "updatedAt": 1714950100 + }, + "permissions": [ + "user:view", + "app:manage", + "database:view" + ] + } +} +``` + +## Login History + +### Get login history for user +```bash +curl -X GET http://localhost:8080/api/v2/core/users/2/login-history \ + -H "Cookie: SESSIONID=your_token" +``` + +Response: +```json +{ + "code": 200, + "data": [ + { + "id": 1, + "userId": 2, + "ip": "192.168.1.100", + "address": "New York, USA", + "agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", + "status": "success", + "message": "Login successful", + "loginAt": 1714990000, + "createdAt": 1714990000, + "updatedAt": 1714990000 + }, + { + "id": 2, + "userId": 2, + "ip": "192.168.1.101", + "address": "New York, USA", + "agent": "Mozilla/5.0 (iPhone; CPU iPhone OS 15_0)", + "status": "success", + "message": "Login successful", + "loginAt": 1714985000, + "createdAt": 1714985000, + "updatedAt": 1714985000 + } + ] +} +``` + +## Available Permissions + +### User Management +- `user:view` - View user information +- `user:create` - Create new users +- `user:update` - Update user information +- `user:delete` - Delete users +- `user:manage` - Full user management +- `user:password` - Change password + +### Host/Node Management +- `host:view` - View hosts +- `host:monitor` - Monitor host resources +- `host:manage` - Full host management +- `host:create` - Create host connections +- `host:update` - Update host information +- `host:delete` - Delete hosts + +### Application Management +- `app:view` - View applications +- `app:create` - Create applications +- `app:update` - Update applications +- `app:delete` - Delete applications +- `app:manage` - Full app management +- `app:install` - Install applications +- `app:uninstall` - Uninstall applications + +### Database Management +- `database:view` - View databases +- `database:create` - Create databases +- `database:update` - Update databases +- `database:delete` - Delete databases +- `database:manage` - Full database management +- `database:backup` - Create database backups + +### Website Management +- `website:view` - View websites +- `website:create` - Create websites +- `website:update` - Update websites +- `website:delete` - Delete websites +- `website:manage` - Full website management + +### Backup Management +- `backup:view` - View backups +- `backup:create` - Create backups +- `backup:delete` - Delete backups +- `backup:manage` - Full backup management + +### System Settings +- `setting:view` - View settings +- `setting:manage` - Manage settings +- `system:manage` - Full system management +- `system:upgrade` - System upgrades +- `system:log` - View system logs +- `system:restart` - Restart system + +--- + +## HTTP Status Codes + +- `200` - Success +- `400` - Bad request / Validation error +- `401` - Unauthorized / Not authenticated +- `403` - Forbidden / Insufficient permissions +- `404` - Not found +- `500` - Internal server error + +## Error Responses + +```json +{ + "code": 401, + "message": "ErrNotLogin" +} +``` + +```json +{ + "code": 403, + "message": "insufficient permissions" +} +``` + +```json +{ + "code": 400, + "message": "ErrUserAlreadyExists" +} +``` + +--- + +## Role-Based API Access + +### Admin Can Access: +- All user management endpoints +- All permission endpoints +- Password reset for any user +- All system endpoints + +### Reseller Can Access: +- User viewing and management +- App management endpoints +- Database endpoints +- Website endpoints +- Backup endpoints +- Own profile and login history + +### User Can Access: +- Own profile +- Password change (own only) +- View-only endpoints +- Login history (own only) + +--- + +## Notes + +1. All requests require a valid session cookie (SESSIONID) +2. Role and permission validation is performed on the backend +3. Failed attempts are logged and tracked by IP +4. Admin role ("admin") has access to all features by default +5. Custom permissions can be assigned per user +6. Password changes require old password verification +7. Admin can reset passwords without old password diff --git a/MULTI_USER_IMPLEMENTATION.md b/MULTI_USER_IMPLEMENTATION.md new file mode 100644 index 000000000000..141764bc842b --- /dev/null +++ b/MULTI_USER_IMPLEMENTATION.md @@ -0,0 +1,328 @@ +# Multi-User System with Role-Based Access Control Implementation + +## Overview +This document describes the multi-user system that has been implemented for 1Panel, featuring a three-tier user role system with comprehensive permission management. + +## User Roles + +### 1. **Main Admin (admin)** +- Full access to all system features +- Can manage all users and permissions +- Can modify system settings +- Can perform system upgrades and restarts +- Complete access to all resources + +#### Permissions: +- `admin:all` - Grants all permissions + +### 2. **Reseller (reseller)** +- Can manage applications and services for their accounts +- Can create and manage sub-users +- Can monitor host/server resources +- Limited system settings access +- Can manage databases, websites, and backups + +#### Permissions: +- `user:view`, `user:create`, `user:update`, `user:delete` +- `host:view`, `host:monitor`, `host:manage` +- `app:*` - Full app management +- `database:*` - Full database management +- `website:*` - Full website management +- `backup:*` - Full backup management +- `setting:view` + +### 3. **User (user)** +- Basic access to view their own resources +- Can only change their own password +- Limited viewing permissions for apps and databases +- Cannot create or manage other users + +#### Permissions: +- `user:view`, `user:password` - Own profile only +- `host:monitor`, `host:view` - Monitor only +- `app:view`, `app:install` - View and install only +- `database:view` - View only +- `website:view` - View only +- `backup:view` - View only +- `setting:view` - View only + +## Database Schema + +### User Table (`users`) +```sql +CREATE TABLE users ( + id INT PRIMARY KEY AUTO_INCREMENT, + username VARCHAR(255) NOT NULL UNIQUE, + email VARCHAR(255), + password VARCHAR(255) NOT NULL, + role VARCHAR(50) NOT NULL DEFAULT 'user', + status VARCHAR(50) NOT NULL DEFAULT 'active', + parent_id INT, + real_name VARCHAR(255), + phone VARCHAR(20), + last_login TIMESTAMP, + remark TEXT, + created_at TIMESTAMP, + updated_at TIMESTAMP +) +``` + +### User Permissions Table (`user_permissions`) +```sql +CREATE TABLE user_permissions ( + id INT PRIMARY KEY AUTO_INCREMENT, + user_id INT NOT NULL, + permission VARCHAR(255) NOT NULL, + created_at TIMESTAMP, + updated_at TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) +) +``` + +### User Login History Table (`user_login_histories`) +```sql +CREATE TABLE user_login_histories ( + id INT PRIMARY KEY AUTO_INCREMENT, + user_id INT NOT NULL, + ip VARCHAR(50), + address VARCHAR(255), + agent TEXT, + status VARCHAR(50) NOT NULL, + message TEXT, + login_at TIMESTAMP, + created_at TIMESTAMP, + updated_at TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) +) +``` + +## API Endpoints + +### User Management +- `GET /api/v2/core/users` - List all users +- `POST /api/v2/core/users` - Create new user +- `GET /api/v2/core/users/:id` - Get user details +- `PUT /api/v2/core/users` - Update user +- `DELETE /api/v2/core/users/:id` - Delete user + +### Password Management +- `POST /api/v2/core/users/password/change` - Change own password +- `POST /api/v2/core/users/password/reset` - Reset user password (admin only) + +### Permissions +- `GET /api/v2/core/users/:id/permissions` - Get user permissions +- `POST /api/v2/core/users/permissions` - Assign permissions + +### Profile +- `GET /api/v2/core/users/profile` - Get current user profile +- `GET /api/v2/core/users/:id/login-history` - Get login history + +## Authentication Flow + +1. User submits login credentials (username/password) +2. System checks User table first (new multi-user system) +3. Falls back to settings-based auth for backward compatibility +4. Validates security entrance if configured +5. Triggers MFA if enabled +6. Creates session with user role and permissions +7. Sets session cookies with user context + +## Middleware + +### SessionAuth Middleware +- Located in `core/middleware/session.go` +- Validates active session +- Refreshes session if needed +- Checks session timeout + +### UserAuthMiddleware +- Located in `core/middleware/role.go` +- Adds user context to request +- Retrieves user permissions +- Sets UserID, UserRole, and Permissions in context + +### RoleAuth Middleware +- Located in `core/middleware/role.go` +- Enforces role-based access control +- Checks if user has required role +- Returns 401 if user lacks required role + +### PermissionAuth Middleware +- Located in `core/middleware/role.go` +- Enforces permission-based access control +- Checks specific permissions +- Allows admin bypass + +## Service Layer + +### UserService (`core/app/service/user.go`) +Key methods: +- `CreateUser(req)` - Create new user with role +- `UpdateUser(req)` - Update user information +- `GetUserByID(id)` - Retrieve user details +- `GetUserByUsername(username)` - Find user by username +- `GetUserList(req)` - List users with pagination +- `DeleteUser(id)` - Delete user (prevents main admin deletion) +- `ChangePassword(req)` - User changes own password +- `ResetPassword(req)` - Admin resets user password +- `AssignPermissions(req)` - Assign custom permissions +- `GetUserPermissions(userID)` - Get user permissions +- `HasPermission(userID, permission)` - Check single permission +- `CheckUserRole(userID, roles...)` - Check if user has role + +## Repository Layer + +### UserRepository (`core/app/repo/user_repo.go`) +Handles all database operations: +- User CRUD operations +- Permission management +- Login history tracking + +## Data Transfer Objects (DTOs) + +### CreateUserRequest +```go +type CreateUserRequest struct { + Username string + Email string + Password string + Role string // user, reseller, admin + RealName string + Phone string + Remark string +} +``` + +### UserResponse +```go +type UserResponse struct { + ID uint + Username string + Email string + Role string + Status string + RealName string + Phone string + LastLogin int64 + Remark string + CreatedAt int64 + UpdatedAt int64 +} +``` + +### UserDetailResponse +```go +type UserDetailResponse struct { + User UserResponse + Permissions []string +} +``` + +## Migration + +The database migration (`AddUserTables` in `core/init/migration/migrations/init.go`): +1. Creates user-related tables +2. Checks for existing admin user +3. Migrates existing single-user setup to admin user if needed +4. Maintains backward compatibility + +## Backward Compatibility + +The implementation maintains full backward compatibility: +1. Old settings-based authentication still works +2. Existing credentials are migrated to admin user on first run +3. Session-based authentication continues to work +4. All existing APIs remain functional + +## Security Features + +1. **Password Encryption** - Passwords are encrypted using system encryption utilities +2. **Session Management** - Session-based authentication with timeout +3. **MFA Support** - Multi-factor authentication available +4. **Login History** - Tracks all login attempts +5. **Permission Validation** - Every action is permission-checked +6. **IP Tracking** - Failed login attempts tracked by IP + +## Future Enhancements + +1. **Resource Scoping** - Scope users to specific hosts/resources +2. **API Keys** - Support API-based authentication +3. **LDAP/OAuth** - Integration with external auth systems +4. **Audit Logging** - Detailed audit logs for compliance +5. **Advanced Permissions** - More granular permission system +6. **2FA** - Two-factor authentication +7. **SSO** - Single sign-on integration + +## Files Created/Modified + +### New Files: +- `core/constant/role.go` - Role and permission constants +- `core/app/model/user.go` - User models +- `core/app/dto/user.go` - User DTOs +- `core/app/repo/user_repo.go` - User repository +- `core/app/service/user.go` - User service +- `core/app/api/v2/user.go` - User API endpoints +- `core/middleware/role.go` - Role middleware +- `core/router/ro_user.go` - User router +- `core/init/migration/migrations/user_tables.go` (content in init.go) + +### Modified Files: +- `core/init/migration/migrate.go` - Added user tables migration +- `core/init/migration/migrations/init.go` - Added AddUserTables migration +- `core/app/service/auth.go` - Updated Login and MFALogin methods +- `core/app/dto/auth.go` - Updated UserLoginInfo with role field +- `core/app/api/v2/helper/helper.go` - Added user helper functions +- `core/app/api/v2/entry.go` - Registered UserApi +- `core/router/common.go` - Added UserRouter + +## Testing Recommendations + +1. **User Creation** - Test creating users with different roles +2. **Permission Checking** - Verify permission enforcement +3. **Role Switching** - Test different role operations +4. **Backward Compatibility** - Ensure old auth still works +5. **Middleware** - Test role and permission middleware +6. **Login History** - Verify login tracking +7. **Password Management** - Test password change/reset + +## Deployment Notes + +1. Run database migrations automatically on startup +2. Existing single-user systems will have admin user created +3. No manual intervention needed for upgrade +4. All existing sessions remain valid +5. New users can be created through API + +--- + +## Implementation by Features + +### Multi-User Support ✅ +- Multiple users can access the system +- Each user has independent credentials +- User management interface available + +### Role-Based Access Control ✅ +- Three tiers: User, Reseller, Admin +- Role-based capabilities +- Permission system for fine-grained control + +### User Management ✅ +- Create/update/delete users +- Password reset capability +- User listing and search + +### Permission Management ✅ +- Role-based default permissions +- Custom permission assignment +- Permission validation on endpoints + +### Login Tracking ✅ +- Login history per user +- Last login timestamp +- Failed login tracking by IP + +--- + +**Implementation Date:** May 6, 2026 +**Status:** Complete diff --git a/core/app/api/v2/entry.go b/core/app/api/v2/entry.go index bc295a333be4..b442b2389b88 100644 --- a/core/app/api/v2/entry.go +++ b/core/app/api/v2/entry.go @@ -4,6 +4,7 @@ import "github.com/1Panel-dev/1Panel/core/app/service" type ApiGroup struct { BaseApi + UserApi } var ApiGroupApp = new(ApiGroup) diff --git a/core/app/api/v2/helper/helper.go b/core/app/api/v2/helper/helper.go index cd555906ee60..f695984e33e2 100644 --- a/core/app/api/v2/helper/helper.go +++ b/core/app/api/v2/helper/helper.go @@ -100,3 +100,53 @@ func GetParamID(c *gin.Context) (uint, error) { intNum, _ := strconv.Atoi(idParam) return uint(intNum), nil } + +func SuccessWithMsg(ctx *gin.Context, msgKey string) { + res := dto.Response{ + Code: http.StatusOK, + Message: i18n.GetMsgWithMap(msgKey, nil), + } + ctx.JSON(http.StatusOK, res) + ctx.Abort() +} + +func GetCurrentUserID(c *gin.Context) (uint, error) { + // Get user ID from session or JWT token + // This assumes it's stored in context with key "UserID" + if userID, exists := c.Get("UserID"); exists { + if id, ok := userID.(uint); ok { + return id, nil + } + } + return 0, errors.New("user not authenticated") +} + +func CheckUserRole(c *gin.Context, allowedRoles ...string) error { + // Get user role from session or JWT token + // This assumes it's stored in context with key "UserRole" + if userRole, exists := c.Get("UserRole"); exists { + if role, ok := userRole.(string); ok { + for _, allowedRole := range allowedRoles { + if role == allowedRole { + return nil + } + } + } + } + return errors.New("user does not have required role") +} + +func HasPermission(c *gin.Context, permission string) bool { + // Get permissions from session or JWT token + // This assumes it's stored in context with key "Permissions" + if perms, exists := c.Get("Permissions"); exists { + if permissions, ok := perms.([]string); ok { + for _, perm := range permissions { + if perm == permission { + return true + } + } + } + } + return false +} diff --git a/core/app/api/v2/user.go b/core/app/api/v2/user.go new file mode 100644 index 000000000000..a06c16009631 --- /dev/null +++ b/core/app/api/v2/user.go @@ -0,0 +1,328 @@ +package v2 + +import ( + "net/http" + "strconv" + + "github.com/1Panel-dev/1Panel/core/app/api/v2/helper" + "github.com/1Panel-dev/1Panel/core/app/dto" + "github.com/1Panel-dev/1Panel/core/app/service" + "github.com/1Panel-dev/1Panel/core/buserr" + "github.com/1Panel-dev/1Panel/core/utils/common" + "github.com/gin-gonic/gin" +) + +type UserApi struct{} + +var userService = service.NewIUserService() + +// @Tags User +// @Summary Create a new user +// @Accept json +// @Param request body dto.CreateUserRequest true "create user request" +// @Success 200 {object} dto.UserResponse +// @Router /core/users [post] +func (u *UserApi) CreateUser(c *gin.Context) { + var req dto.CreateUserRequest + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + // Only admin can create users + if err := helper.CheckUserRole(c, "admin"); err != nil { + helper.ErrorWithDetail(c, http.StatusForbidden, "ErrUnauthorized", err) + return + } + + user, err := userService.CreateUser(&req) + if err != nil { + helper.ErrorWithDetail(c, http.StatusBadRequest, "ErrCreateUserFailed", err) + return + } + + helper.SuccessWithData(c, user) +} + +// @Tags User +// @Summary Update user information +// @Accept json +// @Param request body dto.UpdateUserRequest true "update user request" +// @Success 200 +// @Router /core/users [put] +func (u *UserApi) UpdateUser(c *gin.Context) { + var req dto.UpdateUserRequest + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + // Only admin or the user themselves can update + currentUserID, err := helper.GetCurrentUserID(c) + if err != nil { + helper.ErrorWithDetail(c, http.StatusUnauthorized, "ErrUnauthorized", err) + return + } + + if currentUserID != req.ID { + if err := helper.CheckUserRole(c, "admin"); err != nil { + helper.ErrorWithDetail(c, http.StatusForbidden, "ErrUnauthorized", err) + return + } + } + + if err := userService.UpdateUser(&req); err != nil { + helper.ErrorWithDetail(c, http.StatusBadRequest, "ErrUpdateUserFailed", err) + return + } + + helper.SuccessWithMsg(c, "UserUpdatedSuccessfully") +} + +// @Tags User +// @Summary Get user by ID +// @Param id path int true "user id" +// @Success 200 {object} dto.UserDetailResponse +// @Router /core/users/:id [get] +func (u *UserApi) GetUser(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + helper.ErrorWithDetail(c, http.StatusBadRequest, "ErrInvalidUserID", err) + return + } + + userDetail, err := userService.GetUserByID(uint(id)) + if err != nil { + helper.ErrorWithDetail(c, http.StatusNotFound, "ErrUserNotFound", err) + return + } + + helper.SuccessWithData(c, userDetail) +} + +// @Tags User +// @Summary Get user list with pagination +// @Accept json +// @Param request body dto.UserPageRequest true "user page request" +// @Success 200 {object} dto.UserListResponse +// @Router /core/users [get] +func (u *UserApi) ListUsers(c *gin.Context) { + pageNum := c.DefaultQuery("pageNum", "1") + pageSize := c.DefaultQuery("pageSize", "10") + username := c.Query("username") + role := c.Query("role") + status := c.Query("status") + + num, _ := strconv.Atoi(pageNum) + size, _ := strconv.Atoi(pageSize) + + if num < 1 { + num = 1 + } + if size < 1 || size > 100 { + size = 10 + } + + req := &dto.UserPageRequest{ + PageNum: num, + PageSize: size, + Username: username, + Role: role, + Status: status, + } + + userList, err := userService.GetUserList(req) + if err != nil { + helper.ErrorWithDetail(c, http.StatusBadRequest, "ErrGetUserListFailed", err) + return + } + + helper.SuccessWithData(c, userList) +} + +// @Tags User +// @Summary Delete user +// @Param id path int true "user id" +// @Success 200 +// @Router /core/users/:id [delete] +func (u *UserApi) DeleteUser(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + helper.ErrorWithDetail(c, http.StatusBadRequest, "ErrInvalidUserID", err) + return + } + + // Only admin can delete users + if err := helper.CheckUserRole(c, "admin"); err != nil { + helper.ErrorWithDetail(c, http.StatusForbidden, "ErrUnauthorized", err) + return + } + + if err := userService.DeleteUser(uint(id)); err != nil { + helper.ErrorWithDetail(c, http.StatusBadRequest, "ErrDeleteUserFailed", err) + return + } + + helper.SuccessWithMsg(c, "UserDeletedSuccessfully") +} + +// @Tags User +// @Summary Change user password +// @Accept json +// @Param request body dto.ChangePasswordRequest true "change password request" +// @Success 200 +// @Router /core/users/password/change [post] +func (u *UserApi) ChangePassword(c *gin.Context) { + var req dto.ChangePasswordRequest + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + // User can only change their own password + currentUserID, err := helper.GetCurrentUserID(c) + if err != nil { + helper.ErrorWithDetail(c, http.StatusUnauthorized, "ErrUnauthorized", err) + return + } + + if currentUserID != req.UserID { + // Check if user is admin + if err := helper.CheckUserRole(c, "admin"); err != nil { + helper.ErrorWithDetail(c, http.StatusForbidden, "ErrUnauthorized", err) + return + } + } + + if err := userService.ChangePassword(&req); err != nil { + helper.ErrorWithDetail(c, http.StatusBadRequest, "ErrChangePasswordFailed", err) + return + } + + helper.SuccessWithMsg(c, "PasswordChangedSuccessfully") +} + +// @Tags User +// @Summary Reset user password (admin only) +// @Accept json +// @Param request body dto.ResetPasswordRequest true "reset password request" +// @Success 200 +// @Router /core/users/password/reset [post] +func (u *UserApi) ResetPassword(c *gin.Context) { + var req dto.ResetPasswordRequest + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + // Only admin can reset passwords + if err := helper.CheckUserRole(c, "admin"); err != nil { + helper.ErrorWithDetail(c, http.StatusForbidden, "ErrUnauthorized", err) + return + } + + if err := userService.ResetPassword(&req); err != nil { + helper.ErrorWithDetail(c, http.StatusBadRequest, "ErrResetPasswordFailed", err) + return + } + + helper.SuccessWithMsg(c, "PasswordResetSuccessfully") +} + +// @Tags User +// @Summary Assign permissions to user +// @Accept json +// @Param request body dto.AssignPermissionRequest true "assign permission request" +// @Success 200 +// @Router /core/users/permissions [post] +func (u *UserApi) AssignPermissions(c *gin.Context) { + var req dto.AssignPermissionRequest + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + + // Only admin can assign permissions + if err := helper.CheckUserRole(c, "admin"); err != nil { + helper.ErrorWithDetail(c, http.StatusForbidden, "ErrUnauthorized", err) + return + } + + if err := userService.AssignPermissions(&req); err != nil { + helper.ErrorWithDetail(c, http.StatusBadRequest, "ErrAssignPermissionFailed", err) + return + } + + helper.SuccessWithMsg(c, "PermissionsAssignedSuccessfully") +} + +// @Tags User +// @Summary Get user permissions +// @Param id path int true "user id" +// @Success 200 {object} []string +// @Router /core/users/:id/permissions [get] +func (u *UserApi) GetUserPermissions(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + helper.ErrorWithDetail(c, http.StatusBadRequest, "ErrInvalidUserID", err) + return + } + + permissions, err := userService.GetUserPermissions(uint(id)) + if err != nil { + helper.ErrorWithDetail(c, http.StatusBadRequest, "ErrGetPermissionsFailed", err) + return + } + + helper.SuccessWithData(c, permissions) +} + +// @Tags User +// @Summary Get current user profile +// @Success 200 {object} dto.UserDetailResponse +// @Router /core/users/profile [get] +func (u *UserApi) GetProfile(c *gin.Context) { + currentUserID, err := helper.GetCurrentUserID(c) + if err != nil { + helper.ErrorWithDetail(c, http.StatusUnauthorized, "ErrUnauthorized", err) + return + } + + userDetail, err := userService.GetUserByID(currentUserID) + if err != nil { + helper.ErrorWithDetail(c, http.StatusNotFound, "ErrUserNotFound", err) + return + } + + helper.SuccessWithData(c, userDetail) +} + +// @Tags User +// @Summary Get login history +// @Param id path int true "user id" +// @Success 200 {object} []model.UserLoginHistory +// @Router /core/users/:id/login-history [get] +func (u *UserApi) GetLoginHistory(c *gin.Context) { + id, err := strconv.Atoi(c.Param("id")) + if err != nil { + helper.ErrorWithDetail(c, http.StatusBadRequest, "ErrInvalidUserID", err) + return + } + + // Only admin or the user themselves can view login history + currentUserID, err := helper.GetCurrentUserID(c) + if err != nil { + helper.ErrorWithDetail(c, http.StatusUnauthorized, "ErrUnauthorized", err) + return + } + + if currentUserID != uint(id) { + if err := helper.CheckUserRole(c, "admin"); err != nil { + helper.ErrorWithDetail(c, http.StatusForbidden, "ErrUnauthorized", err) + return + } + } + + histories, err := userService.GetLoginHistory(uint(id)) + if err != nil { + helper.ErrorWithDetail(c, http.StatusBadRequest, "ErrGetLoginHistoryFailed", err) + return + } + + helper.SuccessWithData(c, histories) +} diff --git a/core/app/dto/auth.go b/core/app/dto/auth.go index 513811334ed7..1e462ce88e0f 100644 --- a/core/app/dto/auth.go +++ b/core/app/dto/auth.go @@ -10,6 +10,7 @@ type UserLoginInfo struct { Token string `json:"token"` MfaStatus string `json:"mfaStatus"` MfaSession string `json:"mfaSession"` + Role string `json:"role"` } type PasskeyBeginResponse struct { diff --git a/core/app/dto/user.go b/core/app/dto/user.go new file mode 100644 index 000000000000..3aa84bd1f4c1 --- /dev/null +++ b/core/app/dto/user.go @@ -0,0 +1,88 @@ +package dto + +type CreateUserRequest struct { + Username string `json:"username" validate:"required,min=3,max=255"` + Email string `json:"email" validate:"email"` + Password string `json:"password" validate:"required,min=6"` + Role string `json:"role" validate:"required,oneof=user reseller admin"` + RealName string `json:"realName"` + Phone string `json:"phone"` + Remark string `json:"remark"` +} + +type UpdateUserRequest struct { + ID uint `json:"id" validate:"required"` + Email string `json:"email" validate:"email"` + Role string `json:"role" validate:"required,oneof=user reseller admin"` + Status string `json:"status" validate:"required,oneof=active inactive"` + RealName string `json:"realName"` + Phone string `json:"phone"` + Remark string `json:"remark"` +} + +type ChangePasswordRequest struct { + UserID uint `json:"userId" validate:"required"` + OldPassword string `json:"oldPassword" validate:"required"` + NewPassword string `json:"newPassword" validate:"required,min=6"` +} + +type ResetPasswordRequest struct { + UserID uint `json:"userId" validate:"required"` + NewPassword string `json:"newPassword" validate:"required,min=6"` +} + +type UserResponse struct { + ID uint `json:"id"` + Username string `json:"username"` + Email string `json:"email"` + Role string `json:"role"` + Status string `json:"status"` + RealName string `json:"realName"` + Phone string `json:"phone"` + LastLogin int64 `json:"lastLogin"` + Remark string `json:"remark"` + CreatedAt int64 `json:"createdAt"` + UpdatedAt int64 `json:"updatedAt"` +} + +type UserListResponse struct { + Total int64 `json:"total"` + Items []UserResponse `json:"items"` +} + +type UserDetailResponse struct { + User UserResponse `json:"user"` + Permissions []string `json:"permissions"` +} + +type UserPageRequest struct { + PageNum int `json:"pageNum" validate:"required,min=1"` + PageSize int `json:"pageSize" validate:"required,min=1,max=100"` + Username string `json:"username"` + Role string `json:"role"` + Status string `json:"status"` +} + +type AssignPermissionRequest struct { + UserID uint `json:"userId" validate:"required"` + Permissions []string `json:"permissions" validate:"required"` +} + +type UserLoginResponse struct { + Name string `json:"name"` + Token string `json:"token"` + MfaStatus string `json:"mfaStatus"` + MfaSession string `json:"mfaSession"` + Role string `json:"role"` +} + +type UserResourceAccess struct { + HostID uint `json:"hostId"` + HostName string `json:"hostName"` + Permission string `json:"permission"` +} + +type UserResourceAccessRequest struct { + UserID uint `json:"userId" validate:"required"` + Accesses []UserResourceAccess `json:"accesses" validate:"required"` +} diff --git a/core/app/model/user.go b/core/app/model/user.go new file mode 100644 index 000000000000..79a8786e5e22 --- /dev/null +++ b/core/app/model/user.go @@ -0,0 +1,46 @@ +package model + +import "time" + +type User struct { + BaseModel + Username string `gorm:"column:username;not null;uniqueIndex;size:255" json:"username"` + Email string `gorm:"column:email;size:255" json:"email"` + Password string `gorm:"column:password;not null" json:"password"` + Role string `gorm:"column:role;not null;default:'user'" json:"role"` + Status string `gorm:"column:status;not null;default:'active'" json:"status"` + ParentID uint `gorm:"column:parent_id;index" json:"parentId"` + RealName string `gorm:"column:real_name;size:255" json:"realName"` + Phone string `gorm:"column:phone;size:20" json:"phone"` + LastLogin *time.Time `gorm:"column:last_login" json:"lastLogin"` + Remark string `gorm:"column:remark;type:text" json:"remark"` +} + +func (User) TableName() string { + return "users" +} + +type UserPermission struct { + BaseModel + UserID uint `gorm:"column:user_id;not null;index" json:"userId"` + Permission string `gorm:"column:permission;not null;size:255" json:"permission"` +} + +func (UserPermission) TableName() string { + return "user_permissions" +} + +type UserLoginHistory struct { + BaseModel + UserID uint `gorm:"column:user_id;not null;index" json:"userId"` + IP string `gorm:"column:ip;size:50" json:"ip"` + Address string `gorm:"column:address;size:255" json:"address"` + Agent string `gorm:"column:agent;type:text" json:"agent"` + Status string `gorm:"column:status;not null" json:"status"` + Message string `gorm:"column:message;type:text" json:"message"` + LoginAt time.Time `gorm:"column:login_at" json:"loginAt"` +} + +func (UserLoginHistory) TableName() string { + return "user_login_histories" +} diff --git a/core/app/repo/user_repo.go b/core/app/repo/user_repo.go new file mode 100644 index 000000000000..ca4701a00bf5 --- /dev/null +++ b/core/app/repo/user_repo.go @@ -0,0 +1,164 @@ +package repo + +import ( + "github.com/1Panel-dev/1Panel/core/app/dto" + "github.com/1Panel-dev/1Panel/core/app/model" + "gorm.io/gorm" +) + +type IUserRepo interface { + CreateUser(user *model.User) error + UpdateUser(user *model.User) error + GetUserByID(id uint) (*model.User, error) + GetUserByUsername(username string) (*model.User, error) + GetUserList(req *dto.UserPageRequest) (int64, []model.User, error) + DeleteUser(id uint) error + GetUsersByParentID(parentID uint) ([]model.User, error) + + AddPermission(permission *model.UserPermission) error + DeletePermission(userID uint, permission string) error + GetUserPermissions(userID uint) ([]string, error) + UpdateUserPermissions(userID uint, permissions []string) error + GetPermissionsByRole(role string) ([]string, error) + + AddLoginHistory(history *model.UserLoginHistory) error + GetLoginHistory(userID uint, limit int) ([]model.UserLoginHistory, error) +} + +type UserRepo struct{} + +func NewIUserRepo() IUserRepo { + return &UserRepo{} +} + +func (u *UserRepo) CreateUser(user *model.User) error { + return baseDb.Create(user).Error +} + +func (u *UserRepo) UpdateUser(user *model.User) error { + return baseDb.Save(user).Error +} + +func (u *UserRepo) GetUserByID(id uint) (*model.User, error) { + var user model.User + if err := baseDb.Where("id = ?", id).First(&user).Error; err != nil { + return nil, err + } + return &user, nil +} + +func (u *UserRepo) GetUserByUsername(username string) (*model.User, error) { + var user model.User + if err := baseDb.Where("username = ?", username).First(&user).Error; err != nil { + return nil, err + } + return &user, nil +} + +func (u *UserRepo) GetUserList(req *dto.UserPageRequest) (int64, []model.User, error) { + var users []model.User + var total int64 + + query := baseDb + if req.Username != "" { + query = query.Where("username LIKE ?", "%"+req.Username+"%") + } + if req.Role != "" { + query = query.Where("role = ?", req.Role) + } + if req.Status != "" { + query = query.Where("status = ?", req.Status) + } + + if err := query.Model(&model.User{}).Count(&total).Error; err != nil { + return 0, nil, err + } + + offset := (req.PageNum - 1) * req.PageSize + if err := query.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&users).Error; err != nil { + return 0, nil, err + } + + return total, users, nil +} + +func (u *UserRepo) DeleteUser(id uint) error { + // Delete user permissions first + if err := baseDb.Where("user_id = ?", id).Delete(&model.UserPermission{}).Error; err != nil { + return err + } + // Delete user login history + if err := baseDb.Where("user_id = ?", id).Delete(&model.UserLoginHistory{}).Error; err != nil { + return err + } + // Delete the user + return baseDb.Delete(&model.User{}, id).Error +} + +func (u *UserRepo) GetUsersByParentID(parentID uint) ([]model.User, error) { + var users []model.User + if err := baseDb.Where("parent_id = ?", parentID).Find(&users).Error; err != nil { + return nil, err + } + return users, nil +} + +func (u *UserRepo) AddPermission(permission *model.UserPermission) error { + return baseDb.Create(permission).Error +} + +func (u *UserRepo) DeletePermission(userID uint, permission string) error { + return baseDb.Where("user_id = ? AND permission = ?", userID, permission).Delete(&model.UserPermission{}).Error +} + +func (u *UserRepo) GetUserPermissions(userID uint) ([]string, error) { + var permissions []string + if err := baseDb.Model(&model.UserPermission{}). + Where("user_id = ?", userID). + Pluck("permission", &permissions).Error; err != nil { + return nil, err + } + return permissions, nil +} + +func (u *UserRepo) UpdateUserPermissions(userID uint, permissions []string) error { + return baseDb.Transaction(func(tx *gorm.DB) error { + // Delete all existing permissions for this user + if err := tx.Where("user_id = ?", userID).Delete(&model.UserPermission{}).Error; err != nil { + return err + } + + // Add new permissions + for _, permission := range permissions { + if err := tx.Create(&model.UserPermission{ + UserID: userID, + Permission: permission, + }).Error; err != nil { + return err + } + } + return nil + }) +} + +func (u *UserRepo) GetPermissionsByRole(role string) ([]string, error) { + // This will be retrievedfrom constants in the service layer + // but we keep this interface for future extensibility + return nil, nil +} + +func (u *UserRepo) AddLoginHistory(history *model.UserLoginHistory) error { + return baseDb.Create(history).Error +} + +func (u *UserRepo) GetLoginHistory(userID uint, limit int) ([]model.UserLoginHistory, error) { + var histories []model.UserLoginHistory + if err := baseDb. + Where("user_id = ?", userID). + Order("login_at DESC"). + Limit(limit). + Find(&histories).Error; err != nil { + return nil, err + } + return histories, nil +} diff --git a/core/app/service/auth.go b/core/app/service/auth.go index 760c64c33f23..bd5431ab1c09 100644 --- a/core/app/service/auth.go +++ b/core/app/service/auth.go @@ -53,16 +53,64 @@ func NewIAuthService() IAuthService { } func (u *AuthService) Login(c *gin.Context, info dto.Login, entrance string) (*dto.UserLoginInfo, string, error) { - nameSetting, err := settingRepo.Get(repo.WithByKey("UserName")) - if err != nil { - return nil, "", buserr.New("ErrRecordNotFound") + // First, try to authenticate from the User table (new multi-user system) + userRepo := repo.NewIUserRepo() + user, userErr := userRepo.GetUserByUsername(info.Name) + + // Fallback to settings-based authentication for backward compatibility + if userErr != nil { + nameSetting, err := settingRepo.Get(repo.WithByKey("UserName")) + if err != nil { + return nil, "", buserr.New("ErrRecordNotFound") + } + if nameSetting.Value != info.Name { + return nil, "ErrAuth", buserr.New("ErrAuth") + } + if err = checkPassword(info.Password); err != nil { + return nil, "ErrAuth", err + } + // Continue with old-style login for compatibility + entranceSetting, err := settingRepo.Get(repo.WithByKey("SecurityEntrance")) + if err != nil { + return nil, "", err + } + if len(entranceSetting.Value) != 0 && entranceSetting.Value != entrance { + return nil, "ErrEntrance", buserr.New("ErrEntrance") + } + mfa, err := settingRepo.Get(repo.WithByKey("MFAStatus")) + if err != nil { + return nil, "", err + } + if err = settingRepo.Update("Language", info.Language); err != nil { + return nil, "", err + } + if mfa.Value == constant.StatusEnable { + ip := common.GetRealClientIP(c) + mfaSession := initauth.GetMFASessionStore().Set(nameSetting.Value, entrance, ip) + return &dto.UserLoginInfo{Name: nameSetting.Value, MfaStatus: mfa.Value, MfaSession: mfaSession}, "", nil + } + res, err := u.generateSession(c, info.Name) + if err != nil { + return nil, "", err + } + if entrance != "" { + SetSecurityEntranceCookie(c, entrance) + } + return res, "", nil } - if nameSetting.Value != info.Name { - return nil, "ErrAuth", buserr.New("ErrAuth") + + // New multi-user authentication + // Check user status + if user.Status != constant.UserStatusActive { + return nil, "ErrAuth", buserr.New("ErrUserInactive") } - if err = checkPassword(info.Password); err != nil { - return nil, "ErrAuth", err + + // Verify password + if err := encrypt.StringDecrypt(info.Password, user.Password); err != nil { + return nil, "ErrAuth", buserr.New("ErrAuth") } + + // Check security entrance entranceSetting, err := settingRepo.Get(repo.WithByKey("SecurityEntrance")) if err != nil { return nil, "", err @@ -70,18 +118,24 @@ func (u *AuthService) Login(c *gin.Context, info dto.Login, entrance string) (*d if len(entranceSetting.Value) != 0 && entranceSetting.Value != entrance { return nil, "ErrEntrance", buserr.New("ErrEntrance") } - mfa, err := settingRepo.Get(repo.WithByKey("MFAStatus")) - if err != nil { + + // Update language setting + if err = settingRepo.Update("Language", info.Language); err != nil { return nil, "", err } - if err = settingRepo.Update("Language", info.Language); err != nil { + + // Check if MFA is enabled + mfa, err := settingRepo.Get(repo.WithByKey("MFAStatus")) + if err != nil { return nil, "", err } + if mfa.Value == constant.StatusEnable { ip := common.GetRealClientIP(c) - mfaSession := initauth.GetMFASessionStore().Set(nameSetting.Value, entrance, ip) - return &dto.UserLoginInfo{Name: nameSetting.Value, MfaStatus: mfa.Value, MfaSession: mfaSession}, "", nil + mfaSession := initauth.GetMFASessionStore().Set(info.Name, entrance, ip) + return &dto.UserLoginInfo{Name: info.Name, MfaStatus: mfa.Value, MfaSession: mfaSession, Role: user.Role}, "", nil } + res, err := u.generateSession(c, info.Name) if err != nil { return nil, "", err @@ -89,6 +143,12 @@ func (u *AuthService) Login(c *gin.Context, info dto.Login, entrance string) (*d if entrance != "" { SetSecurityEntranceCookie(c, entrance) } + + // If we got here, add role to response + if res != nil { + res.Role = user.Role + } + return res, "", nil } @@ -120,6 +180,13 @@ func (u *AuthService) MFALogin(c *gin.Context, info dto.MFALogin, entrance strin if err != nil { return nil, "", err } + + // Get user role + userRepo := repo.NewIUserRepo() + if user, err := userRepo.GetUserByUsername(session.Name); err == nil { + res.Role = user.Role + } + mfaSessions.Delete(info.SessionID) if entrance != "" { SetSecurityEntranceCookie(c, entrance) diff --git a/core/app/service/user.go b/core/app/service/user.go new file mode 100644 index 000000000000..4896f6aef1ed --- /dev/null +++ b/core/app/service/user.go @@ -0,0 +1,344 @@ +package service + +import ( + "fmt" + "time" + + "github.com/1Panel-dev/1Panel/core/app/dto" + "github.com/1Panel-dev/1Panel/core/app/model" + "github.com/1Panel-dev/1Panel/core/app/repo" + "github.com/1Panel-dev/1Panel/core/buserr" + "github.com/1Panel-dev/1Panel/core/constant" + "github.com/1Panel-dev/1Panel/core/utils/encrypt" +) + +type UserService struct { + userRepo repo.IUserRepo +} + +type IUserService interface { + CreateUser(req *dto.CreateUserRequest) (*model.User, error) + UpdateUser(req *dto.UpdateUserRequest) error + GetUserByID(id uint) (*dto.UserDetailResponse, error) + GetUserByUsername(username string) (*model.User, error) + GetUserList(req *dto.UserPageRequest) (*dto.UserListResponse, error) + DeleteUser(id uint) error + ChangePassword(req *dto.ChangePasswordRequest) error + ResetPassword(req *dto.ResetPasswordRequest) error + AssignPermissions(req *dto.AssignPermissionRequest) error + GetUserPermissions(userID uint) ([]string, error) + HasPermission(userID uint, permission string) (bool, error) + CheckUserRole(userID uint, allowedRoles ...string) (bool, error) + CreateDefaultAdmin(username, password string) error + RecordLoginHistory(userID uint, req *dto.UserLoginResponse, ip, agent, address string, status string) error + GetLoginHistory(userID uint) ([]model.UserLoginHistory, error) +} + +func NewIUserService() IUserService { + return &UserService{ + userRepo: repo.NewIUserRepo(), + } +} + +func (u *UserService) CreateUser(req *dto.CreateUserRequest) (*model.User, error) { + // Check if user already exists + existingUser, _ := u.userRepo.GetUserByUsername(req.Username) + if existingUser != nil { + return nil, buserr.New("ErrUserAlreadyExists") + } + + // Encrypt password + encryptedPassword, err := encrypt.StringEncrypt(req.Password) + if err != nil { + return nil, err + } + + user := &model.User{ + Username: req.Username, + Email: req.Email, + Password: encryptedPassword, + Role: req.Role, + Status: constant.UserStatusActive, + RealName: req.RealName, + Phone: req.Phone, + Remark: req.Remark, + } + + if err := u.userRepo.CreateUser(user); err != nil { + return nil, err + } + + // Assign default permissions for the role + if defaultPermissions, ok := constant.RolePermissions[req.Role]; ok { + for _, perm := range defaultPermissions { + u.userRepo.AddPermission(&model.UserPermission{ + UserID: user.ID, + Permission: perm, + }) + } + } + + return user, nil +} + +func (u *UserService) UpdateUser(req *dto.UpdateUserRequest) error { + user, err := u.userRepo.GetUserByID(req.ID) + if err != nil { + return buserr.New("ErrRecordNotFound") + } + + user.Email = req.Email + user.Role = req.Role + user.Status = req.Status + user.RealName = req.RealName + user.Phone = req.Phone + user.Remark = req.Remark + + return u.userRepo.UpdateUser(user) +} + +func (u *UserService) GetUserByID(id uint) (*dto.UserDetailResponse, error) { + user, err := u.userRepo.GetUserByID(id) + if err != nil { + return nil, buserr.New("ErrRecordNotFound") + } + + permissions, _ := u.userRepo.GetUserPermissions(id) + + return &dto.UserDetailResponse{ + User: userToDTO(user), + Permissions: permissions, + }, nil +} + +func (u *UserService) GetUserByUsername(username string) (*model.User, error) { + return u.userRepo.GetUserByUsername(username) +} + +func (u *UserService) GetUserList(req *dto.UserPageRequest) (*dto.UserListResponse, error) { + total, users, err := u.userRepo.GetUserList(req) + if err != nil { + return nil, err + } + + var items []dto.UserResponse + for _, user := range users { + items = append(items, userToDTO(&user)) + } + + return &dto.UserListResponse{ + Total: total, + Items: items, + }, nil +} + +func (u *UserService) DeleteUser(id uint) error { + user, err := u.userRepo.GetUserByID(id) + if err != nil { + return buserr.New("ErrRecordNotFound") + } + + // Prevent deletion of the main admin + if user.Role == constant.RoleAdminMain && user.ParentID == 0 { + return buserr.New("ErrCannotDeleteMainAdmin") + } + + return u.userRepo.DeleteUser(id) +} + +func (u *UserService) ChangePassword(req *dto.ChangePasswordRequest) error { + user, err := u.userRepo.GetUserByID(req.UserID) + if err != nil { + return buserr.New("ErrRecordNotFound") + } + + // Verify old password + if err := encrypt.StringDecrypt(req.OldPassword, user.Password); err != nil { + return buserr.New("ErrOldPasswordIncorrect") + } + + // Encrypt new password + encryptedPassword, err := encrypt.StringEncrypt(req.NewPassword) + if err != nil { + return err + } + + user.Password = encryptedPassword + return u.userRepo.UpdateUser(user) +} + +func (u *UserService) ResetPassword(req *dto.ResetPasswordRequest) error { + user, err := u.userRepo.GetUserByID(req.UserID) + if err != nil { + return buserr.New("ErrRecordNotFound") + } + + encryptedPassword, err := encrypt.StringEncrypt(req.NewPassword) + if err != nil { + return err + } + + user.Password = encryptedPassword + return u.userRepo.UpdateUser(user) +} + +func (u *UserService) AssignPermissions(req *dto.AssignPermissionRequest) error { + user, err := u.userRepo.GetUserByID(req.UserID) + if err != nil { + return buserr.New("ErrRecordNotFound") + } + + // Only super admins can assign permissions + if user.Role != constant.RoleAdminMain { + return buserr.New("ErrUnauthorized") + } + + return u.userRepo.UpdateUserPermissions(req.UserID, req.Permissions) +} + +func (u *UserService) GetUserPermissions(userID uint) ([]string, error) { + user, err := u.userRepo.GetUserByID(userID) + if err != nil { + return nil, buserr.New("ErrRecordNotFound") + } + + // Super admin has all permissions + if user.Role == constant.RoleAdminMain { + var allPerms []string + for _, perms := range constant.RolePermissions { + allPerms = append(allPerms, perms...) + } + return allPerms, nil + } + + // Get custom permissions or use role default + permissions, err := u.userRepo.GetUserPermissions(userID) + if err != nil || len(permissions) == 0 { + // Fall back to role-based permissions + if defaultPerms, ok := constant.RolePermissions[user.Role]; ok { + return defaultPerms, nil + } + } + + return permissions, nil +} + +func (u *UserService) HasPermission(userID uint, permission string) (bool, error) { + user, err := u.userRepo.GetUserByID(userID) + if err != nil { + return false, buserr.New("ErrRecordNotFound") + } + + // Super admin has all permissions + if user.Role == constant.RoleAdminMain { + return true, nil + } + + permissions, err := u.GetUserPermissions(userID) + if err != nil { + return false, err + } + + for _, perm := range permissions { + if perm == permission || perm == constant.PermissionAdminAll { + return true, nil + } + } + + return false, nil +} + +func (u *UserService) CheckUserRole(userID uint, allowedRoles ...string) (bool, error) { + user, err := u.userRepo.GetUserByID(userID) + if err != nil { + return false, buserr.New("ErrRecordNotFound") + } + + for _, role := range allowedRoles { + if user.Role == role { + return true, nil + } + } + + return false, nil +} + +func (u *UserService) CreateDefaultAdmin(username, password string) error { + encryptedPassword, err := encrypt.StringEncrypt(password) + if err != nil { + return err + } + + adminUser := &model.User{ + Username: username, + Password: encryptedPassword, + Email: "", + Role: constant.RoleAdminMain, + Status: constant.UserStatusActive, + RealName: "Admin", + } + + if err := u.userRepo.CreateUser(adminUser); err != nil { + return err + } + + // Assign all admin permissions + for _, perm := range constant.RolePermissions[constant.RoleAdminMain] { + u.userRepo.AddPermission(&model.UserPermission{ + UserID: adminUser.ID, + Permission: perm, + }) + } + + return nil +} + +func (u *UserService) RecordLoginHistory(userID uint, loginInfo *dto.UserLoginResponse, ip, agent, address string, status string) error { + history := &model.UserLoginHistory{ + UserID: userID, + IP: ip, + Address: address, + Agent: agent, + Status: status, + LoginAt: time.Now(), + } + + if err := u.userRepo.AddLoginHistory(history); err != nil { + return err + } + + // Update user's last login time + user, err := u.userRepo.GetUserByID(userID) + if err == nil { + now := time.Now() + user.LastLogin = &now + u.userRepo.UpdateUser(user) + } + + return nil +} + +func (u *UserService) GetLoginHistory(userID uint) ([]model.UserLoginHistory, error) { + return u.userRepo.GetLoginHistory(userID, 100) +} + +func userToDTO(user *model.User) dto.UserResponse { + lastLogin := int64(0) + if user.LastLogin != nil { + lastLogin = user.LastLogin.Unix() + } + + return dto.UserResponse{ + ID: user.ID, + Username: user.Username, + Email: user.Email, + Role: user.Role, + Status: user.Status, + RealName: user.RealName, + Phone: user.Phone, + LastLogin: lastLogin, + Remark: user.Remark, + CreatedAt: user.CreatedAt.Unix(), + UpdatedAt: user.UpdatedAt.Unix(), + } +} diff --git a/core/constant/role.go b/core/constant/role.go new file mode 100644 index 000000000000..69c0e8608f68 --- /dev/null +++ b/core/constant/role.go @@ -0,0 +1,153 @@ +package constant + +// User Roles +const ( + RoleUser = "user" + RoleReseller = "reseller" + RoleAdminMain = "admin" +) + +// User Status +const ( + UserStatusActive = "active" + UserStatusInactive = "inactive" +) + +// Permission Actions +const ( + // Admin permissions + PermissionAdminAll = "admin:all" + + // User management permissions + PermissionUserManage = "user:manage" + PermissionUserCreate = "user:create" + PermissionUserUpdate = "user:update" + PermissionUserDelete = "user:delete" + PermissionUserView = "user:view" + PermissionUserPassword = "user:password" + + // Host/Node management + PermissionHostManage = "host:manage" + PermissionHostCreate = "host:create" + PermissionHostUpdate = "host:update" + PermissionHostDelete = "host:delete" + PermissionHostView = "host:view" + PermissionHostMonitor = "host:monitor" + + // App management + PermissionAppManage = "app:manage" + PermissionAppCreate = "app:create" + PermissionAppUpdate = "app:update" + PermissionAppDelete = "app:delete" + PermissionAppView = "app:view" + PermissionAppInstall = "app:install" + PermissionAppUninstall = "app:uninstall" + + // Database management + PermissionDatabaseManage = "database:manage" + PermissionDatabaseCreate = "database:create" + PermissionDatabaseUpdate = "database:update" + PermissionDatabaseDelete = "database:delete" + PermissionDatabaseView = "database:view" + PermissionDatabaseBackup = "database:backup" + + // Website management + PermissionWebsiteManage = "website:manage" + PermissionWebsiteCreate = "website:create" + PermissionWebsiteUpdate = "website:update" + PermissionWebsiteDelete = "website:delete" + PermissionWebsiteView = "website:view" + + // Backup management + PermissionBackupManage = "backup:manage" + PermissionBackupCreate = "backup:create" + PermissionBackupDelete = "backup:delete" + PermissionBackupView = "backup:view" + + // Settings management + PermissionSettingManage = "setting:manage" + PermissionSettingView = "setting:view" + + // System settings + PermissionSystemManage = "system:manage" + PermissionSystemUpgrade = "system:upgrade" + PermissionSystemLog = "system:log" + PermissionSystemRestart = "system:restart" +) + +// Role Permissions mapping +var RolePermissions = map[string][]string{ + RoleAdminMain: { + PermissionAdminAll, + }, + RoleReseller: { + // User management (limited to their sub-users) + PermissionUserView, + PermissionUserCreate, + PermissionUserUpdate, + PermissionUserDelete, + + // Host/Node viewing and monitoring + PermissionHostView, + PermissionHostMonitor, + PermissionHostManage, + + // App management + PermissionAppManage, + PermissionAppCreate, + PermissionAppUpdate, + PermissionAppDelete, + PermissionAppView, + PermissionAppInstall, + PermissionAppUninstall, + + // Database management + PermissionDatabaseManage, + PermissionDatabaseCreate, + PermissionDatabaseUpdate, + PermissionDatabaseDelete, + PermissionDatabaseView, + PermissionDatabaseBackup, + + // Website management + PermissionWebsiteManage, + PermissionWebsiteCreate, + PermissionWebsiteUpdate, + PermissionWebsiteDelete, + PermissionWebsiteView, + + // Backup management + PermissionBackupManage, + PermissionBackupCreate, + PermissionBackupDelete, + PermissionBackupView, + + // Settings (limited) + PermissionSettingView, + }, + RoleUser: { + // User can only view their own profile + PermissionUserView, + PermissionUserPassword, + + // Basic host monitoring + PermissionHostMonitor, + PermissionHostView, + + // App viewing and install (limited) + PermissionAppView, + PermissionAppInstall, + + // Database viewing + PermissionDatabaseView, + + // Website viewing + PermissionWebsiteView, + + // Backup viewing + PermissionBackupView, + + // Settings viewing + PermissionSettingView, + }, +} diff --git a/core/init/migration/migrate.go b/core/init/migration/migrate.go index 10f4446289b3..d844a8e68c63 100644 --- a/core/init/migration/migrate.go +++ b/core/init/migration/migrate.go @@ -38,6 +38,7 @@ func Init() { migrations.UpdateAiModelMenuStructure, migrations.AddDocSourceSetting, migrations.AddAppStoreInstallAllowPortSetting, + migrations.AddUserTables, }) if err := m.Migrate(); err != nil { global.LOG.Error(err) diff --git a/core/init/migration/migrations/init.go b/core/init/migration/migrations/init.go index 6cf65ad8fd2f..6458ba52a7c5 100644 --- a/core/init/migration/migrations/init.go +++ b/core/init/migration/migrations/init.go @@ -990,3 +990,49 @@ func normalizeAiMenuChild(children []dto.ShowMenu, fallback dto.ShowMenu, labels } return fallback } + +var AddUserTables = &gormigrate.Migration{ + ID: "20260506-add-user-tables", + Migrate: func(tx *gorm.DB) error { + // Create users table + if err := tx.AutoMigrate(&model.User{}); err != nil { + return err + } + // Create user permissions table + if err := tx.AutoMigrate(&model.UserPermission{}); err != nil { + return err + } + // Create user login history table + if err := tx.AutoMigrate(&model.UserLoginHistory{}); err != nil { + return err + } + + // Check if admin user exists, if not, migrate the old single user to admin + var adminCount int64 + if err := tx.Model(&model.User{}).Where("role = ?", constant.RoleAdminMain).Count(&adminCount).Error; err != nil { + return err + } + + if adminCount == 0 { + // Get the old credentials from settings + var usernameSetting, passwordSetting model.Setting + if err := tx.Where("key = ?", "UserName").First(&usernameSetting).Error; err == nil { + if err := tx.Where("key = ?", "Password").First(&passwordSetting).Error; err == nil { + // Create initial admin user from old settings + adminUser := &model.User{ + Username: usernameSetting.Value, + Password: passwordSetting.Value, + Email: "", + Role: constant.RoleAdminMain, + Status: constant.UserStatusActive, + RealName: "Admin", + } + if err := tx.Create(adminUser).Error; err != nil { + return err + } + } + } + } + return nil + }, +} diff --git a/core/middleware/role.go b/core/middleware/role.go new file mode 100644 index 000000000000..ea1cbc85d145 --- /dev/null +++ b/core/middleware/role.go @@ -0,0 +1,175 @@ +package middleware + +import ( + "net/http" + "strings" + + "github.com/1Panel-dev/1Panel/core/app/api/v2/helper" + "github.com/1Panel-dev/1Panel/core/app/service" + "github.com/1Panel-dev/1Panel/core/buserr" + "github.com/1Panel-dev/1Panel/core/constant" + "github.com/1Panel-dev/1Panel/core/global" + "github.com/1Panel-dev/1Panel/core/init/session/psession" + "github.com/gin-gonic/gin" +) + +var userService = service.NewIUserService() + +// RoleAuth middleware checks if user has required role +func RoleAuth(requiredRoles ...string) gin.HandlerFunc { + return func(c *gin.Context) { + // Get session user + sessionData, err := global.SESSION.Get(c) + if err != nil { + helper.BadAuth(c, "ErrNotLogin", buserr.New("session not found")) + return + } + + sessionUser, ok := sessionData.(*psession.SessionUser) + if !ok { + helper.BadAuth(c, "ErrNotLogin", buserr.New("invalid session data")) + return + } + + // Get user from database + user, err := userService.GetUserByUsername(sessionUser.Name) + if err != nil { + helper.BadAuth(c, "ErrUserNotFound", buserr.New("user not found")) + return + } + + // Check if user role is allowed + allowed := false + for _, role := range requiredRoles { + if user.Role == role { + allowed = true + break + } + } + + if !allowed && user.Role != constant.RoleAdminMain { + helper.BadAuth(c, "ErrUnauthorized", buserr.New("insufficient permissions")) + return + } + + // Set user info in context + c.Set("UserID", user.ID) + c.Set("UserRole", user.Role) + c.Set("Username", user.Username) + + // Get and set permissions + permissions, _ := userService.GetUserPermissions(user.ID) + c.Set("Permissions", permissions) + + c.Next() + } +} + +// PermissionAuth middleware checks if user has required permission +func PermissionAuth(requiredPermissions ...string) gin.HandlerFunc { + return func(c *gin.Context) { + // Get session user + sessionData, err := global.SESSION.Get(c) + if err != nil { + helper.BadAuth(c, "ErrNotLogin", buserr.New("session not found")) + return + } + + sessionUser, ok := sessionData.(*psession.SessionUser) + if !ok { + helper.BadAuth(c, "ErrNotLogin", buserr.New("invalid session data")) + return + } + + // Get user from database + user, err := userService.GetUserByUsername(sessionUser.Name) + if err != nil { + helper.BadAuth(c, "ErrUserNotFound", buserr.New("user not found")) + return + } + + // Admin has all permissions + if user.Role == constant.RoleAdminMain { + c.Set("UserID", user.ID) + c.Set("UserRole", user.Role) + c.Set("Username", user.Username) + c.Next() + return + } + + // Check if user has required permissions + permissions, _ := userService.GetUserPermissions(user.ID) + hasPermission := false + + for _, requiredPerm := range requiredPermissions { + for _, userPerm := range permissions { + if userPerm == requiredPerm || userPerm == constant.PermissionAdminAll { + hasPermission = true + break + } + } + if hasPermission { + break + } + } + + if !hasPermission { + c.JSON(http.StatusForbidden, gin.H{ + "code": http.StatusForbidden, + "message": "insufficient permissions", + }) + c.Abort() + return + } + + // Set user info in context + c.Set("UserID", user.ID) + c.Set("UserRole", user.Role) + c.Set("Username", user.Username) + c.Set("Permissions", permissions) + + c.Next() + } +} + +// UserAuthMiddleware adds user information to context +func UserAuthMiddleware() gin.HandlerFunc { + return func(c *gin.Context) { + // Skip for auth endpoints + if strings.Contains(c.Request.URL.Path, "/api/v2/core/auth") { + c.Next() + return + } + + // Get session user + sessionData, err := global.SESSION.Get(c) + if err != nil { + c.Next() + return + } + + sessionUser, ok := sessionData.(*psession.SessionUser) + if !ok { + c.Next() + return + } + + // Get user from database + user, err := userService.GetUserByUsername(sessionUser.Name) + if err != nil { + c.Next() + return + } + + // Set user info in context + c.Set("UserID", user.ID) + c.Set("UserRole", user.Role) + c.Set("Username", user.Username) + + // Get and set permissions + permissions, _ := userService.GetUserPermissions(user.ID) + c.Set("Permissions", permissions) + + c.Next() + } +} diff --git a/core/router/common.go b/core/router/common.go index f4180a55f914..7e0c7ff9559d 100644 --- a/core/router/common.go +++ b/core/router/common.go @@ -3,6 +3,7 @@ package router func commonGroups() []CommonRouter { return []CommonRouter{ &BaseRouter{}, + &UserRouter{}, &BackupRouter{}, &LogRouter{}, &SettingRouter{}, diff --git a/core/router/ro_user.go b/core/router/ro_user.go new file mode 100644 index 000000000000..b1f178b0a7f4 --- /dev/null +++ b/core/router/ro_user.go @@ -0,0 +1,39 @@ +package router + +import ( + v2 "github.com/1Panel-dev/1Panel/core/app/api/v2" + "github.com/1Panel-dev/1Panel/core/middleware" + "github.com/gin-gonic/gin" +) + +type UserRouter struct{} + +func (s *UserRouter) InitRouter(Router *gin.RouterGroup) { + userRouter := Router.Group("users"). + Use(middleware.SessionAuth()). + Use(middleware.UserAuthMiddleware()) + { + userApi := v2.ApiGroupApp.UserApi + userRouter.GET("", userApi.ListUsers) + userRouter.POST("", middleware.PermissionAuth("user:create"), userApi.CreateUser) + userRouter.GET("/:id", userApi.GetUser) + userRouter.PUT("", middleware.PermissionAuth("user:update"), userApi.UpdateUser) + userRouter.DELETE("/:id", middleware.PermissionAuth("user:delete"), userApi.DeleteUser) + + // Password management + userRouter.POST("/password/change", userApi.ChangePassword) + userRouter.POST("/password/reset", middleware.PermissionAuth("user:password"), userApi.ResetPassword) + + // Permission management + userRouter.GET("/:id/permissions", userApi.GetUserPermissions) + userRouter.POST("/permissions", middleware.PermissionAuth("user:manage"), userApi.AssignPermissions) + + // Profile + userRouter.GET("/profile", userApi.GetProfile) + userRouter.GET("/:id/login-history", userApi.GetLoginHistory) + } +} + +type CommonRouter interface { + InitRouter(Router *gin.RouterGroup) +}