Compare commits
No commits in common. "78b1b1f078f79f72e244268067659ea30eef93cb" and "405bbc8d4c89506d3d9b7fe0c997e2db15fa2cc7" have entirely different histories.
78b1b1f078
...
405bbc8d4c
@ -1,7 +1,6 @@
|
|||||||
using API.Models;
|
using API.Models;
|
||||||
using API.Persistence.Repositories;
|
using API.Persistence.Repositories;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
|
|
||||||
namespace API.Application.Users.Commands
|
namespace API.Application.Users.Commands
|
||||||
{
|
{
|
||||||
@ -14,30 +13,25 @@ namespace API.Application.Users.Commands
|
|||||||
_repository = repository;
|
_repository = repository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IActionResult> Handle(UpdateUserDTO UpdateUserDTO)
|
public async Task<IActionResult> Handle(UserDTO userDTO)
|
||||||
{
|
{
|
||||||
List<User> existingUsers = await _repository.QueryAllUsersAsync();
|
List<User> existingUsers = await _repository.QueryAllUsersAsync();
|
||||||
User currentUser = await _repository.QueryUserByIdAsync(UpdateUserDTO.Id);
|
User currentUser = await _repository.QueryUserByIdAsync(userDTO.Id);
|
||||||
|
|
||||||
foreach (User existingUser in existingUsers)
|
foreach (User existingUser in existingUsers)
|
||||||
{
|
{
|
||||||
if (existingUser.Username == UpdateUserDTO.Username && existingUser.Username != currentUser.Username)
|
if (existingUser.Username == userDTO.Username && existingUser.Username != currentUser.Username)
|
||||||
{
|
{
|
||||||
return new ConflictObjectResult(new { message = "Username is already in use." });
|
return new ConflictObjectResult(new { message = "Username is already in use." });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingUser.Email == UpdateUserDTO.Email && existingUser.Email != currentUser.Email)
|
if (existingUser.Email == userDTO.Email && existingUser.Email != currentUser.Email)
|
||||||
{
|
{
|
||||||
return new ConflictObjectResult(new { message = "Email is already in use." });
|
return new ConflictObjectResult(new { message = "Email is already in use." });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
currentUser.Username = userDTO.Username;
|
||||||
string hashedPassword = BCrypt.Net.BCrypt.HashPassword(UpdateUserDTO.Password);
|
currentUser.Email = userDTO.Email;
|
||||||
|
|
||||||
|
|
||||||
currentUser.Username = UpdateUserDTO.Username;
|
|
||||||
currentUser.Email = UpdateUserDTO.Email;
|
|
||||||
currentUser.HashedPassword = hashedPassword;
|
|
||||||
|
|
||||||
bool success = await _repository.UpdateUserAsync(currentUser);
|
bool success = await _repository.UpdateUserAsync(currentUser);
|
||||||
if (success)
|
if (success)
|
||||||
@ -46,19 +40,5 @@ namespace API.Application.Users.Commands
|
|||||||
return new StatusCodeResult(StatusCodes.Status500InternalServerError);
|
return new StatusCodeResult(StatusCodes.Status500InternalServerError);
|
||||||
|
|
||||||
}
|
}
|
||||||
private bool IsPasswordSecure(string password)
|
|
||||||
{
|
|
||||||
var hasUpperCase = new Regex(@"[A-Z]+");
|
|
||||||
var hasLowerCase = new Regex(@"[a-z]+");
|
|
||||||
var hasDigits = new Regex(@"[0-9]+");
|
|
||||||
var hasSpecialChar = new Regex(@"[\W_]+");
|
|
||||||
var hasMinimum8Chars = new Regex(@".{8,}");
|
|
||||||
|
|
||||||
return hasUpperCase.IsMatch(password)
|
|
||||||
&& hasLowerCase.IsMatch(password)
|
|
||||||
&& hasDigits.IsMatch(password)
|
|
||||||
&& hasSpecialChar.IsMatch(password)
|
|
||||||
&& hasMinimum8Chars.IsMatch(password);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
57
API/Application/Users/Commands/UpdateUserPassword.cs
Normal file
57
API/Application/Users/Commands/UpdateUserPassword.cs
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
using API.Models;
|
||||||
|
using API.Persistence.Repositories;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
|
namespace API.Application.Users.Commands
|
||||||
|
{
|
||||||
|
public class UpdateUserPassword
|
||||||
|
{
|
||||||
|
private readonly IUserRepository _repository;
|
||||||
|
|
||||||
|
public UpdateUserPassword(IUserRepository repository)
|
||||||
|
{
|
||||||
|
_repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> Handle(ChangePasswordDTO changePasswordDTO)
|
||||||
|
{
|
||||||
|
if (!IsPasswordSecure(changePasswordDTO.NewPassword))
|
||||||
|
{
|
||||||
|
return new ConflictObjectResult(new { message = "New Password is not secure." });
|
||||||
|
}
|
||||||
|
|
||||||
|
User currentUser = await _repository.QueryUserByIdAsync(changePasswordDTO.Id);
|
||||||
|
if (currentUser == null || !BCrypt.Net.BCrypt.Verify(changePasswordDTO.OldPassword, currentUser.HashedPassword))
|
||||||
|
{
|
||||||
|
return new UnauthorizedObjectResult(new { message = "Old Password is incorrect" });
|
||||||
|
}
|
||||||
|
string hashedPassword = BCrypt.Net.BCrypt.HashPassword(changePasswordDTO.NewPassword);
|
||||||
|
|
||||||
|
currentUser.HashedPassword = hashedPassword;
|
||||||
|
|
||||||
|
bool success = await _repository.UpdateUserPasswordAsync(currentUser);
|
||||||
|
if (success)
|
||||||
|
return new OkResult();
|
||||||
|
else
|
||||||
|
return new StatusCodeResult(StatusCodes.Status500InternalServerError);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsPasswordSecure(string password)
|
||||||
|
{
|
||||||
|
var hasUpperCase = new Regex(@"[A-Z]+");
|
||||||
|
var hasLowerCase = new Regex(@"[a-z]+");
|
||||||
|
var hasDigits = new Regex(@"[0-9]+");
|
||||||
|
var hasSpecialChar = new Regex(@"[\W_]+");
|
||||||
|
var hasMinimum8Chars = new Regex(@".{8,}");
|
||||||
|
|
||||||
|
return hasUpperCase.IsMatch(password)
|
||||||
|
&& hasLowerCase.IsMatch(password)
|
||||||
|
&& hasDigits.IsMatch(password)
|
||||||
|
&& hasSpecialChar.IsMatch(password)
|
||||||
|
&& hasMinimum8Chars.IsMatch(password);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@ -20,6 +20,7 @@ namespace API.Controllers
|
|||||||
private readonly QueryUserById _queryUserById;
|
private readonly QueryUserById _queryUserById;
|
||||||
private readonly CreateUser _createUser;
|
private readonly CreateUser _createUser;
|
||||||
private readonly UpdateUser _updateUser;
|
private readonly UpdateUser _updateUser;
|
||||||
|
private readonly UpdateUserPassword _updateUserPassword;
|
||||||
private readonly DeleteUser _deleteUser;
|
private readonly DeleteUser _deleteUser;
|
||||||
private readonly LoginUser _loginUser;
|
private readonly LoginUser _loginUser;
|
||||||
|
|
||||||
@ -28,6 +29,7 @@ namespace API.Controllers
|
|||||||
QueryUserById queryUserById,
|
QueryUserById queryUserById,
|
||||||
CreateUser createUser,
|
CreateUser createUser,
|
||||||
UpdateUser updateUser,
|
UpdateUser updateUser,
|
||||||
|
UpdateUserPassword updateUserPassword,
|
||||||
DeleteUser deleteUser,
|
DeleteUser deleteUser,
|
||||||
LoginUser loginUser)
|
LoginUser loginUser)
|
||||||
{
|
{
|
||||||
@ -35,6 +37,7 @@ namespace API.Controllers
|
|||||||
_queryUserById = queryUserById;
|
_queryUserById = queryUserById;
|
||||||
_createUser = createUser;
|
_createUser = createUser;
|
||||||
_updateUser = updateUser;
|
_updateUser = updateUser;
|
||||||
|
_updateUserPassword = updateUserPassword;
|
||||||
_deleteUser = deleteUser;
|
_deleteUser = deleteUser;
|
||||||
_loginUser = loginUser;
|
_loginUser = loginUser;
|
||||||
}
|
}
|
||||||
@ -62,9 +65,16 @@ namespace API.Controllers
|
|||||||
|
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[HttpPut]
|
[HttpPut]
|
||||||
public async Task<IActionResult> PutUser(UpdateUserDTO UpdateUserDTO)
|
public async Task<IActionResult> PutUser(UserDTO userDTO)
|
||||||
{
|
{
|
||||||
return await _updateUser.Handle(UpdateUserDTO);
|
return await _updateUser.Handle(userDTO);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
|
[HttpPut("password")]
|
||||||
|
public async Task<IActionResult> PutUserPassword(ChangePasswordDTO changePasswordDTO)
|
||||||
|
{
|
||||||
|
return await _updateUserPassword.Handle(changePasswordDTO);
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost]
|
[HttpPost]
|
||||||
|
@ -6,6 +6,7 @@ public class User : BaseModel
|
|||||||
{
|
{
|
||||||
public string? Email { get; set; }
|
public string? Email { get; set; }
|
||||||
public string? Username { get; set; }
|
public string? Username { get; set; }
|
||||||
|
public string? Password { get; set; }
|
||||||
public string HashedPassword { get; set; }
|
public string HashedPassword { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,11 +30,10 @@ public class SignUpDTO
|
|||||||
public string Password { get; set; }
|
public string Password { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class UpdateUserDTO
|
public class ChangePasswordDTO
|
||||||
{
|
{
|
||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
public string Email { get; set; }
|
public string OldPassword { get; set; }
|
||||||
public string Username { get; set; }
|
public string NewPassword { get; set; }
|
||||||
public string Password { get; set; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,5 +10,6 @@ namespace API.Persistence.Repositories
|
|||||||
Task<User> QueryUserByIdAsync(string id);
|
Task<User> QueryUserByIdAsync(string id);
|
||||||
Task<User> QueryUserByEmailAsync(string email);
|
Task<User> QueryUserByEmailAsync(string email);
|
||||||
Task<bool> UpdateUserAsync(User user);
|
Task<bool> UpdateUserAsync(User user);
|
||||||
|
Task<bool> UpdateUserPasswordAsync(User user);
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -57,6 +57,21 @@ namespace API.Persistence.Repositories
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<bool> UpdateUserPasswordAsync(User user)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_context.Entry(user).State = EntityState.Modified;
|
||||||
|
await _context.SaveChangesAsync();
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<bool> DeleteUserAsync(string id)
|
public async Task<bool> DeleteUserAsync(string id)
|
||||||
{
|
{
|
||||||
var user = await _context.Users.FindAsync(id);
|
var user = await _context.Users.FindAsync(id);
|
||||||
|
@ -36,6 +36,7 @@ namespace API
|
|||||||
builder.Services.AddScoped<QueryUserById>();
|
builder.Services.AddScoped<QueryUserById>();
|
||||||
builder.Services.AddScoped<CreateUser>();
|
builder.Services.AddScoped<CreateUser>();
|
||||||
builder.Services.AddScoped<UpdateUser>();
|
builder.Services.AddScoped<UpdateUser>();
|
||||||
|
builder.Services.AddScoped<UpdateUserPassword>();
|
||||||
builder.Services.AddScoped<DeleteUser>();
|
builder.Services.AddScoped<DeleteUser>();
|
||||||
builder.Services.AddScoped<LoginUser>();
|
builder.Services.AddScoped<LoginUser>();
|
||||||
builder.Services.AddScoped<IUserRepository, UserRepository>();
|
builder.Services.AddScoped<IUserRepository, UserRepository>();
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:mobile/models.dart';
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
@ -68,9 +67,7 @@ Future<bool> isLoggedIn(BuildContext context) async {
|
|||||||
|
|
||||||
final token = prefs.getString('token');
|
final token = prefs.getString('token');
|
||||||
if (token == null){
|
if (token == null){
|
||||||
prefs.remove('id');
|
|
||||||
loggedIn = false;
|
loggedIn = false;
|
||||||
user = User as User?;
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,10 +28,8 @@ class _SideMenuState extends State<SideMenu> {
|
|||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
prefs.remove('token');
|
prefs.remove('token');
|
||||||
prefs.remove('id');
|
|
||||||
setState(() {
|
setState(() {
|
||||||
loggedIn = false;
|
loggedIn = false;
|
||||||
user = null;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,187 +0,0 @@
|
|||||||
import 'dart:math';
|
|
||||||
import 'dart:developer' as useMAN;
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:mobile/models.dart';
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
|
||||||
import 'api.dart' as api;
|
|
||||||
import 'base/variables.dart';
|
|
||||||
|
|
||||||
class EditProfilePage extends StatefulWidget {
|
|
||||||
final User? userData;
|
|
||||||
|
|
||||||
const EditProfilePage({super.key, required this.userData});
|
|
||||||
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<EditProfilePage> createState() => _ProfilePageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _ProfilePageState extends State<EditProfilePage> {
|
|
||||||
TextEditingController usernameInput = TextEditingController();
|
|
||||||
TextEditingController emailInput = TextEditingController();
|
|
||||||
TextEditingController passwordInput = TextEditingController();
|
|
||||||
TextEditingController confirmPasswordInput = TextEditingController();
|
|
||||||
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
// Initialize the controllers with existing data
|
|
||||||
usernameInput.text = widget.userData!.username;
|
|
||||||
emailInput.text = widget.userData!.email;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
// Dispose of the controllers when the widget is disposed
|
|
||||||
usernameInput.dispose();
|
|
||||||
emailInput.dispose();
|
|
||||||
passwordInput.dispose();
|
|
||||||
confirmPasswordInput.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _saveProfile() async {
|
|
||||||
if (passwordInput.text != confirmPasswordInput.text) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(content: Text('Passwords do not match')));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
|
||||||
String? id = prefs.getString('id');
|
|
||||||
|
|
||||||
final response = await api
|
|
||||||
.request(context, api.ApiService.auth, 'PUT', '/api/users', {
|
|
||||||
'id' : id,
|
|
||||||
'username': usernameInput.text,
|
|
||||||
'email': emailInput.text,
|
|
||||||
'password': passwordInput.text,
|
|
||||||
});
|
|
||||||
|
|
||||||
useMAN.log('data');
|
|
||||||
|
|
||||||
|
|
||||||
if (response!.isEmpty) {
|
|
||||||
prefs.remove('token');
|
|
||||||
loggedIn = true;
|
|
||||||
user = User(
|
|
||||||
id!,
|
|
||||||
emailInput.text,
|
|
||||||
usernameInput.text,
|
|
||||||
DateTime.now(),
|
|
||||||
);
|
|
||||||
Navigator.of(context).pop(); // Close the dialog
|
|
||||||
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(content: Text('Something went wrong! Please contact an admin.')),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _deleteProfile(BuildContext context) {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (BuildContext context) {
|
|
||||||
return AlertDialog(
|
|
||||||
title: Text('Confirm Deletion'),
|
|
||||||
content: Text('Are you sure you want to delete your profile?'),
|
|
||||||
actions: <Widget>[
|
|
||||||
TextButton(
|
|
||||||
child: Text('Cancel'),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.of(context).pop(); // Close the dialog
|
|
||||||
},
|
|
||||||
),
|
|
||||||
TextButton(
|
|
||||||
child: Text('Delete'),
|
|
||||||
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
|
||||||
onPressed: () async {
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
|
||||||
String? id = prefs.getString('id');
|
|
||||||
|
|
||||||
final response = await api
|
|
||||||
.request(context, api.ApiService.auth, 'DELETE', '/api/users/$id', null);
|
|
||||||
|
|
||||||
if (response!.isEmpty) {
|
|
||||||
prefs.remove('token');
|
|
||||||
prefs.remove('id');
|
|
||||||
setState(() {
|
|
||||||
loggedIn = false;
|
|
||||||
user = null;
|
|
||||||
});
|
|
||||||
Navigator.of(context).pop(); // Close the dialog
|
|
||||||
Navigator.of(context).pop();
|
|
||||||
Navigator.pushReplacementNamed(context, '/register');
|
|
||||||
}
|
|
||||||
else{
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(content: Text('Something went wrong! Please contact an admin.')),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
|
||||||
title: Text('Edit Profile'),
|
|
||||||
leading: IconButton(
|
|
||||||
icon: Icon(Icons.arrow_back),
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(context); // Navigates back when the back button is pressed
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
body: Padding(
|
|
||||||
padding: const EdgeInsets.all(16.0),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
TextField(
|
|
||||||
controller: usernameInput,
|
|
||||||
decoration: InputDecoration(labelText: 'Name'),
|
|
||||||
),
|
|
||||||
TextField(
|
|
||||||
controller: emailInput,
|
|
||||||
decoration: InputDecoration(labelText: 'Email'),
|
|
||||||
),
|
|
||||||
TextField(
|
|
||||||
controller: passwordInput,
|
|
||||||
decoration: InputDecoration(labelText: 'New password'),
|
|
||||||
),
|
|
||||||
TextField(
|
|
||||||
controller: confirmPasswordInput,
|
|
||||||
decoration: InputDecoration(labelText: 'Repeat new password'),
|
|
||||||
),
|
|
||||||
SizedBox(height: 20),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: _saveProfile, // Save and pop
|
|
||||||
child: Text('Save'),
|
|
||||||
),
|
|
||||||
SizedBox(width: 10),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () => _deleteProfile(context),
|
|
||||||
child: Text('Delete'),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
foregroundColor: Colors.red, // Red text
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -6,7 +6,6 @@ import 'package:mobile/models.dart';
|
|||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'base/sidemenu.dart';
|
import 'base/sidemenu.dart';
|
||||||
import 'api.dart' as api;
|
import 'api.dart' as api;
|
||||||
import 'editprofile.dart';
|
|
||||||
|
|
||||||
class ProfilePage extends StatefulWidget {
|
class ProfilePage extends StatefulWidget {
|
||||||
const ProfilePage({super.key});
|
const ProfilePage({super.key});
|
||||||
@ -106,18 +105,9 @@ class _ProfilePageState extends State<ProfilePage> {
|
|||||||
const SizedBox(height: 50),
|
const SizedBox(height: 50),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showModalBottomSheet(
|
// Add your edit action here
|
||||||
context: context,
|
},
|
||||||
isScrollControlled: true,
|
child: const Text('Edit'),
|
||||||
builder: (BuildContext context) {
|
|
||||||
return SizedBox(
|
|
||||||
height: MediaQuery.of(context).size.height * 0.9, // 90% height
|
|
||||||
child: EditProfilePage(userData: user),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: const Text('Edit'),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -42,7 +42,7 @@ Widget build(BuildContext context) {
|
|||||||
selectedIndex: 3,
|
selectedIndex: 3,
|
||||||
body: Scaffold(
|
body: Scaffold(
|
||||||
body: SingleChildScrollView(
|
body: SingleChildScrollView(
|
||||||
child: Center(
|
child: Center( // Added SingleChildScrollView here
|
||||||
child: Container(
|
child: Container(
|
||||||
constraints: const BoxConstraints(minWidth: 100, maxWidth: 400),
|
constraints: const BoxConstraints(minWidth: 100, maxWidth: 400),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
Loading…
Reference in New Issue
Block a user