Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auth it! #8

Merged
merged 2 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
43 changes: 43 additions & 0 deletions api/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,49 @@ servers:
- url: 'http://localhost:4102'
description: Local server
paths:
/api/users/login:
post:
tags:
- Users
summary: Login user
description: Authenticates a user by user_name and password, starts a new session, and returns a session cookie.
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
user_name:
type: string
example: exampleUser
password:
type: string
format: password
example: examplePass
responses:
200:
description: User logged in successfully. A session cookie is set.
headers:
Set-Cookie:
description: Session cookie which needs to be included in subsequent requests.
schema:
type: string
example: 'session_id=abc123; Path=/; Max-Age=345600; HttpOnly'
content:
application/json:
schema:
type: object
properties:
data:
type: string
example: 'User logged in'
400:
description: Invalid request body
401:
description: Invalid user_name or password
500:
description: Internal Server Error
/api/users:
get:
tags:
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ require (
github.com/gorilla/mux v1.8.1
github.com/ilyakaznacheev/cleanenv v1.5.0
github.com/lib/pq v1.2.0
golang.org/x/crypto v0.22.0
)

require (
github.com/BurntSushi/toml v1.2.1 // indirect
github.com/joho/godotenv v1.5.1 // indirect
golang.org/x/crypto v0.22.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect
)
66 changes: 66 additions & 0 deletions internal/app/api/session.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package api

import (
"ctf01d/internal/app/repository"
api_helpers "ctf01d/internal/app/utils"
"database/sql"
"encoding/json"
"net/http"
)

type RequestLogin struct {
Username string `json:"user_name"`
Password string `json:"password"`
}

func LoginSessionHandler(db *sql.DB, w http.ResponseWriter, r *http.Request) {
var req RequestLogin
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid request body", http.StatusBadRequest)
return
}
userRepo := repository.NewUserRepository(db)
user, err := userRepo.GetByUserName(r.Context(), req.Username)
if err != nil || !api_helpers.CheckPasswordHash(req.Password, user.PasswordHash) {
api_helpers.RespondWithJSON(w, http.StatusUnauthorized, map[string]string{"error": "Invalid password or user"})
return
}

sessionRepo := repository.NewSessionRepository(db)
sessionId, err := sessionRepo.StoreSessionInDB(r.Context(), user.Id)
if err != nil {
http.Error(w, "Failed to store session in DB", http.StatusInternalServerError)
return
}

http.SetCookie(w, &http.Cookie{
Name: "session_id",
HttpOnly: true,
Value: sessionId,
Path: "/",
MaxAge: 96 * 3600, // fixme, брать из db
})

api_helpers.RespondWithJSON(w, http.StatusOK, map[string]string{"data": "User logged in"})
}

func LogoutSessionHandler(db *sql.DB, w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("session_id")
if err != nil {
http.Error(w, "No session found", http.StatusUnauthorized)
return
}
sessionRepo := repository.NewSessionRepository(db)
err = sessionRepo.DeleteSessionInDB(r.Context(), cookie.Value)
if err != nil {
http.Error(w, "Failed to delete session", http.StatusInternalServerError)
return
}
http.SetCookie(w, &http.Cookie{
Name: "session_id",
Value: "",
Path: "/",
MaxAge: -1, // Удаление куки
})
api_helpers.RespondWithJSON(w, http.StatusOK, map[string]string{"data": "User logout successful"})
}
54 changes: 54 additions & 0 deletions internal/app/repository/session.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package repository

import (
"context"
"database/sql"
"fmt"
"time"
)

type SessionRepository interface {
GetSessionFromDB(ctx context.Context, sessionId string) (int, error)
StoreSessionInDB(ctx context.Context, userId int) (string, error)
DeleteSessionInDB(ctx context.Context, cookie string) error
}

type sessionRepo struct {
db *sql.DB
}

func NewSessionRepository(db *sql.DB) SessionRepository {
return &sessionRepo{db: db}
}

func (r *sessionRepo) GetSessionFromDB(ctx context.Context, sessionId string) (int, error) {
var userId int
err := r.db.QueryRowContext(ctx, "SELECT user_id FROM sessions WHERE id = $1 AND expires_at > NOW()", sessionId).Scan(&userId)
return userId, err
}

func (r *sessionRepo) StoreSessionInDB(ctx context.Context, userId int) (string, error) {
var session string
query := `
INSERT INTO sessions (user_id, expires_at)
VALUES ($1, $2)
ON CONFLICT (user_id) DO
UPDATE SET expires_at = EXCLUDED.expires_at
RETURNING id
`
err := r.db.QueryRowContext(ctx, query, userId, time.Now().Add(96*time.Hour)).Scan(&session)
fmt.Println(session)
if err != nil {
return "", err
}
return session, nil
}

func (r *sessionRepo) DeleteSessionInDB(ctx context.Context, sessionId string) error {
query := "DELETE FROM sessions where id = $1"
_, err := r.db.ExecContext(ctx, query, sessionId)
if err != nil {
return err
}
return nil
}
11 changes: 11 additions & 0 deletions internal/app/repository/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type UserRepository interface {
Create(ctx context.Context, user *models.User) error
AddUserToTeams(ctx context.Context, userId int, teamIds []string) error
GetById(ctx context.Context, id string) (*models.User, error)
GetByUserName(ctx context.Context, id string) (*models.User, error)
Update(ctx context.Context, user *models.User) error
Delete(ctx context.Context, id string) error
List(ctx context.Context) ([]*models.User, error)
Expand Down Expand Up @@ -52,6 +53,16 @@ func (r *userRepo) GetById(ctx context.Context, id string) (*models.User, error)
return user, nil
}

func (r *userRepo) GetByUserName(ctx context.Context, name string) (*models.User, error) {
query := `SELECT id, password_hash FROM users WHERE user_name = $1`
user := &models.User{}
err := r.db.QueryRowContext(ctx, query, name).Scan(&user.Id, &user.PasswordHash)
if err != nil {
return nil, err
}
return user, nil
}

func (r *userRepo) Update(ctx context.Context, user *models.User) error {
query := `UPDATE users SET user_name = $1, avatar_url = $2, role = $3 WHERE id = $4`
_, err := r.db.ExecContext(ctx, query, user.Username, user.AvatarUrl, user.Role, user.Id)
Expand Down
3 changes: 3 additions & 0 deletions internal/app/routers/routers.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ var apiRoutes = ApiRoutes{
ApiRoute{"ListUsers", strings.ToUpper("Get"), "/api/users", api.ListUsersHandler},
ApiRoute{"UpdateUser", strings.ToUpper("Put"), "/api/users/{id}", api.UpdateUserHandler},

ApiRoute{"LoginUser", strings.ToUpper("Post"), "/api/users/login", api.LoginSessionHandler},
ApiRoute{"LogoutUser", strings.ToUpper("Post"), "/api/users/logout", api.LogoutSessionHandler},

ApiRoute{"ListUniversities", strings.ToUpper("Get"), "/api/universities", api.ListUniversitiesHandler},
}

Expand Down
8 changes: 8 additions & 0 deletions migrations/init.sql
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@ CREATE TABLE users (
role VARCHAR(255) NOT NULL CHECK (role IN ('admin', 'player', 'guest'))
);

-- Сессии пользователей
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE sessions (
id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id INTEGER UNIQUE REFERENCES users(id),
expires_at TIMESTAMP NOT NULL
);

-- Университеты
CREATE TABLE universities (
id SERIAL PRIMARY KEY,
Expand Down
31 changes: 30 additions & 1 deletion web/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,42 @@
text-decoration: underline;
}
</style>
</head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
</head>
<body>
<h3>Welcome to example_go_service2 by ctf01d</h3>
<div class="navigation">
<a href="/games/index.html">List Games</a>
<a href="/users/index.html">List Users</a>
<a href="/teams/index.html">List Teams</a>
</div>
<div class="login-form">
<h4>Login</h4>
<form id="login-form">
<input type="text" id="login-username" placeholder="Username" required>
<input type="password" id="login-password" placeholder="Password" required>
<button type="submit">Login</button>
</form>
</div>
<script>
$('#login-form').on('submit', function (e) {
e.preventDefault();
var user_name = $('#login-username').val();
var password = $('#login-password').val();
$.ajax({
url: '/api/users/login',
method: 'POST',
contentType: 'application/json',
data: JSON.stringify({ user_name: user_name, password: password }),
success: function (response, status, xhr) {
alert('Login successful!');
window.location.href = "/";
},
error: function () {
alert('Login failed. Check username and password.');
}
});
});
</script>
</body>
</html>
4 changes: 2 additions & 2 deletions web/templates/users/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ <h2>Users</h2>
</div>
<h2>Add New User</h2>
<form id="add-user-form">
<input type="text" id="username" name="username" placeholder="Username" required>
<input type="text" id="user_name" name="username" placeholder="Username" required>
<select id="team" name="team" multiple>
</select>
<input type="password" id="password" name="password" placeholder="Password" required>
Expand Down Expand Up @@ -137,7 +137,7 @@ <h2>Add New User</h2>
e.preventDefault();
var selectedTeams = $('#team').val();
var data = {
user_name: $('#username').val(),
user_name: $('#user_name').val(),
password: $('#password').val(),
avatar_url: $('#avatar_url').val(),
status: $('#status').val(),
Expand Down
Loading