Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions Flow.ConfluenceSearch.Test/AuthHeaderBuilderTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
using System.Text;
using Flow.ConfluenceSearch.Settings;
using Shouldly;

namespace Flow.ConfluenceSearch.Test;

public class AuthHeaderBuilderTest
{
[Fact]
public void EmailAndToken_AreCombinedAsBasicCredential()
{
var header = AuthHeaderBuilder.Build("user@example.com", "secret-token");

Decode(header).ShouldBe("user@example.com:secret-token");
}

[Fact]
public void EmptyEmail_FallsBackToTokenAsIs()
{
var header = AuthHeaderBuilder.Build("", "personal-access-token");

Decode(header).ShouldBe("personal-access-token");
}

[Fact]
public void EmptyEmail_PreservesLegacyEmailColonTokenInTokenField()
{
var header = AuthHeaderBuilder.Build("", "user@example.com:secret-token");

Decode(header).ShouldBe("user@example.com:secret-token");
}

[Fact]
public void NullInputs_ProduceEmptyCredential()
{
var header = AuthHeaderBuilder.Build(null, null);

Decode(header).ShouldBe(string.Empty);
}

[Fact]
public void Whitespace_IsTrimmedFromBothFields()
{
var header = AuthHeaderBuilder.Build(" user@example.com ", " secret-token ");

Decode(header).ShouldBe("user@example.com:secret-token");
}

private static string Decode(string base64) =>
Encoding.UTF8.GetString(Convert.FromBase64String(base64));
}
3 changes: 1 addition & 2 deletions Flow.ConfluenceSearch/ServiceProvider.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System.Net.Http;
using System.Text;
using Flow.ConfluenceSearch.ConfluenceClient;
using Flow.ConfluenceSearch.Search;
using Flow.ConfluenceSearch.Settings;
Expand Down Expand Up @@ -28,7 +27,7 @@ PluginSettings settings
BaseAddress = new Uri($"{config.BaseUrl}/"),
Timeout = TimeSpan.FromSeconds(Math.Clamp(config.Timeout.TotalSeconds, 3, 30)),
};
var basic = Convert.ToBase64String(Encoding.UTF8.GetBytes(config.ApiToken));
var basic = AuthHeaderBuilder.Build(config.Email, config.ApiToken);
httpClient.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Basic", basic);
httpClient.DefaultRequestHeaders.Accept.ParseAdd("application/json");
Expand Down
18 changes: 18 additions & 0 deletions Flow.ConfluenceSearch/Settings/AuthHeaderBuilder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.Text;

namespace Flow.ConfluenceSearch.Settings;

internal static class AuthHeaderBuilder
{
public static string Build(string? email, string? apiToken)
{
var trimmedEmail = email?.Trim() ?? string.Empty;
var trimmedToken = apiToken?.Trim() ?? string.Empty;

var credential = trimmedEmail.Length > 0
? $"{trimmedEmail}:{trimmedToken}"
: trimmedToken;

return Convert.ToBase64String(Encoding.UTF8.GetBytes(credential));
}
}
2 changes: 2 additions & 0 deletions Flow.ConfluenceSearch/Settings/PluginSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ public PluginSettings()
{
BaseUrl = "https://www.example.com";
Timeout = TimeSpan.FromSeconds(10);
Email = string.Empty;
ApiToken = string.Empty;
MaxResults = 10;
DefaultSpaces = new List<string>();
}

public string BaseUrl { get; set; }
public TimeSpan Timeout { get; set; }
public string Email { get; set; }
public string ApiToken { get; set; }
public List<string> DefaultSpaces { get; set; }
public int MaxResults { get; set; }
Expand Down
47 changes: 40 additions & 7 deletions Flow.ConfluenceSearch/Settings/SettingsView.xaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<UserControl
<UserControl
x:Class="Flow.ConfluenceSearch.Settings.SettingsView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Expand All @@ -16,6 +16,8 @@
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
Expand Down Expand Up @@ -44,11 +46,28 @@
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
VerticalAlignment="Center"
FontSize="14"
Text="API Token (Cloud: email:token)"
Text="Email (Cloud only)"
/>
<TextBox
Grid.Row="1"
Grid.Column="1"
MaxWidth="600"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
Text="{Binding Settings.Email, UpdateSourceTrigger=PropertyChanged}"
/>
<TextBlock
Grid.Row="2"
Grid.Column="0"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
VerticalAlignment="Center"
FontSize="14"
Text="API Token"
/>
<PasswordBox
x:Name="MaxDecimalPlaces"
Grid.Row="1"
Grid.Row="2"
Grid.Column="1"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
HorizontalAlignment="Left"
Expand All @@ -57,15 +76,29 @@
PasswordChanged="TokenBox_PasswordChanged"
/>
<TextBlock
Grid.Row="2"
Grid.Row="3"
Grid.Column="1"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
HorizontalAlignment="Left"
VerticalAlignment="Center"
>
<Hyperlink
NavigateUri="https://id.atlassian.com/manage-profile/security/api-tokens"
RequestNavigate="Hyperlink_RequestNavigate"
>
Create a Confluence Cloud API token
</Hyperlink>
</TextBlock>
<TextBlock
Grid.Row="4"
Grid.Column="0"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
VerticalAlignment="Center"
FontSize="14"
Text="Maximum Results"
/>
<TextBox
Grid.Row="2"
Grid.Row="4"
Grid.Column="1"
MaxWidth="300"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
Expand All @@ -75,15 +108,15 @@
PreviewTextInput="NumberValidationTextBox"
/>
<TextBlock
Grid.Row="3"
Grid.Row="5"
Grid.Column="0"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
VerticalAlignment="Center"
FontSize="14"
Text="Default Spaces (separate multiple with comma)"
/>
<TextBox
Grid.Row="3"
Grid.Row="5"
Grid.Column="1"
MaxWidth="800"
Margin="{StaticResource SettingPanelItemRightTopBottomMargin}"
Expand Down
10 changes: 9 additions & 1 deletion Flow.ConfluenceSearch/Settings/SettingsView.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
using System.Windows;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Navigation;

namespace Flow.ConfluenceSearch.Settings;

Expand All @@ -21,4 +23,10 @@ private void NumberValidationTextBox(object sender, TextCompositionEventArgs e)
{
e.Handled = !int.TryParse(e.Text, out _);
}

private void Hyperlink_RequestNavigate(object sender, RequestNavigateEventArgs e)
{
Process.Start(new ProcessStartInfo(e.Uri.AbsoluteUri) { UseShellExecute = true });
e.Handled = true;
}
}
2 changes: 1 addition & 1 deletion Flow.ConfluenceSearch/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"Name": "Confluence Search",
"Description": "Search for and open Confluence pages directly from Flow Launcher.",
"Author": "Jan Haug",
"Version": "1.1.0",
"Version": "1.2.0",
"Language": "csharp",
"Website": "https://github.com/haugjan/Flow.ConfluenceSearch",
"IcoPath": "Images/icon.png",
Expand Down
17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,24 +56,27 @@ The plugin is configured through Flow Launcher's settings interface:
| Setting | Description | Example |
|---------|-------------|---------|
| **Base URL** | Your Confluence instance URL | `https://yourcompany.atlassian.net` |
| **API Token** | Cloud: `email:token`. Server / Data Center: token alone. See below. | `your-email@example.com:ATATT3xFfGF0...` |
| **Email** | Cloud only: the address you sign in to Atlassian with. Leave empty on Server / Data Center. | `your-email@example.com` |
| **API Token** | The API token (Cloud) or personal access token (Server / DC). See below. | `ATATT3xFfGF0...` |
| **Timeout** | Request timeout in seconds | `10` |
| **Max Results** | Maximum number of results to display | `10` |
| **Default Spaces** | Space keys to search by default | `["SPACE1", "SPACE2"]` |

### Creating a Confluence API Token

1. Go to [Atlassian Account Settings](https://id.atlassian.com/manage-profile/security/api-tokens).
1. Go to [Atlassian Account Settings](https://id.atlassian.com/manage-profile/security/api-tokens) (also linked from the plugin's settings panel).
2. Click "Create API token".
3. Give it a descriptive name (e.g., "Flow Launcher Plugin").
4. Copy the generated token.

Then paste it into the plugin's **API Token** field, depending on your Confluence flavour:
Then configure the plugin depending on your Confluence flavour:

- **Confluence Cloud** (`*.atlassian.net`): paste `your-email@example.com:<api-token>` — the address you use to sign in to Atlassian, a colon, and the token, with no spaces. The plugin sends this as HTTP Basic auth, which is what Confluence Cloud requires.
- **Confluence Server / Data Center**: paste the personal access token alone.
- **Confluence Cloud** (`*.atlassian.net`): put the address you use to sign in to Atlassian into **Email**, and the API token into **API Token**. The plugin combines them as `email:token` and sends HTTP Basic auth, which is what Confluence Cloud requires.
- **Confluence Server / Data Center**: leave **Email** empty and paste the personal access token into **API Token** alone.

If authentication fails the plugin shows `Confluence authentication failed (HTTP 401)` in the result list — the most common cause is missing the `email:` prefix on Confluence Cloud.
> **Backwards compatibility:** earlier versions had a single field where you pasted `email:token`. That still works — if **Email** is empty and **API Token** already contains a colon, the value is used as-is. New configurations should use the two separate fields.

If authentication fails the plugin shows `Confluence authentication failed (HTTP 401)` in the result list — the most common cause on Cloud is a missing or mistyped **Email**.

## Usage

Expand Down Expand Up @@ -154,7 +157,7 @@ If you configure default spaces in the settings, searches will automatically be

### Authentication Errors
- Regenerate your API token in Atlassian Account Settings.
- Ensure you're using your email address as the username (for Atlassian Cloud).
- On Atlassian Cloud, ensure the **Email** field is filled in with your sign-in address.
- Check that your account has appropriate permissions.

## Development
Expand Down
Loading