Skip to content

Commit a2fe3d4

Browse files
committed
Limit number of websites and thumbnails per user
1 parent 2a0b51a commit a2fe3d4

4 files changed

Lines changed: 192 additions & 36 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ Check the project's current status:
9595

9696
- [x] Use iframes whenever possible to avoid overhead
9797
- [x] Make sure thumbnails are small
98+
- [ ] Limit iframes to 32 per free account
9899
- [ ] Limit thumbnails to 4 per free account
99100
- [x] User deletion
100101
- [ ] Make sign-up process nice

lib/uptimer/websites.ex

Lines changed: 97 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,22 @@ defmodule Uptimer.Websites do
3030
websites
3131
end
3232

33+
@doc """
34+
Counts the number of websites for a given user.
35+
"""
36+
def count_websites_for_user(user_id) do
37+
Repo.one(from w in Website, where: w.user_id == ^user_id, select: count(w.id))
38+
end
39+
40+
@doc """
41+
Counts the number of websites with thumbnails enabled for a given user.
42+
"""
43+
def count_thumbnails_for_user(user_id) do
44+
Repo.one(
45+
from w in Website, where: w.user_id == ^user_id and w.thumbnail == true, select: count(w.id)
46+
)
47+
end
48+
3349
@doc """
3450
Gets a single website.
3551
@@ -59,28 +75,72 @@ defmodule Uptimer.Websites do
5975
6076
"""
6177
def create_website(attrs \\ %{}, user_id) do
62-
{:ok, response} =
63-
Finch.build(:get, attrs["address"])
64-
|> add_browser_headers()
65-
|> Finch.request(Uptimer.Finch)
66-
67-
attrs = Map.put(attrs, "status", Integer.to_string(response.status))
68-
# Add user_id to the attributes
69-
attrs = Map.put(attrs, "user_id", user_id)
70-
71-
result =
72-
%Website{}
73-
|> Website.changeset(attrs)
74-
|> Repo.insert()
75-
76-
case result do
77-
{:ok, website} ->
78-
# Generate thumbnail asynchronously
79-
Task.start(fn -> generate_and_save_thumbnail(website) end)
80-
result
78+
# Check if the user has reached the maximum number of websites (32)
79+
if count_websites_for_user(user_id) >= 32 do
80+
{:error, "Maximum number of websites (32) reached for free accounts"}
81+
else
82+
# Try to make a request to the website, but handle possible connection errors
83+
request_result =
84+
try do
85+
Finch.build(:get, attrs["address"])
86+
|> add_browser_headers()
87+
|> Finch.request(Uptimer.Finch)
88+
rescue
89+
_ -> {:error, :invalid_url}
90+
catch
91+
_ -> {:error, :timeout}
92+
end
8193

82-
error ->
83-
error
94+
# Process the request results
95+
case request_result do
96+
{:ok, response} ->
97+
# Successfully connected to the website
98+
attrs = Map.put(attrs, "status", Integer.to_string(response.status))
99+
# Add user_id to the attributes
100+
attrs = Map.put(attrs, "user_id", user_id)
101+
102+
# Set thumbnail to false by default to prevent automatic enabling
103+
# Users will need to explicitly enable thumbnails
104+
attrs = Map.put_new(attrs, "thumbnail", false)
105+
106+
result =
107+
%Website{}
108+
|> Website.changeset(attrs)
109+
|> Repo.insert()
110+
111+
case result do
112+
{:ok, website} ->
113+
# Generate thumbnail asynchronously if thumbnail is enabled
114+
# However, this should now be false by default
115+
if website.thumbnail do
116+
Task.start(fn -> generate_and_save_thumbnail(website) end)
117+
end
118+
119+
result
120+
121+
error ->
122+
error
123+
end
124+
125+
{:error, %Mint.TransportError{reason: reason}} ->
126+
# Handle specific transport errors
127+
error_message =
128+
case reason do
129+
:nxdomain -> "Domain not found. Please check the URL."
130+
:timeout -> "Connection timed out. Website might be unavailable."
131+
:econnrefused -> "Connection refused. Website might be unavailable."
132+
:closed -> "Connection closed unexpectedly."
133+
_ -> "Error connecting to website: #{inspect(reason)}"
134+
end
135+
136+
{:error, error_message}
137+
138+
{:error, %Mint.HTTPError{}} ->
139+
{:error, "Invalid HTTP response from website"}
140+
141+
{:error, _} ->
142+
{:error, "Could not connect to website. Please check the URL."}
143+
end
84144
end
85145
end
86146

@@ -204,9 +264,22 @@ defmodule Uptimer.Websites do
204264
{:ok, %Website{}}
205265
"""
206266
def toggle_thumbnail(%Website{} = website) do
207-
update_website(website, %{
208-
"thumbnail" => !website.thumbnail
209-
})
267+
# If toggling from false to true, check the thumbnail limit
268+
if !website.thumbnail do
269+
# Check if user already has 4 thumbnails
270+
if count_thumbnails_for_user(website.user_id) >= 4 do
271+
{:error, "Maximum number of thumbnails (4) reached for free accounts"}
272+
else
273+
update_website(website, %{
274+
"thumbnail" => true
275+
})
276+
end
277+
else
278+
# Disabling a thumbnail is always allowed
279+
update_website(website, %{
280+
"thumbnail" => false
281+
})
282+
end
210283
end
211284

212285
def refresh_thumbnail(%Website{} = website) do

lib/uptimer_web/live/website_live/index.ex

Lines changed: 54 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,27 @@ defmodule UptimerWeb.WebsiteLive.Index do
55
alias Uptimer.Websites.Website
66

77
@impl true
8-
def mount(_params, _session, socket) do
8+
# def mount(_params, _session, socket) do
9+
def mount(params, _, %Phoenix.LiveView.Socket{} = socket) do
10+
# Get user ID
11+
user_id = socket.assigns.current_user.id
12+
913
# Stream initial websites
10-
websites = Websites.list_websites_for_user(socket.assigns.current_user.id)
14+
websites = Websites.list_websites_for_user(user_id)
15+
16+
# Get counts for limits
17+
website_count = Websites.count_websites_for_user(user_id)
18+
thumbnail_count = Websites.count_thumbnails_for_user(user_id)
1119

1220
socket =
13-
stream(socket, :websites, websites)
21+
socket
22+
|> stream(:websites, websites)
23+
|> assign(:website_count, website_count)
24+
|> assign(:thumbnail_count, thumbnail_count)
25+
# Free account limit
26+
|> assign(:max_websites, 32)
27+
# Free account limit
28+
|> assign(:max_thumbnails, 4)
1429

1530
# Subscribe to thumbnail generation events for all websites
1631
if connected?(socket) do
@@ -67,15 +82,39 @@ defmodule UptimerWeb.WebsiteLive.Index do
6782
# Unsubscribe from this website's thumbnail updates
6883
Phoenix.PubSub.unsubscribe(Uptimer.PubSub, "website:thumbnail:#{website.id}")
6984

70-
{:noreply, stream_delete(socket, :websites, website)}
85+
# Update counts
86+
user_id = socket.assigns.current_user.id
87+
website_count = Websites.count_websites_for_user(user_id)
88+
thumbnail_count = Websites.count_thumbnails_for_user(user_id)
89+
90+
{:noreply,
91+
socket
92+
|> stream_delete(:websites, website)
93+
|> assign(:website_count, website_count)
94+
|> assign(:thumbnail_count, thumbnail_count)}
7195
end
7296

7397
@impl true
7498
def handle_event("toggle_thumbnail", %{"id" => id}, socket) do
7599
website = Websites.get_website!(id)
76-
{:ok, updated_website} = Websites.toggle_thumbnail(website)
77100

78-
{:noreply, stream_insert(socket, :websites, updated_website)}
101+
case Websites.toggle_thumbnail(website) do
102+
{:ok, updated_website} ->
103+
# Update counts
104+
user_id = socket.assigns.current_user.id
105+
thumbnail_count = Websites.count_thumbnails_for_user(user_id)
106+
107+
{:noreply,
108+
socket
109+
|> stream_insert(:websites, updated_website)
110+
|> assign(:thumbnail_count, thumbnail_count)}
111+
112+
{:error, error_message} when is_binary(error_message) ->
113+
{:noreply, put_flash(socket, :error, error_message)}
114+
115+
{:error, _changeset} ->
116+
{:noreply, put_flash(socket, :error, "Failed to toggle thumbnail")}
117+
end
79118
end
80119

81120
@impl true
@@ -95,11 +134,20 @@ defmodule UptimerWeb.WebsiteLive.Index do
95134
# Subscribe to thumbnail updates for the new website
96135
Phoenix.PubSub.subscribe(Uptimer.PubSub, "website:thumbnail:#{website.id}")
97136

137+
# Update counts
138+
website_count = Websites.count_websites_for_user(user_id)
139+
thumbnail_count = Websites.count_thumbnails_for_user(user_id)
140+
98141
{:noreply,
99142
socket
100143
|> stream_insert(:websites, website)
144+
|> assign(:website_count, website_count)
145+
|> assign(:thumbnail_count, thumbnail_count)
101146
|> put_flash(:info, "Website created successfully")}
102147

148+
{:error, error_message} when is_binary(error_message) ->
149+
{:noreply, put_flash(socket, :error, error_message)}
150+
103151
{:error, %Ecto.Changeset{} = changeset} ->
104152
{:noreply,
105153
put_flash(socket, :error, "Error creating website: #{error_to_string(changeset)}")}

lib/uptimer_web/live/website_live/index.html.heex

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,44 @@
66
<div class="h-full bg-gradient-radial-light dark:bg-gradient-radial-dark">
77
<div class="content w-full py-8">
88
<div class="container mx-auto px-4 sm:px-6">
9-
<!-- Website Grid -->
9+
<!-- Usage Indicators -->
10+
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6 mb-6">
11+
<div class="px-3 py-2 bg-white/90 dark:bg-white/10 rounded-lg shadow-sm backdrop-blur-sm">
12+
<div class="flex items-center justify-between gap-2">
13+
<h2 class="text-base font-semibold text-gray-800 dark:text-white">Limits</h2>
14+
<div class="flex items-center space-x-3">
15+
<div class="flex items-center">
16+
<span class="text-xs text-gray-500 dark:text-gray-400 mr-1">Sites:</span>
17+
<span class="text-xs font-medium text-gray-800 dark:text-white">
18+
{@website_count}/{@max_websites}
19+
</span>
20+
<div class="w-12 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full ml-1 overflow-hidden">
21+
<div
22+
class="h-full bg-blue-500 rounded-full"
23+
style={"width: #{(@website_count / @max_websites) * 100}%"}
24+
>
25+
</div>
26+
</div>
27+
</div>
28+
<div class="flex items-center">
29+
<span class="text-xs text-gray-500 dark:text-gray-400 mr-1">Thumbs:</span>
30+
<span class="text-xs font-medium text-gray-800 dark:text-white">
31+
{@thumbnail_count}/{@max_thumbnails}
32+
</span>
33+
<div class="w-12 h-1.5 bg-gray-200 dark:bg-gray-700 rounded-full ml-1 overflow-hidden">
34+
<div
35+
class="h-full bg-blue-500 rounded-full"
36+
style={"width: #{(@thumbnail_count / @max_thumbnails) * 100}%"}
37+
>
38+
</div>
39+
</div>
40+
</div>
41+
</div>
42+
</div>
43+
</div>
44+
</div>
45+
46+
<!-- Website Grid -->
1047
<div
1148
id="websites-grid"
1249
phx-update="stream"
@@ -128,9 +165,6 @@
128165
</div>
129166
</div>
130167
<div class="p-4 bg-gray-50 dark:bg-black/20 flex justify-between items-center">
131-
<span class="text-sm text-gray-500 dark:text-gray-300 truncate">
132-
Last checked: Just now
133-
</span>
134168
<button
135169
class="text-gray-500 hover:text-red-500 dark:text-gray-400 dark:hover:text-red-400 focus:outline-none transition-colors ml-2"
136170
phx-click="delete"
@@ -146,7 +180,7 @@
146180
<!-- Add New Website Card -->
147181
<div
148182
id="add-card"
149-
class="group bg-white/90 dark:bg-white/5 backdrop-blur-sm rounded-xl border-2 border-gray-200 dark:border-white/10 flex flex-col items-center justify-center h-full min-h-[200px] transition-all duration-300 hover:bg-white hover:border-blue-200 dark:hover:bg-white/10 dark:hover:border-white/20 cursor-pointer shadow-lg"
183+
class="group bg-white/90 dark:bg-white/5 backdrop-blur-sm rounded-xl border-2 border-gray-200 dark:border-white/10 flex flex-col items-center justify-center h-[340px] min-h-[200px] transition-all duration-300 hover:bg-white hover:border-blue-200 dark:hover:bg-white/10 dark:hover:border-white/20 cursor-pointer shadow-lg"
150184
phx-click={JS.show(to: "#website-form") |> JS.hide(to: "#add-card-content")}
151185
phx-click-away={JS.hide(to: "#website-form") |> JS.show(to: "#add-card-content")}
152186
>
@@ -162,7 +196,7 @@
162196

163197
<div
164198
id="website-form"
165-
class="absolute inset-0 w-full h-full p-6 hidden bg-white dark:bg-gray-800/95 backdrop-blur-md rounded-xl border border-gray-200 dark:border-white/20 z-10"
199+
class="absolute inset-0 w-full h-full p-6 hidden bg-white dark:bg-gray-800/95 backdrop-blur-md rounded-xl border border-gray-200 dark:border-white/20 z-10 overflow-y-auto"
166200
phx-click-away={JS.hide(to: "#website-form") |> JS.show(to: "#add-card-content")}
167201
>
168202
<.form for={%{}} phx-submit="save" class="space-y-4">

0 commit comments

Comments
 (0)