fix: native gitlab & github login

This commit is contained in:
Khairul Hidayat 2024-11-17 21:15:25 +07:00
parent 83d0ed380d
commit 95f9f76edd
10 changed files with 92 additions and 112 deletions

2
frontend/.gitignore vendored
View File

@ -22,3 +22,5 @@ expo-env.d.ts
!.env.example !.env.example
*.apk *.apk
*.aab *.aab
android/
ios/

View File

@ -1,41 +1,42 @@
{ {
"name": "Vaulterm", "name": "Vaulterm",
"slug": "vaulterm", "slug": "vaulterm",
"version": "1.0.0", "version": "1.0.0",
"orientation": "portrait", "orientation": "portrait",
"icon": "./assets/images/icon.png", "icon": "./assets/images/icon.png",
"scheme": "vaulterm", "scheme": "vaulterm",
"userInterfaceStyle": "automatic", "userInterfaceStyle": "automatic",
"newArchEnabled": true, "newArchEnabled": true,
"splash": { "splash": {
"image": "./assets/images/splash.png", "image": "./assets/images/splash.png",
"resizeMode": "contain", "resizeMode": "contain",
"backgroundColor": "#ffffff" "backgroundColor": "#ffffff"
}, },
"ios": { "ios": {
"supportsTablet": true "supportsTablet": true,
}, "bundleIdentifier": "sh.rul.vaulterm"
"android": { },
"package": "sh.rul.vaulterm", "android": {
"adaptiveIcon": { "package": "sh.rul.vaulterm",
"foregroundImage": "./assets/images/adaptive-icon.png", "adaptiveIcon": {
"backgroundColor": "#ffffff" "foregroundImage": "./assets/images/adaptive-icon.png",
} "backgroundColor": "#ffffff"
},
"web": {
"bundler": "metro",
"output": "single",
"favicon": "./assets/images/favicon.png"
},
"plugins": [
"expo-router"
],
"experiments": {
"typedRoutes": true
},
"extra": {
"eas": {
"projectId": "3e0112c1-f0ed-423c-b5cf-95633f23f6dc"
}
} }
} },
"web": {
"bundler": "metro",
"output": "single",
"favicon": "./assets/images/favicon.png"
},
"plugins": [
"expo-router"
],
"experiments": {
"typedRoutes": true
},
"extra": {
"eas": {
"projectId": "3e0112c1-f0ed-423c-b5cf-95633f23f6dc"
}
}
}

View File

@ -27,8 +27,9 @@ const api = ofetch.create({
throw new Error("Unauthorized"); throw new Error("Unauthorized");
} }
if (error.response._data) { const data = error.response._data;
const message = error.response._data.message; if (data) {
const message = typeof data === "string" ? data : data?.message;
throw new Error(message || "Something went wrong"); throw new Error(message || "Something went wrong");
} }
}, },

View File

@ -6,8 +6,9 @@
"scripts": { "scripts": {
"start": "expo start", "start": "expo start",
"reset-project": "node ./scripts/reset-project.js", "reset-project": "node ./scripts/reset-project.js",
"android": "expo start --android", "prebuild": "expo prebuild",
"ios": "expo start --ios", "android": "expo run:android",
"ios": "expo run:ios",
"web": "expo start --web", "web": "expo start --web",
"test": "jest --watchAll", "test": "jest --watchAll",
"lint": "expo lint", "lint": "expo lint",

View File

@ -1,9 +1,9 @@
import React, { useEffect, useMemo } from "react"; import React, { useEffect, useMemo } from "react";
import { makeRedirectUri, useAuthRequest } from "expo-auth-session"; import { makeRedirectUri, useAuthRequest } from "expo-auth-session";
import appConfig from "@/app.json";
import { Button } from "tamagui"; import { Button } from "tamagui";
import { useOAuthCallback } from "../hooks"; import { useOAuthCallback } from "../hooks";
import { useServerConfig } from "@/hooks/useServerConfig"; import { useServerConfig } from "@/hooks/useServerConfig";
import { scheme } from "@/app.json";
const LoginGithubButton = () => { const LoginGithubButton = () => {
const { data: clientId } = useServerConfig("github_client_id"); const { data: clientId } = useServerConfig("github_client_id");
@ -20,7 +20,7 @@ const LoginGithubButton = () => {
{ {
clientId: clientId, clientId: clientId,
scopes: ["identity"], scopes: ["identity"],
redirectUri: makeRedirectUri({ scheme: appConfig.scheme }), redirectUri: makeRedirectUri({ scheme, path: "auth/login" }),
}, },
discovery discovery
); );
@ -28,7 +28,7 @@ const LoginGithubButton = () => {
useEffect(() => { useEffect(() => {
if (response?.type === "success") { if (response?.type === "success") {
const { code } = response.params; const { code } = response.params;
oauth.mutate(code); oauth.mutate({ code });
} }
}, [response]); }, [response]);

View File

@ -1,8 +1,9 @@
import React, { useEffect, useMemo } from "react"; import React, { useEffect, useMemo } from "react";
import { useAuthRequest } from "expo-auth-session"; import { makeRedirectUri, useAuthRequest } from "expo-auth-session";
import { Button } from "tamagui"; import { Button } from "tamagui";
import { useOAuthCallback } from "../hooks"; import { useOAuthCallback } from "../hooks";
import { useServerConfig } from "@/hooks/useServerConfig"; import { useServerConfig } from "@/hooks/useServerConfig";
import { scheme } from "@/app.json";
const LoginGitlabButton = () => { const LoginGitlabButton = () => {
const { data: clientId } = useServerConfig("gitlab_client_id"); const { data: clientId } = useServerConfig("gitlab_client_id");
@ -19,8 +20,7 @@ const LoginGitlabButton = () => {
{ {
clientId, clientId,
scopes: ["read_user"], scopes: ["read_user"],
// redirectUri: makeRedirectUri({ scheme: appConfig.scheme }), redirectUri: makeRedirectUri({ scheme, path: "auth/login" }),
redirectUri: "http://localhost:8081",
}, },
discovery discovery
); );

View File

@ -45,14 +45,17 @@ export const useRegisterMutation = () => {
export const useOAuthCallback = (type: string) => { export const useOAuthCallback = (type: string) => {
return useMutation({ return useMutation({
mutationFn: async (params: { code: string; verifier?: string }) => { mutationFn: async (body: { code: string; verifier?: string }) => {
const res = await api(`/auth/oauth/${type}/callback`, { params }); const res = await api(`/auth/${type}`, { body, method: "POST" });
const { data } = loginResultSchema.safeParse(res); const { data } = loginResultSchema.safeParse(res);
if (!data) { if (!data) {
throw new Error("Invalid response!"); throw new Error("Invalid response!");
} }
return data; return data;
}, },
onError: (err) => {
console.log(err);
},
onSuccess(data) { onSuccess(data) {
authStore.setState({ token: data.sessionId }); authStore.setState({ token: data.sessionId });
router.replace("/"); router.replace("/");

View File

@ -27,28 +27,26 @@ func getGithubConfig() *oauth2.Config {
ClientID: os.Getenv("GITHUB_CLIENT_ID"), ClientID: os.Getenv("GITHUB_CLIENT_ID"),
ClientSecret: os.Getenv("GITHUB_CLIENT_SECRET"), ClientSecret: os.Getenv("GITHUB_CLIENT_SECRET"),
Endpoint: github.Endpoint, Endpoint: github.Endpoint,
// RedirectURL: "http://localhost:3000/auth/oauth/github/callback", RedirectURL: "vaulterm://auth/login",
RedirectURL: "http://localhost:8081", Scopes: []string{"read:user"},
Scopes: []string{"read:user"},
} }
return githubCfg return githubCfg
} }
func githubRedir(c *fiber.Ctx) error {
// Redirect to GitHub login page
url := getGithubConfig().AuthCodeURL("state", oauth2.AccessTypeOffline)
return c.Redirect(url)
}
func githubCallback(c *fiber.Ctx) error { func githubCallback(c *fiber.Ctx) error {
code := c.Query("code") var body struct {
if code == "" { Code string `json:"code"`
}
if err := c.BodyParser(&body); err != nil {
return c.Status(fiber.StatusBadRequest).SendString("Failed to parse request body")
}
if body.Code == "" {
return c.Status(fiber.StatusBadRequest).SendString("Missing code") return c.Status(fiber.StatusBadRequest).SendString("Missing code")
} }
// Exchange code for a token // Exchange code for a token
cfg := getGithubConfig() cfg := getGithubConfig()
token, err := cfg.Exchange(c.Context(), code) token, err := cfg.Exchange(c.Context(), body.Code)
if err != nil { if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString("Failed to exchange token") return c.Status(fiber.StatusInternalServerError).SendString("Failed to exchange token")
} }
@ -61,10 +59,10 @@ func githubCallback(c *fiber.Ctx) error {
} }
defer resp.Body.Close() defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body) data, _ := io.ReadAll(resp.Body)
if resp.StatusCode != 200 { if resp.StatusCode != 200 {
return c.Status(fiber.StatusInternalServerError). return c.Status(fiber.StatusInternalServerError).
SendString(fmt.Sprintf("GitHub API error: %s", string(body))) SendString(fmt.Sprintf("GitHub API error: %s", string(data)))
} }
// Parse user info // Parse user info
@ -76,7 +74,7 @@ func githubCallback(c *fiber.Ctx) error {
Email string `json:"email"` Email string `json:"email"`
} }
if err := json.Unmarshal(body, &user); err != nil { if err := json.Unmarshal(data, &user); err != nil {
return c.Status(fiber.StatusInternalServerError).SendString("Failed to parse user info") return c.Status(fiber.StatusInternalServerError).SendString("Failed to parse user info")
} }

View File

@ -5,7 +5,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"log"
"os" "os"
"strconv" "strconv"
@ -17,61 +16,39 @@ import (
"rul.sh/vaulterm/server/utils" "rul.sh/vaulterm/server/utils"
) )
type GitlabCfg struct { var gitlabCfg *oauth2.Config
oauth2.Config
verifier string
challenge string
}
var gitlabCfg *GitlabCfg func getGitlabConfig() *oauth2.Config {
func getGitlabConfig() *GitlabCfg {
if gitlabCfg != nil { if gitlabCfg != nil {
return gitlabCfg return gitlabCfg
} }
oauthCfg := oauth2.Config{ gitlabCfg = &oauth2.Config{
ClientID: os.Getenv("GITLAB_CLIENT_ID"), ClientID: os.Getenv("GITLAB_CLIENT_ID"),
ClientSecret: os.Getenv("GITLAB_CLIENT_SECRET"), ClientSecret: os.Getenv("GITLAB_CLIENT_SECRET"),
Endpoint: gitlab.Endpoint, Endpoint: gitlab.Endpoint,
// RedirectURL: "http://localhost:3000/auth/oauth/gitlab/callback", RedirectURL: "vaulterm://auth/login",
RedirectURL: "http://localhost:8081", Scopes: []string{"read_user"},
Scopes: []string{"read_user"},
}
verifier := oauth2.GenerateVerifier()
challenge := oauth2.S256ChallengeFromVerifier(verifier)
gitlabCfg = &GitlabCfg{
Config: oauthCfg,
verifier: verifier,
challenge: challenge,
} }
return gitlabCfg return gitlabCfg
} }
func gitlabRedir(c *fiber.Ctx) error {
// Redirect to Gitlab login page
url := getGitlabConfig().
AuthCodeURL("login", oauth2.S256ChallengeOption(getGitlabConfig().verifier))
return c.Redirect(url)
}
func gitlabCallback(c *fiber.Ctx) error { func gitlabCallback(c *fiber.Ctx) error {
cfg := getGitlabConfig() var body struct {
code := c.Query("code") Code string `json:"code"`
verifier := c.Query("verifier") Verifier string `json:"verifier"`
if code == "" {
return c.Status(fiber.StatusBadRequest).SendString("Missing code")
} }
if verifier == "" { if err := c.BodyParser(&body); err != nil {
verifier = cfg.verifier return c.Status(fiber.StatusBadRequest).SendString("Failed to parse request body")
}
if body.Code == "" || body.Verifier == "" {
return c.Status(fiber.StatusBadRequest).SendString("Missing code or verifier")
} }
// Exchange code for a token // Exchange code for a token
token, err := cfg.Exchange(c.Context(), code, oauth2.VerifierOption(verifier)) cfg := getGitlabConfig()
token, err := cfg.Exchange(c.Context(), body.Code, oauth2.VerifierOption(body.Verifier))
if err != nil { if err != nil {
log.Println(token, err)
return c.Status(fiber.StatusInternalServerError).SendString("Failed to exchange token") return c.Status(fiber.StatusInternalServerError).SendString("Failed to exchange token")
} }
@ -83,10 +60,10 @@ func gitlabCallback(c *fiber.Ctx) error {
} }
defer resp.Body.Close() defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body) data, _ := io.ReadAll(resp.Body)
if resp.StatusCode != 200 { if resp.StatusCode != 200 {
return c.Status(fiber.StatusInternalServerError). return c.Status(fiber.StatusInternalServerError).
SendString(fmt.Sprintf("Gitlab API error: %s", string(body))) SendString(fmt.Sprintf("Gitlab API error: %s", string(data)))
} }
// Parse user info // Parse user info
@ -98,7 +75,7 @@ func gitlabCallback(c *fiber.Ctx) error {
Email string `json:"email"` Email string `json:"email"`
} }
if err := json.Unmarshal(body, &user); err != nil { if err := json.Unmarshal(data, &user); err != nil {
return c.Status(fiber.StatusInternalServerError).SendString("Failed to parse user info") return c.Status(fiber.StatusInternalServerError).SendString("Failed to parse user info")
} }

View File

@ -16,11 +16,8 @@ func Router(app *fiber.App) {
router.Post("/register", register) router.Post("/register", register)
router.Post("/logout", middleware.Protected(), logout) router.Post("/logout", middleware.Protected(), logout)
oauth := router.Group("/oauth") router.Post("/github", githubCallback)
oauth.Get("/github", githubRedir) router.Post("/gitlab", gitlabCallback)
oauth.Get("/github/callback", githubCallback)
oauth.Get("/gitlab", gitlabRedir)
oauth.Get("/gitlab/callback", gitlabCallback)
} }
func login(c *fiber.Ctx) error { func login(c *fiber.Ctx) error {