From ce300552235a5fb4557c95a91847e63edb0973e9 Mon Sep 17 00:00:00 2001 From: Abhinav Raut Date: Tue, 13 Aug 2024 03:32:31 +0530 Subject: [PATCH] PUSH COMMITS --- cmd/auth.go | 35 ++- cmd/handlers.go | 2 +- cmd/init.go | 35 ++- cmd/main.go | 6 +- cmd/oidc.go | 29 +- frontend/package.json | 2 +- frontend/public/images/github-logo.png | Bin 0 -> 5109 bytes frontend/public/images/google-logo.png | Bin 0 -> 16590 bytes frontend/src/App.vue | 12 +- frontend/src/components/NavBar.vue | 30 ++- .../src/components/account/ProfileEdit.vue | 2 +- frontend/src/components/admin/DataTable.vue | 2 +- .../components/admin/automation/RuleBox.vue | 2 +- .../components/admin/automation/RuleList.vue | 14 +- .../automation/TabConversationCreation.vue | 2 +- .../src/components/admin/common/MenuCard.vue | 6 +- .../components/admin/common/PageHeader.vue | 2 +- .../src/components/admin/oidc/AddEditOIDC.vue | 14 +- .../src/components/admin/oidc/OIDCForm.vue | 81 +++++- .../src/components/admin/oidc/OIDCPage.vue | 13 +- .../components/admin/oidc/dataTableColumns.js | 26 +- .../admin/oidc/dataTableDropdown.vue | 3 + .../src/components/admin/oidc/formSchema.js | 16 +- .../components/admin/team/roles/RoleForm.vue | 2 - .../admin/team/users/AddUserForm.vue | 2 +- .../admin/team/users/EditUserForm.vue | 2 +- .../components/admin/team/users/UserForm.vue | 28 +- .../src/components/admin/uploads/S3Form.vue | 3 +- .../components/admin/uploads/UploadsForm.vue | 249 ------------------ .../conversationlist/ConversationList.vue | 1 - .../dashboard/agent/DashboardBarChart.vue | 5 +- .../src/components/message/MessageList.vue | 1 - .../src/components/ui/select/SelectTag.vue | 4 +- frontend/src/stores/conversation.js | 5 + frontend/src/views/DashboardView.vue | 41 +-- frontend/src/views/UserLoginView.vue | 21 +- frontend/yarn.lock | 220 +++++++++++----- internal/auth/auth.go | 119 +++++---- internal/media/stores/localfs/fs.go | 2 + internal/oidc/models/models.go | 40 ++- internal/oidc/oidc.go | 11 +- internal/oidc/queries.sql | 12 +- 42 files changed, 548 insertions(+), 554 deletions(-) create mode 100644 frontend/public/images/github-logo.png create mode 100644 frontend/public/images/google-logo.png delete mode 100644 frontend/src/components/admin/uploads/UploadsForm.vue diff --git a/cmd/auth.go b/cmd/auth.go index e99e3d0..3320378 100644 --- a/cmd/auth.go +++ b/cmd/auth.go @@ -1,6 +1,8 @@ package main import ( + "strconv" + "github.com/abhinavxd/artemis/internal/envelope" "github.com/abhinavxd/artemis/internal/stringutil" @@ -8,35 +10,52 @@ import ( "github.com/zerodha/fastglue" ) -// handleOIDCLogin initializes an OIDC request and redirects to the OIDC provider for login. +// handleOIDCLogin redirects to the OIDC provider for login. func handleOIDCLogin(r *fastglue.Request) error { var ( - app = r.Context.(*App) + app = r.Context.(*App) + providerID, err = strconv.Atoi(r.RequestCtx.UserValue("id").(string)) ) + if err != nil { + app.lo.Error("error parsing provider id", "error", err) + return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, "Error parsing provider id.", nil, envelope.GeneralError) + } + + // TODO: Figure csrf thing out state, err := stringutil.RandomAlNumString(30) if err != nil { app.lo.Error("error generating random string", "error", err) return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, "Something went wrong, Please try again.", nil, envelope.GeneralError) } - authURL := app.auth.LoginURL(state) + + authURL, err := app.auth.LoginURL(providerID, state) + if err != nil { + return sendErrorEnvelope(r, err) + } + return r.Redirect(authURL, fasthttp.StatusFound, nil, "") } // handleOIDCCallback receives the redirect callback from the OIDC provider and completes the handshake. func handleOIDCCallback(r *fastglue.Request) error { var ( - app = r.Context.(*App) - code = string(r.RequestCtx.QueryArgs().Peek("code")) - state = string(r.RequestCtx.QueryArgs().Peek("state")) + app = r.Context.(*App) + code = string(r.RequestCtx.QueryArgs().Peek("code")) + state = string(r.RequestCtx.QueryArgs().Peek("state")) + providerID, err = strconv.Atoi(string(r.RequestCtx.QueryArgs().Peek("id"))) ) + if err != nil { + app.lo.Error("error parsing provider id", "error", err) + return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, "Error parsing provider id.", nil, envelope.GeneralError) + } - _, claims, err := app.auth.ExchangeOIDCToken(r.RequestCtx, code) + _, claims, err := app.auth.ExchangeOIDCToken(r.RequestCtx, providerID, code) if err != nil { app.lo.Error("error exchanging oidc token", "error", err) return err } - // Get user by e-mail received from OIDC. + // Get user by e-mail received. user, err := app.user.GetByEmail(claims.Email) if err != nil { return err diff --git a/cmd/handlers.go b/cmd/handlers.go index ab56662..c2c3e39 100644 --- a/cmd/handlers.go +++ b/cmd/handlers.go @@ -14,7 +14,7 @@ import ( func initHandlers(g *fastglue.Fastglue, hub *ws.Hub) { g.POST("/api/login", handleLogin) g.GET("/api/logout", handleLogout) - g.GET("/api/oidc/login", handleOIDCLogin) + g.GET("/api/oidc/{id}/login", handleOIDCLogin) g.GET("/api/oidc/finish", handleOIDCCallback) // Health check. diff --git a/cmd/init.go b/cmd/init.go index 16bd5a1..e63c420 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -269,7 +269,7 @@ func initMedia(db *sqlx.DB) *media.Manager { case "localfs": store, err = localfs.New(localfs.Opts{ UploadURI: "/uploads", - UploadPath: filepath.Clean(ko.String("app.localfs.upload_path")), + UploadPath: filepath.Clean(ko.String("upload.localfs.upload_path")), RootURL: ko.String("app.root_url"), }) if err != nil { @@ -411,16 +411,31 @@ func registerInboxes(mgr *inbox.Manager, store inbox.MessageStore) { } } -func initAuth(rd *redis.Client) *auth.Auth { - lo := initLogger("auth") +func initAuth(o *oidc.Manager, rd *redis.Client) *auth.Auth { + var lo = initLogger("auth") + + oidc, err := o.GetAll() + if err != nil { + log.Fatalf("error initializing auth: %v", err) + } + + var providers = make([]auth.Provider, 0, len(oidc)) + for _, o := range oidc { + if o.Disabled { + continue + } + providers = append(providers, auth.Provider{ + ID: o.ID, + Provider: o.Provider, + ProviderURL: o.ProviderURL, + RedirectURL: o.RedirectURI, + ClientID: o.ClientID, + ClientSecret: o.ClientSecret, + }) + } + a, err := auth.New(auth.Config{ - OIDC: auth.OIDCConfig{ - Enabled: true, - ProviderURL: "https://accounts.google.com", - RedirectURL: "http://localhost:5173/auth/oidc/finish", - ClientID: "a", - ClientSecret: "a", - }, + Providers: providers, }, rd, lo) if err != nil { log.Fatalf("error initializing auth: %v", err) diff --git a/cmd/main.go b/cmd/main.go index e16b67a..bfea5ad 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -83,6 +83,8 @@ func main() { i18n = initI18n(fs) lo = initLogger("artemis") rdb = initRedis() + oidc = initOIDC(db) + auth = initAuth(oidc, rdb) template = initTemplate(db) media = initMedia(db) contact = initContact(db) @@ -119,7 +121,7 @@ func main() { var app = &App{ lo: lo, rdb: rdb, - auth: initAuth(rdb), + auth: auth, fs: fs, i18n: i18n, media: media, @@ -131,7 +133,7 @@ func main() { tmpl: template, conversation: conversation, automation: automation, - oidc: initOIDC(db), + oidc: oidc, role: initRole(db), constant: initConstants(), tag: initTags(db), diff --git a/cmd/oidc.go b/cmd/oidc.go index bb47a3a..ecf9f57 100644 --- a/cmd/oidc.go +++ b/cmd/oidc.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "strconv" "github.com/abhinavxd/artemis/internal/envelope" @@ -9,32 +10,38 @@ import ( "github.com/zerodha/fastglue" ) -// handleGetAllOIDC returns all oidc records +const ( + redirectURI = "/api/oidc/finish?id=%d" +) + +// handleGetAllOIDC returns all OIDC records func handleGetAllOIDC(r *fastglue.Request) error { - var ( - app = r.Context.(*App) - ) - o, err := app.oidc.GetAll() + app := r.Context.(*App) + + out, err := app.oidc.GetAll() if err != nil { return sendErrorEnvelope(r, err) } - return r.SendEnvelope(o) + return r.SendEnvelope(out) } // handleGetOIDC returns an OIDC record by id. func handleGetOIDC(r *fastglue.Request) error { - var ( - app = r.Context.(*App) - ) + app := r.Context.(*App) + id, err := strconv.Atoi(r.RequestCtx.UserValue("id").(string)) - if err != nil || id == 0 { + if err != nil || id <= 0 { return r.SendErrorEnvelope(fasthttp.StatusBadRequest, - "Invalid oidc `id`", nil, envelope.InputError) + "Invalid OIDC `id`", nil, envelope.InputError) } + o, err := app.oidc.Get(id) if err != nil { return sendErrorEnvelope(r, err) } + + o.RedirectURI = fmt.Sprintf("%s%s", app.constant.AppBaseURL, fmt.Sprintf(redirectURI, o.ID)) + return r.SendEnvelope(o) } diff --git a/frontend/package.json b/frontend/package.json index cd3d8a5..3eacbe5 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -54,7 +54,7 @@ "textarea": "^0.3.0", "tiptap-extension-resize-image": "^1.1.5", "vee-validate": "^4.13.2", - "vue": "^3.4.15", + "vue": "^3.4.37", "vue-draggable-resizable": "^3.0.0", "vue-i18n": "9", "vue-letter": "^0.2.0", diff --git a/frontend/public/images/github-logo.png b/frontend/public/images/github-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..a463869d6f3846484836f7be575fd3e9bce91e2f GIT binary patch literal 5109 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 X=?sv}gPo5(00000NkvXXu0mjfkv<7! literal 0 HcmV?d00001 diff --git a/frontend/public/images/google-logo.png b/frontend/public/images/google-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..a4a9918dad10832bc8aaa5574d71fe15f089c8cb GIT binary patch literal 16590 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 literal 0 HcmV?d00001 diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 6b0665b..7fe2be3 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -7,7 +7,7 @@ - + @@ -70,6 +70,16 @@ const allNavLinks = ref([ permission: 'admin:get', }, ]); + +const bottomLinks = ref( + [ + { + to: '/logout', + icon: 'lucide:log-out', + title: 'Logout' + } + ] +) const userStore = useUserStore(); const router = useRouter(); diff --git a/frontend/src/components/NavBar.vue b/frontend/src/components/NavBar.vue index 2ea5ce1..811ce56 100644 --- a/frontend/src/components/NavBar.vue +++ b/frontend/src/components/NavBar.vue @@ -8,7 +8,8 @@ import { Tooltip, TooltipContent, TooltipTrigger, TooltipProvider } from '@/comp defineProps({ isCollapsed: Boolean, - links: Array + links: Array, + bottomLinks: Array }) const route = useRoute() @@ -25,7 +26,7 @@ const getButtonVariant = (to) => { diff --git a/frontend/src/components/account/ProfileEdit.vue b/frontend/src/components/account/ProfileEdit.vue index bf93d3b..4a8bb1b 100644 --- a/frontend/src/components/account/ProfileEdit.vue +++ b/frontend/src/components/account/ProfileEdit.vue @@ -1,7 +1,7 @@