Skip to content

Commit 866302f

Browse files
committed
initial commit
0 parents  commit 866302f

38 files changed

Lines changed: 10496 additions & 0 deletions

.gitignore

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
.output
12+
stats.html
13+
stats-*.json
14+
.wxt
15+
web-ext.config.ts
16+
17+
# Editor directories and files
18+
.vscode/*
19+
!.vscode/extensions.json
20+
.idea
21+
.DS_Store
22+
*.suo
23+
*.ntvs*
24+
*.njsproj
25+
*.sln
26+
*.sw?

.vscode/extensions.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"recommendations": ["Vue.volar"]
3+
}

LICENSE.md

Lines changed: 650 additions & 0 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Solidtime Browser Extension
2+
3+
The official browser extension for [Solidtime](https://www.solidtime.io) - the modern open-source time tracker.
4+
5+
Track time directly from your project management tools with seamless integration into Linear, Jira, and Plane.
6+
7+
## Download
8+
9+
- **Chrome/Edge/Brave:** [Chrome Web Store](https://chromewebstore.google.com/detail/solidtime/hpanifeankiobmgbemnhjmhpjeebdhdd)
10+
- **Firefox:** [Firefox Add-ons](https://addons.mozilla.org/en-US/firefox/addon/solidtime/)
11+
12+
## Features
13+
14+
- ⏱️ **Quick Time Tracking** - Start and stop timers directly from issue pages
15+
- 🔗 **Platform Integration** - Works seamlessly with Linear, Jira, and Plane
16+
- 🎯 **Issue Context** - Automatically captures issue IDs and titles in your time entries
17+
- 🏢 **Organization Management** - Switch between multiple organizations
18+
- 🔒 **Secure OAuth** - Safe authentication with PKCE
19+
- 🌐 **Self-Hosted Support** - Connect to your own Solidtime instance
20+
21+
## Supported Platforms
22+
23+
- Linear
24+
- Jira
25+
- Plane
26+
27+
More coming soon!
28+
29+
## Setup
30+
31+
### For Self-Hosted Instances
32+
33+
If you're using a self-hosted Solidtime instance, see the [Docker setup guide](https://docs.solidtime.io/self-hosting/guides/docker) for instructions on configuring browser extension access.
34+
35+
## Development
36+
37+
### Prerequisites
38+
39+
- Node.js 18+
40+
- npm or pnpm
41+
42+
### Installation
43+
44+
```bash
45+
npm install
46+
```
47+
48+
### Development
49+
50+
```bash
51+
# Chrome
52+
npm run dev
53+
54+
# Firefox
55+
npm run dev:firefox
56+
```
57+
58+
### Build
59+
60+
```bash
61+
# Chrome
62+
npm run build
63+
64+
# Firefox
65+
npm run build:firefox
66+
```
67+
68+
## License
69+
70+
See the main [Solidtime repository](https://github.com/solidtime-io/solidtime) for license information.

assets/vue.svg

Lines changed: 1 addition & 0 deletions
Loading

entrypoints/background.ts

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
export default defineBackground(() => {
2+
// OAuth state
3+
let oauthState = "";
4+
let oauthVerifier = "";
5+
let oauthChallenge = "";
6+
7+
// Helper functions
8+
function sha256(plain: string) {
9+
const encoder = new TextEncoder();
10+
const data = encoder.encode(plain);
11+
return crypto.subtle.digest("SHA-256", data);
12+
}
13+
14+
function base64urlencode(a: ArrayBuffer) {
15+
let str = "";
16+
const bytes = new Uint8Array(a);
17+
const len = bytes.byteLength;
18+
for (let i = 0; i < len; i++) {
19+
str += String.fromCharCode(bytes[i]);
20+
}
21+
return btoa(str).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
22+
}
23+
24+
function createRandomString(num: number) {
25+
return [...Array(num)].map(() => Math.random().toString(36)[2]).join("");
26+
}
27+
28+
function getRedirectUrl() {
29+
const extensionId = browser.runtime.id;
30+
if (navigator.userAgent.includes("Firefox")) {
31+
return `https://${extensionId}.extensions.allizom.org/`;
32+
}
33+
return `https://${extensionId}.chromiumapp.org/`;
34+
}
35+
36+
// Listen for messages from popup or content scripts
37+
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
38+
if (message.type === "START_OAUTH_FLOW") {
39+
// Handle entire OAuth flow in background
40+
const { endpoint, clientId } = message.payload;
41+
42+
(async () => {
43+
try {
44+
// Initialize PKCE
45+
oauthState = createRandomString(40);
46+
oauthVerifier = createRandomString(128);
47+
const hashed = await sha256(oauthVerifier);
48+
oauthChallenge = base64urlencode(hashed);
49+
50+
const redirectUrl = getRedirectUrl();
51+
const loginUrl =
52+
endpoint +
53+
"/oauth/authorize?client_id=" +
54+
clientId +
55+
"&redirect_uri=" +
56+
encodeURIComponent(redirectUrl) +
57+
"&response_type=code&state=" +
58+
oauthState +
59+
"&code_challenge=" +
60+
oauthChallenge +
61+
"&code_challenge_method=S256&scope=*";
62+
63+
// Launch OAuth flow
64+
browser.identity.launchWebAuthFlow(
65+
{
66+
url: loginUrl,
67+
interactive: true,
68+
},
69+
async (responseUrl) => {
70+
if (browser.runtime.lastError) {
71+
console.error("OAuth error:", browser.runtime.lastError);
72+
sendResponse({
73+
success: false,
74+
error: browser.runtime.lastError.message || "OAuth failed",
75+
});
76+
return;
77+
}
78+
79+
if (!responseUrl) {
80+
sendResponse({ success: false, error: "No response URL" });
81+
return;
82+
}
83+
84+
try {
85+
const url = new URL(responseUrl);
86+
const code = url.searchParams.get("code");
87+
const responseState = url.searchParams.get("state");
88+
const error = url.searchParams.get("error");
89+
90+
if (error) {
91+
throw new Error(`OAuth error: ${error}`);
92+
}
93+
94+
if (responseState !== oauthState || !code) {
95+
throw new Error("Invalid state or missing code");
96+
}
97+
98+
// Exchange code for tokens
99+
const tokenResponse = await fetch(endpoint + "/oauth/token", {
100+
method: "POST",
101+
headers: {
102+
"Content-Type": "application/x-www-form-urlencoded",
103+
},
104+
body: new URLSearchParams({
105+
grant_type: "authorization_code",
106+
client_id: clientId,
107+
redirect_uri: redirectUrl,
108+
code_verifier: oauthVerifier,
109+
code: code,
110+
}),
111+
});
112+
113+
if (!tokenResponse.ok) {
114+
throw new Error("Token exchange failed");
115+
}
116+
117+
const tokens = await tokenResponse.json();
118+
119+
// Store tokens in chrome.storage
120+
await browser.storage.local.set({
121+
access_token: tokens.access_token,
122+
refresh_token: tokens.refresh_token,
123+
});
124+
125+
sendResponse({
126+
success: true,
127+
data: {
128+
access_token: tokens.access_token,
129+
refresh_token: tokens.refresh_token,
130+
},
131+
});
132+
} catch (error) {
133+
console.error("OAuth error:", error);
134+
sendResponse({
135+
success: false,
136+
error:
137+
error instanceof Error ? error.message : "Unknown error",
138+
});
139+
}
140+
},
141+
);
142+
} catch (error) {
143+
console.error("OAuth initialization error:", error);
144+
sendResponse({
145+
success: false,
146+
error: error instanceof Error ? error.message : "Unknown error",
147+
});
148+
}
149+
})();
150+
151+
return true; // Will respond asynchronously
152+
}
153+
154+
if (message.type === "REFRESH_TOKEN") {
155+
const { endpoint, clientId, refreshToken } = message.payload;
156+
157+
fetch(endpoint + "/oauth/token", {
158+
method: "POST",
159+
headers: {
160+
"Content-Type": "application/x-www-form-urlencoded",
161+
},
162+
body: new URLSearchParams({
163+
grant_type: "refresh_token",
164+
client_id: clientId,
165+
refresh_token: refreshToken,
166+
}),
167+
})
168+
.then(async (response) => {
169+
if (!response.ok) {
170+
throw new Error("Failed to refresh token");
171+
}
172+
return response.json();
173+
})
174+
.then((data) => {
175+
sendResponse({ success: true, data });
176+
})
177+
.catch((error) => {
178+
console.error("Token refresh error:", error);
179+
sendResponse({ success: false, error: error.message });
180+
});
181+
182+
return true;
183+
}
184+
185+
return false;
186+
});
187+
});

0 commit comments

Comments
 (0)