aven 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +35 -0
- data/Rakefile +19 -0
- data/app/assets/stylesheets/aven/application.css +14 -0
- data/app/assets/stylesheets/aven/application.tailwind.css +7 -0
- data/app/assets/stylesheets/aven/tailwind.css +224 -0
- data/app/channels/aven/chat/thread_channel.rb +39 -0
- data/app/components/aven/application_view_component.rb +15 -0
- data/app/components/aven/views/admin/dashboard/index/component.html.erb +1 -0
- data/app/components/aven/views/admin/dashboard/index/component.rb +5 -0
- data/app/components/aven/views/articles/edit/component.html.erb +14 -0
- data/app/components/aven/views/articles/edit/component.rb +14 -0
- data/app/components/aven/views/articles/form/component.html.erb +45 -0
- data/app/components/aven/views/articles/form/component.rb +27 -0
- data/app/components/aven/views/articles/index/component.html.erb +93 -0
- data/app/components/aven/views/articles/index/component.rb +29 -0
- data/app/components/aven/views/articles/new/component.html.erb +13 -0
- data/app/components/aven/views/articles/new/component.rb +14 -0
- data/app/components/aven/views/articles/show/component.html.erb +110 -0
- data/app/components/aven/views/articles/show/component.rb +34 -0
- data/app/components/aven/views/oauth/error/component.html.erb +44 -0
- data/app/components/aven/views/oauth/error/component.rb +30 -0
- data/app/components/aven/views/static/index/component.html.erb +17 -0
- data/app/components/aven/views/static/index/component.rb +16 -0
- data/app/components/aven/views/static/index/controller.js +7 -0
- data/app/controllers/aven/admin/base.rb +16 -0
- data/app/controllers/aven/admin/dashboard_controller.rb +9 -0
- data/app/controllers/aven/agentic/agents_controller.rb +56 -0
- data/app/controllers/aven/agentic/documents_controller.rb +51 -0
- data/app/controllers/aven/agentic/mcp_controller.rb +124 -0
- data/app/controllers/aven/agentic/tools_controller.rb +37 -0
- data/app/controllers/aven/ai/text_controller.rb +41 -0
- data/app/controllers/aven/application_controller.rb +27 -0
- data/app/controllers/aven/articles_controller.rb +114 -0
- data/app/controllers/aven/auth_controller.rb +12 -0
- data/app/controllers/aven/chat/threads_controller.rb +67 -0
- data/app/controllers/aven/oauth/auth0_controller.rb +84 -0
- data/app/controllers/aven/oauth/base_controller.rb +183 -0
- data/app/controllers/aven/oauth/documentation/auth0.md +387 -0
- data/app/controllers/aven/oauth/documentation/entra_id.md +608 -0
- data/app/controllers/aven/oauth/documentation/github.md +329 -0
- data/app/controllers/aven/oauth/documentation/google.md +253 -0
- data/app/controllers/aven/oauth/entra_id_controller.rb +92 -0
- data/app/controllers/aven/oauth/github_controller.rb +91 -0
- data/app/controllers/aven/oauth/google_controller.rb +64 -0
- data/app/controllers/aven/static_controller.rb +7 -0
- data/app/controllers/aven/tags_controller.rb +44 -0
- data/app/controllers/aven/workspaces_controller.rb +20 -0
- data/app/controllers/concerns/aven/authentication.rb +49 -0
- data/app/controllers/concerns/aven/controller_helpers.rb +38 -0
- data/app/helpers/aven/application_helper.rb +16 -0
- data/app/javascript/aven/application.js +3 -0
- data/app/javascript/aven/controllers/application.js +5 -0
- data/app/javascript/aven/controllers/index.js +11 -0
- data/app/jobs/aven/agentic/document_embedding_job.rb +28 -0
- data/app/jobs/aven/agentic/document_ocr_job.rb +28 -0
- data/app/jobs/aven/application_job.rb +4 -0
- data/app/jobs/aven/chat/calculate_cost_job.rb +26 -0
- data/app/jobs/aven/chat/run_job.rb +27 -0
- data/app/mailers/aven/application_mailer.rb +6 -0
- data/app/models/aven/agentic/agent.rb +76 -0
- data/app/models/aven/agentic/agent_document.rb +37 -0
- data/app/models/aven/agentic/agent_tool.rb +37 -0
- data/app/models/aven/agentic/document.rb +162 -0
- data/app/models/aven/agentic/document_embedding.rb +39 -0
- data/app/models/aven/agentic/tool.rb +106 -0
- data/app/models/aven/agentic/tool_parameter.rb +56 -0
- data/app/models/aven/application_record.rb +5 -0
- data/app/models/aven/article.rb +86 -0
- data/app/models/aven/article_attachment.rb +18 -0
- data/app/models/aven/article_relationship.rb +26 -0
- data/app/models/aven/chat/message.rb +135 -0
- data/app/models/aven/chat/thread.rb +159 -0
- data/app/models/aven/import/entry.rb +45 -0
- data/app/models/aven/import/item_link.rb +36 -0
- data/app/models/aven/import/processor.rb +123 -0
- data/app/models/aven/import.rb +102 -0
- data/app/models/aven/item/embed.rb +54 -0
- data/app/models/aven/item/embeddable.rb +141 -0
- data/app/models/aven/item/linkable.rb +212 -0
- data/app/models/aven/item/schema/builder.rb +139 -0
- data/app/models/aven/item/schemaed.rb +252 -0
- data/app/models/aven/item/schemas/base.rb +108 -0
- data/app/models/aven/item.rb +128 -0
- data/app/models/aven/item_link.rb +43 -0
- data/app/models/aven/item_schema.rb +87 -0
- data/app/models/aven/log.rb +66 -0
- data/app/models/aven/loggable.rb +20 -0
- data/app/models/aven/user.rb +40 -0
- data/app/models/aven/workspace.rb +93 -0
- data/app/models/aven/workspace_role.rb +46 -0
- data/app/models/aven/workspace_user.rb +54 -0
- data/app/models/aven/workspace_user_role.rb +38 -0
- data/app/models/concerns/aven/agentic/document_embeddable.rb +58 -0
- data/app/models/concerns/aven/searchable.rb +61 -0
- data/app/services/aven/agentic/dynamic_tool_builder.rb +81 -0
- data/app/services/aven/agentic/mcp/adapter.rb +77 -0
- data/app/services/aven/agentic/mcp/result_formatter.rb +57 -0
- data/app/services/aven/agentic/mcp/server_factory.rb +43 -0
- data/app/services/aven/agentic/ocr/base_extractor.rb +39 -0
- data/app/services/aven/agentic/ocr/excel_extractor.rb +43 -0
- data/app/services/aven/agentic/ocr/image_extractor.rb +22 -0
- data/app/services/aven/agentic/ocr/pdf_extractor.rb +48 -0
- data/app/services/aven/agentic/ocr/processor.rb +36 -0
- data/app/services/aven/agentic/ocr/textract_client.rb +131 -0
- data/app/services/aven/agentic/ocr/word_extractor.rb +34 -0
- data/app/services/aven/agentic/tool_result_formatter.rb +76 -0
- data/app/services/aven/agentic/tools/base.rb +55 -0
- data/app/services/aven/agentic/tools/concerns/boolean_filtering.rb +40 -0
- data/app/services/aven/agentic/tools/concerns/enum_filtering.rb +47 -0
- data/app/services/aven/agentic/tools/concerns/geo_filtering.rb +56 -0
- data/app/services/aven/agentic/tools/concerns/range_filtering.rb +51 -0
- data/app/services/aven/chat/broadcaster.rb +59 -0
- data/app/services/aven/chat/config.rb +93 -0
- data/app/services/aven/chat/message_builder.rb +42 -0
- data/app/services/aven/chat/orchestrator.rb +69 -0
- data/app/services/aven/chat/runner.rb +105 -0
- data/app/services/aven/chat/title_generator.rb +61 -0
- data/app/services/aven/external/gmail_client.rb +173 -0
- data/app/services/aven/external/google_contacts_client.rb +95 -0
- data/app/views/layouts/aven/admin.html.erb +16 -0
- data/app/views/layouts/aven/application.html.erb +18 -0
- data/config/importmap.rb +16 -0
- data/config/routes.rb +63 -0
- data/db/migrate/20200101000001_create_aven_users.rb +19 -0
- data/db/migrate/20200101000002_create_aven_workspaces.rb +14 -0
- data/db/migrate/20200101000003_create_aven_workspace_users.rb +12 -0
- data/db/migrate/20200101000004_create_aven_workspace_roles.rb +13 -0
- data/db/migrate/20200101000005_create_aven_workspace_user_roles.rb +12 -0
- data/db/migrate/20200101000006_create_aven_logs.rb +21 -0
- data/db/migrate/20200101000009_create_aven_items.rb +17 -0
- data/db/migrate/20200101000010_create_aven_item_links.rb +17 -0
- data/db/migrate/20200101000011_create_aven_agentic_tools.rb +19 -0
- data/db/migrate/20200101000012_create_aven_agentic_tool_parameters.rb +20 -0
- data/db/migrate/20200101000013_create_aven_agentic_documents.rb +22 -0
- data/db/migrate/20200101000014_create_aven_agentic_document_embeddings.rb +18 -0
- data/db/migrate/20200101000015_create_aven_agentic_agents.rb +18 -0
- data/db/migrate/20200101000016_create_aven_agentic_agent_tools.rb +13 -0
- data/db/migrate/20200101000017_create_aven_agentic_agent_documents.rb +13 -0
- data/db/migrate/20200101000018_create_aven_chat_threads.rb +19 -0
- data/db/migrate/20200101000019_create_aven_chat_messages.rb +26 -0
- data/db/migrate/20200101000020_add_pg_search_support.rb +21 -0
- data/db/migrate/20200101000021_create_aven_item_schemas.rb +18 -0
- data/db/migrate/20200101000022_create_aven_imports.rb +23 -0
- data/db/migrate/20200101000023_create_aven_import_entries.rb +13 -0
- data/db/migrate/20200101000024_create_aven_import_item_links.rb +13 -0
- data/db/migrate/20200101000025_create_aven_articles.rb +19 -0
- data/db/migrate/20200101000026_create_aven_article_attachments.rb +13 -0
- data/db/migrate/20200101000027_create_aven_article_relationships.rb +15 -0
- data/lib/aven/configuration.rb +87 -0
- data/lib/aven/engine.rb +43 -0
- data/lib/aven/model/tenant_model.rb +91 -0
- data/lib/aven/model.rb +6 -0
- data/lib/aven/version.rb +3 -0
- data/lib/aven.rb +8 -0
- data/lib/tasks/annotate_rb.rake +10 -0
- data/lib/tasks/aven_tasks.rake +21 -0
- metadata +426 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aven
|
|
4
|
+
class ArticlesController < Aven::ApplicationController
|
|
5
|
+
include Aven::Authentication
|
|
6
|
+
|
|
7
|
+
before_action :authenticate_user!
|
|
8
|
+
before_action :set_article, only: [:show, :edit, :update, :destroy]
|
|
9
|
+
|
|
10
|
+
# GET /articles
|
|
11
|
+
def index
|
|
12
|
+
@articles = current_workspace.aven_articles.recent
|
|
13
|
+
|
|
14
|
+
respond_to do |format|
|
|
15
|
+
format.html { view_component("articles/index", articles: @articles, current_user:) }
|
|
16
|
+
format.json { render json: @articles }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# GET /articles/:id
|
|
21
|
+
def show
|
|
22
|
+
respond_to do |format|
|
|
23
|
+
format.html { view_component("articles/show", article: @article, current_user:) }
|
|
24
|
+
format.json { render json: @article }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# GET /articles/new
|
|
29
|
+
def new
|
|
30
|
+
@article = current_workspace.aven_articles.build
|
|
31
|
+
|
|
32
|
+
respond_to do |format|
|
|
33
|
+
format.html { view_component("articles/new", article: @article, current_user:) }
|
|
34
|
+
format.json { render json: @article }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# GET /articles/:id/edit
|
|
39
|
+
def edit
|
|
40
|
+
respond_to do |format|
|
|
41
|
+
format.html { view_component("articles/edit", article: @article, current_user:) }
|
|
42
|
+
format.json { render json: @article }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# POST /articles
|
|
47
|
+
def create
|
|
48
|
+
@article = current_workspace.aven_articles.build(article_params)
|
|
49
|
+
@article.author = current_user
|
|
50
|
+
|
|
51
|
+
if @article.save
|
|
52
|
+
respond_to do |format|
|
|
53
|
+
format.html { redirect_to article_path(@article), notice: "Article was successfully created." }
|
|
54
|
+
format.json { render json: @article, status: :created }
|
|
55
|
+
end
|
|
56
|
+
else
|
|
57
|
+
respond_to do |format|
|
|
58
|
+
format.html do
|
|
59
|
+
response.status = :unprocessable_entity
|
|
60
|
+
view_component("articles/new", article: @article, current_user:)
|
|
61
|
+
end
|
|
62
|
+
format.json { render json: { errors: @article.errors }, status: :unprocessable_entity }
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# PATCH/PUT /articles/:id
|
|
68
|
+
def update
|
|
69
|
+
if @article.update(article_params)
|
|
70
|
+
respond_to do |format|
|
|
71
|
+
format.html { redirect_to article_path(@article), notice: "Article was successfully updated." }
|
|
72
|
+
format.json { render json: @article }
|
|
73
|
+
end
|
|
74
|
+
else
|
|
75
|
+
respond_to do |format|
|
|
76
|
+
format.html do
|
|
77
|
+
response.status = :unprocessable_entity
|
|
78
|
+
view_component("articles/edit", article: @article, current_user:)
|
|
79
|
+
end
|
|
80
|
+
format.json { render json: { errors: @article.errors }, status: :unprocessable_entity }
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# DELETE /articles/:id
|
|
86
|
+
def destroy
|
|
87
|
+
@article.destroy
|
|
88
|
+
|
|
89
|
+
respond_to do |format|
|
|
90
|
+
format.html { redirect_to articles_path, notice: "Article was successfully deleted." }
|
|
91
|
+
format.json { head :no_content }
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def set_article
|
|
98
|
+
@article = current_workspace.aven_articles.friendly.find(params[:id])
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def article_params
|
|
102
|
+
params.require(:article).permit(
|
|
103
|
+
:title,
|
|
104
|
+
:intro,
|
|
105
|
+
:description,
|
|
106
|
+
:published_at,
|
|
107
|
+
:main_visual,
|
|
108
|
+
tag_list: [],
|
|
109
|
+
article_attachments_attributes: [:id, :file, :position, :_destroy],
|
|
110
|
+
article_relationships_attributes: [:id, :related_article_id, :position, :_destroy]
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module Aven
|
|
2
|
+
class AuthController < ApplicationController
|
|
3
|
+
def logout
|
|
4
|
+
sign_out
|
|
5
|
+
begin
|
|
6
|
+
redirect_to(main_app.root_path, notice: "You have been signed out successfully.")
|
|
7
|
+
rescue NoMethodError
|
|
8
|
+
redirect_to(root_path, notice: "You have been signed out successfully.")
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aven
|
|
4
|
+
module Chat
|
|
5
|
+
class ThreadsController < Aven::ApplicationController
|
|
6
|
+
before_action :authenticate_user!
|
|
7
|
+
before_action :set_thread, only: [:show, :ask, :ask_agent]
|
|
8
|
+
|
|
9
|
+
def index
|
|
10
|
+
@threads = current_workspace.aven_chat_threads
|
|
11
|
+
.where(user: current_user)
|
|
12
|
+
.recent
|
|
13
|
+
.limit(50)
|
|
14
|
+
|
|
15
|
+
render json: @threads
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def show
|
|
19
|
+
render json: @thread.as_json(
|
|
20
|
+
include: {
|
|
21
|
+
messages: { only: [:id, :role, :status, :content, :created_at] }
|
|
22
|
+
}
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def create
|
|
27
|
+
@thread = current_workspace.aven_chat_threads.build(
|
|
28
|
+
user: current_user,
|
|
29
|
+
**thread_params
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if @thread.save
|
|
33
|
+
render json: @thread, status: :created
|
|
34
|
+
else
|
|
35
|
+
render json: { errors: @thread.errors }, status: :unprocessable_entity
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# POST /chat/threads/:id/ask
|
|
40
|
+
def ask
|
|
41
|
+
message = @thread.ask(params[:question])
|
|
42
|
+
render json: { message:, thread: @thread }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# POST /chat/threads/:id/ask_agent
|
|
46
|
+
def ask_agent
|
|
47
|
+
agent = current_workspace.aven_agentic_agents.find(params[:agent_id])
|
|
48
|
+
question = params[:question].presence
|
|
49
|
+
|
|
50
|
+
message = @thread.ask_with_agent(agent, question)
|
|
51
|
+
render json: { message:, thread: @thread }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
def set_thread
|
|
57
|
+
@thread = current_workspace.aven_chat_threads
|
|
58
|
+
.where(user: current_user)
|
|
59
|
+
.find(params[:id])
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def thread_params
|
|
63
|
+
params.permit(:title, :context_markdown)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module Aven
|
|
7
|
+
module Oauth
|
|
8
|
+
class Auth0Controller < BaseController
|
|
9
|
+
# Auth0 uses your domain (e.g., your-tenant.auth0.com or your-tenant.us.auth0.com)
|
|
10
|
+
# These URLs will be constructed dynamically based on the configured domain
|
|
11
|
+
DEFAULT_SCOPE = "openid email profile"
|
|
12
|
+
|
|
13
|
+
protected
|
|
14
|
+
|
|
15
|
+
def authorization_url(state)
|
|
16
|
+
params = {
|
|
17
|
+
client_id: oauth_config[:client_id],
|
|
18
|
+
redirect_uri: callback_url,
|
|
19
|
+
response_type: "code",
|
|
20
|
+
scope: oauth_config[:scope] || DEFAULT_SCOPE,
|
|
21
|
+
state:
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
# Optionally add audience parameter if specified (for API access)
|
|
25
|
+
params[:audience] = oauth_config[:audience] if oauth_config[:audience].present?
|
|
26
|
+
|
|
27
|
+
"#{auth0_authorization_url}?#{params.to_query}"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def exchange_code_for_token(code)
|
|
31
|
+
params = {
|
|
32
|
+
grant_type: "authorization_code",
|
|
33
|
+
client_id: oauth_config[:client_id],
|
|
34
|
+
client_secret: oauth_config[:client_secret],
|
|
35
|
+
code:,
|
|
36
|
+
redirect_uri: callback_url
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
oauth_request(URI(auth0_token_url), params)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def fetch_user_info(access_token)
|
|
43
|
+
response = oauth_get_request(URI(auth0_userinfo_url), access_token)
|
|
44
|
+
|
|
45
|
+
{
|
|
46
|
+
id: response[:sub],
|
|
47
|
+
email: response[:email],
|
|
48
|
+
name: response[:name] || response[:nickname],
|
|
49
|
+
picture: response[:picture]
|
|
50
|
+
}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def callback_url
|
|
56
|
+
aven.oauth_auth0_callback_url(host: request.host, protocol: request.protocol)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def oauth_config
|
|
60
|
+
@oauth_config ||= Aven.configuration.oauth_providers[:auth0] || raise("Auth0 OAuth not configured")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def auth0_domain
|
|
64
|
+
@auth0_domain ||= oauth_config[:domain] || raise("Auth0 domain not configured")
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def auth0_base_url
|
|
68
|
+
@auth0_base_url ||= auth0_domain.start_with?("http") ? auth0_domain : "https://#{auth0_domain}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def auth0_authorization_url
|
|
72
|
+
"#{auth0_base_url}/authorize"
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def auth0_token_url
|
|
76
|
+
"#{auth0_base_url}/oauth/token"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def auth0_userinfo_url
|
|
80
|
+
"#{auth0_base_url}/userinfo"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Aven
|
|
4
|
+
module Oauth
|
|
5
|
+
class BaseController < ApplicationController
|
|
6
|
+
skip_before_action :verify_authenticity_token, only: [:callback]
|
|
7
|
+
|
|
8
|
+
# Initiates OAuth flow
|
|
9
|
+
def create
|
|
10
|
+
state = SecureRandom.hex(16)
|
|
11
|
+
session[:oauth_state] = state
|
|
12
|
+
|
|
13
|
+
redirect_to authorization_url(state), allow_other_host: true
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Handles OAuth callback
|
|
17
|
+
def callback
|
|
18
|
+
validate_state!
|
|
19
|
+
|
|
20
|
+
token_data = exchange_code_for_token(params[:code])
|
|
21
|
+
user_info = fetch_user_info(token_data[:access_token])
|
|
22
|
+
|
|
23
|
+
user = find_or_create_user(user_info, token_data)
|
|
24
|
+
|
|
25
|
+
if user.persisted?
|
|
26
|
+
sign_in_and_redirect(user)
|
|
27
|
+
else
|
|
28
|
+
handle_failed_authentication(user)
|
|
29
|
+
end
|
|
30
|
+
rescue => e
|
|
31
|
+
Rails.logger.error("OAuth authentication failed: #{e.class.name} - #{e.message}")
|
|
32
|
+
Rails.logger.error(e.backtrace.first(10).join("\n")) unless Rails.env.production?
|
|
33
|
+
|
|
34
|
+
error_message = if Rails.env.production?
|
|
35
|
+
"Authentication failed. Please try again."
|
|
36
|
+
else
|
|
37
|
+
"#{e.message}"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
error_class = Rails.env.production? ? nil : e.class.name
|
|
41
|
+
render_error_page(error_message, error_class)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Renders OAuth error page
|
|
45
|
+
def error
|
|
46
|
+
@error_message = params[:message] || "Authentication failed"
|
|
47
|
+
@error_class = params[:error_class]
|
|
48
|
+
|
|
49
|
+
view_component(
|
|
50
|
+
"oauth/error",
|
|
51
|
+
error_message: @error_message,
|
|
52
|
+
error_class: @error_class,
|
|
53
|
+
current_user:
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
protected
|
|
58
|
+
|
|
59
|
+
# Must be implemented by subclasses
|
|
60
|
+
def authorization_url(state)
|
|
61
|
+
raise NotImplementedError
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def exchange_code_for_token(code)
|
|
65
|
+
raise NotImplementedError
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def fetch_user_info(access_token)
|
|
69
|
+
raise NotImplementedError
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Common helper methods
|
|
73
|
+
def validate_state!
|
|
74
|
+
if params[:state] != session[:oauth_state]
|
|
75
|
+
raise StandardError, "Invalid state parameter"
|
|
76
|
+
end
|
|
77
|
+
session.delete(:oauth_state)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def find_or_create_user(user_info, token_data)
|
|
81
|
+
auth_tenant = request.host
|
|
82
|
+
|
|
83
|
+
user = Aven::User.where(auth_tenant:)
|
|
84
|
+
.where("remote_id = ? OR email = ?", user_info[:id].to_s, user_info[:email])
|
|
85
|
+
.first_or_initialize
|
|
86
|
+
|
|
87
|
+
user.tap do |u|
|
|
88
|
+
u.auth_tenant = auth_tenant
|
|
89
|
+
u.remote_id = user_info[:id].to_s
|
|
90
|
+
u.email = user_info[:email]
|
|
91
|
+
u.access_token = token_data[:access_token]
|
|
92
|
+
u.save
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def sign_in_and_redirect(user)
|
|
97
|
+
sign_in(user)
|
|
98
|
+
set_current_workspace_for(user)
|
|
99
|
+
redirect_to after_sign_in_path_for(user)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def set_current_workspace_for(user)
|
|
103
|
+
workspace = user.workspaces.first
|
|
104
|
+
|
|
105
|
+
# Create default workspace if user has none (new sign up)
|
|
106
|
+
if workspace.nil?
|
|
107
|
+
workspace = Aven::Workspace.create!(label: "Default Workspace")
|
|
108
|
+
Aven::WorkspaceUser.create!(user:, workspace:)
|
|
109
|
+
user.reload
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Set current workspace in session
|
|
113
|
+
session[:workspace_id] = workspace.id
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def handle_failed_authentication(user)
|
|
117
|
+
error_message = if !Rails.env.production? && user.errors.any?
|
|
118
|
+
user.errors.full_messages.join(", ")
|
|
119
|
+
else
|
|
120
|
+
"Failed to create user account"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
error_class = Rails.env.production? ? nil : "User::ValidationError"
|
|
124
|
+
render_error_page(error_message, error_class)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def render_error_page(message, error_class = nil)
|
|
128
|
+
view_component(
|
|
129
|
+
"oauth/error",
|
|
130
|
+
error_message: message,
|
|
131
|
+
error_class:,
|
|
132
|
+
current_user:
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def after_sign_in_path_for(resource)
|
|
137
|
+
stored_location_for(resource) ||
|
|
138
|
+
Aven.configuration.resolve_authenticated_root_path ||
|
|
139
|
+
begin
|
|
140
|
+
main_app.root_path
|
|
141
|
+
rescue NoMethodError
|
|
142
|
+
root_path
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# HTTP helper for OAuth requests
|
|
147
|
+
def oauth_request(uri, params, headers = {})
|
|
148
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
149
|
+
http.use_ssl = true
|
|
150
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE if Rails.env.development?
|
|
151
|
+
|
|
152
|
+
request = Net::HTTP::Post.new(uri)
|
|
153
|
+
request.set_form_data(params)
|
|
154
|
+
headers.each { |key, value| request[key] = value }
|
|
155
|
+
|
|
156
|
+
response = http.request(request)
|
|
157
|
+
|
|
158
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
159
|
+
raise StandardError, "OAuth request failed: #{response.body}"
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
JSON.parse(response.body, symbolize_names: true)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def oauth_get_request(uri, access_token)
|
|
166
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
167
|
+
http.use_ssl = true
|
|
168
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE if Rails.env.development?
|
|
169
|
+
|
|
170
|
+
request = Net::HTTP::Get.new(uri)
|
|
171
|
+
request["Authorization"] = "Bearer #{access_token}"
|
|
172
|
+
|
|
173
|
+
response = http.request(request)
|
|
174
|
+
|
|
175
|
+
unless response.is_a?(Net::HTTPSuccess)
|
|
176
|
+
raise StandardError, "OAuth request failed: #{response.body}"
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
JSON.parse(response.body, symbolize_names: true)
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|