using System.Text;
using Sunfish.Auth;
using Sunfish.Config;
namespace Sunfish.Cli;
/// <summary>sunfish-cli user add|passwd|del|list: usuarios del fichero de configuracion.</summary>
internal static class UserCommands
{
public static int Run(string[] args)
{
string? configPath = null;
var rest = new List<string>();
for (int i = 0; i < args.Length; i++)
{
if (args[i] is "-c" or "--config" && i + 1 < args.Length)
configPath = args[++i];
else if (args[i] == "--lang" && i + 1 < args.Length)
Strings.Force(args[++i]);
else
rest.Add(args[i]);
}
if (rest.Count == 0 || (rest[0] != "list" && rest.Count != 2))
{
Console.Error.WriteLine(Texts.UserUsage);
return 1;
}
string path = ConfigLocator.Find(configPath) ?? ConfigLocator.PortablePath;
var config = File.Exists(path) ? SunfishConfig.Load(path) : new SunfishConfig { FilePath = path };
string verb = rest[0];
string name = rest.Count > 1 ? rest[1] : "";
var user = config.Users.FirstOrDefault(u => u.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
switch (verb)
{
case "list":
if (config.Users.Count == 0)
Console.WriteLine(Texts.NoUsers);
foreach (var u in config.Users)
{
var shares = config.Shares.Where(s => s.Users.Contains(u.Name, StringComparer.OrdinalIgnoreCase)).Select(s => s.Name);
Console.WriteLine($"{u.Name} ({Texts.UserShares(string.Join(", ", shares))})");
}
return 0;
case "add":
if (!IsValidName(name))
{
Console.Error.WriteLine(Texts.BadUserName(name));
return 1;
}
if (user is not null)
{
Console.Error.WriteLine(Texts.UserExists(name));
return 1;
}
string? password = AskNewPassword();
if (password is null)
return 1;
config.Users.Add(new UserConfig { Name = name, PasswordHash = Passwords.Hash(password) });
config.Save(path);
Console.WriteLine(Texts.UserSaved(name, path));
Console.WriteLine(Texts.GrantHint);
return 0;
case "passwd":
if (user is null)
{
Console.Error.WriteLine(Texts.UserMissing(name));
return 1;
}
string? changed = AskNewPassword();
if (changed is null)
return 1;
user.PasswordHash = Passwords.Hash(changed);
config.Save(path);
Console.WriteLine(Texts.UserSaved(user.Name, path));
return 0;
case "del":
if (user is null)
{
Console.Error.WriteLine(Texts.UserMissing(name));
return 1;
}
config.Users.Remove(user);
foreach (var share in config.Shares)
share.Users.RemoveAll(u => u.Equals(user.Name, StringComparison.OrdinalIgnoreCase));
config.Save(path);
Console.WriteLine(Texts.UserDeleted(user.Name));
return 0;
default:
Console.Error.WriteLine(Texts.UserUsage);
return 1;
}
}
/// <summary>Sin espacios, comas ni ':' (separa usuario y contrasena en HTTP Basic).</summary>
internal static bool IsValidName(string name) =>
name.Length is > 0 and <= 64 && name.All(c => c > ' ' && c != ',' && c != ':' && c != '[' && c != ']' && c != 0x7F);
internal static string? AskNewPassword()
{
string first = ReadPassword(Texts.PasswordPrompt);
if (first.Length == 0)
{
Console.Error.WriteLine(Texts.PasswordEmpty);
return null;
}
if (!Console.IsInputRedirected && ReadPassword(Texts.PasswordRepeat) != first)
{
Console.Error.WriteLine(Texts.PasswordMismatch);
return null;
}
return first;
}
/// <summary>Lee sin eco en un terminal; con la entrada redirigida, una linea.</summary>
private static string ReadPassword(string prompt)
{
Console.Error.Write(prompt);
if (Console.IsInputRedirected)
{
string line = Console.ReadLine() ?? "";
Console.Error.WriteLine();
return line;
}
var sb = new StringBuilder();
while (true)
{
var key = Console.ReadKey(intercept: true);
if (key.Key == ConsoleKey.Enter)
break;
if (key.Key == ConsoleKey.Backspace)
{
if (sb.Length > 0)
sb.Length--;
}
else if (!char.IsControl(key.KeyChar))
{
sb.Append(key.KeyChar);
}
}
Console.Error.WriteLine();
return sb.ToString();
}
}