Skip to content

Commit 17874ea

Browse files
committed
feat(organization): refactor GitHub credential management and UI integration
1 parent 289f551 commit 17874ea

10 files changed

Lines changed: 261 additions & 186 deletions

File tree

app/Domains/Organization/Http/Controllers/GitCredentialController.php

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public function index(Organization $organization): Response
3030
'organization' => OrganizationData::fromModel($organization),
3131
'credentials' => $credentials,
3232
'providers' => GitProvider::options(),
33-
'hasGitHubConnected' => auth()->user()?->hasGitHubConnected(),
33+
'githubConnectUrl' => route('auth.github.connect', $organization),
3434
]);
3535
}
3636

@@ -44,29 +44,11 @@ public function store(StoreGitCredentialRequest $request, Organization $organiza
4444
->with('error', 'Credentials for this provider already exist. Please update the existing credentials instead.');
4545
}
4646

47-
$user = $request->user();
48-
$isOAuth = $request->validated('source') === 'oauth';
49-
50-
if ($isOAuth && $provider === GitProvider::GitHub) {
51-
if (! $user?->hasGitHubConnected()) {
52-
return redirect()
53-
->route('organizations.settings.git-credentials.index', $organization)
54-
->with('error', 'You must connect your GitHub account first.');
55-
}
56-
57-
OrganizationGitCredential::create([
58-
'organization_uuid' => $organization->uuid,
59-
'provider' => $provider,
60-
'credentials' => ['token' => $user->github_token],
61-
'source_user_uuid' => $user->uuid,
62-
]);
63-
} else {
64-
OrganizationGitCredential::create([
65-
'organization_uuid' => $organization->uuid,
66-
'provider' => $provider,
67-
'credentials' => $request->validated('credentials'),
68-
]);
69-
}
47+
OrganizationGitCredential::create([
48+
'organization_uuid' => $organization->uuid,
49+
'provider' => $provider,
50+
'credentials' => $request->validated('credentials'),
51+
]);
7052

7153
return redirect()
7254
->route('organizations.settings.git-credentials.index', $organization)

app/Domains/Organization/Http/Requests/StoreGitCredentialRequest.php

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,11 @@ public function authorize(): bool
2626
public function rules(): array
2727
{
2828
$provider = GitProvider::tryFrom($this->input('provider'));
29-
$isOAuth = $this->input('source') === 'oauth';
3029

3130
$baseRules = [
3231
'provider' => ['required', 'string', Rule::enum(GitProvider::class)],
33-
'source' => ['nullable', 'string', 'in:oauth'],
3432
];
3533

36-
if ($isOAuth && $provider === GitProvider::GitHub) {
37-
return $baseRules;
38-
}
39-
4034
$credentialRules = match ($provider) {
4135
GitProvider::GitHub => [
4236
'credentials.token' => ['required', 'string'],

app/Http/Controllers/Auth/GitHubController.php

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace App\Http\Controllers\Auth;
44

55
use App\Http\Controllers\Controller;
6+
use App\Models\Organization;
67
use App\Models\OrganizationGitCredential;
78
use App\Models\User;
89
use Illuminate\Http\RedirectResponse;
@@ -15,7 +16,7 @@ class GitHubController extends Controller
1516
public function redirect(): SymfonyRedirectResponse
1617
{
1718
return Socialite::driver('github')
18-
->scopes(['user:email', 'repo', 'read:org'])
19+
->scopes(['user:email'])
1920
->redirect();
2021
}
2122

@@ -80,6 +81,62 @@ public function callback(): RedirectResponse
8081
return redirect()->intended(route('dashboard'));
8182
}
8283

84+
public function connect(Organization $organization): SymfonyRedirectResponse
85+
{
86+
session(['github_connect_organization' => $organization->slug]);
87+
88+
return Socialite::driver('github')
89+
->scopes(['repo', 'read:org'])
90+
->redirect();
91+
}
92+
93+
public function connectCallback(): RedirectResponse
94+
{
95+
$organizationSlug = session()->pull('github_connect_organization');
96+
97+
if (! $organizationSlug) {
98+
return redirect()->route('dashboard')->with('error', 'No organization found for GitHub connection.');
99+
}
100+
101+
$organization = Organization::where('slug', $organizationSlug)->firstOrFail();
102+
103+
try {
104+
$githubUser = Socialite::driver('github')->user();
105+
} catch (\Exception) {
106+
return redirect()
107+
->route('organizations.settings.git-credentials.index', $organization)
108+
->with('error', 'GitHub authentication failed. Please try again.');
109+
}
110+
111+
$user = Auth::user();
112+
113+
$user->update([
114+
'github_token' => $githubUser->token,
115+
'github_id' => $user->github_id ?? $githubUser->getId(),
116+
'github_nickname' => $githubUser->getNickname(),
117+
'avatar_url' => $githubUser->getAvatar(),
118+
]);
119+
120+
if ($organization->gitCredentials()->where('provider', 'github')->exists()) {
121+
return redirect()
122+
->route('organizations.settings.git-credentials.index', $organization)
123+
->with('error', 'GitHub credentials already exist for this organization. Please update the existing credentials instead.');
124+
}
125+
126+
OrganizationGitCredential::create([
127+
'organization_uuid' => $organization->uuid,
128+
'provider' => 'github',
129+
'credentials' => ['token' => $githubUser->token],
130+
'source_user_uuid' => $user->uuid,
131+
]);
132+
133+
$this->refreshOrganizationCredentials($user);
134+
135+
return redirect()
136+
->route('organizations.settings.git-credentials.index', $organization)
137+
->with('status', 'GitHub credentials connected successfully.');
138+
}
139+
83140
private function refreshOrganizationCredentials(User $user): void
84141
{
85142
OrganizationGitCredential::where('source_user_uuid', $user->uuid)

resources/js/components/git-credential-dialog.tsx

Lines changed: 59 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@ import { Label } from '@/components/ui/label';
1616
import { Separator } from '@/components/ui/separator';
1717
import { Textarea } from '@/components/ui/textarea';
1818
import { Form } from '@inertiajs/react';
19-
import { useState } from 'react';
2019

2120
type GitCredentialData =
2221
App.Domains.Organization.Contracts.Data.GitCredentialData;
@@ -27,7 +26,7 @@ interface GitCredentialDialogProps {
2726
credential?: GitCredentialData | null;
2827
provider: string;
2928
providers: Record<string, string>;
30-
hasGitHubConnected?: boolean;
29+
githubConnectUrl?: string;
3130
isOpen: boolean;
3231
onClose: () => void;
3332
}
@@ -38,15 +37,14 @@ export default function GitCredentialDialog({
3837
credential,
3938
provider,
4039
providers,
41-
hasGitHubConnected,
40+
githubConnectUrl,
4241
isOpen,
4342
onClose,
4443
}: GitCredentialDialogProps) {
4544
const isEditing = !!credential;
4645
const providerLabel = providers[provider] || provider;
47-
const [useOAuth, setUseOAuth] = useState(false);
4846
const showOAuthOption =
49-
provider === 'github' && hasGitHubConnected && !isEditing;
47+
provider === 'github' && !!githubConnectUrl && !isEditing;
5048

5149
const getGitHubTokenUrl = (): string => {
5250
const description = organizationName
@@ -65,86 +63,68 @@ export default function GitCredentialDialog({
6563
{showOAuthOption && (
6664
<>
6765
<div className="grid space-y-2">
68-
<Button
69-
type={useOAuth ? 'button' : 'button'}
70-
variant={
71-
useOAuth ? 'default' : 'secondary'
72-
}
73-
className="w-full"
74-
onClick={() => setUseOAuth(true)}
75-
>
76-
<svg
77-
className="h-4 w-4"
78-
viewBox="0 0 24 24"
79-
fill="currentColor"
80-
>
81-
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
82-
</svg>
83-
Use my GitHub account
66+
<Button asChild className="w-full">
67+
<a href={githubConnectUrl}>
68+
<svg
69+
className="h-4 w-4"
70+
viewBox="0 0 24 24"
71+
fill="currentColor"
72+
>
73+
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
74+
</svg>
75+
Use my GitHub account
76+
</a>
8477
</Button>
85-
<p className="text-xs text-muted-foreground">
86-
Uses the token from your connected
87-
GitHub account. The token will be
78+
<p className="text-sm text-muted-foreground">
79+
Authorizes Pricore to access your GitHub
80+
repositories. The token will be
8881
automatically refreshed when you sign in
8982
via GitHub.
9083
</p>
9184
</div>
9285

93-
{useOAuth && (
94-
<input
95-
type="hidden"
96-
name="source"
97-
value="oauth"
98-
/>
99-
)}
100-
101-
{!useOAuth && (
102-
<div className="relative">
103-
<div className="absolute inset-0 flex items-center">
104-
<Separator />
105-
</div>
106-
<div className="relative flex justify-center text-xs uppercase">
107-
<span className="bg-background px-2 text-muted-foreground">
108-
or enter a token manually
109-
</span>
110-
</div>
86+
<div className="relative">
87+
<div className="absolute inset-0 flex items-center">
88+
<Separator />
89+
</div>
90+
<div className="relative flex justify-center text-xs uppercase">
91+
<span className="bg-background px-2 text-muted-foreground">
92+
or enter a token manually
93+
</span>
11194
</div>
112-
)}
95+
</div>
11396
</>
11497
)}
11598

116-
{!useOAuth && (
117-
<div className="grid space-y-2">
118-
<Label htmlFor="token">
119-
Personal Access Token{' '}
120-
<span className="text-red-500">*</span>
121-
</Label>
122-
<Input
123-
id="token"
124-
name="credentials[token]"
125-
type="password"
126-
required
127-
placeholder="ghp_xxxxxxxxxxxx"
128-
autoFocus
129-
/>
130-
<p className="text-xs text-muted-foreground">
131-
Create a personal access token in{' '}
132-
<a
133-
href={getGitHubTokenUrl()}
134-
target="_blank"
135-
rel="noopener noreferrer"
136-
className="text-primary hover:underline"
137-
>
138-
GitHub Settings → Developer settings →
139-
Personal access tokens
140-
</a>
141-
. Make sure to select the{' '}
142-
<strong>repo</strong> scope (and{' '}
143-
<strong>read:org</strong> if accessing
144-
organization repositories).
145-
</p>
146-
</div>
147-
)}
99+
<div className="grid space-y-2">
100+
<Label htmlFor="token">
101+
Personal Access Token{' '}
102+
<span className="text-red-500">*</span>
103+
</Label>
104+
<Input
105+
id="token"
106+
name="credentials[token]"
107+
type="password"
108+
required
109+
placeholder="ghp_xxxxxxxxxxxx"
110+
autoFocus
111+
/>
112+
<p className="text-sm text-muted-foreground">
113+
Create a personal access token in{' '}
114+
<a
115+
href={getGitHubTokenUrl()}
116+
target="_blank"
117+
rel="noopener noreferrer"
118+
className="text-primary hover:underline"
119+
>
120+
GitHub Settings → Developer settings →
121+
Personal access tokens
122+
</a>
123+
. Make sure to select the <strong>repo</strong>{' '}
124+
scope (and <strong>read:org</strong> if
125+
accessing organization repositories).
126+
</p>
127+
</div>
148128
</>
149129
);
150130
case 'gitlab':
@@ -163,7 +143,7 @@ export default function GitCredentialDialog({
163143
placeholder="glpat-xxxxxxxxxxxx"
164144
autoFocus
165145
/>
166-
<p className="text-xs text-muted-foreground">
146+
<p className="text-sm text-muted-foreground">
167147
Create a personal access token in{' '}
168148
<a
169149
href="https://gitlab.com/-/profile/personal_access_tokens"
@@ -183,7 +163,7 @@ export default function GitCredentialDialog({
183163
type="url"
184164
placeholder="https://gitlab.com"
185165
/>
186-
<p className="text-xs text-muted-foreground">
166+
<p className="text-sm text-muted-foreground">
187167
Leave empty for GitLab.com, or enter your
188168
self-hosted GitLab instance URL
189169
</p>
@@ -217,7 +197,7 @@ export default function GitCredentialDialog({
217197
required
218198
placeholder="xxxxxxxxxxxx"
219199
/>
220-
<p className="text-xs text-muted-foreground">
200+
<p className="text-sm text-muted-foreground">
221201
Create an app password in{' '}
222202
<a
223203
href="https://bitbucket.org/account/settings/app-passwords/"
@@ -248,7 +228,7 @@ export default function GitCredentialDialog({
248228
placeholder="-----BEGIN OPENSSH PRIVATE KEY-----&#10;...&#10;-----END OPENSSH PRIVATE KEY-----"
249229
autoFocus
250230
/>
251-
<p className="text-xs text-muted-foreground">
231+
<p className="text-sm text-muted-foreground">
252232
Paste your SSH private key. Make sure the
253233
corresponding public key is added to your Git
254234
server. Learn how to{' '}

resources/js/layouts/auth/auth-card-layout.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
CardHeader,
77
CardTitle,
88
} from '@/components/ui/card';
9-
import { home } from '@/routes';
9+
import { dashboard } from '@/routes';
1010
import { Link } from '@inertiajs/react';
1111
import { type PropsWithChildren } from 'react';
1212

@@ -23,7 +23,7 @@ export default function AuthCardLayout({
2323
<div className="flex min-h-svh flex-col items-center justify-center gap-6 bg-muted p-6 md:p-10">
2424
<div className="flex w-full max-w-md flex-col gap-6">
2525
<Link
26-
href={home()}
26+
href={dashboard.url()}
2727
className="flex items-center gap-2 self-center font-medium"
2828
>
2929
<div className="flex h-9 w-9 items-center justify-center">

0 commit comments

Comments
 (0)