Passed
Pull Request — main (#166)
by Yume
02:03
created

app/v1/controllers/oauth.go   A

Size/Duplication

Total Lines 209
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
cc 23
eloc 97
dl 0
loc 209
rs 10
c 0
b 0
f 0

5 Methods

Rating   Name   Duplication   Size   Complexity  
A controllers.*OAuthController.DiscordLogin 0 14 3
A controllers.*OAuthController.GithubLogin 0 17 3
B controllers.*OAuthController.GithubCallback 0 40 7
A controllers.NewOAuthController 0 2 1
C controllers.*OAuthController.DiscordCallback 0 57 9
1
package controllers
2
3
import (
4
	"fmt"
5
	"log/slog"
6
7
	"github.com/gofiber/fiber/v2"
8
	"github.com/gofiber/fiber/v2/log"
9
	views2 "github.com/memnix/memnix-rest/app/v1/views"
10
	"github.com/memnix/memnix-rest/cmd/v1/config"
11
	"github.com/memnix/memnix-rest/domain"
12
	"github.com/memnix/memnix-rest/infrastructures"
13
	"github.com/memnix/memnix-rest/pkg/oauth"
14
	"github.com/memnix/memnix-rest/pkg/random"
15
	"github.com/memnix/memnix-rest/services/auth"
16
	"go.opentelemetry.io/otel/attribute"
17
)
18
19
const secretCodeLength = 16
20
21
// OAuthController is the controller for the OAuth routes.
22
type OAuthController struct {
23
	auth auth.IUseCase // auth usecase
24
	auth.IAuthRedisRepository
25
}
26
27
// NewOAuthController creates a new OAuthController.
28
func NewOAuthController(auth auth.IUseCase, redisRepository auth.IAuthRedisRepository) OAuthController {
29
	return OAuthController{auth: auth, IAuthRedisRepository: redisRepository}
30
}
31
32
// GithubLogin redirects the user to the github login page
33
//
34
//	@Summary		Redirects the user to the github login page
35
//	@Description	Redirects the user to the github login page
36
//	@Tags			OAuth
37
//	@Accept			json
38
//	@Produce		json
39
//	@Success		302	{string}	string					"redirecting to github login"
40
//	@Failure		500	{object}	views.HTTPResponseVM	"internal server error"
41
//	@Router			/v2/security/github [get]
42
func (a *OAuthController) GithubLogin(c *fiber.Ctx) error {
43
	state, _ := random.GetRandomGeneratorInstance().GenerateSecretCode(config.OauthStateLength)
44
	// Create the dynamic redirect URL for login
45
	redirectURL := fmt.Sprintf(
46
		"https://github.com/login/oauth/authorize?client_id=%s&redirect_uri=%s&state=%s",
47
		oauth.GetGithubClientID(),
48
		oauth.GetCallbackURL()+"/v2/security/github_callback",
49
		state,
50
	)
51
	// Save the state in the cache
52
	if err := a.IAuthRedisRepository.SetState(c.UserContext(), state); err != nil {
53
		return err
54
	}
55
	if err := c.Redirect(redirectURL, fiber.StatusSeeOther); err != nil {
56
		return err
57
	}
58
	return c.JSON(fiber.Map{"message": "redirecting to github login", "redirect_url": redirectURL})
59
}
60
61
// GithubCallback handles the callback from github
62
//
63
//	@Summary		Handles the callback from github
64
//	@Description	Handles the callback from github
65
//	@Tags			OAuth
66
//	@Accept			json
67
//	@Produce		json
68
//	@Param			code	query		string					true	"code from github"
69
//	@Success		200		{object}	views.LoginTokenVM		"login token"
70
//	@Failure		401		{object}	views.HTTPResponseVM	"invalid credentials"
71
//	@Failure		500		{object}	views.HTTPResponseVM	"internal server error"
72
//	@Router			/v2/security/github_callback [get]
73
func (a *OAuthController) GithubCallback(c *fiber.Ctx) error {
74
	// get the code from the query string
75
	code := c.Query("code")
76
	state := c.Query("state")
77
78
	// check if the state is valid
79
	if ok, _ := a.IAuthRedisRepository.HasState(c.UserContext(), state); !ok {
80
		return c.Status(fiber.StatusUnauthorized).JSON(views2.NewLoginTokenVM("", views2.InvalidCredentials))
81
	}
82
83
	// get the access token from github
84
	accessToken, err := oauth.GetGithubAccessToken(c.UserContext(), code)
85
	if err != nil {
86
		return c.Status(fiber.StatusUnauthorized).JSON(views2.NewLoginTokenVM("", views2.InvalidCredentials))
87
	}
88
89
	// get the user from github
90
	user, err := oauth.GetGithubData(c.UserContext(), accessToken)
91
	if err != nil {
92
		return c.Status(fiber.StatusUnauthorized).JSON(views2.NewLoginTokenVM("", views2.InvalidCredentials))
93
	}
94
95
	var githubUser domain.GithubLogin
96
	err = GetJSONHelperInstance().GetJSONHelper().Unmarshal([]byte(user), &githubUser)
97
	if err != nil {
98
		return c.Status(fiber.StatusUnauthorized).JSON(views2.NewLoginTokenVM("", views2.InvalidCredentials))
99
	}
100
101
	// log the user
102
	jwtToken, err := a.auth.LoginOauth(c.UserContext(), githubUser.ToUser())
103
	if err != nil {
104
		return c.Status(fiber.StatusUnauthorized).JSON(views2.NewLoginTokenVM("", views2.InvalidCredentials))
105
	}
106
107
	// Delete the state from the cache
108
	if err = a.IAuthRedisRepository.DeleteState(c.UserContext(), state); err != nil {
109
		log.WithContext(c.UserContext()).Error("failed to delete state from cache", slog.Any("error", err))
110
	}
111
112
	return c.Redirect(oauth.GetFrontendURL()+"/callback/"+jwtToken, fiber.StatusSeeOther)
113
}
114
115
// DiscordLogin redirects the user to the discord login page
116
//
117
//	@Summary		Redirects the user to the discord login page
118
//	@Description	Redirects the user to the discord login page
119
//	@Tags			OAuth
120
//	@Accept			json
121
//	@Produce		json
122
//	@Success		302	{string}	string					"redirecting to github login"
123
//	@Failure		500	{object}	views.HTTPResponseVM	"internal server error"
124
//	@Router			/v2/security/discord [get]
125
func (a *OAuthController) DiscordLogin(c *fiber.Ctx) error {
126
	// Create the dynamic redirect URL for login
127
	state, _ := random.GetRandomGeneratorInstance().GenerateSecretCode(secretCodeLength)
128
	if err := a.IAuthRedisRepository.SetState(c.UserContext(), state); err != nil {
129
		return err
130
	}
131
132
	redirectURL := oauth.GetDiscordURL() + "&state=" + state
133
134
	err := c.Redirect(redirectURL, fiber.StatusSeeOther)
135
	if err != nil {
136
		return err
137
	}
138
	return c.JSON(fiber.Map{"message": "redirecting to discord login", "redirect_url": redirectURL})
139
}
140
141
// DiscordCallback handles the callback from discord
142
//
143
//	@Summary		Handles the callback from discord
144
//	@Description	Handles the callback from discord
145
//	@Tags			OAuth
146
//	@Accept			json
147
//	@Produce		json
148
//	@Param			code	query		string					true	"code from discord"
149
//	@Success		200		{object}	views.LoginTokenVM		"login token"
150
//	@Failure		401		{object}	views.HTTPResponseVM	"invalid credentials"
151
//	@Failure		500		{object}	views.HTTPResponseVM	"internal server error"
152
//	@Router			/v2/security/discord_callback [get]
153
func (a *OAuthController) DiscordCallback(c *fiber.Ctx) error {
154
	_, span := infrastructures.GetTracerInstance().Tracer().Start(c.UserContext(), "DiscordCallback")
155
	defer span.End()
156
	// get the code from the query string
157
	code := c.Query("code")
158
	state := c.Query("state")
159
160
	span.SetAttributes(attribute.String("code", code), attribute.String("state", state))
161
	if ok, _ := a.IAuthRedisRepository.HasState(c.UserContext(), state); !ok {
162
		log.WithContext(c.UserContext()).Warn("state not found", slog.String("state", state))
163
		return c.Status(fiber.StatusUnauthorized).JSON(views2.NewLoginTokenVM("", views2.InvalidCredentials))
164
	}
165
166
	// get the access token from discord
167
	accessToken, err := oauth.GetDiscordAccessToken(c.UserContext(), code)
168
	if err != nil {
169
		log.WithContext(c.UserContext()).Error("failed to get access token from discord", slog.Any("error", err))
170
		return c.Status(fiber.StatusUnauthorized).JSON(views2.NewLoginTokenVM("", views2.InvalidCredentials))
171
	}
172
173
	if accessToken == "" {
174
		log.WithContext(c.UserContext()).Error("access token is empty")
175
		return c.Status(fiber.StatusUnauthorized).JSON(views2.NewLoginTokenVM("", views2.InvalidCredentials))
176
	}
177
178
	// get the user from discord
179
	user, err := oauth.GetDiscordData(c.UserContext(), accessToken)
180
	if err != nil {
181
		log.WithContext(c.UserContext()).Error("failed to get user from discord", slog.Any("error", err))
182
		return c.Status(fiber.StatusUnauthorized).JSON(views2.NewLoginTokenVM("", views2.InvalidCredentials))
183
	}
184
185
	var discordUser domain.DiscordLogin
186
	// print the user to the console
187
	err = GetJSONHelperInstance().GetJSONHelper().Unmarshal([]byte(user), &discordUser)
188
	if err != nil {
189
		log.WithContext(c.UserContext()).Error("failed to unmarshal discord user", slog.Any("error", err))
190
		return c.Status(fiber.StatusUnauthorized).JSON(views2.NewLoginTokenVM("", views2.InvalidCredentials))
191
	}
192
193
	if discordUser == (domain.DiscordLogin{}) {
194
		log.WithContext(c.UserContext()).Error("discord user is empty")
195
		return c.Status(fiber.StatusUnauthorized).JSON(views2.NewLoginTokenVM("", views2.InvalidCredentials))
196
	}
197
198
	// log the user
199
	jwtToken, err := a.auth.LoginOauth(c.UserContext(), discordUser.ToUser())
200
	if err != nil {
201
		log.WithContext(c.UserContext()).Error("failed to login user", slog.Any("error", err))
202
		return c.Status(fiber.StatusUnauthorized).JSON(views2.NewLoginTokenVM("", views2.InvalidCredentials))
203
	}
204
205
	if err = a.IAuthRedisRepository.DeleteState(c.UserContext(), state); err != nil {
206
		log.WithContext(c.UserContext()).Error("error deleting state", slog.Any("error", err))
207
	}
208
209
	return c.Redirect(oauth.GetFrontendURL()+"/callback/"+jwtToken, fiber.StatusSeeOther)
210
}
211