diff --git a/Makefile b/Makefile index 7a7bb12..bde898f 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ fmt: vet: fmt go vet ./... -build: vet +build: cd web && pnpm run build go build -v -o solopm-server ./cmd/server diff --git a/assets/avatars/system.png b/assets/avatars/system.png new file mode 100644 index 0000000..5fb9f9a Binary files /dev/null and b/assets/avatars/system.png differ diff --git a/assets/avatars/zombie.png b/assets/avatars/zombie.png new file mode 100644 index 0000000..f815152 Binary files /dev/null and b/assets/avatars/zombie.png differ diff --git a/cmd/server/main.go b/cmd/server/main.go index c7b71a8..9484e2b 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -9,6 +9,8 @@ import ( "path/filepath" "time" + "solopm.com/solopm-server/auth" + "github.com/alexedwards/scs/v2" "github.com/joho/godotenv" ) @@ -22,7 +24,7 @@ func main() { } // handle authentication - sessionManager = scs.New() + sessionManager = scs.NewServerMux() sessionManager.Lifetime = 24 * time.Hour // handle frontend views diff --git a/internal/auth/handler.go b/internal/auth/handler.go new file mode 100644 index 0000000..aadb0ff --- /dev/null +++ b/internal/auth/handler.go @@ -0,0 +1,21 @@ +package auth + +import ( + + "github.com/alexedwards/scs/v2" + "golang.org/x/crypto/bcrypt" +) + +// remember, if anyone attempts to login as the system or zombie user, fail them. hard. + +// sessionManager needs to be figured out. do i inherit that from somewhere else? +// does it get associated with any other one like PHP's `session_open()` if i call it here? + + +func set(writer http.ResponseWriter, request *http.Request) { + sessionManager.Put(request.Context(), "sessionKey", "message to be passed around") +} + +func get(writer http.ResponseWriter, request *http.Request) String { + return sessionManager.GetString(request.Context(), "sessionKey") +} diff --git a/internal/auth/middleware.go b/internal/auth/middleware.go new file mode 100644 index 0000000..bf068dc --- /dev/null +++ b/internal/auth/middleware.go @@ -0,0 +1,3 @@ +package auth + +//import () diff --git a/internal/auth/oauth.go b/internal/auth/oauth.go new file mode 100644 index 0000000..9b3d757 --- /dev/null +++ b/internal/auth/oauth.go @@ -0,0 +1,4 @@ +package auth + +//import () + diff --git a/internal/auth/service.go b/internal/auth/service.go new file mode 100644 index 0000000..b7bb249 --- /dev/null +++ b/internal/auth/service.go @@ -0,0 +1,16 @@ +package auth + +import ( + "net/http" +) + + +func requireAuth(next http.Handler) http.Handler { + return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + if !sessionManager.Exists(request.Context(), "userID") { + http.Redirect(writer, request, "/login", http.StatusSeeOther) + return + } + next.ServeHTTP(writer, request) + }) +} \ No newline at end of file diff --git a/internal/db/database.sqlite b/internal/db/database.sqlite new file mode 100644 index 0000000..ffc5f17 Binary files /dev/null and b/internal/db/database.sqlite differ diff --git a/internal/db/migrations/copy.sh b/internal/db/migrations/copy.sh new file mode 100644 index 0000000..532225d --- /dev/null +++ b/internal/db/migrations/copy.sh @@ -0,0 +1,40 @@ +#!/usr/bin/bash + +for up_file in *.up.sql; do + down_file="${up_file%.up.sql}.down.sql" + quoted_text=$(sed -n '3s/[^"]*\("[^"]*"\).*/\1/p' "$up_file") + first_line=$(head -n 1 "$up_file") + + if [[ "$first_line" != *".up.sql" ]]; then + sed -i "1s/$/\.up\.sql/" "$up_file" + fi + + head -n 3 "$up_file" > "$down_file" + echo "DROP TABLE IF EXISTS ${quoted_text};" >> "$down_file" + tail -n 1 "$up_file" >> "$down_file" +done + +sed -i 's/\bCREATE\b/DROP/g' *.down.sql + +for file in *.sql; do + [ -f "$file" ] || continue + line3=$(sed -n '3p' "$file") + len=${#line3} + + if [ "$len" -lt 64 ]; then + needed=$((64 - len)) + dashes=$(printf '%*s' "$needed" | tr ' ' '-') + sed -i "3s/$/$dashes/" "$file" + fi +done + + + +for file in *.down.sql; do + [ -f "$file" ] || continue + first_line=$(head -n 1 "$file") + + if [[ "$first_line" == *".up.sql" ]]; then + sed -i '1s/\.up\.sql$/\.down\.sql/' "$file" + fi +done \ No newline at end of file diff --git a/internal/db/seeders/.gitignore b/internal/db/seeders/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/internal/db/seeders/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/internal/issue/handler.go b/internal/issue/handler.go new file mode 100644 index 0000000..e69de29 diff --git a/internal/issue/model.go b/internal/issue/model.go new file mode 100644 index 0000000..e69de29 diff --git a/internal/issue/repository.go b/internal/issue/repository.go new file mode 100644 index 0000000..e69de29 diff --git a/internal/issue/service.go b/internal/issue/service.go new file mode 100644 index 0000000..e69de29 diff --git a/internal/project/handler.go b/internal/project/handler.go new file mode 100644 index 0000000..e69de29 diff --git a/internal/project/model.go b/internal/project/model.go new file mode 100644 index 0000000..e69de29 diff --git a/internal/project/repository.go b/internal/project/repository.go new file mode 100644 index 0000000..e69de29 diff --git a/internal/project/service.go b/internal/project/service.go new file mode 100644 index 0000000..e69de29 diff --git a/internal/server/router.go b/internal/server/router.go new file mode 100644 index 0000000..e69de29 diff --git a/internal/server/server.go b/internal/server/server.go new file mode 100644 index 0000000..e69de29 diff --git a/internal/user/handler.go b/internal/user/handler.go new file mode 100644 index 0000000..e69de29 diff --git a/internal/user/model.go b/internal/user/model.go new file mode 100644 index 0000000..e69de29 diff --git a/internal/user/repository.go b/internal/user/repository.go new file mode 100644 index 0000000..e69de29 diff --git a/internal/user/service.go b/internal/user/service.go new file mode 100644 index 0000000..e69de29 diff --git a/web/index.html b/web/index.html index ab2bbaf..472e5dc 100644 --- a/web/index.html +++ b/web/index.html @@ -1,17 +1,75 @@ - - - - - SoloPM + + + + - - - - - - -
- - + + + + + + + + + + + + + + + + + + + Page Title + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ diff --git a/web/src/api/user.ts b/web/src/api/user.ts new file mode 100644 index 0000000..81e6626 --- /dev/null +++ b/web/src/api/user.ts @@ -0,0 +1,21 @@ +import client from './client' +import type { User } from '@/stores/auth' + +export interface UpdateProfilePayload { + name: string + email: string + avatar_url?: string +} + +export interface ChangePasswordPayload { + current_password: string + new_password: string +} + +export const userApi = { + updateProfile: (payload: UpdateProfilePayload) => + client.put('/me', payload), + + changePassword: (payload: ChangePasswordPayload) => + client.put('/me/password', payload), +} diff --git a/web/src/components/layout/AppNavbar.vue b/web/src/components/layout/AppNavbar.vue index 146672e..ca56749 100644 --- a/web/src/components/layout/AppNavbar.vue +++ b/web/src/components/layout/AppNavbar.vue @@ -42,13 +42,19 @@ diff --git a/web/src/components/layout/AppSidebar.vue b/web/src/components/layout/AppSidebar.vue index 74bae2d..4f21785 100644 --- a/web/src/components/layout/AppSidebar.vue +++ b/web/src/components/layout/AppSidebar.vue @@ -31,21 +31,9 @@ -
+
+ +
+ diff --git a/web/src/pages/auth/LoginPage.vue b/web/src/pages/auth/LoginPage.vue index be302ee..de90c08 100644 --- a/web/src/pages/auth/LoginPage.vue +++ b/web/src/pages/auth/LoginPage.vue @@ -2,7 +2,7 @@
-
+
@@ -15,24 +15,24 @@
- +
- +
@@ -41,7 +41,7 @@ @@ -56,7 +56,7 @@
Sign in with GitHub @@ -64,7 +64,7 @@ Sign in with Gitea diff --git a/web/src/pages/dashboard/DashboardPage.vue b/web/src/pages/dashboard/DashboardPage.vue index ad16ed0..79fa8dc 100644 --- a/web/src/pages/dashboard/DashboardPage.vue +++ b/web/src/pages/dashboard/DashboardPage.vue @@ -4,7 +4,7 @@
-
+
@@ -12,7 +12,7 @@
-
+
@@ -22,7 +22,7 @@
-
+
@@ -30,7 +30,7 @@
-
+
@@ -40,7 +40,7 @@
-
+
@@ -48,7 +48,7 @@
-
+
@@ -58,7 +58,7 @@
-
+
@@ -80,24 +80,24 @@
-
-
+
+
Activity

Recent updates

-
+

No activity yet.

-
-
+
+
Recent Projects

Your active projects

-
+

No projects yet.

diff --git a/web/src/pages/profile/ProfilePage.vue b/web/src/pages/profile/ProfilePage.vue new file mode 100644 index 0000000..150dd06 --- /dev/null +++ b/web/src/pages/profile/ProfilePage.vue @@ -0,0 +1,238 @@ + + + diff --git a/web/src/router/index.ts b/web/src/router/index.ts index 5e4e144..4918c1c 100644 --- a/web/src/router/index.ts +++ b/web/src/router/index.ts @@ -26,6 +26,12 @@ const router = createRouter({ component: () => import('@/pages/dashboard/DashboardPage.vue'), meta: { title: 'Dashboard' }, }, + { + path: '/profile', + name: 'profile', + component: () => import('@/pages/profile/ProfilePage.vue'), + meta: { title: 'Profile' }, + }, ], }) diff --git a/web/src/stores/auth.ts b/web/src/stores/auth.ts index e3a08d3..bdb1944 100644 --- a/web/src/stores/auth.ts +++ b/web/src/stores/auth.ts @@ -3,9 +3,12 @@ import { ref, computed } from 'vue' export interface User { id: number + username: string name: string email: string avatar_url?: string + is_admin: boolean + provider: string } export const useAuthStore = defineStore('auth', () => { diff --git a/web/src/styles/main.css b/web/src/styles/main.css index b5c61c9..62e3744 100644 --- a/web/src/styles/main.css +++ b/web/src/styles/main.css @@ -1,3 +1,59 @@ @tailwind base; @tailwind components; @tailwind utilities; + +@layer components { + /* ── Card ─────────────────────────────────────────────────────────── */ + .card { + @apply relative flex flex-col min-w-0 break-words bg-white shadow-soft-xl rounded-2xl bg-clip-border; + } + .card-header { + /* border-black/12.5 */ + @apply rounded-t-2xl border-b-0 border-solid p-6 pb-0; + } + .card-body { + @apply flex-auto p-6 pt-4; + } + + /* ── Buttons ──────────────────────────────────────────────────────── */ + /* Use display (inline-block / inline-flex) and sizing (px-* py-* w-*) as utilities alongside these. */ + .btn-primary { + @apply font-bold text-center text-white uppercase align-middle transition-all ease-in border-0 rounded-lg select-none shadow-soft-md bg-150 bg-x-25 leading-pro text-xs bg-gradient-to-tl from-purple-700 to-pink-500 hover:shadow-soft-2xl hover:scale-102 disabled:opacity-60 disabled:cursor-not-allowed disabled:hover:scale-100; + } + .btn-white { + @apply font-bold text-center text-slate-700 uppercase align-middle transition-all ease-in bg-white border border-solid border-gray-300 rounded-lg shadow-soft-sm leading-pro text-xs hover:shadow-soft-md hover:scale-102; + } + .btn-outlined { + @apply font-bold text-center uppercase align-middle transition-all bg-transparent border border-solid rounded-lg shadow-none cursor-pointer leading-pro text-xs border-fuchsia-500 ease-soft-in text-fuchsia-500 hover:border-fuchsia-500 active:bg-fuchsia-500 hover:bg-transparent hover:shadow-none; + } + + /* ── Forms ────────────────────────────────────────────────────────── */ + .form-label { + @apply mb-2 text-xs font-semibold text-slate-700 uppercase block; + } + .form-input { + @apply text-sm focus:shadow-soft-primary-outline ease-soft block w-full rounded-lg border border-solid border-gray-300 bg-white bg-clip-padding py-2 px-3 text-gray-700 transition-all placeholder:text-gray-500 focus:border-fuchsia-300 focus:outline-none focus:transition-shadow; + } + + /* ── Sidebar nav ──────────────────────────────────────────────────── */ + /* .nav-item-active and .nav-item-inactive are modifiers applied alongside .nav-item */ + .nav-item { + @apply py-2.7 text-sm ease-nav-brand my-0 mx-4 flex items-center whitespace-nowrap rounded-lg px-4 font-semibold transition-colors; + } + .nav-item-active { + @apply shadow-soft-xl bg-white text-slate-700; + } + .nav-item-inactive { + @apply text-slate-500 hover:text-slate-700; + } + /* .nav-icon-active and .nav-icon-inactive are modifiers applied alongside .nav-icon */ + .nav-icon { + @apply mr-2 flex h-8 w-8 items-center justify-center rounded-lg bg-center stroke-0 text-center xl:p-2.5; + } + .nav-icon-active { + @apply bg-gradient-to-tl from-purple-700 to-pink-500 shadow-soft-2xl; + } + .nav-icon-inactive { + @apply bg-white shadow-soft-sm; + } +}