main 6af0a61

user.go RawHistoryFolder

1package user
2
3import (
4	"errors"
5	"strings"
6)
7
8// User is an entity representing a system user account
9type User struct {
10	username string
11	groups   []string
12	shell    string
13	home     string
14}
15
16// NewUser creates a new User entity with validation
17func NewUser(username string, shell string) (*User, error) {
18	// Validate username
19	if err := ValidateUsername(username); err != nil {
20		return nil, err
21	}
22
23	// Validate shell
24	if err := ValidateShell(shell); err != nil {
25		return nil, err
26	}
27
28	user := &User{
29		username: username,
30		shell:    shell,
31		groups:   []string{},
32		home:     "/home/" + username,
33	}
34
35	return user, nil
36}
37
38// Username returns the username
39func (u *User) Username() string {
40	return u.username
41}
42
43// Shell returns the login shell
44func (u *User) Shell() string {
45	return u.shell
46}
47
48// Home returns the home directory path
49func (u *User) Home() string {
50	return u.home
51}
52
53// Groups returns a copy of the user groups list
54func (u *User) Groups() []string {
55	groups := make([]string, len(u.groups))
56	copy(groups, u.groups)
57	return groups
58}
59
60// AddGroup adds a user to a group
61func (u *User) AddGroup(group string) error {
62	if group == "" {
63		return errors.New("group name cannot be empty")
64	}
65
66	// Check if already a member
67	for _, g := range u.groups {
68		if g == group {
69			return errors.New("user already in group " + group)
70		}
71	}
72
73	u.groups = append(u.groups, group)
74	return nil
75}
76
77// HasGroup checks if user is in a group
78func (u *User) HasGroup(group string) bool {
79	for _, g := range u.groups {
80		if g == group {
81			return true
82		}
83	}
84	return false
85}
86
87// String returns human-readable representation
88func (u *User) String() string {
89	return "User(username=" + u.username + ", shell=" + u.shell + ", groups=" + strings.Join(u.groups, ",") + ")"
90}
91
92// ValidateUsername validates username format
93// Usernames: lowercase letters, digits, underscore, hyphen (3-32 chars)
94func ValidateUsername(username string) error {
95	if username == "" {
96		return errors.New("username cannot be empty")
97	}
98
99	if len(username) < 3 {
100		return errors.New("username must be at least 3 characters")
101	}
102
103	if len(username) > 32 {
104		return errors.New("username cannot exceed 32 characters")
105	}
106
107	// Must start with lowercase letter
108	if username[0] < 'a' || username[0] > 'z' {
109		return errors.New("username must start with lowercase letter")
110	}
111
112	// Can contain lowercase, digits, underscore, hyphen
113	for _, ch := range username {
114		if !isUsernameChar(ch) {
115			return errors.New("username contains invalid characters")
116		}
117	}
118
119	return nil
120}
121
122func isUsernameChar(ch rune) bool {
123	switch {
124	case ch >= 'a' && ch <= 'z':
125		return true
126	case ch >= '0' && ch <= '9':
127		return true
128	case ch == '_' || ch == '-':
129		return true
130	default:
131		return false
132	}
133}
134
135// ValidateShell validates shell path
136func ValidateShell(shell string) error {
137	if shell == "" {
138		return errors.New("shell cannot be empty")
139	}
140
141	if !strings.HasPrefix(shell, "/") {
142		return errors.New("shell path must be absolute")
143	}
144
145	// Common valid shells
146	validShells := map[string]bool{
147		"/bin/bash":     true,
148		"/bin/sh":       true,
149		"/bin/zsh":      true,
150		"/bin/fish":     true,
151		"/usr/bin/zsh":  true,
152		"/usr/bin/fish": true,
153	}
154
155	if validShells[shell] {
156		return nil
157	}
158
159	// Allow other shells if they look reasonable
160	if len(shell) > 256 {
161		return errors.New("shell path too long")
162	}
163
164	return nil
165}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 0

    ↑↓ moveenter openesc close