feat: organization type on signup
Add org_type field (bank/pe_vc/company) to organizations table with additive migration. Add type selector to signup form and admin org edit form. Fix signup handler column names and session creation. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
d99b455282
commit
c2a8808983
|
|
@ -0,0 +1,331 @@
|
||||||
|
# Dealspace — Full Feature Build
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
Go + templ (compiled to `_templ.go`) + HTMX + SQLite. No external CSS framework CDN — uses Tailwind via the existing class approach already in templates. Server runs on port 8080 (mapped to 9300 externally).
|
||||||
|
|
||||||
|
**Key files:**
|
||||||
|
- `internal/model/models.go` — data types
|
||||||
|
- `internal/db/migrate.go` — schema + seed
|
||||||
|
- `internal/handler/*.go` — HTTP handlers
|
||||||
|
- `internal/store/store.go` — DB queries
|
||||||
|
- `internal/rbac/rbac.go` — permissions
|
||||||
|
- `templates/*.templ` — UI templates (must run `templ generate` after editing)
|
||||||
|
- `cmd/dealroom/main.go` — entrypoint
|
||||||
|
|
||||||
|
**Build & run:**
|
||||||
|
```bash
|
||||||
|
cd /home/johan/dev/dealroom
|
||||||
|
templ generate && go build -o bin/dealroom ./cmd/dealroom/main.go
|
||||||
|
systemctl --user restart dealroom 2>/dev/null || (pkill dealroom; sleep 1; ./bin/dealroom &)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test user:** misha@muskepo.com / Dealspace2026! (owner role, org-1 "Apex Capital Partners")
|
||||||
|
|
||||||
|
**Current routes (from handler.go):** Check internal/handler/handler.go for the router.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## WHAT'S ALREADY BUILT (do NOT redo these)
|
||||||
|
|
||||||
|
- Auth (login, logout, signup with org creation, view toggle seller↔buyer)
|
||||||
|
- Dashboard with stage stat cards, last accessed per deal, recent activity linked to deal
|
||||||
|
- New Room modal (functional, creates deal with name/company/stage/dealsize/IOI/LOI/description)
|
||||||
|
- Deal rooms list page
|
||||||
|
- Deal detail with folder tree, file list, request list tabs, Atlas notes, file status change
|
||||||
|
- "All Documents" virtual folder already in folder tree
|
||||||
|
- Analytics page with deal filter dropdown
|
||||||
|
- Audit log with deal filter dropdown
|
||||||
|
- Contacts page with deal filter dropdown
|
||||||
|
- Admin panel (CRUD for deals, users, orgs, contacts)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## FEATURES TO BUILD
|
||||||
|
|
||||||
|
Work through these in order. Commit after each major section with `git add -A && git commit -m "feat: ..."`.
|
||||||
|
|
||||||
|
### 1. Organization Type on Signup
|
||||||
|
|
||||||
|
Add `org_type` field to `organizations` table: `TEXT DEFAULT 'company' CHECK (org_type IN ('bank','pe_vc','company'))`.
|
||||||
|
|
||||||
|
Add migration (additive ALTER TABLE or check-and-add pattern). Add org type selector to `signup.templ` with options: "Investment Bank", "PE / VC Firm", "Company". Store it.
|
||||||
|
|
||||||
|
Show org type in admin panel org edit form.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Invite System
|
||||||
|
|
||||||
|
**Goal:** Org owners/admins can invite users by email. Invitee clicks a link, sets password, joins the org.
|
||||||
|
|
||||||
|
**Schema — add to migrate.go (additive):**
|
||||||
|
```sql
|
||||||
|
CREATE TABLE IF NOT EXISTS invites (
|
||||||
|
token TEXT PRIMARY KEY,
|
||||||
|
org_id TEXT NOT NULL,
|
||||||
|
email TEXT NOT NULL,
|
||||||
|
role TEXT NOT NULL DEFAULT 'member',
|
||||||
|
invited_by TEXT NOT NULL,
|
||||||
|
expires_at DATETIME NOT NULL,
|
||||||
|
used_at DATETIME,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Routes:**
|
||||||
|
- `POST /invites/create` — owner/admin only; takes email + role; generates 32-byte hex token, 7-day expiry; stores in DB; returns invite link (just show it in a modal/toast — no email sending needed, Misha will copy/paste)
|
||||||
|
- `GET /invites/accept?token=XXX` — show accept page (name + password fields)
|
||||||
|
- `POST /invites/accept` — validate token not expired/used; create profile in same org; mark invite used
|
||||||
|
|
||||||
|
**UI:**
|
||||||
|
- Add "Team" page at `/team` — lists all profiles in org, shows role badges, invite button
|
||||||
|
- Invite button opens modal: email input + role dropdown (admin/member/viewer) → POST → show generated link
|
||||||
|
- Nav item "Team" in sidebar (between Contacts and Admin)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Remove Close Probability
|
||||||
|
|
||||||
|
In `admin.templ`: remove the `close_probability` form field (around line 263).
|
||||||
|
In `model/models.go`: keep the field (backward compat with existing DB) but don't expose it in any UI.
|
||||||
|
Search all templates for `close_probability` or `CloseProbability` and remove any display.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. New Room Modal Enhancements
|
||||||
|
|
||||||
|
Add to the existing new room modal (in `dashboard.templ` AND `dealrooms.templ`):
|
||||||
|
|
||||||
|
- **Industry** field: text input (e.g. "Healthcare", "Fintech", "SaaS")
|
||||||
|
- **Exclusivity End** date field (already in Deal model as `ExclusivityEnd`, just add to modal form)
|
||||||
|
|
||||||
|
Add `industry TEXT DEFAULT ''` to deals table (additive migration).
|
||||||
|
Add `Industry string` to Deal model.
|
||||||
|
Persist both in `handleCreateDeal`.
|
||||||
|
|
||||||
|
For the "New Room" button on the deal rooms page (`dealrooms.templ`) — add the same modal that's currently on the dashboard. It should be identical functionality.
|
||||||
|
|
||||||
|
**Folder auto-create from paths:** Add an optional textarea in the modal: "Folder Structure (one path per line, e.g. Financial/Q4 Reports)". Parse on submit in `handleCreateDeal`: split by newline, for each path split by `/`, create nested folders. If field is empty, skip.
|
||||||
|
|
||||||
|
**Invite on create:** Add a textarea "Initial Team (one email per line, role: member)". After deal creation, create invite tokens for each email and show all generated links in a success modal. Keep it simple.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Permission Controls on Request Lists
|
||||||
|
|
||||||
|
Add to deals table (additive):
|
||||||
|
```sql
|
||||||
|
ALTER TABLE deals ADD COLUMN buyer_can_comment INTEGER DEFAULT 1;
|
||||||
|
ALTER TABLE deals ADD COLUMN seller_can_comment INTEGER DEFAULT 1;
|
||||||
|
```
|
||||||
|
|
||||||
|
In deal detail request list view: if `buyer_can_comment=0` and user is buyer, hide buyer_comment input. If `seller_can_comment=0` and user is seller, hide seller_comment input.
|
||||||
|
|
||||||
|
In admin deal edit form: add checkboxes "Allow buyer comments" and "Allow seller comments".
|
||||||
|
|
||||||
|
**Folder/file visibility per buyer group:**
|
||||||
|
Add tables:
|
||||||
|
```sql
|
||||||
|
CREATE TABLE IF NOT EXISTS buyer_groups (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
deal_id TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
CREATE TABLE IF NOT EXISTS folder_access (
|
||||||
|
folder_id TEXT NOT NULL,
|
||||||
|
buyer_group TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (folder_id, buyer_group)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
In deal detail documents tab (seller/admin view): add "Restrict Access" button per folder that opens a modal to select which buyer groups can see it. If `folder_access` has rows for a folder, only those groups see it; if empty = visible to all. Filter files shown to buyer based on their `buyer_group` (from diligence_requests or a new field on profiles `buyer_group TEXT DEFAULT ''`).
|
||||||
|
|
||||||
|
Add `buyer_group TEXT DEFAULT ''` to profiles table (additive migration). Admin can set it per user.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Deal Room: Folder Management
|
||||||
|
|
||||||
|
In deal detail documents tab, make the "New Folder" button functional:
|
||||||
|
- POST `/deals/{id}/folders/create` with `name` and optional `parent_id`
|
||||||
|
- Handler creates folder, redirects back to deal
|
||||||
|
|
||||||
|
Add "Rename" and "Delete" (soft delete or actual) per folder in the sidebar — small kebab menu or inline edit on hover.
|
||||||
|
|
||||||
|
Add drag-to-reorder: add `sort_order INTEGER DEFAULT 0` to folders (additive). Simple up/down buttons per folder to change order (HTMX swap).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. File Upload & Management
|
||||||
|
|
||||||
|
**Real file upload:**
|
||||||
|
|
||||||
|
Add `uploads_dir` config (default: `data/uploads/`).
|
||||||
|
|
||||||
|
`POST /deals/{id}/files/upload` — multipart form: `folder_id` (optional), `request_item_id` (optional). Save to `data/uploads/{deal_id}/{uuid}_{filename}`. Insert file record. Create DealActivity. Redirect back.
|
||||||
|
|
||||||
|
Add `storage_path TEXT DEFAULT ''` to files table.
|
||||||
|
|
||||||
|
In document list: add "Upload" button that opens a modal with file picker + folder selector + optional "Link to request item" dropdown.
|
||||||
|
|
||||||
|
Add "Delete" per file (owner/admin only): removes from DB + disk. POST `/deals/{id}/files/{fileID}/delete`.
|
||||||
|
|
||||||
|
Add `uploaded_at` display: `files.created_at` already exists — show it in the file list as "Uploaded Jan 15" next to file size.
|
||||||
|
|
||||||
|
**File download:** `GET /deals/{id}/files/{fileID}/download` — serve the file from disk with original filename. Log download activity.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. Doc ↔ Request List Linking
|
||||||
|
|
||||||
|
When uploading a file (see #7), the upload modal has an optional "Link to request item" dropdown showing open request items for this deal. On save, append the new file ID to `diligence_requests.linked_file_ids` (comma-separated).
|
||||||
|
|
||||||
|
In request list view: for each request item with `linked_file_ids != ""`, show linked file names as small pills with download links.
|
||||||
|
|
||||||
|
In the upload modal: when a buyer uploads (buyer view), they see only their own request items in the dropdown.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. Buyer-Specific Request Lists
|
||||||
|
|
||||||
|
Add to diligence_requests:
|
||||||
|
```sql
|
||||||
|
ALTER TABLE diligence_requests ADD COLUMN is_buyer_specific INTEGER DEFAULT 0;
|
||||||
|
ALTER TABLE diligence_requests ADD COLUMN visible_to_buyer_group TEXT DEFAULT '';
|
||||||
|
```
|
||||||
|
|
||||||
|
In seller view of request list: show all requests. Add "Add Buyer-Specific Request" button that creates a request visible only to a specific buyer_group.
|
||||||
|
|
||||||
|
In buyer view: show general requests (is_buyer_specific=0) AND their own specific ones (visible_to_buyer_group = their buyer_group).
|
||||||
|
|
||||||
|
In `getRequests()` in deals.go: apply this filter for buyer profiles.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10. Document Comments
|
||||||
|
|
||||||
|
**Schema:**
|
||||||
|
```sql
|
||||||
|
CREATE TABLE IF NOT EXISTS file_comments (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
file_id TEXT NOT NULL,
|
||||||
|
deal_id TEXT NOT NULL,
|
||||||
|
user_id TEXT NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
In file list view: add a chat bubble icon per file. If comments exist, show count badge. Clicking opens a slide-over panel (HTMX `hx-get="/deals/{id}/files/{fileID}/comments"` → returns comment list + add form).
|
||||||
|
|
||||||
|
Routes:
|
||||||
|
- `GET /deals/{id}/files/{fileID}/comments` — returns HTMX partial with comments + form
|
||||||
|
- `POST /deals/{id}/files/{fileID}/comments` — add comment, return updated partial
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11. Search Within Deal
|
||||||
|
|
||||||
|
In deal detail header area, add a search input. On input (debounce 300ms with HTMX), `hx-get="/deals/{id}/search?q=..."` → searches file names + folder names + request descriptions → returns HTMX partial replacing the active tab content with results.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 12. Request Lists Page: Organized by Deal then Buyer
|
||||||
|
|
||||||
|
In `templates/requests.templ` and `internal/handler/requests.go`:
|
||||||
|
|
||||||
|
Restructure to show:
|
||||||
|
- Deal name as section header
|
||||||
|
- "General Request List" subsection
|
||||||
|
- Per buyer_group subsections (if buyer-specific items exist)
|
||||||
|
|
||||||
|
Add deal filter in header. Add buyer group filter.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 13. Analytics: Per-Buyer Stats
|
||||||
|
|
||||||
|
**Schema additions:**
|
||||||
|
```sql
|
||||||
|
ALTER TABLE deal_activity ADD COLUMN buyer_group TEXT DEFAULT '';
|
||||||
|
ALTER TABLE deal_activity ADD COLUMN time_spent_seconds INTEGER DEFAULT 0;
|
||||||
|
```
|
||||||
|
|
||||||
|
When logging view/download activity, capture buyer_group from profile and time_spent if provided.
|
||||||
|
|
||||||
|
In analytics template: when a deal is selected, show a "Buyer Activity" section with a table:
|
||||||
|
- Buyer Group | Files Downloaded | Last Accessed | Total Time | Files Viewed
|
||||||
|
- Expandable row: individual user breakdown (hx-get to load per-user stats)
|
||||||
|
|
||||||
|
Add handler `GET /analytics/buyers?deal_id=XXX` returning HTMX partial.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 14. Contacts: Deal Association
|
||||||
|
|
||||||
|
Add junction table:
|
||||||
|
```sql
|
||||||
|
CREATE TABLE IF NOT EXISTS contact_deals (
|
||||||
|
contact_id TEXT NOT NULL,
|
||||||
|
deal_id TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (contact_id, deal_id)
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
In contacts page: when deal filter is active, filter by contact_deals join.
|
||||||
|
|
||||||
|
In contact add/edit form (admin panel): add multi-select for which deals this contact is associated with.
|
||||||
|
|
||||||
|
Show deal name tags on each contact row.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 15. Audit Logs: Buyer Filter
|
||||||
|
|
||||||
|
Add buyer_group filter dropdown to audit log page alongside the existing deal filter. Query: `WHERE deal_id = ? AND (buyer_group = ? OR ? = '')`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 16. Subscription Plan Page (UI Mock)
|
||||||
|
|
||||||
|
Add `/subscription` route and nav item (gear/settings area or sidebar).
|
||||||
|
|
||||||
|
Show three plan cards:
|
||||||
|
- **Starter** — 3 deal rooms, 5 users, 10GB storage — $299/mo
|
||||||
|
- **Growth** — 15 deal rooms, 25 users, 100GB storage — $799/mo
|
||||||
|
- **Enterprise** — Unlimited rooms, unlimited users, 1TB storage — Custom
|
||||||
|
|
||||||
|
Show current plan (hardcode "Growth" for demo), usage bars (rooms used/limit, users used/limit, storage used/limit — all mock values based on actual DB counts for rooms/users, mock 23GB for storage).
|
||||||
|
|
||||||
|
Add "Upgrade" button per plan that shows a "Contact sales" toast. No real billing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Notes
|
||||||
|
|
||||||
|
- **templ generate:** Run after EVERY template change. The `_templ.go` files must be regenerated or the build fails.
|
||||||
|
- **Additive migrations:** Always use `ALTER TABLE ADD COLUMN IF NOT EXISTS` pattern or `CREATE TABLE IF NOT EXISTS`. Never drop columns.
|
||||||
|
- **HTMX:** Use `hx-target`, `hx-swap`, `hx-post/get` for partial updates. Return bare HTML fragments from HTMX endpoints (no full Layout wrapper).
|
||||||
|
- **Activity logging:** Use `logActivity(dealID, userID, orgID, actType, resourceType, resourceName, resourceID)` pattern already in deals.go.
|
||||||
|
- **Error handling:** On bad input, redirect back with `?error=message` and show it in the template.
|
||||||
|
- **Buyer view toggle:** Check `profile.ViewAsBuyer` or `profile.Role == "viewer"` to apply buyer restrictions.
|
||||||
|
- **File storage:** Keep it simple — local disk at `data/uploads/`. No S3 needed for now.
|
||||||
|
- **Service restart:** After build, run `systemctl --user restart dealroom` or kill+restart the binary.
|
||||||
|
|
||||||
|
## After Each Section
|
||||||
|
|
||||||
|
1. `templ generate`
|
||||||
|
2. `go build -o bin/dealroom ./cmd/dealroom/main.go`
|
||||||
|
3. Fix any compile errors
|
||||||
|
4. `systemctl --user restart dealroom` (or kill/restart)
|
||||||
|
5. Quick smoke test: curl http://localhost:8080/ should redirect to login
|
||||||
|
6. `git add -A && git commit -m "feat: <section name>"`
|
||||||
|
|
||||||
|
## Final Steps
|
||||||
|
|
||||||
|
After all sections:
|
||||||
|
1. `git add -A && git commit -m "feat: complete Misha feature list"`
|
||||||
|
2. `git push origin main`
|
||||||
|
3. Run: `openclaw system event --text "Done: Built all Dealspace features — invite system, file upload, folder mgmt, buyer-specific requests, doc comments, analytics by buyer, contacts/audit by deal, subscription page" --mode now`
|
||||||
|
|
@ -26,6 +26,11 @@ func Migrate(db *sql.DB) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Run additive ALTER TABLE migrations (ignore errors for already-existing columns)
|
||||||
|
for _, stmt := range additiveMigrationStmts {
|
||||||
|
db.Exec(stmt)
|
||||||
|
}
|
||||||
|
|
||||||
// Seed demo data if empty
|
// Seed demo data if empty
|
||||||
var count int
|
var count int
|
||||||
db.QueryRow("SELECT COUNT(*) FROM organizations").Scan(&count)
|
db.QueryRow("SELECT COUNT(*) FROM organizations").Scan(&count)
|
||||||
|
|
@ -184,6 +189,12 @@ CREATE INDEX IF NOT EXISTS idx_activity_deal ON deal_activity(deal_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id);
|
CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id);
|
||||||
`
|
`
|
||||||
|
|
||||||
|
// Additive migrations - each statement is run individually, errors ignored (for already-existing columns)
|
||||||
|
var additiveMigrationStmts = []string{
|
||||||
|
// Section 1: org_type
|
||||||
|
`ALTER TABLE organizations ADD COLUMN org_type TEXT DEFAULT 'company'`,
|
||||||
|
}
|
||||||
|
|
||||||
func seed(db *sql.DB) error {
|
func seed(db *sql.DB) error {
|
||||||
stmts := []string{
|
stmts := []string{
|
||||||
// Organization
|
// Organization
|
||||||
|
|
|
||||||
|
|
@ -363,7 +363,7 @@ func (h *Handler) handleAdminOrgForm(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
var org model.Organization
|
var org model.Organization
|
||||||
if id != "" {
|
if id != "" {
|
||||||
h.db.QueryRow("SELECT id, name, slug FROM organizations WHERE id = ?", id).Scan(&org.ID, &org.Name, &org.Slug)
|
h.db.QueryRow("SELECT id, name, slug, COALESCE(org_type, 'company') FROM organizations WHERE id = ?", id).Scan(&org.ID, &org.Name, &org.Slug, &org.OrgType)
|
||||||
}
|
}
|
||||||
|
|
||||||
templates.AdminOrgForm(profile, &org).Render(r.Context(), w)
|
templates.AdminOrgForm(profile, &org).Render(r.Context(), w)
|
||||||
|
|
@ -375,6 +375,10 @@ func (h *Handler) handleAdminOrgSave(w http.ResponseWriter, r *http.Request) {
|
||||||
id := r.FormValue("id")
|
id := r.FormValue("id")
|
||||||
name := strings.TrimSpace(r.FormValue("name"))
|
name := strings.TrimSpace(r.FormValue("name"))
|
||||||
slug := strings.TrimSpace(r.FormValue("slug"))
|
slug := strings.TrimSpace(r.FormValue("slug"))
|
||||||
|
orgType := r.FormValue("org_type")
|
||||||
|
if orgType == "" {
|
||||||
|
orgType = "company"
|
||||||
|
}
|
||||||
|
|
||||||
if name == "" || slug == "" {
|
if name == "" || slug == "" {
|
||||||
http.Error(w, "Name and slug are required", 400)
|
http.Error(w, "Name and slug are required", 400)
|
||||||
|
|
@ -383,13 +387,13 @@ func (h *Handler) handleAdminOrgSave(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
if id == "" {
|
if id == "" {
|
||||||
id = generateID("org")
|
id = generateID("org")
|
||||||
_, err := h.db.Exec("INSERT INTO organizations (id, name, slug) VALUES (?, ?, ?)", id, name, slug)
|
_, err := h.db.Exec("INSERT INTO organizations (id, name, slug, org_type) VALUES (?, ?, ?, ?)", id, name, slug, orgType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Error creating organization", 500)
|
http.Error(w, "Error creating organization", 500)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_, err := h.db.Exec("UPDATE organizations SET name=?, slug=? WHERE id=?", name, slug, id)
|
_, err := h.db.Exec("UPDATE organizations SET name=?, slug=?, org_type=? WHERE id=?", name, slug, orgType, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Error updating organization", 500)
|
http.Error(w, "Error updating organization", 500)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -149,13 +149,17 @@ func (h *Handler) handleSignup(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
|
|
||||||
orgName := r.FormValue("org_name")
|
orgName := r.FormValue("org_name")
|
||||||
|
orgType := r.FormValue("org_type")
|
||||||
|
if orgType == "" {
|
||||||
|
orgType = "company"
|
||||||
|
}
|
||||||
name := r.FormValue("name")
|
name := r.FormValue("name")
|
||||||
email := r.FormValue("email")
|
email := r.FormValue("email")
|
||||||
password := r.FormValue("password")
|
password := r.FormValue("password")
|
||||||
|
|
||||||
// Create organization
|
// Create organization
|
||||||
orgID := generateID()
|
orgID := generateID("org")
|
||||||
_, err = h.db.Exec(`INSERT INTO organizations (id, name, slug) VALUES (?, ?, ?)`, orgID, orgName, orgName)
|
_, err = h.db.Exec(`INSERT INTO organizations (id, name, slug, org_type) VALUES (?, ?, ?, ?)`, orgID, orgName, orgName, orgType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Redirect(w, r, "/signup", http.StatusSeeOther)
|
http.Redirect(w, r, "/signup", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
|
|
@ -163,34 +167,17 @@ func (h *Handler) handleSignup(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// Create user
|
// Create user
|
||||||
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||||
userID := generateID()
|
userID := generateID("user")
|
||||||
_, err = h.db.Exec(`INSERT INTO profiles (id, email, full_name, role, organization_id, hashed_password) VALUES (?, ?, ?, ?, ?, ?)`,
|
_, err = h.db.Exec(`INSERT INTO profiles (id, email, full_name, role, organization_id, password_hash) VALUES (?, ?, ?, ?, ?, ?)`,
|
||||||
userID, email, name, "owner", orgID, hashedPassword)
|
userID, email, name, "owner", orgID, string(hashedPassword))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Redirect(w, r, "/signup", http.StatusSeeOther)
|
http.Redirect(w, r, "/signup", http.StatusSeeOther)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auto login
|
// Auto login
|
||||||
token := make([]byte, 32)
|
h.createSession(w, userID)
|
||||||
rand.Read(token)
|
|
||||||
tokenHex := hex.EncodeToString(token)
|
|
||||||
|
|
||||||
_, err = h.db.Exec(`INSERT INTO sessions (id, user_id, token, expires_at) VALUES (?, ?, ?, datetime('now', '+24 hours'))`,
|
|
||||||
generateID(), userID, tokenHex)
|
|
||||||
if err != nil {
|
|
||||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
|
||||||
Name: "session",
|
|
||||||
Value: tokenHex,
|
|
||||||
Path: "/",
|
|
||||||
Expires: time.Now().Add(24 * time.Hour),
|
|
||||||
HttpOnly: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ type Organization struct {
|
||||||
ID string
|
ID string
|
||||||
Name string
|
Name string
|
||||||
Slug string
|
Slug string
|
||||||
|
OrgType string // bank, pe_vc, company
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -415,6 +415,11 @@ templ AdminOrgForm(profile *model.Profile, org *model.Organization) {
|
||||||
<input type="hidden" name="id" value={ org.ID }/>
|
<input type="hidden" name="id" value={ org.ID }/>
|
||||||
@formField("name", "Name", "text", org.Name, true)
|
@formField("name", "Name", "text", org.Name, true)
|
||||||
@formField("slug", "Slug", "text", org.Slug, true)
|
@formField("slug", "Slug", "text", org.Slug, true)
|
||||||
|
@formSelect("org_type", "Organization Type", org.OrgType, []SelectOption{
|
||||||
|
{Value: "company", Label: "Company"},
|
||||||
|
{Value: "bank", Label: "Investment Bank"},
|
||||||
|
{Value: "pe_vc", Label: "PE / VC Firm"},
|
||||||
|
})
|
||||||
@formActions("/admin/organizations")
|
@formActions("/admin/organizations")
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,14 @@ templ Signup() {
|
||||||
<input type="text" name="org_name" placeholder="e.g. Apex Capital Partners" class="w-full h-11 pl-10 pr-4 rounded-lg border border-gray-200 text-sm focus:outline-none focus:ring-2 focus:ring-teal-500/50 focus:border-teal-500"/>
|
<input type="text" name="org_name" placeholder="e.g. Apex Capital Partners" class="w-full h-11 pl-10 pr-4 rounded-lg border border-gray-200 text-sm focus:outline-none focus:ring-2 focus:ring-teal-500/50 focus:border-teal-500"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-medium text-gray-600 uppercase tracking-wider mb-1.5">Organization Type</label>
|
||||||
|
<select name="org_type" class="w-full h-11 px-4 rounded-lg border border-gray-200 text-sm focus:outline-none focus:ring-2 focus:ring-teal-500/50 focus:border-teal-500 bg-white">
|
||||||
|
<option value="company">Company</option>
|
||||||
|
<option value="bank">Investment Bank</option>
|
||||||
|
<option value="pe_vc">PE / VC Firm</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-medium text-gray-600 uppercase tracking-wider mb-1.5">Your Name</label>
|
<label class="block text-xs font-medium text-gray-600 uppercase tracking-wider mb-1.5">Your Name</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue