Automatically Add SSH keys to the SSH agent upon Login

You can configure your SSH client to automatically add your key when making an SSH connection.

  1. Edit or create the SSH configuration file:
    nano ~/.ssh/config
    
  2. Add the following configuration:
    Host *
        AddKeysToAgent yes
        IdentityFile ~/.ssh/id_rsa
    

    Again, replace ~/.ssh/id_rsa with the path to your SSH key if necessary.

  3. Save and close the file.

This will automatically add your SSH key to the agent whenever you initiate an SSH connection.

Git useful Commands

Basic Commands

  • git init: Initialize a new Git repository in your current directory.
  • git clone <repository>: Clone a repository from a remote source to your local machine.
  • git status: Show the current status of your working directory and staging area.
  • git add <file>: Add a file to the staging area.
  • git add .: Add all changes in the current directory to the staging area.
  • git commit -m "message": Commit changes in the staging area with a message.
  • git push <remote> <branch>: Push your committed changes to a remote repository.
  • git pull: Fetch changes from a remote repository and merge them into your current branch.

Branching and Merging

  • git branch: List all branches in your repository.
  • git branch <branch-name>: Create a new branch.
  • git checkout <branch-name>: Switch to a different branch.
  • git merge <branch-name>: Merge the specified branch into your current branch.
  • git branch -d <branch-name>: Delete a branch locally.

Viewing History and Logs

  • git log: View the commit history for the current branch.
  • git log --oneline: View a simplified version of the commit history.
  • git diff: Show changes between your working directory and the staging area.
  • git diff <branch1> <branch2>: Compare changes between two branches.

Working with Remotes

  • git remote -v: List all remote repositories associated with your local repository.
  • git remote add <name> <url>: Add a new remote repository.
  • git fetch <remote>: Fetch changes from a remote repository without merging them.
  • git push origin --delete <branch-name>: Delete a branch from the remote repository.

Stashing and Reverting

  • git stash: Temporarily save your changes without committing them.
  • git stash apply: Apply the stashed changes back to your working directory.
  • git revert <commit>: Create a new commit that undoes the changes from a specific commit.
  • git reset --hard <commit>: Reset your working directory and staging area to match a specific commit.

Tagging

  • git tag <tag-name>: Create a new tag for marking a specific commit.
  • git push origin <tag-name>: Push a tag to the remote repository.

Undoing Changes

  • git checkout -- <file>: Discard changes in a working directory file.
  • git reset HEAD <file>: Unstage a file without discarding changes.
  • git reset --soft <commit>: Reset to a previous commit but keep changes staged.
  • git reset --hard <commit>: Reset to a previous commit and discard all changes.

Blazor WebAssembly with Cookie Authentication

In the Backend I’ve started by adding Cookie Authentication in the startup and override the OnRedirectToLogin event handlers, so they are going to return a HTTP Status Code 401 to the consumer. This is handled in the Exception Handling Middleware and not shown here.

// Cookie Authentication
builder.Services
    .AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie(options =>
    {
        options.Cookie.HttpOnly = true;
        options.Cookie.SameSite = SameSiteMode.Lax; // We don't want to deal with CSRF Tokens

        options.Events.OnRedirectToAccessDenied = context => throw new AuthenticationFailedException();
        options.Events.OnRedirectToLogin = context => throw new AuthenticationFailedException();
    });

The user is signed in using HttpContext#SignInAsync with something along the lines of a AuthenticationController:

// Licensed under the MIT license. See LICENSE file in the project root for full license information.

// ...

namespace RebacExperiments.Server.Api.Controllers
{
    public class AuthenticationController : ODataController
    {
        // ...

        [HttpPost("odata/SignInUser")]
        public async Task SignInUser([FromServices] IUserService userService, [FromBody] ODataActionParameters parameters, CancellationToken cancellationToken)
        {
            // ...

            // Create the ClaimsPrincipal
            var claimsIdentity = new ClaimsIdentity(userClaims, CookieAuthenticationDefaults.AuthenticationScheme);
            var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);

            // It's a valid ClaimsPrincipal, sign in
            await HttpContext.SignInAsync(claimsPrincipal, new AuthenticationProperties { IsPersistent = rememberMe });
            // ...
        }
    }
}        

You can then open your Browsers Developer Tools and see, that an (encrypted) Cookie has been created.

Once we have successfully logged in and got our Cookie, we need to send the Authorization Cookie on every request to the API. So we start by adding a CookieDelegatingHandler, that does just that:

// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.AspNetCore.Components.WebAssembly.Http;
using RebacExperiments.Blazor.Shared.Logging;

namespace RebacExperiments.Blazor.Infrastructure
{
    public class CookieDelegatingHandler : DelegatingHandler
    {
        private readonly ILogger _logger;

        public CookieDelegatingHandler(ILogger logger)
        {
            _logger = logger;
        }

        protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            _logger.TraceMethodEntry();

            request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include);

            return await base.SendAsync(request, cancellationToken);
        }
    }
}

The CookieDelegatingHandler needs to be registered for the HttpClient, so we use the IHttpClientBuilder#AddHttpMessageHandler extension method like this:

builder.Services
    .AddHttpClient(client => client.BaseAddress = new Uri("https://localhost:5000"))
    .AddHttpMessageHandler();

The Blazor Authorization Infrastructure uses an AuthenticationStateProvider to pass the user information into the components. We want to persist the user information across page refreshes, so the local storage of a Browser seems to be a good place to persist it.

We don’t need to take additional dependencies, just write a small LocalStorageService.

// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.JSInterop;
using System.Text.Json;

namespace RebacExperiments.Blazor.Infrastructure
{
    public class LocalStorageService
    {
        private IJSRuntime _jsRuntime;

        public LocalStorageService(IJSRuntime jsRuntime)
        {
            _jsRuntime = jsRuntime;
        }

        public async Task GetItemAsync(string key)
        {
            var json = await _jsRuntime.InvokeAsync("localStorage.getItem", key);

            if (json == null)
            {
                return default;
            }

            return JsonSerializer.Deserialize(json);
        }

        public async Task SetItem(string key, T value)
        {
            await _jsRuntime.InvokeVoidAsync("localStorage.setItem", key, JsonSerializer.Serialize(value));
        }

        public async Task RemoveItemAsync(string key)
        {
            await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", key);
        }
    }
}

And register it in the Program.cs.

builder.Services.AddSingleton();

We can then implement an AuthenticationStateProvider, that allows us to set a User (think of User Profile) and notify subscribers about the new AuthenticationState. The User is persisted using our LocalStorageService.

// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.AspNetCore.Components.Authorization;
using RebacExperiments.Shared.ApiSdk.Models;
using System.Security.Claims;

namespace RebacExperiments.Blazor.Infrastructure
{
    public class CustomAuthenticationStateProvider : AuthenticationStateProvider
    {
        private const string LocalStorageKey = "currentUser";

        private readonly LocalStorageService _localStorageService;

        public CustomAuthenticationStateProvider(LocalStorageService localStorageService)
        {
            _localStorageService = localStorageService;
        }

        public override async Task GetAuthenticationStateAsync()
        {
            var currentUser = await GetCurrentUserAsync();

            if(currentUser == null)
            {
                return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
            }

            Claim[] claims = [
                new Claim(ClaimTypes.NameIdentifier, currentUser.Id!.ToString()!),
                new Claim(ClaimTypes.Name, currentUser.LogonName!.ToString()!),
                new Claim(ClaimTypes.Email, currentUser.LogonName!.ToString()!)
            ];

            var authenticationState = new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(claims, authenticationType: nameof(CustomAuthenticationStateProvider))));

            return authenticationState;
        }

        public async Task SetCurrentUserAsync(User? currentUser)
        { 
            await _localStorageService.SetItem(LocalStorageKey, currentUser);

            NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
        }

        public Task GetCurrentUserAsync() => _localStorageService.GetItemAsync(LocalStorageKey);
    }
}

Don’t forget to register all authentication related services.

builder.Services.AddAuthorizationCore();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddSingleton();
builder.Services.AddSingleton(s => s.GetRequiredService());

In the App.razor add the CascadingAuthenticationState and AuthorizeRouteView components, so the AuthenticationState flows down to the components automagically.

@using Microsoft.AspNetCore.Components.Authorization



    
        
            
        
        
            Not found
            
                

Sorry, there's nothing at this address.

In the MainLayout, you can then use the <AuthorizeView> component, that allows to check, if a given user is authorized or not. If the User is not authorized, we are redirecting to the Login page using a <RedirectToLogin> component.

@using Microsoft.AspNetCore.Components
@using System.Runtime.InteropServices
@using RebacExperiments.Blazor.Components
@using RebacExperiments.Blazor.Components.RedirectToLogin
@namespace RebacExperiments.Blazor.Shared

Relationship-based Experiments with ASP.NET Core OData

    
        
    
    
        
    

The <RedirectToLogin> component simply uses the NavigationManager to navigate to the Login Page.

@inject NavigationManager Navigation

@code {
    protected override void OnInitialized()
    {
        var baseRelativePath = Navigation.ToBaseRelativePath(Navigation.Uri);

        if(string.IsNullOrWhiteSpace(baseRelativePath))
        {
            Navigation.NavigateTo($"Login", true);
        } else {
            Navigation.NavigateTo($"Login?returnUrl={Uri.EscapeDataString(baseRelativePath)}", true);
        }
    }
}

Now what happens, if the Web service returns a HTTP Status Code 401 (Unauthorized) and we still have the User in the Local Storage? Yes, it will be out of sync. So we need to update the AuthenticationState and clear the User information, if the service returns a HTTP Status Code 401.

This can be done by using a DelegatingHandler, that takes a dependency on our CustomAuthenticationStateProvider, and sets the current User to null. This should inform all subscribers, that we are now unauthorized to perform actions.

// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using RebacExperiments.Blazor.Shared.Logging;

namespace RebacExperiments.Blazor.Infrastructure
{
    public class UnauthorizedDelegatingHandler : DelegatingHandler
    {
        private readonly ILogger _logger;

        private readonly CustomAuthenticationStateProvider _customAuthenticationStateProvider;

        public UnauthorizedDelegatingHandler(ILogger logger, CustomAuthenticationStateProvider customAuthenticationStateProvider)
        {
            _logger = logger;
            _customAuthenticationStateProvider = customAuthenticationStateProvider;
        }

        protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            _logger.TraceMethodEntry();

            var response = await base.SendAsync(request, cancellationToken);

            if(response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
            {
                var currentUser = await _customAuthenticationStateProvider.GetCurrentUserAsync();

                if(currentUser != null)
                {
                    await _customAuthenticationStateProvider.SetCurrentUserAsync(null);
                }
            }

            return response;
        }
    }
}

You need to add the UnauthorizedDelegatingHandler to the HttpClient.

builder.Services
    .AddHttpClient(client => client.BaseAddress = new Uri("https://localhost:5000"))
    .AddHttpMessageHandler()
    .AddHttpMessageHandler();

Now let’s connect everything!

I want the Login Page to have its own layout and don’t want to use the MainLayout. So I am adding an <EmptyLayout> component.

@inherits LayoutComponentBase

@Body

This EmptyLayout is then used as the Layout for the Login Page, so I can style it to my needs. The example uses a <SimpleValidator> for validation, that has been developed in a previous article. You could easily replace it with a <DataAnnotationsValidator>, to use Blazors built-in validations.

@page "/Login"
@layout EmptyLayout

@using RebacExperiments.Shared.ApiSdk

@inject ApiClient ApiClient
@inject IStringLocalizer Loc
@inject NavigationManager NavigationManager
@inject CustomAuthenticationStateProvider AuthStateProvider

Login

Login @if(!string.IsNullOrWhiteSpace(ErrorMessage)) { }

Let’s take a look at the Login.razor.cs Code-Behind.

The Login#SignInUserAsync methods starts by logging the User in. The Server will return the HttpOnly Cookie, that’s going to be sent with every request to the API. To get the User information for populating the AuthenticationState the /Me endpoint is called. The User is the set in the AuthStateProvider and we navigate to our application.

// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using Microsoft.AspNetCore.Components;
using RebacExperiments.Shared.ApiSdk.Odata.SignInUser;
using System.ComponentModel.DataAnnotations;
using RebacExperiments.Blazor.Infrastructure;
using Microsoft.Extensions.Localization;

namespace RebacExperiments.Blazor.Pages
{
    public partial class Login
    {
        /// 
        /// Data Model for binding to the Form.
        /// 
        private sealed class InputModel
        {
            /// 
            /// Gets or sets the Email.
            /// 
            [Required]
            [EmailAddress]
            public required string Email { get; set; }

            /// 
            /// Gets or sets the Password.
            /// 
            [Required]
            [DataType(DataType.Password)]
            public required string Password { get; set; }

            /// 
            /// Gets or sets the RememberMe Flag.
            /// 
            [Required]
            public bool RememberMe { get; set; } = false;
        }

        // Default Values.
        private static class Defaults
        {
            public static class Philipp
            {
                public const string Email = "[email protected]";
                public const string Password = "5!F25GbKwU3P";
                public const bool RememberMe = true;
            }

            public static class MaxMustermann
            {
                public const string Email = "[email protected]";
                public const string Password = "5!F25GbKwU3P";
                public const bool RememberMe = true;
            }
        }


        /// 
        /// If a Return URL is given, we will navigate there after login.
        /// 
        [SupplyParameterFromQuery(Name = "returnUrl")]
        private string? ReturnUrl { get; set; }

        /// 
        /// The Model the Form is going to bind to.
        /// 
        [SupplyParameterFromForm]
        private InputModel Input { get; set; } = new()
        {
            Email = Defaults.Philipp.Email,
            Password = Defaults.Philipp.Password,
            RememberMe = Defaults.Philipp.RememberMe
        };

        /// 
        /// Error Message.
        /// 
        private string? ErrorMessage;

        /// 
        /// Signs in the User to the Service using Cookie Authentication.
        /// 
        /// 
        public async Task SignInUserAsync()
        {
            ErrorMessage = null;

            try
            {
                await ApiClient.Odata.SignInUser.PostAsync(new SignInUserPostRequestBody
                {
                    Username = Input.Email,
                    Password = Input.Password,
                    RememberMe = true
                });

                // Now refresh the Authentication State:
                var me = await ApiClient.Odata.Me.GetAsync();

                await AuthStateProvider.SetCurrentUserAsync(me);

                var navigationUrl = GetNavigationUrl();

                NavigationManager.NavigateTo(navigationUrl);
            }
            catch
            {
                ErrorMessage = Loc["Login_Failed"];
            }
        }

        private string GetNavigationUrl()
        {
            if(string.IsNullOrWhiteSpace(ReturnUrl))
            {
                return "/";
            }

            return ReturnUrl;
        }

        /// 
        /// Validates an <see cref="InputModel"/>.
        /// 
        /// InputModel to validate
        /// The list of validation errors for the EditContext model fields
        private IEnumerable ValidateInputModel(InputModel model)
        {
            if(string.IsNullOrWhiteSpace(model.Email))
            {
                yield return new ValidationError
                {
                    PropertyName = nameof(model.Email),
                    ErrorMessage = Loc.GetString("Validation_IsRequired", nameof(model.Email))
                };
            }

            if(string.IsNullOrWhiteSpace(model.Password))
            {
                yield return new ValidationError
                {
                    PropertyName = nameof(model.Password),
                    ErrorMessage = Loc.GetString("Validation_IsRequired", nameof(model.Password))
                };
            }
        }
    }
}

In the Login.razor.css we add a bit of styling.

@keyframes fade {
    from {
        opacity: 0;
    }

    to {
        opacity: 1;
    }
}

.container {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    animation: fade 0.2s ease-in-out forwards;
}

h1 {
    font-size: 35px;
    font-weight: 100;
    text-align: center;
}

Conclusion

And that’s it! You will now be able to use Cookie Authentication in your Blazor Application.

References
https://www.bytefish.de/blog/blazor_wasm_cookie_authentication.html

Custom NavLink to Support Complex URL Matching in ASP.NET Blazor

To make the “Home” NavLink selected when navigating to http://localhost:3002/ or http://localhost:3002/Monitoring/, you can adjust the Match attribute of the NavLink to use a custom match condition. Blazor does not support complex URL matching directly out of the box, but you can achieve this by creating a custom CustomNavLink component.

CustomNavLink.razor

@inject NavigationManager Navigation
@implements IDisposable

@if (IsActive)
{
    <NavLink class="nav-link active" href="" Match="NavLinkMatch.All">
        @ChildContent
    </NavLink>
}
else
{
    <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
        @ChildContent
    </NavLink>
}

@code {
    [Parameter] public RenderFragment ChildContent { get; set; } = default!;

    private bool IsActive { get; set; }

    protected override void OnInitialized()
    {
        Navigation.LocationChanged += OnLocationChanged;
    }
    
    private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
    {
        // Handle the URL change here
        IsActive = MatchUrl();
        StateHasChanged(); // Update the UI
    }
    
    private bool MatchUrl()
    {
        var uri = NavigationManager.ToAbsoluteUri(NavigationManager.Uri).PathAndQuery;

        if (uri.Equals("/"))
        {
            return true;
        }

        if (uri.StartsWith("/Monitoring", StringComparison.OrdinalIgnoreCase))
        {
            return true;
        }
        
        return false;
    }

    [Inject] private NavigationManager NavigationManager { get; set; } = default!;
    public void Dispose()
    {
        Navigation.LocationChanged -= OnLocationChanged;
    }

}
<CustomNavLink>
    <span class="bi bi-house-door-fill-nav-menu" aria-hidden="true"></span> Home
</CustomNavLink>

 

Handle an Unknown Number of Route Parameters in ASP.NET Blazor

To handle an unknown number of route parameters in ASP.NET Blazor, you can define a single route with a wildcard parameter to capture the entire path.

Here’s how you can achieve this in Blazor:

  1. Define the Route: Use a route parameter in the @page directive. The wildcard parameter is specified using an asterisk (*). This allows you to capture the entire path as a single string.
  2. Process the Parameters: Once you have the full path, you can split it into individual parameters and process them as needed.
@page "/Monitoring/{**Parameters}"
@inject NavigationManager Navigation


<h3>Monitoring</h3>

<ul>
    @foreach (var param in RouteParameters)
    {
    <li>@param</li>
    }
</ul>
[Parameter]
public string Parameters { get; set; }

private List<string> RouteParameters { get; set; } = new();

protected override void OnParametersSet()
{
    var uri = new Uri(Navigation.Uri);
    var path = uri.AbsolutePath;

    // Remove the initial part of the path
    var trimmedPath = path.Substring("/Monitoring/".Length);

    // Split the remaining part of the path by '/'
    RouteParameters = trimmedPath.Split('/', StringSplitOptions.RemoveEmptyEntries).ToList();
}

 

Update Kernel arguments for better performance in Ubuntu 24.04

Here’s how you can apply these kernel parameters in Ubuntu 24.04:

  1. Edit GRUB Configuration File:

    Open the GRUB configuration file in a text editor. For example, you can use nano:

    sudo nano /etc/default/grub
    
  2. Add Kernel Parameters:

    Find the line that starts with GRUB_CMDLINE_LINUX_DEFAULT and add your parameters to the list. It should look something like this:

    GRUB_CMDLINE_LINUX_DEFAULT="quiet splash mitigations=off mem_sleep_default=s2idle nvidia-drm.modeset=1"
    

    Ensure that all parameters are enclosed within the same set of quotes.

  3. Update GRUB:

    After saving the changes, you need to update the GRUB configuration:

    sudo update-grub
    
  4. Reboot:

    Finally, reboot your system to apply the changes:

    sudo reboot
    

This will apply the desired kernel parameters on all boot entries.

Install Android ADB on Ubuntu Linux

To install Android ADB (Android Debug Bridge) on Ubuntu, you can follow these steps:

Identify the Vendor ID:

Connect your Android device to your computer and run the following command to identify the vendor ID:

lsusb

Look for the line that corresponds to your Android device. The vendor ID is the first part of the ID after ID, for example, 18d1 for Google.

Update Your Package List: Open your terminal and update the package list to ensure you have the latest information on the newest versions of packages and their dependencies.

sudo apt update

Install ADB: You can install the ADB package using the following command:

sudo apt install android-tools-adb

Verify Installation: After installation, you can verify that ADB is installed correctly by checking its version:

adb version

Add Your User to the Plugdev Group (Optional): This step ensures that you can use ADB without root permissions. It’s especially useful when working with devices.

sudo usermod -aG plugdev $USER

Then, log out and log back in to apply the changes.

Set Up Udev Rules (Optional): To communicate with your Android device over USB, you might need to set up udev rules. Create a new udev rules file:

sudo nano /etc/udev/rules.d/51-android.rules

Add the following line to the file, replacing xxxx with your device’s USB vendor ID (you can find a list of these IDs online or in the documentation for your device):

SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", MODE="0666", GROUP="plugdev"

Save and close the file, then reload the udev rules:

sudo udevadm control --reload-rules

Now, you should have ADB installed and configured on your Ubuntu system. You can connect your Android device and start using ADB commands.

Filter a list of objects by a property in Python

To filter a list of objects by a property in Python, you can use a list comprehension. Here is an example demonstrating how to filter a list of objects by a specific property:

# Define a sample class
class Employee:
    def __init__(self, name, department, salary):
        self.name = name
        self.department = department
        self.salary = salary

# Create a list of Employee objects
employees = [
    Employee("Alice", "HR", 60000),
    Employee("Bob", "IT", 75000),
    Employee("Charlie", "HR", 55000),
    Employee("David", "IT", 80000),
    Employee("Eve", "Finance", 65000)
]

# Filter the list by department
filtered_employees = [employee for employee in employees if employee.department == "HR"]

# Print the filtered list
for employee in filtered_employees:
    print(f"Name: {employee.name}, Department: {employee.department}, Salary: {employee.salary}")

In this example, we define a class Employee and create a list of Employee objects. We then use a list comprehension to filter the list by the department property, keeping only the employees who work in the “HR” department. Finally, we print the details of the filtered employees.

You can modify the filter condition inside the list comprehension to filter by any other property or criteria.

Migrating a Git repository from HTTPS to SSH

Step 1: Verify SSH Keys

First, ensure you have SSH keys set up on your machine and added to your GitHub account.

  1. Check for existing SSH keys:
    ls -al ~/.ssh

    Look for files named id_rsa and id_rsa.pub or similar.

  2. Generate a new SSH key (if necessary):
    ssh-keygen -t rsa -b 4096 -C "[email protected]"
    

    Follow the prompts to save the key (default location is fine).

  3. Add your SSH key to the SSH agent:
    eval "$(ssh-agent -s)"
    ssh-add ~/.ssh/id_rsa
    
  4. Add the SSH key to your GitHub account: Copy the contents of your public key file to the clipboard:
    cat ~/.ssh/id_rsa.pub
    

    Then, add it to your GitHub account by going to Settings > SSH and GPG keys > New SSH key.

Step 2: Update Remote URL

  1. Navigate to your local repository:
    cd /path/to/your/repo
    
  2. Get the current remote URL:
    git remote -v
    
  3. Update the remote URL to use SSH: Replace origin with the name of your remote if it’s different.
    git remote set-url origin [email protected]:username/repo.git
    

    Replace username with your GitHub username and repo with the name of your repository.

  4. Verify the change:
    git remote -v
    

    You should see the SSH URL listed.

Step 3: Test the Connection

  1. Test the SSH connection:

    You should see a success message like “Hi username! You’ve successfully authenticated, but GitHub does not provide shell access.”

  2. Fetch from the remote to ensure everything is working:
    git fetch
    

If everything is set up correctly, your repository should now be using SSH instead of HTTPS.

Install Android ADB on Fedora Linux

sudo dnf install android-tools

Create a plugdev group (if it doesn’t already exist) and add the user to it:

sudo groupadd plugdev
sudo usermod -aG plugdev $LOGNAME

Log out and log back in for the group change to take effect. Use the id command to verify that you are in the plugdev group:

id

Identify the Vendor ID:

Connect your Android device to your computer and run the following command to identify the vendor ID:

lsusb

Look for the line that corresponds to your Android device. The vendor ID is the first part of the ID after ID, for example, 18d1 for Google.

Create a udev Rule:

Create a file for the udev rule:

sudo nano /etc/udev/rules.d/51-android.rules

Add the Following Content:

In the file, add a rule to grant the necessary permissions. Replace YOUR-VENDOR-ID with your device’s vendor ID. Here’s an example rule:

SUBSYSTEM=="usb", ATTR{idVendor}=="YOUR-VENDOR-ID", MODE="0666", GROUP="plugdev"

For example, if the vendor ID is 18d1 (Google):

SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", MODE="0666", GROUP="plugdev"

Change File Permissions:

Change the permissions of the file to make it readable:

sudo chmod a+r /etc/udev/rules.d/51-android.rules

Reload udev rules:

sudo udevadm control --reload-rules
sudo udevadm trigger

Restart your machine.

Check ADB Devices:

Verify that your device is recognized correctly:

adb devices

References
https://developer.android.com/studio/run/device