Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
cca0aeb
Merge branch 'fix-QA' of https://github.com/CC-0000/Indeq into staging
amay-patel Apr 20, 2025
e1f113a
Merge branch 'staging' of https://github.com/CC-0000/Indeq into staging
amay-patel Apr 20, 2025
c6abaa0
Merge branch 'staging' of https://github.com/CC-0000/Indeq into staging
amay-patel Apr 21, 2025
cd54031
Merge branch 'staging' of https://github.com/CC-0000/Indeq into beta-…
amay-patel Apr 21, 2025
4ad706f
feat(frontend+backend): start of beta codes
amay-patel Apr 21, 2025
702f4ec
feat(backend): beta code validation
amay-patel Apr 21, 2025
24d2f01
feat(backend): minor bug fixes
amay-patel Apr 21, 2025
e87a26a
feat(frontend): landing page and beta code takes alphanumerics
amay-patel Apr 21, 2025
44f34d8
feat(backend): otp method turned into code method
amay-patel Apr 22, 2025
75bb5ef
feat(frontend): i hate frontend
amay-patel Apr 22, 2025
2131331
feat(frontend): more frontend fixes
amay-patel Apr 22, 2025
9ec90be
feat(frontend): redirect work
amay-patel Apr 22, 2025
e1a04fd
Merge branch 'staging' of https://github.com/CC-0000/Indeq into beta-…
amay-patel Apr 22, 2025
6e41615
feat(frontend): beta code component on home page
amay-patel Apr 22, 2025
660fbc7
feat(frontend): some more changes
amay-patel Apr 22, 2025
1a0f3ab
feat(frontend): hooks changes
amay-patel Apr 22, 2025
897b9a6
feat(frontend): hooks changes
amay-patel Apr 22, 2025
c2a10e8
Merge branch 'staging' of https://github.com/CC-0000/Indeq into beta-…
amay-patel Apr 22, 2025
db45457
Merge branch 'staging' of https://github.com/CC-0000/Indeq into beta-…
amay-patel Apr 22, 2025
d51f8ae
Merge branch 'staging' of https://github.com/CC-0000/Indeq into beta-…
amay-patel Apr 22, 2025
ac40ef9
Merge branch 'staging' of https://github.com/CC-0000/Indeq into beta-…
amay-patel Apr 22, 2025
0e68773
feat(frontend): redesign of beta-code page
amay-patel Apr 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions backend/authentication/rpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"time"

pb "github.com/cc-0000/indeq/common/api"
"github.com/cc-0000/indeq/common/util"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"golang.org/x/crypto/argon2"
Expand Down Expand Up @@ -241,7 +242,7 @@ func (s *authServer) Register(ctx context.Context, req *pb.RegisterRequest) (*pb
}

// Generate a random OTP
otp, err := generateOTP()
otp, err := util.GenerateCode("numeric")
if err != nil {
// if the OTP generation fails, return an error
return &pb.RegisterResponse{
Expand Down Expand Up @@ -337,7 +338,7 @@ func (s *authServer) ResendOTP(ctx context.Context, req *pb.ResendOTPRequest) (*
Error: "Something went wrong. Please try again later.",
}, nil
}
newOTP, err := generateOTP()
newOTP, err := util.GenerateCode("numeric")
if err != nil {
// if the OTP generation fails, return an error
return &pb.ResendOTPResponse{
Expand Down Expand Up @@ -639,7 +640,7 @@ func (s *authServer) ForgotPassword(ctx context.Context, req *pb.ForgotPasswordR
req.Email = strings.ToLower(req.Email)

// generate a random OTP
otp, err := generateOTP()
otp, err := util.GenerateCode("numeric")
if err != nil {
// if the OTP generation fails, return an error
return &pb.ForgotPasswordResponse{
Expand Down
32 changes: 0 additions & 32 deletions backend/authentication/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,41 +10,9 @@ import (
"net/smtp"
"os"
"strings"

"golang.org/x/crypto/argon2"
)

// func() (string, error)
// - generates a secure 6-digit numeric OTP using crypto/rand
// - avoids modulo bias by discarding bytes > 250
// - returns: the OTP string and error (if random generation fails)
func generateOTP() (string, error) {
const digits = "0123456789"
const length = 6
otp := make([]byte, 0, length)
// Use 250 as the maximum to avoid modulo bias
max := byte(250)
// buffer to read random bytes in batches
buf := make([]byte, 16)
for len(otp) < length {
_, err := rand.Read(buf)
if err != nil {
return "", fmt.Errorf("failed to generate OTP: %w", err)
}
for _, b := range buf {
if b > max {
continue
}
otp = append(otp, digits[b%10])
if len(otp) == length {
break
}
}
}

return string(otp), nil
}

// func(password string, argonParams *params) (string, error)
// - generates a secure password hash using the given Argon2 params
// - returns: the hashed password and error (if hashing fails)
Expand Down
10 changes: 10 additions & 0 deletions backend/common/api/http.proto
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,16 @@ message HttpAddToWaitlistResponse {
string message = 2;
}

message HttpValidateBetaCodeRequest {
string email = 1;
string beta_code = 2;
}

message HttpValidateBetaCodeResponse {
bool success = 1;
string message = 2;
}

message HttpGetDesktopStatsRequest {
}

Expand Down
11 changes: 11 additions & 0 deletions backend/common/api/waitlist.proto
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package api;

service WaitlistService {
rpc AddToWaitlist(AddToWaitlistRequest) returns (AddToWaitlistResponse);
rpc ValidateBetaCode(ValidateBetaCodeRequest) returns (ValidateBetaCodeResponse);
}

message AddToWaitlistRequest {
Expand All @@ -15,4 +16,14 @@ message AddToWaitlistRequest {
message AddToWaitlistResponse {
bool success = 1;
string message = 2;
}

message ValidateBetaCodeRequest {
string email = 1;
string beta_code = 2;
}

message ValidateBetaCodeResponse {
bool success = 1;
string message = 2;
}
44 changes: 44 additions & 0 deletions backend/common/util/code.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package util

import (
"crypto/rand"
"fmt"
)

// GenerateCode generates a secure 6-character OTP.
// Accepts mode: "numeric" or "alphanumeric".
// Uses crypto/rand and avoids modulo bias.
func GenerateCode(mode string) (string, error) {
var charset string
switch mode {
case "numeric":
charset = "0123456789"
case "alphanumeric":
charset = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
default:
return "", fmt.Errorf("invalid mode: %s", mode)
}

const otpLength = 6
const maxByte = 250 // to avoid modulo bias
otp := make([]byte, 0, otpLength)
buf := make([]byte, 16)

for len(otp) < otpLength {
_, err := rand.Read(buf)
if err != nil {
return "", fmt.Errorf("failed to generate OTP: %w", err)
}
for _, b := range buf {
if b > maxByte {
continue
}
otp = append(otp, charset[b%byte(len(charset))])
if len(otp) == otpLength {
break
}
}
}

return string(otp), nil
}
34 changes: 34 additions & 0 deletions backend/gateway/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,39 @@ func handleAddToWaitlist(clients *ServiceClients) http.HandlerFunc {
}
}

func handleValidateBetaCode(clients *ServiceClients) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var validateBetaCodeRequest pb.HttpValidateBetaCodeRequest
log.Println("Received validate beta code request")
if err := json.NewDecoder(r.Body).Decode(&validateBetaCodeRequest); err != nil {
log.Printf("Error: %v", err)
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}

if validateBetaCodeRequest.Email == "" || validateBetaCodeRequest.BetaCode == "" {
http.Error(w, "Missing email or beta code", http.StatusBadRequest)
return
}

res, err := clients.waitlistClient.ValidateBetaCode(r.Context(), &pb.ValidateBetaCodeRequest{
BetaCode: validateBetaCodeRequest.BetaCode,
Email: validateBetaCodeRequest.Email,
})
if err != nil {
http.Error(w, "Failed to validate beta code", http.StatusInternalServerError)
return
}

httpResponse := &pb.HttpValidateBetaCodeResponse{
Success: res.Success,
Message: res.Message,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(httpResponse)
}
}

func handleGetDesktopStatsGenerator(clients *ServiceClients) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// Set up context
Expand Down Expand Up @@ -1279,6 +1312,7 @@ func main() {
mux.HandleFunc("POST /api/ssooauth", handleSSOOAuthGenerator(serviceClients))
mux.HandleFunc("POST /api/ssologin", handleSSOLoginGenerator(serviceClients))
mux.HandleFunc("POST /api/waitlist", handleAddToWaitlist(serviceClients))
mux.HandleFunc("POST /api/validate-beta-code", handleValidateBetaCode(serviceClients))
mux.HandleFunc("GET /api/desktop_stats", authMiddleware(handleGetDesktopStatsGenerator(serviceClients), serviceClients))
mux.HandleFunc("POST /api/manualcrawl", authMiddleware(handleManualCrawlGenerator(serviceClients), serviceClients))
mux.HandleFunc("POST /api/verify-otp", handleVerifyOTPGenerator(serviceClients))
Expand Down
52 changes: 49 additions & 3 deletions backend/waitlist/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import (
"net"
"net/mail"
"os"
"strings"
"time"

pb "github.com/cc-0000/indeq/common/api"
"github.com/cc-0000/indeq/common/util"
"github.com/cc-0000/indeq/common/config"
_ "github.com/lib/pq"
"google.golang.org/grpc"
Expand All @@ -23,6 +25,7 @@ type WaitlistServer struct {

func (s *WaitlistServer) AddToWaitlist(ctx context.Context, req *pb.AddToWaitlistRequest) (*pb.AddToWaitlistResponse, error) {
log.Println("Adding to waitlist:", req.Email)
req.Email = strings.ToLower(req.Email)
_, err := mail.ParseAddress(req.Email)
if err != nil {
return &pb.AddToWaitlistResponse{
Expand All @@ -31,10 +34,18 @@ func (s *WaitlistServer) AddToWaitlist(ctx context.Context, req *pb.AddToWaitlis
}, nil
}

betaCode, err := util.GenerateCode("alphanumeric")
if err != nil {
return &pb.AddToWaitlistResponse{
Success: false,
Message: "Something went wrong. Please try again later.",
}, nil
}

result, err := s.db.ExecContext(ctx, `
INSERT INTO waitlist (email)
VALUES ($1)
ON CONFLICT (email) DO NOTHING`, req.Email)
INSERT INTO waitlist (email, beta_code)
VALUES ($1, $2)
ON CONFLICT (email) DO NOTHING`, req.Email, betaCode)

if err != nil {
log.Println("Database insert error:", err)
Expand Down Expand Up @@ -66,6 +77,40 @@ func (s *WaitlistServer) AddToWaitlist(ctx context.Context, req *pb.AddToWaitlis
}, nil
}

func (s *WaitlistServer) ValidateBetaCode(ctx context.Context, req *pb.ValidateBetaCodeRequest) (*pb.ValidateBetaCodeResponse, error) {
log.Println("Validating beta code:", req.BetaCode)

if req.BetaCode == "" || req.Email == "" {
return &pb.ValidateBetaCodeResponse{
Success: false,
Message: "Invalid request",
}, nil
}

var betaCode string
err := s.db.QueryRowContext(ctx, `
SELECT beta_code FROM waitlist WHERE email = $1
`, req.Email).Scan(&betaCode)
if err != nil {
return &pb.ValidateBetaCodeResponse{
Success: false,
Message: "Could not validate beta code. Please try again later.",
}, nil
}

if betaCode != req.BetaCode {
return &pb.ValidateBetaCodeResponse{
Success: false,
Message: "Invalid beta code",
}, nil
}

return &pb.ValidateBetaCodeResponse{
Success: true,
Message: "Beta code validated successfully",
}, nil
}

func main() {
log.Println("Starting the waitlist server...")

Expand Down Expand Up @@ -99,6 +144,7 @@ func main() {
CREATE TABLE IF NOT EXISTS waitlist (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
beta_code VARCHAR(6) NOT NULL,
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);
`)
Expand Down
18 changes: 11 additions & 7 deletions frontend/src/hooks.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import { APP_ENV } from '$env/static/private';

export const handle: Handle = async ({ event, resolve }) => {
const jwt = event.cookies.get('jwt');
const betaPassed = event.cookies.get('betaPassed') === 'true';
const isAuthenticated = jwt && (await verifyToken(jwt));

const publicRoutes = [
'/',
'/login',
'/register',
'/beta-code',
'/enter-code',
'/forgot-password',
'/reset-password',
Expand All @@ -24,11 +26,14 @@ export const handle: Handle = async ({ event, resolve }) => {
];

const authRoutes = ['/login', '/register'];
const productionRoutes = ['/', '/terms', '/privacy', '/api/waitlist', '/sitemap.xml', '/login', '/beta-code'];

const productionRoutes = ['/', '/terms', '/privacy', '/api/waitlist', '/sitemap.xml'];

if (APP_ENV === 'PRODUCTION' && !productionRoutes.includes(event.url.pathname)) {
return redirect(302, '/');
if (APP_ENV === 'PRODUCTION') {
if (!productionRoutes.includes(event.url.pathname)) {
if (!(event.url.pathname === '/register' && betaPassed)) {
return redirect(302, '/');
}
}
}

// Redirect authenticated users away from login and register pages
Expand All @@ -37,12 +42,11 @@ export const handle: Handle = async ({ event, resolve }) => {
}

if (!publicRoutes.includes(event.url.pathname)) {
const isValid = jwt && (await verifyToken(jwt));

if (!isValid) {
if (!isAuthenticated) {
return redirect(302, '/login');
}
}

return resolve(event);
};

16 changes: 16 additions & 0 deletions frontend/src/lib/components/beta/BetaCodeCallout.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<script lang="ts">
import { ArrowRightIcon } from 'svelte-feather-icons';
import { goto } from '$app/navigation';
</script>

<button
type="button"
on:click={() => goto('/beta-code')}
class="hidden md:flex fixed top-6 right-6 z-50
items-center gap-2 px-4 py-2 rounded-full
bg-primary text-white shadow-lg cursor-pointer select-none
transition hover:bg-primary/90 focus-within:ring-2 focus-within:ring-primary/50"
>
<span class="text-sm font-medium">Enter&nbsp;Beta&nbsp;Code</span>
<ArrowRightIcon class="w-4 h-4" />
</button>
Loading