mirror of
https://github.com/arkorty/Reduce.git
synced 2026-03-17 16:41:42 +00:00
fix: link authentication
This commit is contained in:
@@ -44,12 +44,10 @@ func uniqueCode() string {
|
|||||||
// shortenURL creates a new shortened link
|
// shortenURL creates a new shortened link
|
||||||
func shortenURL(c echo.Context) error {
|
func shortenURL(c echo.Context) error {
|
||||||
type Req struct {
|
type Req struct {
|
||||||
LongURL string `json:"lurl"`
|
LongURL string `json:"lurl"`
|
||||||
BaseURL string `json:"base_url"`
|
BaseURL string `json:"base_url"`
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
RequiresAuth bool `json:"requires_auth"`
|
RequiresAuth bool `json:"requires_auth"`
|
||||||
AccessUsername string `json:"access_username"`
|
|
||||||
AccessPassword string `json:"access_password"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
r := new(Req)
|
r := new(Req)
|
||||||
@@ -103,26 +101,14 @@ func shortenURL(c echo.Context) error {
|
|||||||
UpdatedAt: time.Now(),
|
UpdatedAt: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Protected links use the creator's account credentials
|
||||||
if r.RequiresAuth {
|
if r.RequiresAuth {
|
||||||
// If no explicit credentials provided, use the logged-in user's credentials
|
var user User
|
||||||
if r.AccessUsername == "" && r.AccessPassword == "" && userID != nil {
|
if err := db.First(&user, *userID).Error; err != nil {
|
||||||
var user User
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to load user")
|
||||||
if err := db.First(&user, *userID).Error; err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to load user")
|
|
||||||
}
|
|
||||||
link.AccessUsername = user.Username
|
|
||||||
link.AccessPassword = user.Password // Already hashed
|
|
||||||
} else if r.AccessUsername != "" && r.AccessPassword != "" {
|
|
||||||
// Custom credentials provided
|
|
||||||
hash, err := bcrypt.GenerateFromPassword([]byte(r.AccessPassword), bcrypt.DefaultCost)
|
|
||||||
if err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to process password")
|
|
||||||
}
|
|
||||||
link.AccessUsername = r.AccessUsername
|
|
||||||
link.AccessPassword = string(hash)
|
|
||||||
} else {
|
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "Access credentials required for protected links")
|
|
||||||
}
|
}
|
||||||
|
link.AccessUsername = user.Username
|
||||||
|
link.AccessPassword = user.Password // Already hashed
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := db.Create(&link).Error; err != nil {
|
if err := db.Create(&link).Error; err != nil {
|
||||||
@@ -148,11 +134,10 @@ func fetchLURL(c echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if link.RequiresAuth {
|
if link.RequiresAuth {
|
||||||
// Check if user is authenticated and authorized
|
// Check if user is authenticated and is the link owner
|
||||||
if username, ok := c.Get("username").(string); ok {
|
if userID, ok := c.Get("user_id").(uint); ok {
|
||||||
// User is logged in, check if they match the access credentials
|
if link.UserID != nil && *link.UserID == userID {
|
||||||
if username == link.AccessUsername {
|
// Auto-authorize link owner
|
||||||
// Auto-authorize logged-in user
|
|
||||||
db.Model(&link).UpdateColumn("click_count", gorm.Expr("click_count + 1"))
|
db.Model(&link).UpdateColumn("click_count", gorm.Expr("click_count + 1"))
|
||||||
return c.JSON(http.StatusOK, map[string]interface{}{
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||||
"lurl": link.LongURL,
|
"lurl": link.LongURL,
|
||||||
@@ -162,7 +147,7 @@ func fetchLURL(c echo.Context) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// User not logged in or doesn't match - require manual auth
|
// User not logged in or not the owner - require manual auth
|
||||||
return c.JSON(http.StatusOK, map[string]interface{}{
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||||
"requires_auth": true,
|
"requires_auth": true,
|
||||||
"code": link.Code,
|
"code": link.Code,
|
||||||
@@ -204,18 +189,14 @@ func verifyAndRedirect(c echo.Context) error {
|
|||||||
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid credentials")
|
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid credentials")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if access_username matches a user account (owner's credentials)
|
// Verify against the link owner's account password
|
||||||
var user User
|
var user User
|
||||||
if db.Where("username = ?", link.AccessUsername).First(&user).Error == nil {
|
if db.Where("username = ?", link.AccessUsername).First(&user).Error != nil {
|
||||||
// Verify against user's account password
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to verify credentials")
|
||||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(r.Password)); err != nil {
|
}
|
||||||
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid credentials")
|
|
||||||
}
|
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(r.Password)); err != nil {
|
||||||
} else {
|
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid credentials")
|
||||||
// Verify against link's custom password
|
|
||||||
if err := bcrypt.CompareHashAndPassword([]byte(link.AccessPassword), []byte(r.Password)); err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid credentials")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
db.Model(&link).UpdateColumn("click_count", gorm.Expr("click_count + 1"))
|
db.Model(&link).UpdateColumn("click_count", gorm.Expr("click_count + 1"))
|
||||||
@@ -246,11 +227,9 @@ func updateLink(c echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Req struct {
|
type Req struct {
|
||||||
Code *string `json:"code"`
|
Code *string `json:"code"`
|
||||||
LongURL *string `json:"long_url"`
|
LongURL *string `json:"long_url"`
|
||||||
RequiresAuth *bool `json:"requires_auth"`
|
RequiresAuth *bool `json:"requires_auth"`
|
||||||
AccessUsername *string `json:"access_username"`
|
|
||||||
AccessPassword *string `json:"access_password"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
r := new(Req)
|
r := new(Req)
|
||||||
@@ -279,38 +258,20 @@ func updateLink(c echo.Context) error {
|
|||||||
|
|
||||||
if r.RequiresAuth != nil {
|
if r.RequiresAuth != nil {
|
||||||
if *r.RequiresAuth {
|
if *r.RequiresAuth {
|
||||||
uname := ""
|
// Enable protection with owner's credentials
|
||||||
if r.AccessUsername != nil {
|
var user User
|
||||||
uname = *r.AccessUsername
|
if err := db.First(&user, uid).Error; err != nil {
|
||||||
}
|
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to load user")
|
||||||
pass := ""
|
|
||||||
if r.AccessPassword != nil {
|
|
||||||
pass = *r.AccessPassword
|
|
||||||
}
|
|
||||||
if uname == "" || pass == "" {
|
|
||||||
return echo.NewHTTPError(http.StatusBadRequest, "Access credentials required for protected links")
|
|
||||||
}
|
|
||||||
hash, err := bcrypt.GenerateFromPassword([]byte(pass), bcrypt.DefaultCost)
|
|
||||||
if err != nil {
|
|
||||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to process password")
|
|
||||||
}
|
}
|
||||||
link.RequiresAuth = true
|
link.RequiresAuth = true
|
||||||
link.AccessUsername = uname
|
link.AccessUsername = user.Username
|
||||||
link.AccessPassword = string(hash)
|
link.AccessPassword = user.Password // Already hashed
|
||||||
} else {
|
} else {
|
||||||
|
// Disable protection
|
||||||
link.RequiresAuth = false
|
link.RequiresAuth = false
|
||||||
link.AccessUsername = ""
|
link.AccessUsername = ""
|
||||||
link.AccessPassword = ""
|
link.AccessPassword = ""
|
||||||
}
|
}
|
||||||
} else if link.RequiresAuth {
|
|
||||||
// Auth already on — allow credential updates without toggling
|
|
||||||
if r.AccessUsername != nil && *r.AccessUsername != "" {
|
|
||||||
link.AccessUsername = *r.AccessUsername
|
|
||||||
}
|
|
||||||
if r.AccessPassword != nil && *r.AccessPassword != "" {
|
|
||||||
hash, _ := bcrypt.GenerateFromPassword([]byte(*r.AccessPassword), bcrypt.DefaultCost)
|
|
||||||
link.AccessPassword = string(hash)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
link.UpdatedAt = time.Now()
|
link.UpdatedAt = time.Now()
|
||||||
|
|||||||
@@ -215,23 +215,12 @@ interface LinkModalProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function LinkModal({ mode, link, onClose, onSaved }: LinkModalProps) {
|
function LinkModal({ mode, link, onClose, onSaved }: LinkModalProps) {
|
||||||
const { user } = useAuth();
|
|
||||||
const [longUrl, setLongUrl] = useState(link?.long_url || '');
|
const [longUrl, setLongUrl] = useState(link?.long_url || '');
|
||||||
const [code, setCode] = useState(link?.code || '');
|
const [code, setCode] = useState(link?.code || '');
|
||||||
const [requiresAuth, setRequiresAuth] = useState(link?.requires_auth || false);
|
const [requiresAuth, setRequiresAuth] = useState(link?.requires_auth || false);
|
||||||
const [useOwnCredentials, setUseOwnCredentials] = useState(true);
|
|
||||||
const [accessUsername, setAccessUsername] = useState(link?.access_username || '');
|
|
||||||
const [accessPassword, setAccessPassword] = useState('');
|
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
|
|
||||||
// If editing a link with custom credentials (not matching user), default to custom mode
|
|
||||||
useEffect(() => {
|
|
||||||
if (link && link.access_username && link.access_username !== user?.username) {
|
|
||||||
setUseOwnCredentials(false);
|
|
||||||
}
|
|
||||||
}, [link, user]);
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError('');
|
setError('');
|
||||||
@@ -248,18 +237,12 @@ function LinkModal({ mode, link, onClose, onSaved }: LinkModalProps) {
|
|||||||
base_url: baseURL,
|
base_url: baseURL,
|
||||||
code: code || undefined,
|
code: code || undefined,
|
||||||
requires_auth: requiresAuth,
|
requires_auth: requiresAuth,
|
||||||
access_username: requiresAuth && accessUsername ? accessUsername : undefined,
|
|
||||||
access_password: requiresAuth && accessPassword ? accessPassword : undefined,
|
|
||||||
});
|
});
|
||||||
} else if (link) {
|
} else if (link) {
|
||||||
const body: Record<string, any> = {};
|
const body: Record<string, any> = {};
|
||||||
if (code !== link.code) body.code = code;
|
if (code !== link.code) body.code = code;
|
||||||
if (longUrl !== link.long_url) body.long_url = longUrl;
|
if (longUrl !== link.long_url) body.long_url = longUrl;
|
||||||
if (requiresAuth !== link.requires_auth) body.requires_auth = requiresAuth;
|
if (requiresAuth !== link.requires_auth) body.requires_auth = requiresAuth;
|
||||||
if (requiresAuth) {
|
|
||||||
if (accessUsername) body.access_username = accessUsername;
|
|
||||||
if (accessPassword) body.access_password = accessPassword;
|
|
||||||
}
|
|
||||||
await api.put(`/links/${link.id}`, body);
|
await api.put(`/links/${link.id}`, body);
|
||||||
}
|
}
|
||||||
onSaved();
|
onSaved();
|
||||||
@@ -334,58 +317,11 @@ function LinkModal({ mode, link, onClose, onSaved }: LinkModalProps) {
|
|||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
{/* Auth credentials */}
|
|
||||||
{requiresAuth && (
|
{requiresAuth && (
|
||||||
<div className="flex flex-col gap-3 pl-4 border-l-2 border-amber-900/50">
|
<div className="flex flex-col gap-2 pl-4 border-l-2 border-amber-900/50">
|
||||||
<label className="flex items-center gap-2 cursor-pointer group text-xs">
|
<p className="text-zinc-600 text-xs">
|
||||||
<input
|
Protected links can only be accessed with your account credentials. You will be auto-authenticated when logged in.
|
||||||
type="checkbox"
|
</p>
|
||||||
checked={useOwnCredentials}
|
|
||||||
onChange={(e) => setUseOwnCredentials(e.target.checked)}
|
|
||||||
className="w-3.5 h-3.5"
|
|
||||||
/>
|
|
||||||
<span className="text-zinc-500 group-hover:text-zinc-300 transition-colors">
|
|
||||||
Use my account credentials
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{useOwnCredentials ? (
|
|
||||||
<p className="text-zinc-600 text-xs">
|
|
||||||
Visitors will need to enter your account username and password to access this link.
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<div>
|
|
||||||
<label className="block text-zinc-500 text-xs uppercase tracking-wider mb-1.5">
|
|
||||||
Access Username
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder="visitor_username"
|
|
||||||
value={accessUsername}
|
|
||||||
onChange={(e) => setAccessUsername(e.target.value)}
|
|
||||||
className="w-full bg-zinc-950 border border-zinc-700 p-3 text-zinc-300 placeholder-zinc-700 focus:outline-none focus:border-zinc-400 focus:ring-1 focus:ring-zinc-400 transition-all font-mono text-sm"
|
|
||||||
required={requiresAuth && !useOwnCredentials}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-zinc-500 text-xs uppercase tracking-wider mb-1.5">
|
|
||||||
Access Password{' '}
|
|
||||||
{mode === 'edit' && (
|
|
||||||
<span className="text-zinc-700">(leave blank to keep current)</span>
|
|
||||||
)}
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
placeholder="••••••••"
|
|
||||||
value={accessPassword}
|
|
||||||
onChange={(e) => setAccessPassword(e.target.value)}
|
|
||||||
className="w-full bg-zinc-950 border border-zinc-700 p-3 text-zinc-300 placeholder-zinc-700 focus:outline-none focus:border-zinc-400 focus:ring-1 focus:ring-zinc-400 transition-all font-mono text-sm"
|
|
||||||
required={mode === 'create' && requiresAuth && !useOwnCredentials}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user