Skip to content

Commit 32e3b47

Browse files
justserdaratinux
andauthored
feat: add Gitea Oauth Provider (#335)
Co-authored-by: Sébastien Chopin <[email protected]> Co-authored-by: Sébastien Chopin <[email protected]>
1 parent e6371c7 commit 32e3b47

File tree

7 files changed

+183
-1
lines changed

7 files changed

+183
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,7 @@ It can also be set using environment variables:
222222
- Facebook
223223
- GitHub
224224
- GitLab
225+
- Gitea
225226
- Google
226227
- Hubspot
227228
- Instagram

playground/.env.example

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ NUXT_OAUTH_GITHUB_CLIENT_SECRET=
55
# GitLab OAuth
66
NUXT_OAUTH_GITLAB_CLIENT_ID=
77
NUXT_OAUTH_GITLAB_CLIENT_SECRET=
8+
# Gitea OAuth
9+
NUXT_OAUTH_GITEA_CLIENT_ID=
10+
NUXT_OAUTH_GITEA_CLIENT_SECRET=
811
# Spotify OAuth
912
NUXT_OAUTH_SPOTIFY_CLIENT_ID=
1013
NUXT_OAUTH_SPOTIFY_CLIENT_SECRET=

playground/auth.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ declare module '#auth-utils' {
44
email?: string
55
password?: string
66
spotify?: string
7+
gitea?: string
78
github?: string
89
gitlab?: string
910
google?: string
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export default defineOAuthGiteaEventHandler({
2+
config: {
3+
emailRequired: true,
4+
},
5+
async onSuccess(event, { user }) {
6+
await setUserSession(event, {
7+
user: {
8+
gitea: user.email,
9+
},
10+
loggedInAt: Date.now(),
11+
})
12+
return sendRedirect(event, '/')
13+
},
14+
15+
})

src/module.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,13 @@ export default defineNuxtModule<ModuleOptions>({
161161

162162
// OAuth settings
163163
runtimeConfig.oauth = defu(runtimeConfig.oauth, {})
164+
// Gitea OAuth
165+
runtimeConfig.oauth.gitea = defu(runtimeConfig.oauth.gitea, {
166+
clientId: '',
167+
clientSecret: '',
168+
redirectURL: '',
169+
baseURL: '',
170+
})
164171
// GitHub OAuth
165172
runtimeConfig.oauth.github = defu(runtimeConfig.oauth.github, {
166173
clientId: '',

src/runtime/server/lib/oauth/gitea.ts

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import type { H3Event } from 'h3'
2+
import { eventHandler, getQuery, sendRedirect } from 'h3'
3+
import { withQuery } from 'ufo'
4+
import { defu } from 'defu'
5+
import {
6+
handleMissingConfiguration,
7+
handleAccessTokenErrorResponse,
8+
getOAuthRedirectURL,
9+
requestAccessToken,
10+
} from '../utils'
11+
import { useRuntimeConfig, createError } from '#imports'
12+
import type { OAuthConfig } from '#auth-utils'
13+
14+
export interface OAuthGiteaConfig {
15+
/**
16+
* Gitea OAuth Client ID
17+
* @default process.env.NUXT_OAUTH_GITEA_CLIENT_ID
18+
*/
19+
clientId?: string
20+
/**
21+
* Gitea OAuth Client Secret
22+
* @default process.env.NUXT_OAUTH_GITEA_CLIENT_SECRET
23+
*/
24+
clientSecret?: string
25+
/**
26+
* Gitea OAuth Scope
27+
* @default ['read:user']
28+
* @see https://docs.gitea.io/en-us/oauth2-provider/
29+
* @example ['read:user']
30+
*/
31+
scope?: string[]
32+
/**
33+
* Require email from user, adds the ['email'] scope if not present
34+
* @default false
35+
*/
36+
emailRequired?: boolean
37+
38+
/**
39+
* Gitea OAuth Authorization URL
40+
* @default '/login/oauth/authorize'
41+
*/
42+
authorizationURL?: string
43+
44+
/**
45+
* Gitea OAuth Token URL
46+
* @default '/login/oauth/access_token'
47+
*/
48+
tokenURL?: string
49+
50+
/**
51+
* Extra authorization parameters to provide to the authorization URL
52+
* @see https://docs.gitea.io/en-us/oauth2-provider/
53+
*/
54+
authorizationParams?: Record<string, string>
55+
56+
/**
57+
* Redirect URL to to allow overriding for situations like prod failing to determine public hostname
58+
* @default process.env.NUXT_OAUTH_GITEA_REDIRECT_URL
59+
*/
60+
redirectURL?: string
61+
62+
/**
63+
* URL of your Gitea instance
64+
* @default 'https://gitea.com'
65+
*/
66+
baseURL?: string
67+
}
68+
69+
export function defineOAuthGiteaEventHandler({
70+
config,
71+
onSuccess,
72+
onError,
73+
}: OAuthConfig<OAuthGiteaConfig>) {
74+
return eventHandler(async (event: H3Event) => {
75+
const runtimeConfig = useRuntimeConfig(event).oauth?.gitea
76+
const baseURL = config?.baseURL ?? runtimeConfig.baseURL ?? 'https://gitea.com'
77+
config = defu(config, runtimeConfig, {
78+
authorizationURL: `${baseURL}/login/oauth/authorize`,
79+
tokenURL: `${baseURL}/login/oauth/access_token`,
80+
authorizationParams: {},
81+
}) as OAuthGiteaConfig
82+
83+
const query = getQuery<{ code?: string, error?: string }>(event)
84+
85+
if (query.error) {
86+
const error = createError({
87+
statusCode: 401,
88+
message: `Gitea login failed: ${query.error || 'Unknown error'}`,
89+
data: query,
90+
})
91+
if (!onError) throw error
92+
return onError(event, error)
93+
}
94+
95+
if (!config.clientId || !config.clientSecret) {
96+
return handleMissingConfiguration(
97+
event,
98+
'gitea',
99+
['clientId', 'clientSecret'],
100+
onError,
101+
)
102+
}
103+
104+
const redirectURL = config.redirectURL || getOAuthRedirectURL(event)
105+
106+
if (!query.code) {
107+
config.scope = config.scope || []
108+
if (!config.scope.length) {
109+
config.scope.push('read:user')
110+
}
111+
if (config.emailRequired && !config.scope.includes('email')) {
112+
config.scope.push('email')
113+
}
114+
115+
return sendRedirect(
116+
event,
117+
withQuery(config.authorizationURL as string, {
118+
response_type: 'code',
119+
client_id: config.clientId,
120+
redirect_uri: redirectURL,
121+
scope: config.scope.join(' '),
122+
...config.authorizationParams,
123+
}),
124+
)
125+
}
126+
127+
const tokens = await requestAccessToken(config.tokenURL as string, {
128+
body: {
129+
grant_type: 'authorization_code',
130+
client_id: config.clientId,
131+
client_secret: config.clientSecret,
132+
redirect_uri: redirectURL,
133+
code: query.code,
134+
},
135+
})
136+
137+
if (tokens.error) {
138+
return handleAccessTokenErrorResponse(event, 'gitea', tokens, onError)
139+
}
140+
141+
const accessToken = tokens.access_token
142+
143+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
144+
const user: any = await $fetch(`${baseURL}/api/v1/user`, {
145+
headers: {
146+
Authorization: `token ${accessToken}`,
147+
},
148+
})
149+
150+
return onSuccess(event, {
151+
user,
152+
tokens,
153+
})
154+
})
155+
}

src/runtime/types/oauth-config.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { H3Event, H3Error } from 'h3'
22

3-
export type OAuthProvider = 'atlassian' | 'auth0' | 'authentik' | 'battledotnet' | 'cognito' | 'discord' | 'dropbox' | 'facebook' | 'github' | 'gitlab' | 'google' | 'hubspot' | 'instagram' | 'keycloak' | 'line' | 'linear' | 'linkedin' | 'microsoft' | 'paypal' | 'polar' | 'spotify' | 'seznam' | 'steam' | 'strava' | 'tiktok' | 'twitch' | 'vk' | 'workos' | 'x' | 'xsuaa' | 'yandex' | 'zitadel' | 'apple' | (string & {})
3+
export type OAuthProvider = 'atlassian' | 'auth0' | 'authentik' | 'battledotnet' | 'cognito' | 'discord' | 'dropbox' | 'facebook' | 'gitea' | 'github' | 'gitlab' | 'google' | 'hubspot' | 'instagram' | 'keycloak' | 'line' | 'linear' | 'linkedin' | 'microsoft' | 'paypal' | 'polar' | 'spotify' | 'seznam' | 'steam' | 'strava' | 'tiktok' | 'twitch' | 'vk' | 'workos' | 'x' | 'xsuaa' | 'yandex' | 'zitadel' | 'apple' | (string & {})
44

55
export type OnError = (event: H3Event, error: H3Error) => Promise<void> | void
66

0 commit comments

Comments
 (0)